Spaces:
Paused
Paused
| /** | |
| * ============================================ | |
| * βοΈ commands.js β Comandos de AdministraciΓ³n | |
| * ============================================ | |
| * Solo comandos de admin. Los usuarios no tienen comandos. | |
| * Todos ephemeral (solo lo ve quien lo ejecuta). | |
| */ | |
| import { REST, Routes, EmbedBuilder } from 'discord.js'; | |
| import { readConfig, formatDate } from './utils.js'; | |
| import * as db from './db.js'; | |
| import * as memory from './memory.js'; | |
| import { getDailyStats, getProviderStatus } from './ai.js'; | |
| import * as minecraft from './minecraft.js'; | |
| import * as extractor from './extractor.js'; | |
| import { getCommunityPulse } from './memory.js'; | |
| import { getServerStats, getTopUsers } from './db.js'; | |
| const config = readConfig(); | |
| // ββ DefiniciΓ³n de comandos ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const SLASH_COMMANDS = [ | |
| { | |
| name : 'zelin_estado', | |
| description : 'Estado completo de Zelin (stats, providers, DB)', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| }, | |
| { | |
| name : 'zelin_usuario', | |
| description : 'Ver perfil completo de un usuario', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| options: [{ | |
| type : 6, // USER | |
| name : 'usuario', | |
| description: 'Usuario a consultar', | |
| required : true, | |
| }], | |
| }, | |
| { | |
| name : 'zelin_historial', | |
| description : 'Ver historial de moderaciΓ³n de un usuario', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| options: [{ | |
| type : 6, | |
| name : 'usuario', | |
| description: 'Usuario a consultar', | |
| required : true, | |
| }], | |
| }, | |
| { | |
| name : 'zelin_buscar', | |
| description : 'Buscar mensajes en el historial', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| options: [{ | |
| type : 3, // STRING | |
| name : 'texto', | |
| description: 'Texto a buscar', | |
| required : true, | |
| }], | |
| }, | |
| { | |
| name : 'zelin_pulso', | |
| description : 'Ver el estado emocional del servidor (ΓΊltimos 7 dΓas)', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| }, | |
| { | |
| name : 'zelin_stats', | |
| description : 'EstadΓsticas generales del servidor', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| }, | |
| { | |
| name : 'zelin_ia', | |
| description : 'Estado del sistema de IA (providers, cache, RAG, local AI)', | |
| dm_permission : false, | |
| default_member_permissions: '0', // SOLO OWNER β se verifica en el handler | |
| }, | |
| { | |
| name : 'zelin_extraer', | |
| description : 'Sincronizar toda la info del servidor con la DB', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| }, | |
| { | |
| name : 'zelin_mc', | |
| description : 'Estado actual del servidor de Minecraft', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| }, | |
| { | |
| name : 'zelin_audit', | |
| description : 'Ver las ΓΊltimas acciones de administraciΓ³n ejecutadas por Zelin', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| }, | |
| { | |
| name : 'zelin_learning', | |
| description : 'Ver estadΓsticas de aprendizaje de Zelin', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| }, | |
| { | |
| name : 'zelin_memoria', | |
| description : 'Ver o editar la memoria de Zelin', | |
| dm_permission : false, | |
| default_member_permissions: '8', | |
| options: [ | |
| { | |
| type : 3, | |
| name : 'clave', | |
| description: 'Clave de memoria (ej: config.cleanup_days)', | |
| required : false, | |
| }, | |
| { | |
| type : 3, | |
| name : 'valor', | |
| description: 'Nuevo valor (opcional, para editar)', | |
| required : false, | |
| }, | |
| ], | |
| }, | |
| ]; | |
| // ββ Registro de comandos ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export async function registerCommands() { | |
| // Try multiple Discord API hosts β HF Spaces may block discord.com REST | |
| // Note: discordapp.com only supports v9, not v10 | |
| const apiConfigs = [ | |
| { api: 'https://discordapp.com/api', version: '9' }, | |
| { api: 'https://discord.com/api', version: '10' }, | |
| ]; | |
| for (const { api, version } of apiConfigs) { | |
| try { | |
| const rest = new REST({ version, retries: 5, timeout: 30_000, api }) | |
| .setToken(config.discord.token); | |
| console.log(`[Commands] Registrando comandos slash via ${api} v${version}...`); | |
| await rest.put( | |
| Routes.applicationGuildCommands(config.discord.clientId, config.discord.guildId), | |
| { body: SLASH_COMMANDS } | |
| ); | |
| console.log(`[Commands] β Comandos registrados via ${api} v${version}`); | |
| return; // success | |
| } catch (err) { | |
| console.warn(`[Commands] FallΓ³ registro via ${api} v${version}: ${err.message?.slice(0, 100)}`); | |
| } | |
| } | |
| // Last resort: try without explicit API base (uses discord.js default) | |
| try { | |
| const rest = new REST({ version: '9', retries: 3, timeout: 30_000 }).setToken(config.discord.token); | |
| console.log('[Commands] Intentando registro con API default...'); | |
| await rest.put( | |
| Routes.applicationGuildCommands(config.discord.clientId, config.discord.guildId), | |
| { body: SLASH_COMMANDS } | |
| ); | |
| console.log('[Commands] β Comandos registrados via API default'); | |
| } catch (err) { | |
| console.error('[Commands] β No se pudieron registrar comandos en ningΓΊn host:', err.message?.slice(0, 100)); | |
| } | |
| } | |
| // ββ Manejador de comandos βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export async function handleCommand(interaction) { | |
| // Verificar que es admin (permisos Discord) O el owner exacto | |
| // Los comandos con default_member_permissions:'0' son SOLO para el owner | |
| const ownerOnlyCommands = ['zelin_ia']; | |
| if (ownerOnlyCommands.includes(interaction.commandName) && interaction.user.id !== config.admin.userId) { | |
| return interaction.reply({ content: 'β Solo el owner puede usar este comando.', ephemeral: true }); | |
| } | |
| // Solo admins | |
| if (!interaction.member?.permissions?.has('Administrator')) { | |
| return interaction.reply({ content: 'β Solo administradores.', ephemeral: true }); | |
| } | |
| await interaction.deferReply({ ephemeral: true }); | |
| const { commandName } = interaction; | |
| try { | |
| switch (commandName) { | |
| // ββ /zelin_estado ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_estado': { | |
| const stats = await getServerStats(); | |
| const mcStatus = minecraft.getServerStatus(); | |
| const pulse = await getCommunityPulse(7); | |
| const embed = new EmbedBuilder() | |
| .setTitle('π€ Zelin v5.8 β Estado') | |
| .setColor(0x5865f2) | |
| .addFields( | |
| { name: 'πΎ Base de datos', value: `Mensajes: **${stats.totalMessages}**\nUsuarios activos: **${stats.activeUsers}**\nAcciones mod: **${stats.modActions}**\nActivos hoy: **${stats.activeToday}**`, inline: true }, | |
| { name: 'βοΈ Servidor MC', value: mcStatus.online ? 'β Online' : `β Offline (${mcStatus.failCount} fallos)`, inline: true }, | |
| { name: 'π‘οΈ Clima', value: `**${pulse.mood}**\n+${pulse.totals.positive} / -${pulse.totals.negative} / β‘${pulse.totals.conflict}`, inline: true }, | |
| ) | |
| .setFooter({ text: 'Zelin v5.8 β’ TomateSMP' }) | |
| .setTimestamp(); | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| // ββ /zelin_usuario βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_usuario': { | |
| const target = interaction.options.getUser('usuario'); | |
| const user = await db.getUserWithRoles(target.id); | |
| if (!user) { | |
| return interaction.editReply('β Usuario no encontrado en la DB. ΒΏHa enviado algΓΊn mensaje?'); | |
| } | |
| const observations = await memory.getUserObservations(target.id); | |
| const embed = new EmbedBuilder() | |
| .setTitle(`π€ ${user.username}`) | |
| .setThumbnail(target.displayAvatarURL({ size: 128 })) | |
| .setColor(0x5865f2) | |
| .addFields( | |
| { name: 'Nickname', value: user.nickname || 'sin nick', inline: true }, | |
| { name: 'Mensajes', value: String(user.message_count || 0), inline: true }, | |
| { name: 'EntrΓ³', value: formatDate(user.joined_at), inline: true }, | |
| { name: 'Cuenta creada', value: formatDate(user.account_created), inline: true }, | |
| { name: 'Activo', value: user.is_active ? 'β SΓ' : 'β Se fue', inline: true }, | |
| { name: 'Bot', value: user.bot ? 'SΓ' : 'No', inline: true }, | |
| { name: 'Roles', value: user.roles?.join(', ') || 'ninguno', inline: false }, | |
| ) | |
| .setFooter({ text: `ID: ${user.user_id}` }); | |
| if (observations) { | |
| embed.addFields({ name: 'π Notas de Zelin', value: observations, inline: false }); | |
| } | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| // ββ /zelin_historial βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_historial': { | |
| const target = interaction.options.getUser('usuario'); | |
| const history = await db.getUserModHistory(target.id, 10); | |
| if (history.length === 0) { | |
| return interaction.editReply(`β ${target.username} no tiene historial de moderaciΓ³n.`); | |
| } | |
| const lines = history.map(h => | |
| `**${h.action}** | ${formatDate(h.created_at)}\nMotivo: ${h.reason || 'sin especificar'}\n${h.rule_violated ? `Regla: ${h.rule_violated}` : ''}` | |
| ).join('\nβββββ\n'); | |
| const embed = new EmbedBuilder() | |
| .setTitle(`π Historial de ${target.username}`) | |
| .setColor(0xff6b35) | |
| .setDescription(lines.substring(0, 2000)) | |
| .setFooter({ text: `${history.length} acciones β’ Zelin v5.8` }); | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| // ββ /zelin_buscar ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_buscar': { | |
| const query = interaction.options.getString('texto'); | |
| const results = await db.searchMessages(query, 10); | |
| if (results.length === 0) { | |
| return interaction.editReply(`π Sin resultados para: "${query}"`); | |
| } | |
| const lines = results.map(r => | |
| `**${r.username}** en #${r.channel_id} | ${formatDate(r.created_at)}\n${r.content.substring(0, 150)}` | |
| ).join('\nβββββ\n'); | |
| return interaction.editReply(`π **${results.length} resultados para "${query}":**\n\n${lines}`.substring(0, 2000)); | |
| } | |
| // ββ /zelin_pulso βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_pulso': { | |
| const pulse = await getCommunityPulse(7); | |
| const topUsers = await getTopUsers(7, 5); | |
| const topStr = topUsers.map((u, i) => | |
| `${i + 1}. ${u.nickname || u.username}: **${u.count}** msgs` | |
| ).join('\n'); | |
| const moodEmoji = { positivo: 'π', neutral: 'π', tenso: 'π€' }[pulse.mood] ?? 'β'; | |
| const embed = new EmbedBuilder() | |
| .setTitle('π‘οΈ Pulso de la Comunidad β Γltimos 7 dΓas') | |
| .setColor(pulse.mood === 'positivo' ? 0x00ff00 : pulse.mood === 'tenso' ? 0xff0000 : 0xffff00) | |
| .addFields( | |
| { name: 'Clima general', value: `${moodEmoji} **${pulse.mood}**`, inline: true }, | |
| { | |
| name : 'Sentimientos', | |
| value: `β Positivo: ${pulse.totals.positive}\nβ Negativo: ${pulse.totals.negative}\nβ‘ Conflicto: ${pulse.totals.conflict}`, | |
| inline: true, | |
| }, | |
| { name: 'MΓ‘s activos', value: topStr || 'sin datos', inline: false }, | |
| ) | |
| .setFooter({ text: 'Zelin v5.8 β’ TomateSMP' }); | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| // ββ /zelin_stats βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_stats': { | |
| const stats = await getServerStats(); | |
| const topUsers = await getTopUsers(7, 10); | |
| const topStr = topUsers.map((u, i) => | |
| `${i + 1}. ${u.nickname || u.username}: ${u.count} msgs` | |
| ).join('\n'); | |
| const embed = new EmbedBuilder() | |
| .setTitle('π EstadΓsticas de TomateSMP') | |
| .setColor(0x5865f2) | |
| .addFields( | |
| { name: 'π₯ Usuarios activos', value: String(stats.activeUsers), inline: true }, | |
| { name: 'π¬ Total mensajes', value: String(stats.totalMessages), inline: true }, | |
| { name: 'π‘οΈ Acciones mod', value: String(stats.modActions), inline: true }, | |
| { name: 'π’ Activos hoy', value: String(stats.activeToday), inline: true }, | |
| { name: 'π Top esta semana', value: topStr || 'sin datos', inline: false }, | |
| ) | |
| .setFooter({ text: 'Zelin v5.8 β’ TomateSMP' }) | |
| .setTimestamp(); | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| // ββ /zelin_extraer βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_extraer': { | |
| await interaction.editReply('β³ Iniciando sincronizaciΓ³n completa... (puede tardar varios minutos)'); | |
| const results = await extractor.runFullExtraction( | |
| interaction.guild, | |
| async (msg) => { | |
| await interaction.editReply(`β³ ${msg}`).catch(() => {}); | |
| } | |
| ); | |
| return interaction.editReply( | |
| `β SincronizaciΓ³n completada:\nβ’ ${results.members} miembros\nβ’ ${results.roles} roles\nβ’ ${results.channels} canales\nβ’ ${results.messages} mensajes` | |
| ); | |
| } | |
| // ββ /zelin_mc ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_audit': { | |
| const r = await db.db.execute({ | |
| sql : "SELECT value FROM zelin_memory WHERE category = 'audit_log' ORDER BY key DESC LIMIT 20", | |
| args: [], | |
| }); | |
| if (!r.rows.length) { | |
| return interaction.editReply('Sin acciones admin registradas todavΓa.'); | |
| } | |
| const entries = r.rows.map(row => { | |
| try { | |
| const d = typeof row.value === 'string' ? JSON.parse(row.value) : row.value; | |
| const ts = d.timestamp?.substring(0, 16) ?? '?'; | |
| return `**${ts}** | \`${d.action}\` | ${Object.entries(d.details ?? {}).map(([k,v]) => `${k}: ${v}`).join(', ')}`; | |
| } catch { return String(row.value).substring(0, 120); } | |
| }); | |
| const embed = new EmbedBuilder() | |
| .setTitle('π Audit Log β Acciones Admin de Zelin') | |
| .setColor(0xf59e0b) | |
| .setDescription(entries.join('\n').substring(0, 4000)) | |
| .setTimestamp(); | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| case 'zelin_learning': { | |
| const { getStyleProfile } = await import('./learning.js'); | |
| const profile = await getStyleProfile(); | |
| if (!profile) { | |
| return interaction.editReply('sin datos de aprendizaje aΓΊn (necesita al menos 10 feedbacks)'); | |
| } | |
| const embed = new EmbedBuilder() | |
| .setTitle('π§ Aprendizaje de Zelin') | |
| .setColor(0x7c3aed) | |
| .addFields( | |
| { name: 'Score promedio', value: String(profile.avgScore), inline: true }, | |
| { name: 'Engagement', value: profile.engagementRate + '%', inline: true }, | |
| { name: 'Mensajes ignorados', value: profile.ignoreRate + '%', inline: true }, | |
| { name: 'Total feedback', value: String(profile.totalFeedback), inline: true }, | |
| { name: 'Mensajes espontΓ‘neos', value: profile.spontaneousOK ? 'β funcionan' : 'β οΈ no recibidos bien', inline: true }, | |
| { name: 'Revivir chat', value: profile.revivalOK ? 'β funciona' : 'β οΈ mejorar', inline: true }, | |
| ); | |
| if (profile.goodExamples.length) | |
| embed.addFields({ name: 'π Ejemplos buenos', value: profile.goodExamples.map(e => 'β’ ' + e.substring(0,80)).join('\n').substring(0,1000), inline: false }); | |
| if (profile.badExamples.length) | |
| embed.addFields({ name: 'π Ejemplos malos', value: profile.badExamples.map(e => 'β’ ' + e.substring(0,60)).join('\n').substring(0,1000), inline: false }); | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| case 'zelin_mc': { | |
| const status = minecraft.getServerStatus(); | |
| return interaction.editReply( | |
| status.online | |
| ? `β Servidor de Minecraft online: play.tomatesmp.pw` | |
| : `β Servidor offline (${status.failCount} fallos consecutivos)` | |
| ); | |
| } | |
| // ββ /zelin_memoria βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_memoria': { | |
| const key = interaction.options.getString('clave'); | |
| const value = interaction.options.getString('valor'); | |
| if (!key) { | |
| // Listar categorΓas de memoria | |
| const cats = await db.memGetCategory('config'); | |
| const lines = Object.entries(cats) | |
| .map(([k, v]) => `\`${k}\`: ${JSON.stringify(v)}`) | |
| .join('\n'); | |
| return interaction.editReply(`π¦ **Memoria de configuraciΓ³n:**\n${lines || 'vacΓa'}`.substring(0, 2000)); | |
| } | |
| if (value !== null) { | |
| await db.memSet(key, value); | |
| return interaction.editReply(`β Memoria actualizada: \`${key}\` = \`${value}\``); | |
| } | |
| const val = await db.memGet(key); | |
| return interaction.editReply( | |
| val !== null | |
| ? `π¦ \`${key}\` = \`${JSON.stringify(val)}\`` | |
| : `β Clave \`${key}\` no encontrada` | |
| ); | |
| } | |
| // ββ /zelin_ia βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| case 'zelin_ia': { | |
| const stats = getDailyStats(); | |
| const providers = getProviderStatus(); | |
| const lines = providers.map(p => | |
| `**${p.name}**: ${p.state} (score: ${p.score})` | |
| ).join('\n'); | |
| const embed = new EmbedBuilder() | |
| .setTitle('π§ Estado del Sistema de IA') | |
| .setColor(0x5865f2) | |
| .setDescription(lines.substring(0, 4000)) | |
| .setTimestamp(); | |
| return interaction.editReply({ embeds: [embed] }); | |
| } | |
| default: | |
| return interaction.editReply('β Comando no reconocido.'); | |
| } | |
| } catch (err) { | |
| console.error('[Command] Error en', commandName, ':', err.message); | |
| return interaction.editReply(`β Error: ${err.message.substring(0, 200)}`); | |
| } | |
| } | |