API y Base de Datos
📊 API y Base de Datos
Documentación técnica completa del sistema de base de datos y API interna del bot.
🗄️ Sistema de Base de Datos
Tecnología
- Motor: Better-SQLite3
- Tipo: Base de datos embebida
- Ubicación:
./data/bot.db - Backup: Automático cada 24 horas
Estructura de Tablas
guild_configs - Configuraciones por Servidor
CREATE TABLE guild_configs ( guild_id TEXT PRIMARY KEY, config TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);Estructura JSON del campo config:
{ "language": "es", "prefix": "!", "welcome": { "enabled": false, "channel_id": null, "message": "¡Bienvenido {user} a {server}!", "image_url": null }, "tickets": { "enabled": false, "category_id": null, "panel_channel_id": null, "log_channel_id": null, "staff_role_id": null, "categories": [] }, "levels": { "enabled": true, "xp_per_message": { "min": 15, "max": 25 }, "cooldown": 60000, "excluded_channels": [] }, "moderation": { "log_channel_id": null, "auto_mod": false, "warn_threshold": 3 }}user_levels - Sistema de Niveles
CREATE TABLE 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));tickets - Sistema de Tickets
CREATE TABLE tickets ( id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id TEXT NOT NULL, channel_id TEXT NOT NULL, user_id TEXT NOT NULL, category TEXT NOT NULL, status TEXT DEFAULT 'open', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, closed_at DATETIME, closed_by TEXT, transcript_path TEXT);role_panels - Sistema de Roles
CREATE TABLE role_panels ( id INTEGER PRIMARY KEY AUTOINCREMENT, guild_id TEXT NOT NULL, channel_id TEXT NOT NULL, message_id TEXT NOT NULL, config TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);🔧 API Interna
Database Manager
class DatabaseManager { constructor(dbPath = './data/bot.db') { this.db = new Database(dbPath); this.initTables(); }
// Configuraciones de Guild getGuildConfig(guildId) { return this.db.prepare('SELECT config FROM guild_configs WHERE guild_id = ?').get(guildId); }
setGuildConfig(guildId, config) { return this.db.prepare(` INSERT OR REPLACE INTO guild_configs (guild_id, config, updated_at) VALUES (?, ?, datetime('now')) `).run(guildId, JSON.stringify(config)); }
// Sistema de Niveles getUserLevel(userId, guildId) { return this.db.prepare(` SELECT * FROM user_levels WHERE user_id = ? AND guild_id = ? `).get(userId, guildId); }
addXP(userId, guildId, xpAmount) { const stmt = this.db.prepare(` INSERT OR REPLACE INTO user_levels (user_id, guild_id, xp, level, total_messages, last_xp_gain) VALUES (?, ?, COALESCE((SELECT xp FROM user_levels WHERE user_id = ? AND guild_id = ?), 0) + ?, ?, ?, datetime('now')) `); return stmt.run(userId, guildId, userId, guildId, xpAmount, newLevel, totalMessages); }
getLeaderboard(guildId, limit = 10, offset = 0) { return this.db.prepare(` SELECT user_id, xp, level FROM user_levels WHERE guild_id = ? ORDER BY xp DESC LIMIT ? OFFSET ? `).all(guildId, limit, offset); }}Config Manager
class ConfigManager { static getDefaultConfig() { return { language: 'es', welcome: { enabled: false, channel_id: null, message: '¡Bienvenido {user} a {server}!', image_url: null }, tickets: { enabled: false, category_id: null, panel_channel_id: null, log_channel_id: null, staff_role_id: null, categories: [ { id: 'support', name: 'Soporte Técnico', description: 'Ayuda técnica general', emoji: '🔧' }, { id: 'report', name: 'Reportar Bug', description: 'Reportar errores o problemas', emoji: '🐛' } ] }, levels: { enabled: true, xp_per_message: { min: 15, max: 25 }, cooldown: 60000, excluded_channels: [] } }; }
static async getGuildConfig(guildId) { const data = db.getGuildConfig(guildId); if (!data) { const defaultConfig = this.getDefaultConfig(); db.setGuildConfig(guildId, defaultConfig); return defaultConfig; } return JSON.parse(data.config); }
static async updateGuildConfig(guildId, updates) { const current = await this.getGuildConfig(guildId); const merged = { ...current, ...updates }; db.setGuildConfig(guildId, merged); return merged; }}Level Manager
class LevelManager { static calculateLevel(xp) { return Math.floor(Math.sqrt(xp / 100)); }
static calculateXPForLevel(level) { return level * level * 100; }
static async addXP(userId, guildId, interaction) { const config = await ConfigManager.getGuildConfig(guildId); if (!config.levels.enabled) return false;
// Verificar cooldown const userData = db.getUserLevel(userId, guildId); if (userData && userData.last_xp_gain) { const timeDiff = Date.now() - new Date(userData.last_xp_gain).getTime(); if (timeDiff < config.levels.cooldown) return false; }
// Calcular XP const { min, max } = config.levels.xp_per_message; const xpGain = Math.floor(Math.random() * (max - min + 1)) + min;
// Actualizar base de datos const currentXP = userData ? userData.xp : 0; const newXP = currentXP + xpGain; const newLevel = this.calculateLevel(newXP); const oldLevel = userData ? userData.level : 0;
db.addXP(userId, guildId, xpGain);
// Verificar level up if (newLevel > oldLevel) { await this.handleLevelUp(userId, guildId, newLevel, interaction); }
return { xpGain, newLevel, levelUp: newLevel > oldLevel }; }
static async handleLevelUp(userId, guildId, newLevel, interaction) { const t = getTranslation(guildId); const user = await interaction.client.users.fetch(userId);
// Mensaje de level up const embed = new EmbedBuilder() .setTitle(t('levels.levelUp.title')) .setDescription(t('levels.levelUp.description', { user, level: newLevel })) .setColor('#00FF00') .setThumbnail(user.displayAvatarURL());
if (interaction.channel) { await interaction.channel.send({ embeds: [embed] }); }
// Roles automáticos (si están configurados) await this.checkAutoRoles(userId, guildId, newLevel, interaction); }}🔒 Seguridad y Validación
Sanitización de Datos
class DataValidator { static sanitizeString(str, maxLength = 2000) { if (typeof str !== 'string') return ''; return str.slice(0, maxLength).replace(/[<>@#&]/g, ''); }
static validateGuildId(guildId) { return /^\d{17,19}$/.test(guildId); }
static validateUserId(userId) { return /^\d{17,19}$/.test(userId); }
static validateConfig(config) { const schema = { language: ['es', 'en'], welcome: { enabled: 'boolean', channel_id: 'string|null', message: 'string', image_url: 'string|null' } }; return this.validateSchema(config, schema); }}Rate Limiting
class RateLimiter { constructor() { this.limits = new Map(); }
isLimited(key, limit = 5, window = 60000) { const now = Date.now(); const userLimits = this.limits.get(key) || [];
// Limpiar entradas viejas const validLimits = userLimits.filter(time => now - time < window);
if (validLimits.length >= limit) { return true; }
validLimits.push(now); this.limits.set(key, validLimits); return false; }}📈 Monitoreo y Logs
Sistema de Logs
class Logger { static levels = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3 };
static log(level, message, data = {}) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level: Object.keys(this.levels)[level], message, data };
// Escribir a archivo const logFile = `./logs/bot-${new Date().toISOString().split('T')[0]}.log`; fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
// Console output console.log(`[${logEntry.level}] ${timestamp} - ${message}`); }
static error(message, data) { this.log(0, message, data); } static warn(message, data) { this.log(1, message, data); } static info(message, data) { this.log(2, message, data); } static debug(message, data) { this.log(3, message, data); }}Métricas de Performance
class MetricsCollector { static metrics = { commandsExecuted: 0, messagesProcessed: 0, errorsCount: 0, uptime: process.uptime() };
static recordCommand(commandName) { this.metrics.commandsExecuted++; Logger.debug(`Command executed: ${commandName}`); }
static recordError(error) { this.metrics.errorsCount++; Logger.error('Error recorded', { error: error.message, stack: error.stack }); }
static getMetrics() { return { ...this.metrics, uptime: process.uptime(), memoryUsage: process.memoryUsage() }; }}🔄 Backup y Recuperación
Sistema de Backup
class BackupManager { static async createBackup() { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = `./data/backups/backup-${timestamp}.db`;
try { await fs.copyFile('./data/bot.db', backupPath); Logger.info(`Backup created: ${backupPath}`);
// Limpiar backups antiguos (mantener solo 7 días) await this.cleanOldBackups();
return backupPath; } catch (error) { Logger.error('Failed to create backup', error); throw error; } }
static async cleanOldBackups(daysToKeep = 7) { const backupDir = './data/backups'; const files = await fs.readdir(backupDir); const cutoffDate = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000);
for (const file of files) { const filePath = path.join(backupDir, file); const stats = await fs.stat(filePath);
if (stats.mtime.getTime() < cutoffDate) { await fs.unlink(filePath); Logger.info(`Old backup deleted: ${file}`); } } }
static async restoreFromBackup(backupPath) { try { await fs.copyFile(backupPath, './data/bot.db'); Logger.info(`Database restored from: ${backupPath}`); return true; } catch (error) { Logger.error('Failed to restore backup', error); return false; } }}🔌 Extensibilidad
Plugin System (Futuro)
class PluginManager { static plugins = new Map();
static async loadPlugin(pluginPath) { try { const plugin = await import(pluginPath); const instance = new plugin.default();
this.plugins.set(instance.name, instance); Logger.info(`Plugin loaded: ${instance.name}`);
return true; } catch (error) { Logger.error(`Failed to load plugin: ${pluginPath}`, error); return false; } }
static async executeHook(hookName, ...args) { for (const [name, plugin] of this.plugins) { if (typeof plugin[hookName] === 'function') { try { await plugin[hookName](...args); } catch (error) { Logger.error(`Plugin ${name} hook ${hookName} failed`, error); } } } }}Event Hooks
// Hooks disponibles para pluginsconst hooks = { 'message.create': async (message) => { await PluginManager.executeHook('onMessage', message); }, 'member.join': async (member) => { await PluginManager.executeHook('onMemberJoin', member); }, 'command.execute': async (interaction) => { await PluginManager.executeHook('onCommand', interaction); }};📊 Optimización de Performance
Connection Pooling
class DatabasePool { constructor(maxConnections = 5) { this.connections = []; this.maxConnections = maxConnections; this.currentConnections = 0; }
async getConnection() { if (this.connections.length > 0) { return this.connections.pop(); }
if (this.currentConnections < this.maxConnections) { this.currentConnections++; return new Database('./data/bot.db'); }
// Esperar por una conexión disponible return new Promise(resolve => { const check = setInterval(() => { if (this.connections.length > 0) { clearInterval(check); resolve(this.connections.pop()); } }, 10); }); }
releaseConnection(connection) { this.connections.push(connection); }}Cache Layer
class CacheManager { constructor(ttl = 300000) { // 5 minutos por defecto this.cache = new Map(); this.ttl = ttl; }
set(key, value) { const expires = Date.now() + this.ttl; this.cache.set(key, { value, expires }); }
get(key) { const item = this.cache.get(key); if (!item) return null;
if (Date.now() > item.expires) { this.cache.delete(key); return null; }
return item.value; }
clear() { this.cache.clear(); }}Esta documentación técnica está dirigida a desarrolladores que quieran extender o modificar el bot. Para uso básico, consulta las guías de usuario en otras secciones.