Spaces:
Paused
Paused
| /** | |
| * admin_tools.js — Herramientas de Administración Discord para Zelin | |
| * v2.8: | |
| * - executeAdminActions ahora resuelve nombres → IDs con identity-resolver | |
| * - user_messages arreglado (no usa channelId null) | |
| * - dm_history usa db importado directamente | |
| * - Imports redundantes eliminados | |
| */ | |
| import { ChannelType, EmbedBuilder } from 'discord.js'; | |
| import * as db from './db.js'; | |
| import { readConfig } from './utils.js'; | |
| import { resolvePlayer } from './identity-resolver.js'; | |
| const config = readConfig(); | |
| const OWNER_ID = config.admin.userId; | |
| // ── Registro de auditoría ───────────────────────────────────────────────────── | |
| async function auditLog(guild, action, details, executedBy = 'zelin') { | |
| const entry = { action, details, executedBy, timestamp: new Date().toISOString() }; | |
| await db.memSet(`audit.${Date.now()}`, entry, 'audit_log').catch(() => {}); | |
| console.log(`[AdminTools] 📋 ${action} — ${JSON.stringify(details)} — por ${executedBy}`); | |
| try { | |
| const logChannel = guild?.channels.cache.find(c => | |
| c.isTextBased?.() && (c.name.includes('log') || c.name.includes('audit') || c.name.includes('registro')) && !c.name.includes('ticket') | |
| ); | |
| if (logChannel) { | |
| const embed = new EmbedBuilder() | |
| .setTitle(`🔧 Acción admin: ${action}`) | |
| .setDescription(Object.entries(details).map(([k,v]) => `**${k}:** ${v}`).join('\n')) | |
| .setFooter({ text: `Ejecutado por: ${executedBy}` }) | |
| .setTimestamp() | |
| .setColor(0xf59e0b); | |
| await logChannel.send({ embeds: [embed] }).catch(() => {}); | |
| } | |
| } catch {} | |
| } | |
| function canActOn(guild, targetMember) { | |
| if (!targetMember) return false; | |
| const zelinMember = guild.members.cache.get(guild.client.user.id); | |
| if (!zelinMember) return false; | |
| if (targetMember.id === OWNER_ID) return false; | |
| if (targetMember.roles.highest.position >= zelinMember.roles.highest.position) return false; | |
| return true; | |
| } | |
| // ── NUEVA FUNCIÓN: Resolver userId (nombre o ID) a Discord ID real ──────────── | |
| // Fix del bug principal: si el owner dice "ban a Pedro", p.userId llega como "Pedro" | |
| // Esta función convierte "Pedro" → "123456789012345678" | |
| async function resolveUserId(identifier, guild) { | |
| if (!identifier || !guild) return identifier; | |
| // Ya es un snowflake | |
| if (/^\d{17,19}$/.test(identifier)) return identifier; | |
| // Mención | |
| const mentionMatch = identifier.match(/^<@!?(\d{17,19})>$/); | |
| if (mentionMatch) return mentionMatch[1]; | |
| // Resolver por nombre | |
| const resolved = await resolvePlayer(identifier, guild.members.cache); | |
| if (resolved.member && resolved.confidence >= 0.7) { | |
| console.log(`[AdminTools] Resuelto "${identifier}" → ${resolved.member.id} (${(resolved.confidence*100).toFixed(0)}% confianza, método: ${resolved.method})`); | |
| return resolved.member.id; | |
| } | |
| return identifier; // devolver tal cual si no se pudo resolver | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════════════ | |
| // ROLES | |
| // ═══════════════════════════════════════════════════════════════════════════════ | |
| export async function addRoleToUser(guild, userId, roleQuery, reason = 'Solicitado por owner') { | |
| const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null); | |
| if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` }; | |
| const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase() || r.name.toLowerCase().includes(roleQuery.toLowerCase())); | |
| if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` }; | |
| if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' }; | |
| try { | |
| await member.roles.add(role, reason); | |
| await auditLog(guild, 'add_role', { usuario: member.user.username, rol: role.name, motivo: reason }); | |
| return { ok: true, message: `✅ Rol **${role.name}** añadido a ${member.displayName}` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function removeRoleFromUser(guild, userId, roleQuery, reason = 'Solicitado por owner') { | |
| const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null); | |
| if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` }; | |
| const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase() || r.name.toLowerCase().includes(roleQuery.toLowerCase())); | |
| if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` }; | |
| if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' }; | |
| try { | |
| await member.roles.remove(role, reason); | |
| await auditLog(guild, 'remove_role', { usuario: member.user.username, rol: role.name }); | |
| return { ok: true, message: `✅ Rol **${role.name}** quitado a ${member.displayName}` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function createRole(guild, { name, color = null, hoist = false, mentionable = false, reason = 'Creado por Zelin' }) { | |
| try { | |
| const role = await guild.roles.create({ name, color: color ?? null, hoist, mentionable, reason }); | |
| await auditLog(guild, 'create_role', { nombre: role.name, id: role.id }); | |
| return { ok: true, role, message: `✅ Rol **${role.name}** creado (ID: \`${role.id}\`)` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function deleteRole(guild, roleQuery, reason = 'Eliminado por owner') { | |
| const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase()); | |
| if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` }; | |
| if (role.managed) return { ok: false, error: 'No se pueden eliminar roles gestionados' }; | |
| try { | |
| const name = role.name; | |
| await role.delete(reason); | |
| await auditLog(guild, 'delete_role', { nombre: name, motivo: reason }); | |
| return { ok: true, message: `✅ Rol **${name}** eliminado` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function editRole(guild, roleQuery, { name, color, hoist, mentionable } = {}) { | |
| const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase()); | |
| if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` }; | |
| try { | |
| const options = {}; | |
| if (name !== undefined) options.name = name; | |
| if (color !== undefined) options.color = color; | |
| if (hoist !== undefined) options.hoist = hoist; | |
| if (mentionable !== undefined) options.mentionable = mentionable; | |
| await role.edit(options); | |
| await auditLog(guild, 'edit_role', { rol: role.name, cambios: JSON.stringify(options) }); | |
| return { ok: true, message: `✅ Rol **${role.name}** editado` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════════════ | |
| // CANALES | |
| // ═══════════════════════════════════════════════════════════════════════════════ | |
| export async function createChannel(guild, { name, type = 'text', category = null, topic = null, reason = 'Creado por Zelin' }) { | |
| const typeMap = { text: ChannelType.GuildText, voice: ChannelType.GuildVoice, forum: ChannelType.GuildForum }; | |
| const channelType = typeMap[type] ?? ChannelType.GuildText; | |
| let parent = null; | |
| if (category) { | |
| parent = guild.channels.cache.find(c => c.type === ChannelType.GuildCategory && (c.id === category || c.name.toLowerCase().includes(category.toLowerCase()))); | |
| } | |
| try { | |
| const channel = await guild.channels.create({ name, type: channelType, parent: parent ?? null, topic: topic ?? null, reason }); | |
| await auditLog(guild, 'create_channel', { nombre: channel.name, tipo: type, categoria: parent?.name ?? 'ninguna' }); | |
| return { ok: true, channel, message: `✅ Canal **#${channel.name}** creado` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function deleteChannel(guild, channelQuery, reason = 'Eliminado por owner') { | |
| const channel = guild.channels.cache.find(c => c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase()); | |
| if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` }; | |
| try { | |
| const name = channel.name; | |
| await channel.delete(reason); | |
| await auditLog(guild, 'delete_channel', { nombre: name, motivo: reason }); | |
| return { ok: true, message: `✅ Canal **#${name}** eliminado` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function editChannel(guild, channelQuery, { name, topic, slowmode, nsfw } = {}) { | |
| const channel = guild.channels.cache.find(c => c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase()); | |
| if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` }; | |
| try { | |
| const options = {}; | |
| if (name !== undefined) options.name = name; | |
| if (topic !== undefined) options.topic = topic; | |
| if (slowmode !== undefined) options.rateLimitPerUser = slowmode; | |
| if (nsfw !== undefined) options.nsfw = nsfw; | |
| await channel.edit(options); | |
| await auditLog(guild, 'edit_channel', { canal: channel.name, cambios: JSON.stringify(options) }); | |
| return { ok: true, message: `✅ Canal **#${channel.name}** editado` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function createCategory(guild, name, reason = 'Creado por Zelin') { | |
| try { | |
| const category = await guild.channels.create({ name, type: ChannelType.GuildCategory, reason }); | |
| await auditLog(guild, 'create_category', { nombre: category.name }); | |
| return { ok: true, category, message: `✅ Categoría **${category.name}** creada` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function purgeChannel(guild, channelQuery, amount = 10, reason = 'Limpieza por owner') { | |
| const channel = guild.channels.cache.find(c => c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase()); | |
| if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` }; | |
| if (!channel.isTextBased?.()) return { ok: false, error: 'No es un canal de texto' }; | |
| const n = Math.min(Math.max(1, amount), 100); | |
| try { | |
| const deleted = await channel.bulkDelete(n, true); | |
| await auditLog(guild, 'purge_channel', { canal: channel.name, mensajes: deleted.size, motivo: reason }); | |
| return { ok: true, message: `✅ **${deleted.size}** mensajes eliminados en #${channel.name}` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| // ═══════════════════════════════════════════════════════════════════════════════ | |
| // MIEMBROS | |
| // ═══════════════════════════════════════════════════════════════════════════════ | |
| export async function setNickname(guild, userId, nickname, reason = 'Cambiado por Zelin') { | |
| const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null); | |
| if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` }; | |
| if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' }; | |
| try { | |
| const old = member.nickname ?? member.user.username; | |
| await member.setNickname(nickname, reason); | |
| await auditLog(guild, 'set_nickname', { usuario: member.user.username, antes: old, después: nickname }); | |
| return { ok: true, message: `✅ Nickname de **${member.user.username}** cambiado a **${nickname ?? '(original)'}**` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function kickMember(guild, userId, reason = 'Expulsado por owner') { | |
| const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null); | |
| if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` }; | |
| if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' }; | |
| try { | |
| const name = member.user.username; | |
| await member.kick(reason); | |
| await auditLog(guild, 'kick', { usuario: name, motivo: reason }); | |
| return { ok: true, message: `✅ **${name}** expulsado del servidor` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function banMember(guild, userId, { reason = 'Baneado por owner', deleteMessageDays = 0 } = {}) { | |
| const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null); | |
| if (member && !canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' }; | |
| try { | |
| const target = member?.user?.username ?? userId; | |
| await guild.bans.create(userId, { reason, deleteMessageSeconds: deleteMessageDays * 86400 }); | |
| await auditLog(guild, 'ban', { usuario: target, motivo: reason, deleteMsgDays: deleteMessageDays }); | |
| return { ok: true, message: `✅ **${target}** baneado del servidor` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function unbanMember(guild, userId, reason = 'Desban por owner') { | |
| try { | |
| await guild.bans.remove(userId, reason); | |
| await auditLog(guild, 'unban', { userId, motivo: reason }); | |
| return { ok: true, message: `✅ Usuario \`${userId}\` desbaneado` }; | |
| } catch (e) { return { ok: false, error: `No se pudo desbanear: ${e.message}` }; } | |
| } | |
| export async function timeoutMember(guild, userId, durationMs, reason = 'Timeout por Zelin') { | |
| const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null); | |
| if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` }; | |
| if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' }; | |
| const clampedMs = Math.min(durationMs, 28 * 24 * 60 * 60 * 1000); | |
| try { | |
| await member.timeout(clampedMs, reason); | |
| const mins = Math.round(clampedMs / 60000); | |
| await auditLog(guild, 'timeout', { usuario: member.user.username, duracion: `${mins}min`, motivo: reason }); | |
| return { ok: true, message: `✅ **${member.user.username}** en timeout por ${mins} minutos` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function removeTimeout(guild, userId) { | |
| const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null); | |
| if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` }; | |
| try { | |
| await member.timeout(null); | |
| await auditLog(guild, 'remove_timeout', { usuario: member.user.username }); | |
| return { ok: true, message: `✅ Timeout de **${member.user.username}** eliminado` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function setServerName(guild, name, reason = 'Cambiado por owner') { | |
| try { | |
| await guild.setName(name, reason); | |
| await auditLog(guild, 'set_server_name', { nombre: name }); | |
| return { ok: true, message: `✅ Nombre del servidor cambiado a **${name}**` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function listBans(guild) { | |
| try { | |
| const bans = await guild.bans.fetch(); | |
| if (!bans.size) return { ok: true, message: 'No hay usuarios baneados actualmente', list: [] }; | |
| const list = [...bans.values()].slice(0, 20).map(b => `• **${b.user.username}** — ${b.reason ?? 'sin motivo'}`); | |
| return { ok: true, message: `**${bans.size} bans activos:**\n${list.join('\n')}`, list }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function sendToChannel(guild, channelQuery, content) { | |
| const channel = guild.channels.cache.find(c => | |
| c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase() || c.name.toLowerCase().includes(channelQuery.toLowerCase()) | |
| ); | |
| if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` }; | |
| if (!channel.isTextBased?.()) return { ok: false, error: 'No es un canal de texto' }; | |
| try { | |
| await channel.send(content.substring(0, 1900)); | |
| return { ok: true, message: `✅ Mensaje enviado en #${channel.name}` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function sendDMToUser(client, userId, content) { | |
| try { | |
| const user = await client.users.fetch(userId); | |
| const dm = await user.createDM(); | |
| await dm.send(content.substring(0, 1900)); | |
| return { ok: true, msg: `✅ DM enviado a ${user.username}` }; | |
| } catch (e) { return { ok: false, error: e.message }; } | |
| } | |
| export async function transformToZelinStyle(rawText, callAI) { | |
| try { | |
| const result = await callAI([ | |
| { role: 'system', content: `Eres Zelin, una IA de Discord con personalidad única: habla de forma natural, casual y directa en español, usa jerga juvenil latinoamericana ocasionalmente, evita formalismos. Tu tarea: reescribe el mensaje que te dan EXACTAMENTE con tu estilo — misma intención, mismas ideas, pero sonando 100% a ti. No añadas ni quites información. No expliques nada. Solo devuelve el mensaje reescrito, sin comillas ni explicaciones.` }, | |
| { role: 'user', content: rawText }, | |
| ], 'spanish', 300); | |
| return result?.trim() || rawText; | |
| } catch { return rawText; } | |
| } | |
| // ── Definición de herramientas para el prompt ───────────────────────────────── | |
| export const ADMIN_TOOL_DEFINITIONS = `## HERRAMIENTAS DE ADMINISTRACIÓN DISCORD | |
| Solo disponibles cuando el owner las pide o hay una necesidad legítima de moderación. | |
| <admin_tools>[{"action":"...","params":{}}]</admin_tools> | |
| ACCIONES DISPONIBLES: | |
| - add_role → params: { userId, role } | |
| - remove_role → params: { userId, role } | |
| - create_role → params: { name, color?, hoist?, mentionable? } | |
| - delete_role → params: { role } | |
| - edit_role → params: { role, name?, color?, hoist?, mentionable? } | |
| - create_channel → params: { name, type?, category?, topic? } | |
| - delete_channel → params: { channel } | |
| - edit_channel → params: { channel, name?, topic?, slowmode? } | |
| - create_category → params: { name } | |
| - purge_channel → params: { channel, amount } | |
| - set_nickname → params: { userId, nickname } | |
| - kick → params: { userId, reason? } | |
| - ban → params: { userId, reason?, deleteMessageDays? } | |
| - unban → params: { userId } | |
| - timeout → params: { userId, durationMs, reason? } | |
| - remove_timeout → params: { userId } | |
| - set_server_name → params: { name } | |
| - list_bans → params: {} | |
| - send_to_channel → params: { channel, content } | |
| - ignore_user → params: { userId } | |
| - unignore_user → params: { userId } | |
| - dm_history → params: { userId, limit? } | |
| - get_deleted → params: { userId?, channel?, limit? } | |
| - user_messages → params: { userId, limit? } | |
| - send_dm → params: { userId, content } | |
| - send_dm_raw → params: { userId, content } | |
| - create_skill → params: { request } | |
| - skills_list → params: {} | |
| - user_info → params: { userId } | |
| IMPORTANTE: userId puede ser un nombre de usuario, nickname, mención o ID numérico. | |
| Zelin resolverá automáticamente el nombre al ID correcto. | |
| REGLAS CRÍTICAS: | |
| - Acciones destructivas (ban/delete/kick/purge): confirmar brevemente si no hay orden explícita | |
| - Si el owner habla contigo CASUALMENTE (saludo, pregunta, chat), NO uses admin_tools — responde normal | |
| - Si el mensaje tiene @BotZelin es porque el owner te está hablando A TI, NO es una petición de DM | |
| - NUNCA generes el texto "No hay usuario mencionado en tu mensaje" — no existe esa respuesta | |
| - Si el owner menciona @roles o @usuarios en su mensaje para notificarles, NO interpretes eso como acción admin | |
| - Solo usa admin_tools cuando el owner PIDE EXPLÍCITAMENTE una acción (banear, dar rol, crear canal, etc.) | |
| `; | |
| export function parseAdminTools(text) { | |
| const match = text.match(/<admin_tools>([\s\S]*?)<\/admin_tools>/); | |
| if (!match) return null; | |
| try { | |
| const raw = match[1].trim(); | |
| const clean = raw.slice(raw.indexOf('['), raw.lastIndexOf(']') + 1); | |
| return JSON.parse(clean); | |
| } catch { return null; } | |
| } | |
| export function stripAdminToolBlock(text) { | |
| let clean = text.replace(/<admin_tools>[\s\S]*?<\/admin_tools>\s*/g, ''); | |
| clean = clean.replace(/<admin_tools>[\s\S]*/g, ''); | |
| clean = clean.replace(/<\/?admin_tools>/g, ''); | |
| return clean.trim(); | |
| } | |
| // ── Ejecutar acciones admin ─────────────────────────────────────────────────── | |
| // FIX IDENTIDAD: verifica Discord ID real del solicitante, nunca confía en el nombre | |
| // El parámetro requestingUserId es el message.author.id real — no el nombre | |
| export async function executeAdminActions(actions, guild, requestedBy = 'owner', { client = null, callAI = null, requestingUserId = null } = {}) { | |
| const results = []; | |
| // VERIFICACIÓN CRÍTICA: si se pasa un userId, debe coincidir con el owner | |
| // Esto previene que la IA ejecute acciones aunque alguien diga "soy tomatito" | |
| if (requestingUserId && requestingUserId !== OWNER_ID) { | |
| console.error(`[AdminTools] 🚨 INTENTO DE EJECUCIÓN NO AUTORIZADO — User ID: ${requestingUserId} (esperado: ${OWNER_ID})`); | |
| return [{ action: 'blocked', ok: false, error: 'Usuario no autorizado por ID de Discord.' }]; | |
| } | |
| for (const action of actions.slice(0, 5)) { | |
| const p = action.params ?? {}; | |
| // ── Resolver userId si es un nombre o mención ────────────────────────── | |
| // Aplica a todas las acciones que tienen userId | |
| if (p.userId && guild) { | |
| p.userId = await resolveUserId(p.userId, guild); | |
| } | |
| let result; | |
| // Acciones destructivas — requieren que el userId esté verificado | |
| const DESTRUCTIVE = ['purge_channel', 'ban', 'kick', 'delete_channel', 'delete_role']; | |
| if (DESTRUCTIVE.includes(action.action) && requestingUserId !== OWNER_ID) { | |
| results.push({ action: action.action, ok: false, error: 'Acción destructiva bloqueada: ID no verificado.' }); | |
| continue; | |
| } | |
| switch (action.action) { | |
| case 'add_role': result = await addRoleToUser(guild, p.userId, p.role, `Solicitado por ${requestedBy}`); break; | |
| case 'remove_role': result = await removeRoleFromUser(guild, p.userId, p.role, `Solicitado por ${requestedBy}`); break; | |
| case 'create_role': result = await createRole(guild, { ...p, reason: `Creado por ${requestedBy}` }); break; | |
| case 'delete_role': result = await deleteRole(guild, p.role, `Eliminado por ${requestedBy}`); break; | |
| case 'edit_role': result = await editRole(guild, p.role, p); break; | |
| case 'create_channel': result = await createChannel(guild, { ...p, reason: `Creado por ${requestedBy}` }); break; | |
| case 'delete_channel': result = await deleteChannel(guild, p.channel, `Eliminado por ${requestedBy}`); break; | |
| case 'edit_channel': result = await editChannel(guild, p.channel, p); break; | |
| case 'create_category':result = await createCategory(guild, p.name, `Creado por ${requestedBy}`); break; | |
| case 'purge_channel': result = await purgeChannel(guild, p.channel, p.amount ?? 10, `Limpieza por ${requestedBy}`); break; | |
| case 'set_nickname': result = await setNickname(guild, p.userId, p.nickname, `Por ${requestedBy}`); break; | |
| case 'kick': result = await kickMember(guild, p.userId, p.reason); break; | |
| case 'ban': result = await banMember(guild, p.userId, p); break; | |
| case 'unban': result = await unbanMember(guild, p.userId, p.reason); break; | |
| case 'timeout': result = await timeoutMember(guild, p.userId, p.durationMs, p.reason); break; | |
| case 'remove_timeout': result = await removeTimeout(guild, p.userId); break; | |
| case 'set_server_name':result = await setServerName(guild, p.name); break; | |
| case 'list_bans': result = await listBans(guild); break; | |
| case 'send_to_channel':result = await sendToChannel(guild, p.channel, p.content); break; | |
| case 'ignore_user': { | |
| const { ignoreUser } = await import('./state.js'); | |
| await ignoreUser(p.userId); | |
| result = { ok: true, msg: `Usuario ${p.userId} ignorado.` }; | |
| break; | |
| } | |
| case 'unignore_user': { | |
| const { unignoreUser } = await import('./state.js'); | |
| await unignoreUser(p.userId); | |
| result = { ok: true, msg: `Usuario ${p.userId} des-ignorado.` }; | |
| break; | |
| } | |
| // FIX: dm_history usa db directamente, no import dinámico | |
| case 'dm_history': { | |
| const rows = await db.getContext(`dm_${p.userId}`, p.limit ?? 20); | |
| result = { ok: true, msg: rows.map(r => `[${r.role}] ${r.content}`).join('\n') || 'Sin DMs registrados.' }; | |
| break; | |
| } | |
| // FIX: user_messages ya no usa channelId null | |
| case 'user_messages': { | |
| try { | |
| // Buscar mensajes del usuario en la tabla messages directamente por user_id | |
| const r = await db.db.execute({ | |
| sql: `SELECT m.content, m.channel_id, m.created_at | |
| FROM messages m | |
| WHERE m.user_id = ? AND m.is_deleted = 0 | |
| ORDER BY m.created_at DESC LIMIT ?`, | |
| args: [p.userId, p.limit ?? 30], | |
| }); | |
| result = { | |
| ok: true, | |
| msg: r.rows.length | |
| ? r.rows.map(r => `[${r.created_at?.slice(0,16)}] #${r.channel_id}: ${r.content}`).join('\n') | |
| : 'Sin mensajes encontrados para ese usuario.', | |
| }; | |
| } catch (e) { | |
| result = { ok: false, error: e.message }; | |
| } | |
| break; | |
| } | |
| case 'get_deleted': { | |
| const rows = await db.getDeletedMessages({ userId: p.userId, channelId: p.channel, limit: p.limit ?? 15 }); | |
| result = { ok: true, msg: rows.length ? rows.map(r => `[${r.deleted_at?.slice(0,16)}] ${r.username} en #${r.channel_name}: ${r.content}`).join('\n') : 'Sin mensajes eliminados.' }; | |
| break; | |
| } | |
| case 'create_skill': { | |
| const skillsMod = await import('./skills.js'); | |
| const res = await skillsMod.generateSkill(p.request ?? 'skill genérico', guild); | |
| result = res ? { ok: true, msg: `skill "${res.name}" creado: ${res.description}` } : { ok: false, error: 'No se pudo generar el skill' }; | |
| if (res) await skillsMod.saveSkill(res); | |
| break; | |
| } | |
| case 'skills_list': { | |
| const skillsMod = await import('./skills.js'); | |
| const list = skillsMod.listSkills(); | |
| result = { ok: true, msg: list.length ? list.map(s => `• ${s.name}: ${s.description}`).join('\n') : 'Sin skills.' }; | |
| break; | |
| } | |
| case 'user_info': { | |
| const u = await db.getUserWithRoles(p.userId); | |
| result = { ok: !!u, msg: u ? `${u.username} | msgs: ${u.message_count} | roles: ${u.roles?.join(', ') || 'ninguno'}` : 'Usuario no encontrado.' }; | |
| break; | |
| } | |
| case 'send_dm': { | |
| if (!client) { result = { ok: false, error: 'client no disponible' }; break; } | |
| const transformed = callAI ? await transformToZelinStyle(p.content, callAI) : p.content; | |
| result = await sendDMToUser(client, p.userId, transformed); | |
| if (result.ok) { | |
| await db.addContext(`dm_${p.userId}`, 'assistant', transformed, null, 'Zelin').catch(() => {}); | |
| } | |
| break; | |
| } | |
| case 'send_dm_raw': { | |
| if (!client) { result = { ok: false, error: 'client no disponible' }; break; } | |
| result = await sendDMToUser(client, p.userId, p.content); | |
| if (result.ok) { | |
| await db.addContext(`dm_${p.userId}`, 'assistant', p.content, null, 'Zelin').catch(() => {}); | |
| } | |
| break; | |
| } | |
| default: result = { ok: false, error: `Acción desconocida: ${action.action}` }; | |
| } | |
| results.push({ action: action.action, ...result }); | |
| } | |
| return results; | |
| } | |