Utilidades
🛠️ Utilidades
Documentación completa de todas las funciones auxiliares y utilidades que facilitan el desarrollo y funcionamiento del bot.
📊 Sistema de Embeds
Embed Builder Personalizado
const { EmbedBuilder } = require('discord.js');
class CustomEmbedBuilder { static success(title, description) { return new EmbedBuilder() .setTitle(`✅ ${title}`) .setDescription(description) .setColor('#00FF00') .setTimestamp(); }
static error(title, description) { return new EmbedBuilder() .setTitle(`❌ ${title}`) .setDescription(description) .setColor('#FF0000') .setTimestamp(); }
static warning(title, description) { return new EmbedBuilder() .setTitle(`⚠️ ${title}`) .setDescription(description) .setColor('#FFA500') .setTimestamp(); }
static info(title, description) { return new EmbedBuilder() .setTitle(`ℹ️ ${title}`) .setDescription(description) .setColor('#0099FF') .setTimestamp(); }
static loading(title, description) { return new EmbedBuilder() .setTitle(`⏳ ${title}`) .setDescription(description) .setColor('#FFFF00') .setTimestamp(); }}
module.exports = { CustomEmbedBuilder };Embeds de Sistema Específicos
// Embed para sistema de ticketsfunction createTicketEmbed(ticketData) { return new EmbedBuilder() .setTitle('🎫 Nuevo Ticket') .setDescription(`Ticket creado por <@${ticketData.userId}>`) .addFields( { name: 'Categoría', value: ticketData.category, inline: true }, { name: 'Canal', value: `<#${ticketData.channelId}>`, inline: true }, { name: 'ID', value: ticketData.id.toString(), inline: true } ) .setColor('#5865F2') .setTimestamp();}
// Embed para sistema de nivelesfunction createLevelUpEmbed(user, level) { return new EmbedBuilder() .setTitle('🎉 ¡Level Up!') .setDescription(`¡Felicidades ${user}! Has alcanzado el nivel **${level}**`) .setThumbnail(user.displayAvatarURL()) .setColor('#FFD700') .setTimestamp();}
// Embed para bienvenidafunction createWelcomeEmbed(member, config) { const embed = new EmbedBuilder() .setTitle('🛬 ¡Bienvenido!') .setDescription(replacePlaceholders(config.message, member)) .setThumbnail(member.user.displayAvatarURL()) .setColor('#00FF00') .setTimestamp();
if (config.image_url) { embed.setImage(config.image_url); }
return embed;}🔧 Validadores
Validación de Permisos
class PermissionValidator { static hasPermission(member, permission) { return member.permissions.has(permission); }
static isAdmin(member) { return member.permissions.has('Administrator'); }
static isModerator(member, guildConfig) { if (this.isAdmin(member)) return true;
const modRoles = guildConfig.moderation?.moderatorRoles || []; return member.roles.cache.some(role => modRoles.includes(role.id)); }
static isOwner(userId) { return userId === process.env.OWNER_ID; }
static canManageChannel(member, channel) { return channel.permissionsFor(member).has('ManageChannels'); }
static canSendMessages(member, channel) { return channel.permissionsFor(member).has('SendMessages'); }}Validación de Datos
class DataValidator { static isValidSnowflake(id) { return /^\d{17,19}$/.test(id); }
static isValidURL(url) { try { new URL(url); return true; } catch { return false; } }
static isValidImageURL(url) { if (!this.isValidURL(url)) return false; return /\.(jpg|jpeg|png|gif|webp)$/i.test(url); }
static sanitizeString(str, maxLength = 2000) { if (typeof str !== 'string') return ''; return str.slice(0, maxLength) .replace(/[<>@#&]/g, '') .trim(); }
static isValidHexColor(color) { return /^#[0-9A-F]{6}$/i.test(color); }
static validateConfig(config, schema) { const errors = [];
function validateObject(obj, schemaObj, path = '') { for (const [key, expectedType] of Object.entries(schemaObj)) { const fullPath = path ? `${path}.${key}` : key; const value = obj[key];
if (value === undefined) { errors.push(`Missing required field: ${fullPath}`); continue; }
if (typeof expectedType === 'string') { if (typeof value !== expectedType) { errors.push(`Invalid type for ${fullPath}: expected ${expectedType}, got ${typeof value}`); } } else if (typeof expectedType === 'object') { if (typeof value === 'object' && value !== null) { validateObject(value, expectedType, fullPath); } else { errors.push(`Invalid type for ${fullPath}: expected object`); } } } }
validateObject(config, schema); return { valid: errors.length === 0, errors }; }}🔤 Sistema de Variables
Reemplazo de Placeholders
class PlaceholderReplacer { static replace(text, data) { if (!text || typeof text !== 'string') return '';
const placeholders = { // Usuario '{user}': () => data.user?.toString() || '', '{user.mention}': () => data.user?.toString() || '', '{user.tag}': () => data.user?.tag || '', '{user.username}': () => data.user?.username || '', '{user.id}': () => data.user?.id || '', '{user.avatar}': () => data.user?.displayAvatarURL() || '',
// Servidor '{server}': () => data.guild?.name || '', '{server.name}': () => data.guild?.name || '', '{server.id}': () => data.guild?.id || '', '{server.memberCount}': () => data.guild?.memberCount?.toString() || '0', '{memberCount}': () => data.guild?.memberCount?.toString() || '0',
// Canal '{channel}': () => data.channel?.toString() || '', '{channel.name}': () => data.channel?.name || '', '{channel.id}': () => data.channel?.id || '',
// Fechas '{date}': () => new Date().toLocaleDateString('es-ES'), '{time}': () => new Date().toLocaleTimeString('es-ES'), '{timestamp}': () => `<t:${Math.floor(Date.now() / 1000)}:f>`,
// Números '{random}': () => Math.floor(Math.random() * 100).toString(), '{level}': () => data.level?.toString() || '0', '{xp}': () => data.xp?.toString() || '0',
// Ticket específicos '{ticket.id}': () => data.ticketId?.toString() || '', '{ticket.category}': () => data.category || '', '{staff}': () => data.staff?.toString() || '' };
let result = text; for (const [placeholder, replacer] of Object.entries(placeholders)) { const regex = new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'gi'); result = result.replace(regex, replacer()); }
return result; }
static getAvailablePlaceholders() { return [ '{user}', '{user.tag}', '{user.username}', '{user.id}', '{server}', '{server.name}', '{memberCount}', '{channel}', '{channel.name}', '{date}', '{time}', '{timestamp}', '{level}', '{xp}', '{random}' ]; }}
// Función helper para uso comúnfunction replacePlaceholders(text, member, additionalData = {}) { return PlaceholderReplacer.replace(text, { user: member.user || member, guild: member.guild, channel: member.guild?.channels?.cache?.first(), ...additionalData });}⏱️ Utilidades de Tiempo
Formateo de Tiempo
class TimeUtils { static formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h`; if (hours > 0) return `${hours}h ${minutes % 60}m`; if (minutes > 0) return `${minutes}m ${seconds % 60}s`; return `${seconds}s`; }
static parseDuration(str) { const regex = /(\d+)([smhd])/g; let total = 0; let match;
while ((match = regex.exec(str)) !== null) { const value = parseInt(match[1]); const unit = match[2];
switch (unit) { case 's': total += value * 1000; break; case 'm': total += value * 60 * 1000; break; case 'h': total += value * 60 * 60 * 1000; break; case 'd': total += value * 24 * 60 * 60 * 1000; break; } }
return total; }
static getRelativeTime(date) { const now = new Date(); const diff = now - date;
const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const months = Math.floor(days / 30); const years = Math.floor(days / 365);
if (years > 0) return `hace ${years} año${years !== 1 ? 's' : ''}`; if (months > 0) return `hace ${months} mes${months !== 1 ? 'es' : ''}`; if (days > 0) return `hace ${days} día${days !== 1 ? 's' : ''}`; if (hours > 0) return `hace ${hours} hora${hours !== 1 ? 's' : ''}`; if (minutes > 0) return `hace ${minutes} minuto${minutes !== 1 ? 's' : ''}`; return 'hace unos segundos'; }
static addTime(date, duration) { return new Date(date.getTime() + this.parseDuration(duration)); }}📝 Utilidades de Texto
Formateo de Texto
class TextUtils { static truncate(text, length = 100, suffix = '...') { if (text.length <= length) return text; return text.slice(0, length - suffix.length) + suffix; }
static capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); }
static titleCase(str) { return str.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() ); }
static escapeMarkdown(text) { return text.replace(/([*_`~\\])/g, '\\$1'); }
static unescapeMarkdown(text) { return text.replace(/\\([*_`~\\])/g, '$1'); }
static stripMentions(text) { return text.replace(/<@[!&]?\d+>/g, '').trim(); }
static extractMentions(text) { const userMentions = text.match(/<@!?(\d+)>/g) || []; const roleMentions = text.match(/<@&(\d+)>/g) || []; const channelMentions = text.match(/<#(\d+)>/g) || [];
return { users: userMentions.map(m => m.match(/\d+/)[0]), roles: roleMentions.map(m => m.match(/\d+/)[0]), channels: channelMentions.map(m => m.match(/\d+/)[0]) }; }
static formatNumber(num) { return num.toLocaleString('es-ES'); }
static generateId(length = 8) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }}🔍 Utilidades de Búsqueda
Fuzzy Search
class SearchUtils { static fuzzySearch(query, items, key = null) { if (!query) return items;
const searchTerm = query.toLowerCase();
return items .map(item => { const text = key ? item[key] : item; const textLower = text.toLowerCase();
// Coincidencia exacta if (textLower === searchTerm) return { item, score: 100 };
// Comienza con if (textLower.startsWith(searchTerm)) return { item, score: 90 };
// Contiene if (textLower.includes(searchTerm)) return { item, score: 70 };
// Distancia de Levenshtein const distance = this.levenshteinDistance(searchTerm, textLower); const similarity = 1 - distance / Math.max(searchTerm.length, textLower.length);
if (similarity > 0.6) return { item, score: similarity * 50 };
return null; }) .filter(result => result !== null) .sort((a, b) => b.score - a.score) .map(result => result.item); }
static levenshteinDistance(a, b) { const matrix = [];
for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; }
for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; }
for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } }
return matrix[b.length][a.length]; }}📊 Utilidades de Formato
Progress Bars
class ProgressUtils { static createProgressBar(current, max, length = 10, filledChar = '█', emptyChar = '░') { const percentage = Math.max(0, Math.min(1, current / max)); const filled = Math.round(length * percentage); const empty = length - filled;
return filledChar.repeat(filled) + emptyChar.repeat(empty); }
static createPercentageBar(percentage, length = 10) { return this.createProgressBar(percentage, 100, length); }
static formatBytes(bytes) { const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`; }}🎨 Utilidades de Color
Color Management
class ColorUtils { static hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }
static rgbToHex(r, g, b) { return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; }
static randomColor() { return `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`; }
static darken(hex, percent) { const rgb = this.hexToRgb(hex); if (!rgb) return hex;
const factor = (100 - percent) / 100; return this.rgbToHex( Math.round(rgb.r * factor), Math.round(rgb.g * factor), Math.round(rgb.b * factor) ); }
static lighten(hex, percent) { const rgb = this.hexToRgb(hex); if (!rgb) return hex;
const factor = percent / 100; return this.rgbToHex( Math.round(rgb.r + (255 - rgb.r) * factor), Math.round(rgb.g + (255 - rgb.g) * factor), Math.round(rgb.b + (255 - rgb.b) * factor) ); }}🔄 Utilidades de Caché
Cache Manager
class CacheUtils { constructor(defaultTTL = 300000) { // 5 minutos this.cache = new Map(); this.ttl = defaultTTL; }
set(key, value, customTTL = null) { const expires = Date.now() + (customTTL || 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; }
has(key) { return this.get(key) !== null; }
delete(key) { return this.cache.delete(key); }
clear() { this.cache.clear(); }
size() { return this.cache.size; }
cleanup() { const now = Date.now(); for (const [key, item] of this.cache) { if (now > item.expires) { this.cache.delete(key); } } }}
// Cache global para el botconst botCache = new CacheUtils();
// Limpiar caché cada 5 minutossetInterval(() => botCache.cleanup(), 300000);
module.exports = { CacheUtils, botCache };🛡️ Utilidades de Seguridad
Security Utils
class SecurityUtils { static sanitizeInput(input) { if (typeof input !== 'string') return '';
return input .replace(/[<>]/g, '') // Evitar XSS básico .replace(/javascript:/gi, '') // Evitar JavaScript injection .replace(/on\w+=/gi, '') // Evitar event handlers .trim(); }
static isValidDiscordId(id) { return /^\d{17,19}$/.test(id); }
static hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(); }
static generateToken(length = 32) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }
static rateLimit(identifier, maxRequests = 5, windowMs = 60000) { if (!this.rateLimits) this.rateLimits = new Map();
const now = Date.now(); const windowStart = now - windowMs;
let requests = this.rateLimits.get(identifier) || []; requests = requests.filter(time => time > windowStart);
if (requests.length >= maxRequests) { return false; // Rate limited }
requests.push(now); this.rateLimits.set(identifier, requests); return true; // Not rate limited }}Estas utilidades están diseñadas para ser reutilizables en todo el bot y facilitar el desarrollo de nuevas funcionalidades manteniendo la consistencia y calidad del código.