Saltearse al contenido

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

src/utils/embeds.js
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 tickets
function 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 niveles
function 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 bienvenida
function 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

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

src/utils/placeholders.js
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ún
function 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

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

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

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

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

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

src/utils/cache.js
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 bot
const botCache = new CacheUtils();
// Limpiar caché cada 5 minutos
setInterval(() => botCache.cleanup(), 300000);
module.exports = { CacheUtils, botCache };

🛡️ Utilidades de Seguridad

Security Utils

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