diff --git a/prisma/dev.db b/prisma/dev.db index 8e532b6..c7a45de 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/src/commands/messages/alliaces/createEmbedv2.ts b/src/commands/messages/alliaces/createEmbedv2.ts index 25d7052..7bbe778 100644 --- a/src/commands/messages/alliaces/createEmbedv2.ts +++ b/src/commands/messages/alliaces/createEmbedv2.ts @@ -1,6 +1,6 @@ import { CommandMessage } from "../../../core/types/commands"; // @ts-ignore -import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message } from "discord.js"; +import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags } from "discord.js"; import { replaceVars } from "../../../core/lib/vars"; /** @@ -168,8 +168,9 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { }; export const command: CommandMessage = { - name: "blockcreatev2", + name: "crear-embed", type: "message", + aliases: ["embed-crear", "nuevo-embed", "blockcreatev2"], cooldown: 20, run: async (message, args, client) => { if (!message.member?.permissions.has("Administrator")) { @@ -233,7 +234,7 @@ export const command: CommandMessage = { collector.on("collect", async (i: any) => { if (i.user.id !== message.author.id) { - await i.reply({ content: "No puedes usar este menú.", ephemeral: true }); + await i.reply({ content: "No puedes usar este menú.", flags: MessageFlags.Ephemeral }); return; } @@ -841,18 +842,20 @@ export const command: CommandMessage = { } }); - // Agregar manejo de modales - //@ts-ignore - client.on('interactionCreate', async (interaction) => { + // Agregar manejo de modales mejorado con mejor gestión de errores + let modalHandlerActive = true; + + const modalHandler = async (interaction: any) => { if (!interaction.isModalSubmit()) return; if (interaction.user.id !== message.author.id) return; if (!interaction.customId.endsWith('_modal')) return; + if (!modalHandlerActive) return; // Evitar procesar si ya no está activo try { switch (interaction.customId) { case 'edit_title_modal': { blockState.title = interaction.fields.getTextInputValue('title_input'); - await interaction.reply({ content: '✅ Título actualizado.', ephemeral: true }); + await interaction.reply({ content: '✅ Título actualizado.', flags: MessageFlags.Ephemeral }); break; } case 'edit_description_modal': { @@ -863,7 +866,7 @@ export const command: CommandMessage = { } else { blockState.components.push({ type: 10, content: newDescription, thumbnail: null }); } - await interaction.reply({ content: '✅ Descripción actualizada.', ephemeral: true }); + await interaction.reply({ content: '✅ Descripción actualizada.', flags: MessageFlags.Ephemeral }); break; } case 'edit_color_modal': { @@ -875,26 +878,26 @@ export const command: CommandMessage = { if (/^[0-9A-F]{6}$/i.test(hexColor)) { blockState.color = parseInt(hexColor, 16); } else { - await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', ephemeral: true }); + await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', flags: MessageFlags.Ephemeral }); return; } } - await interaction.reply({ content: '✅ Color actualizado.', ephemeral: true }); + await interaction.reply({ content: '✅ Color actualizado.', flags: MessageFlags.Ephemeral }); break; } case 'add_content_modal': { const newContent = interaction.fields.getTextInputValue('content_input'); blockState.components.push({ type: 10, content: newContent, thumbnail: null }); - await interaction.reply({ content: '✅ Contenido añadido.', ephemeral: true }); + await interaction.reply({ content: '✅ Contenido añadido.', flags: MessageFlags.Ephemeral }); break; } case 'add_image_modal': { const imageUrl = interaction.fields.getTextInputValue('image_url_input'); if (isValidUrl(imageUrl)) { blockState.components.push({ type: 12, url: imageUrl }); - await interaction.reply({ content: '✅ Imagen añadida.', ephemeral: true }); + await interaction.reply({ content: '✅ Imagen añadida.', flags: MessageFlags.Ephemeral }); } else { - await interaction.reply({ content: '❌ URL de imagen inválida.', ephemeral: true }); + await interaction.reply({ content: '❌ URL de imagen inválida.', flags: MessageFlags.Ephemeral }); return; } break; @@ -904,9 +907,9 @@ export const command: CommandMessage = { const coverUrl = interaction.fields.getTextInputValue('cover_input'); if (isValidUrl(coverUrl)) { blockState.coverImage = coverUrl; - await interaction.reply({ content: '✅ Imagen de portada actualizada.', ephemeral: true }); + await interaction.reply({ content: '✅ Imagen de portada actualizada.', flags: MessageFlags.Ephemeral }); } else { - await interaction.reply({ content: '❌ URL de portada inválida.', ephemeral: true }); + await interaction.reply({ content: '❌ URL de portada inválida.', flags: MessageFlags.Ephemeral }); return; } break; @@ -919,7 +922,7 @@ export const command: CommandMessage = { const spacing = Math.min(3, Math.max(1, parseInt(spacingStr) || 1)); blockState.components.push({ type: 14, divider, spacing }); - await interaction.reply({ content: '✅ Separador añadido.', ephemeral: true }); + await interaction.reply({ content: '✅ Separador añadido.', flags: MessageFlags.Ephemeral }); break; } case 'edit_thumbnail_modal': { @@ -927,12 +930,18 @@ export const command: CommandMessage = { const textComp = blockState.components.find((c: any) => c.type === 10); if (textComp) { - if (thumbnailUrl.trim() === '' || !isValidUrl(thumbnailUrl)) { + if (thumbnailUrl.trim() === '') { + // Si está vacío, eliminar thumbnail textComp.thumbnail = null; - await interaction.reply({ content: '✅ Thumbnail eliminado.', ephemeral: true }); + await interaction.reply({ content: '✅ Thumbnail eliminado.', flags: MessageFlags.Ephemeral }); + } else if (!isValidUrl(thumbnailUrl)) { + // Si no es una URL válida, mostrar error + await interaction.reply({ content: '❌ URL de thumbnail inválida.', flags: MessageFlags.Ephemeral }); + return; } else { + // Si es una URL válida, añadir thumbnail textComp.thumbnail = thumbnailUrl; - await interaction.reply({ content: '✅ Thumbnail actualizado.', ephemeral: true }); + await interaction.reply({ content: '✅ Thumbnail actualizado.', flags: MessageFlags.Ephemeral }); } } break; @@ -951,13 +960,13 @@ export const command: CommandMessage = { components: Array.isArray(importedData.components) ? importedData.components : blockState.components }; - await interaction.reply({ content: '✅ JSON importado correctamente.', ephemeral: true }); + await interaction.reply({ content: '✅ JSON importado correctamente.', flags: MessageFlags.Ephemeral }); } else { - await interaction.reply({ content: '❌ Estructura JSON inválida.', ephemeral: true }); + await interaction.reply({ content: '❌ Estructura JSON inválida.', flags: MessageFlags.Ephemeral }); return; } } catch (error) { - await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', ephemeral: true }); + await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', flags: MessageFlags.Ephemeral }); return; } break; @@ -966,33 +975,68 @@ export const command: CommandMessage = { return; } - // Actualizar la vista previa después de cada cambio en el modal + // Actualizar la vista previa después de cada cambio en el modal con mejor manejo de errores setTimeout(async () => { + if (!modalHandlerActive) return; // Evitar actualizar si ya no está activo + try { + // Verificar si el mensaje aún existe antes de intentar editarlo + const messageExists = await editorMessage.fetch().catch(() => null); + if (!messageExists) { + console.log('El mensaje del editor ya no existe'); + return; + } + await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] }); - } catch (error) { - console.error('Error actualizando preview:', error); + } catch (error: any) { + // Manejar diferentes tipos de errores + if (error.code === 10008) { + console.log('Mensaje del editor eliminado'); + } else if (error.code === 10062) { + console.log('Interacción expirada'); + } else { + console.error('Error actualizando preview:', error.message || error); + } } }, 1000); - } catch (error) { + } catch (error: any) { console.error('Error en modal:', error); try { - await interaction.reply({ content: '❌ Error procesando el modal.', ephemeral: true }); - } catch {} + // Solo intentar responder si la interacción no ha expirado + if (error.code !== 10062 && !interaction.replied && !interaction.deferred) { + await interaction.reply({ content: '❌ Error procesando el modal.', flags: MessageFlags.Ephemeral }); + } + } catch (replyError) { + console.log('No se pudo responder a la interacción (probablemente expirada)'); + } } - }); + }; + + // Registrar el manejador de modales + client.on('interactionCreate', modalHandler); //@ts-ignore collector.on("end", async (_, reason) => { + // Desactivar el manejador de modales cuando el collector termine + modalHandlerActive = false; + client.off('interactionCreate', modalHandler); + if (reason === "time") { - await editorMessage.edit({ - components: [ - { type: 17, components: [{ type: 10, content: "⏰ Editor finalizado por inactividad." }] } - ] - }); + try { + const messageExists = await editorMessage.fetch().catch(() => null); + if (messageExists) { + await editorMessage.edit({ + components: [ + { type: 17, components: [{ type: 10, content: "⏰ Editor finalizado por inactividad." }] } + ] + }); + } + } catch (error) { + console.log('No se pudo actualizar el mensaje final'); + } } }); } diff --git a/src/commands/messages/alliaces/displayComponentsDemo.ts b/src/commands/messages/alliaces/displayComponentsDemo.ts new file mode 100644 index 0000000..f903109 --- /dev/null +++ b/src/commands/messages/alliaces/displayComponentsDemo.ts @@ -0,0 +1,404 @@ +import { CommandMessage } from "../../../core/types/commands"; + +export const command: CommandMessage = { + name: "displaydemo", + type: "message", + aliases: ["ddemo", "componentsdemo"], + cooldown: 10, + run: async (message, args, client) => { + if (!message.member?.permissions.has("Administrator")) { + await message.reply("❌ No tienes permisos de Administrador."); + return; + } + + // 🎯 DEMOSTRACIÓN COMPLETA DE DISPLAYCOMPONENTS CON ACCESORIOS + + // Panel principal con accessory de thumbnail + const mainPanel = { + type: 17, // Container + accent_color: 0x5865f2, + components: [ + { + type: 10, // TextDisplay + content: "🎨 **Demostración de DisplayComponents Avanzados**" + }, + { + type: 14, // Separator + divider: true, + spacing: 2 + }, + // Sección con accessory de botón + { + type: 9, // Section + components: [ + { + type: 10, + content: "🔘 **Sección con Botón Accesorio**\n\nEste texto aparece junto a un botón como accesorio. Los accesorios permiten añadir elementos interactivos sin ocupar una fila completa." + } + ], + accessory: { + type: 2, // Button + style: 1, // Primary + label: "Acción Rápida", + custom_id: "quick_action", + emoji: { name: "⚡" } + } + }, + { + type: 14, + divider: true, + spacing: 1 + }, + // Sección con accessory de thumbnail + { + type: 9, // Section + components: [ + { + type: 10, + content: "🖼️ **Sección con Thumbnail**\n\nAquí se muestra texto con una imagen en miniatura como accesorio. Perfecto para mostrar íconos de servidores, avatares o logotipos." + } + ], + accessory: { + type: 11, // Thumbnail + media: { + url: message.guild?.iconURL({ forceStatic: false }) || "https://cdn.discordapp.com/embed/avatars/0.png" + } + } + }, + { + type: 14, + divider: true, + spacing: 1 + }, + // Sección con accessory de link button + { + type: 9, // Section + components: [ + { + type: 10, + content: "🔗 **Sección con Botón de Enlace**\n\nEste tipo de accesorio permite enlaces externos directos sin necesidad de interacciones complejas." + } + ], + accessory: { + type: 2, // Button + style: 5, // Link + label: "Ir a Discord", + url: "https://discord.com", + emoji: { name: "🚀" } + } + } + ] + }; + + // Fila de botones normales para interacción + const actionRow = { + type: 1, // ActionRow + components: [ + { + type: 2, + style: 3, // Success + label: "✨ Más Ejemplos", + custom_id: "show_more_examples" + }, + { + type: 2, + style: 2, // Secondary + label: "🔄 Cambiar Estilos", + custom_id: "change_styles" + }, + { + type: 2, + style: 4, // Danger + label: "❌ Cerrar", + custom_id: "close_demo" + } + ] + }; + + const demoMessage = await message.reply({ + flags: 4096, // SuppressEmbeds + components: [mainPanel, actionRow] + }); + + const collector = demoMessage.createMessageComponentCollector({ + time: 300000, // 5 minutos + filter: (i: any) => i.user.id === message.author.id + }); + + collector.on("collect", async (interaction: any) => { + switch (interaction.customId) { + case "quick_action": + await interaction.reply({ + content: "⚡ **Acción Rápida Ejecutada!**\n\nEste botón estaba como accesorio en una sección.", + flags: 64 // Ephemeral + }); + break; + + case "show_more_examples": + // Panel con múltiples ejemplos de accesorios + const examplesPanel = { + type: 17, + accent_color: 0xff9500, + components: [ + { + type: 10, + content: "🎯 **Más Ejemplos de Accesorios**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + // Ejemplo con avatar del usuario + { + type: 9, + components: [ + { + type: 10, + content: `👤 **Perfil de ${message.author.username}**\n\nEjemplo usando tu avatar como thumbnail accesorio.` + } + ], + accessory: { + type: 11, + media: { + url: message.author.displayAvatarURL({ forceStatic: false }) + } + } + }, + { + type: 14, + divider: false, + spacing: 1 + }, + // Ejemplo con botón de estilo diferente + { + type: 9, + components: [ + { + type: 10, + content: "🎨 **Botones con Diferentes Estilos**\n\nLos accesorios pueden tener distintos estilos y emojis personalizados." + } + ], + accessory: { + type: 2, + style: 4, // Danger + label: "Peligro", + custom_id: "danger_button", + emoji: { name: "⚠️" } + } + }, + { + type: 14, + divider: false, + spacing: 1 + }, + // Imagen como accessory + { + type: 9, + components: [ + { + type: 10, + content: "🖼️ **Imágenes Personalizadas**\n\nTambién puedes usar imágenes personalizadas, íconos de servidores invitados, etc." + } + ], + accessory: { + type: 11, + media: { + url: "https://cdn.discordapp.com/attachments/123/456/discord-logo.png" + } + } + } + ] + }; + + await interaction.update({ + components: [examplesPanel, { + type: 1, + components: [ + { + type: 2, + style: 2, // Secondary + label: "↩️ Volver", + custom_id: "back_to_main" + } + ] + }] + }); + break; + + case "change_styles": + // Panel mostrando diferentes combinaciones de estilos + const stylesPanel = { + type: 17, + accent_color: 0x57f287, + components: [ + { + type: 10, + content: "🎨 **Galería de Estilos**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + // Botón Primary como accesorio + { + type: 9, + components: [ + { + type: 10, + content: "🔵 **Botón Primary (Azul)**\nEstilo: 1 - Para acciones principales" + } + ], + accessory: { + type: 2, + style: 1, // Primary + label: "Principal", + custom_id: "style_primary" + } + }, + // Botón Secondary como accesorio + { + type: 9, + components: [ + { + type: 10, + content: "⚫ **Botón Secondary (Gris)**\nEstilo: 2 - Para acciones secundarias" + } + ], + accessory: { + type: 2, + style: 2, // Secondary + label: "Secundario", + custom_id: "style_secondary" + } + }, + // Botón Success como accesorio + { + type: 9, + components: [ + { + type: 10, + content: "🟢 **Botón Success (Verde)**\nEstilo: 3 - Para confirmar acciones" + } + ], + accessory: { + type: 2, + style: 3, // Success + label: "Confirmar", + custom_id: "style_success" + } + }, + // Botón Danger como accesorio + { + type: 9, + components: [ + { + type: 10, + content: "🔴 **Botón Danger (Rojo)**\nEstilo: 4 - Para acciones destructivas" + } + ], + accessory: { + type: 2, + style: 4, // Danger + label: "Eliminar", + custom_id: "style_danger" + } + } + ] + }; + + await interaction.update({ + components: [stylesPanel, { + type: 1, + components: [ + { + type: 2, + style: 2, + label: "↩️ Volver", + custom_id: "back_to_main" + } + ] + }] + }); + break; + + case "danger_button": + case "style_primary": + case "style_secondary": + case "style_success": + case "style_danger": + await interaction.reply({ + content: `🎯 **Botón ${interaction.customId.replace('style_', '').replace('_', ' ')} activado!**\n\nEste botón era un accesorio de una sección.`, + flags: 64 // Ephemeral + }); + break; + + case "back_to_main": + await interaction.update({ + components: [mainPanel, actionRow] + }); + break; + + case "close_demo": + const closedPanel = { + type: 17, + accent_color: 0x36393f, + components: [ + { + type: 10, + content: "✅ **Demostración Finalizada**" + }, + { + type: 14, + divider: true, + spacing: 1 + }, + { + type: 10, + content: "Gracias por probar DisplayComponents con accesorios!\n\n💡 **Recuerda:** Los accesorios son ideales para:\n• Botones de acción rápida\n• Thumbnails e íconos\n• Enlaces externos\n• Elementos decorativos" + } + ] + }; + + await interaction.update({ + components: [closedPanel] + }); + collector.stop(); + break; + } + }); + + collector.on("end", async (collected: any, reason: string) => { + if (reason === "time") { + try { + const timeoutPanel = { + type: 17, + accent_color: 0x36393f, + components: [ + { + type: 10, + content: "⏰ **Demostración Expirada**" + }, + { + type: 14, + divider: true, + spacing: 1 + }, + { + type: 10, + content: "La demostración ha expirado por inactividad.\nUsa `!displaydemo` nuevamente para verla." + } + ] + }; + + await demoMessage.edit({ + components: [timeoutPanel] + }); + } catch (error) { + // Mensaje eliminado o error de edición + } + } + }); + }, +}; diff --git a/src/commands/messages/alliaces/editEmbedv2.ts b/src/commands/messages/alliaces/editEmbedv2.ts index 091ea22..fb29d85 100644 --- a/src/commands/messages/alliaces/editEmbedv2.ts +++ b/src/commands/messages/alliaces/editEmbedv2.ts @@ -1,13 +1,56 @@ import { CommandMessage } from "../../../core/types/commands"; // @ts-ignore -import { ComponentType, ButtonStyle } from "discord.js"; +import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags } from "discord.js"; import { replaceVars } from "../../../core/lib/vars"; +/** + * Botones de edición - VERSIÓN MEJORADA + */ +const btns = (disabled = false) => ([ + { + type: 1, + components: [ + { style: ButtonStyle.Secondary, type: 2, label: "📝 Título", disabled, custom_id: "edit_title" }, + { style: ButtonStyle.Secondary, type: 2, label: "📄 Descripción", disabled, custom_id: "edit_description" }, + { style: ButtonStyle.Secondary, type: 2, label: "🎨 Color", disabled, custom_id: "edit_color" }, + { style: ButtonStyle.Secondary, type: 2, label: "➕ Contenido", disabled, custom_id: "add_content" }, + { style: ButtonStyle.Secondary, type: 2, label: "➖ Separador", disabled, custom_id: "add_separator" } + ] + }, + { + type: 1, + components: [ + { style: ButtonStyle.Secondary, type: 2, label: "🖼️ Imagen", disabled, custom_id: "add_image" }, + { style: ButtonStyle.Secondary, type: 2, label: "🖼️ Portada", disabled, custom_id: "cover_image" }, + { style: ButtonStyle.Secondary, type: 2, label: "📎 Thumbnail", disabled, custom_id: "edit_thumbnail" }, + { style: ButtonStyle.Primary, type: 2, label: "🔄 Mover", disabled, custom_id: "move_block" }, + { style: ButtonStyle.Danger, type: 2, label: "🗑️ Eliminar", disabled, custom_id: "delete_block" } + ] + }, + { + type: 1, + components: [ + { style: ButtonStyle.Secondary, type: 2, label: "🎯 Variables", disabled, custom_id: "show_variables" }, + { style: ButtonStyle.Secondary, type: 2, label: "📋 Duplicar", disabled, custom_id: "duplicate_block" }, + { style: ButtonStyle.Secondary, type: 2, label: "📊 Vista Raw", disabled, custom_id: "show_raw" }, + { style: ButtonStyle.Secondary, type: 2, label: "📥 Importar", disabled, custom_id: "import_json" }, + { style: ButtonStyle.Secondary, type: 2, label: "📤 Exportar", disabled, custom_id: "export_json" } + ] + }, + { + type: 1, + components: [ + { style: ButtonStyle.Success, type: 2, label: "💾 Guardar", disabled, custom_id: "save_block" }, + { style: ButtonStyle.Danger, type: 2, label: "❌ Cancelar", disabled, custom_id: "cancel_block" } + ] + } +]); + /** * Validar si una URL es válida */ const isValidUrl = (url: string): boolean => { - if (!url || typeof url !== 'string') return false; + if (!url) return false; try { new URL(url); return url.startsWith('http://') || url.startsWith('https://'); @@ -17,37 +60,26 @@ const isValidUrl = (url: string): boolean => { }; /** - * Botones de edición + * Validar y limpiar contenido para Discord */ -const btns = (disabled = false) => ([ - { - type: 1, - components: [ - { style: ButtonStyle.Secondary, type: 2, label: "Editar Título", disabled, custom_id: "edit_title" }, - { style: ButtonStyle.Secondary, type: 2, label: "Editar Descripción", disabled, custom_id: "edit_description" }, - { style: ButtonStyle.Secondary, type: 2, label: "Editar Color", disabled, custom_id: "edit_color" }, - { style: ButtonStyle.Secondary, type: 2, label: "Añadir Contenido", disabled, custom_id: "add_content" }, - { style: ButtonStyle.Secondary, type: 2, label: "Añadir Separador", disabled, custom_id: "add_separator" } - ] - }, - { - type: 1, - components: [ - { style: ButtonStyle.Secondary, type: 2, label: "Añadir Imagen", disabled, custom_id: "add_image" }, - { style: ButtonStyle.Secondary, type: 2, label: "Imagen Portada", disabled, custom_id: "cover_image" }, - { style: ButtonStyle.Primary, type: 2, label: "Mover Bloque", disabled, custom_id: "move_block" }, - { style: ButtonStyle.Danger, type: 2, label: "Eliminar Bloque", disabled, custom_id: "delete_block" }, - { style: ButtonStyle.Secondary, type: 2, label: "Editar Thumbnail", disabled, custom_id: "edit_thumbnail" } - ] - }, - { - type: 1, - components: [ - { style: ButtonStyle.Success, type: 2, label: "Guardar", disabled, custom_id: "save_block" }, - { style: ButtonStyle.Danger, type: 2, label: "Cancelar", disabled, custom_id: "cancel_block" } - ] +const validateContent = (content: string): string => { + if (!content || typeof content !== 'string') { + return "Sin contenido"; // Contenido por defecto } -]); + + // Limpiar contenido y asegurar que tenga al menos 1 carácter + const cleaned = content.trim(); + if (cleaned.length === 0) { + return "Sin contenido"; + } + + // Truncar si excede el límite de Discord (4000 caracteres) + if (cleaned.length > 4000) { + return cleaned.substring(0, 3997) + "..."; + } + + return cleaned; +}; /** * Generar vista previa @@ -67,11 +99,12 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { } } - // Añadir título después de la portada + // Añadir título después de la portada - VALIDAR CONTENIDO + //@ts-ignore + const processedTitle = await replaceVars(blockState.title ?? "Sin título", member, guild); previewComponents.push({ type: 10, - //@ts-ignore - content: await replaceVars(blockState.title ?? "Sin título", member, guild) + content: validateContent(processedTitle) }); // Procesar componentes en orden @@ -80,6 +113,9 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { // Componente de texto con thumbnail opcional //@ts-ignore const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, member, guild) : null; + //@ts-ignore + const processedContent = await replaceVars(c.content || "Sin contenido", member, guild); + const validatedContent = validateContent(processedContent); if (processedThumbnail && isValidUrl(processedThumbnail)) { // Si tiene thumbnail válido, usar contenedor tipo 9 con accessory @@ -88,8 +124,7 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { components: [ { type: 10, - //@ts-ignore - content: await replaceVars(c.content || " ", member, guild) + content: validatedContent } ], accessory: { @@ -101,8 +136,7 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { // Sin thumbnail o thumbnail inválido, componente normal previewComponents.push({ type: 10, - //@ts-ignore - content: await replaceVars(c.content || " ", member, guild) + content: validatedContent }); } } else if (c.type === 14) { @@ -134,32 +168,34 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { }; export const command: CommandMessage = { - name: "blockedit", + name: "editar-embed", type: "message", + aliases: ["embed-editar", "modificar-embed", "blockeditv2"], cooldown: 20, run: async (message, args, client) => { if (!message.member?.permissions.has("Administrator")) { - return message.reply("❌ No tienes permisos de Administrador."); + await message.reply("❌ No tienes permisos de Administrador."); + return; } const blockName: string | null = args[0] ?? null; if (!blockName) { - return message.reply("Debes proporcionar un nombre. Uso: `!blockedit `"); + await message.reply("Debes proporcionar un nombre. Uso: `!blockeditv2 `"); + return; } - // Buscar el block existente + // Buscar el bloque existente const existingBlock = await client.prisma.blockV2Config.findFirst({ where: { guildId: message.guild!.id, name: blockName } }); if (!existingBlock) { - return message.reply(`❌ No se encontró un block con el nombre: **${blockName}**`); + await message.reply("❌ Block no encontrado. Usa `!blockcreatev2 ` para crear uno nuevo."); + return; } - // Cargar configuración existente + // Estado inicial basado en el bloque existente let blockState: any = { - ...existingBlock.config, - // Asegurar que las propiedades necesarias existan title: existingBlock.config.title || `Block: ${blockName}`, color: existingBlock.config.color || null, coverImage: existingBlock.config.coverImage || null, @@ -168,6 +204,22 @@ export const command: CommandMessage = { //@ts-ignore const editorMessage = await message.channel.send({ + content: "⚠️ **EDITANDO BLOCK EXISTENTE**\n\n" + + "Este editor usa **modales interactivos** y no podrás ver el chat mientras los usas.\n\n" + + "📝 **Recomendaciones:**\n" + + "• Ten preparados tus títulos y descripciones\n" + + "• Ten las URLs de imágenes listas para copiar\n" + + "• Los colores en formato HEX (#FF5733)\n" + + "• Las variables de usuario/servidor que necesites\n\n" + + "*Iniciando editor en 3 segundos...*" + }); + + // Esperar 3 segundos para que lean el mensaje + await new Promise(resolve => setTimeout(resolve, 3000)); + + //@ts-ignore + await editorMessage.edit({ + content: null, flags: 32768, components: [ await renderPreview(blockState, message.member, message.guild), @@ -176,24 +228,20 @@ export const command: CommandMessage = { }); const collector = editorMessage.createMessageComponentCollector({ - time: 300000 + time: 3600000 // 1 hora }); - collector.on("collect", async (i) => { + collector.on("collect", async (i: any) => { if (i.user.id !== message.author.id) { - await i.reply({ content: "No puedes usar este menú.", ephemeral: true }); + await i.reply({ content: "No puedes usar este menú.", flags: MessageFlags.Ephemeral }); return; } // --- BOTONES --- if (i.isButton()) { - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(true)] - }); - await i.deferUpdate(); - switch (i.customId) { case "save_block": { + await i.deferUpdate(); await client.prisma.blockV2Config.update({ where: { guildId_name: { guildId: message.guildId!, name: blockName } }, data: { config: blockState } @@ -204,8 +252,8 @@ export const command: CommandMessage = { type: 17, accent_color: blockState.color ?? null, components: [ - { type: 10, content: `✅ Block actualizado: ${blockName}` }, - { type: 10, content: "Configuración guardada exitosamente." } + { type: 10, content: `✅ Actualizado: ${blockName}` }, + { type: 10, content: "Cambios guardados en la base de datos." } ] } ] @@ -214,165 +262,132 @@ export const command: CommandMessage = { return; } case "cancel_block": { - await editorMessage.edit({ - components: [ - { - type: 17, - components: [ - { type: 10, content: "❌ Edición cancelada." }, - { type: 10, content: "No se guardaron los cambios." } - ] - } - ] - }); + await i.deferUpdate(); + await editorMessage.delete(); collector.stop(); return; } case "edit_title": { - const prompt = await message.channel.send("Escribe el nuevo **título**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.title = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + // Crear modal para editar título + const modal = new ModalBuilder() + .setCustomId('edit_title_modal') + .setTitle('📝 Editar Título del Block'); + + const titleInput = new TextInputBuilder() + .setCustomId('title_input') + .setLabel('Nuevo Título') + .setStyle(TextInputStyle.Short) + .setPlaceholder('Escribe el nuevo título aquí...') + .setValue(blockState.title || '') + .setMaxLength(256) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(titleInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); break; } case "edit_description": { - const prompt = await message.channel.send("Escribe la nueva **descripción**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - const descComp = blockState.components.find((c: any) => c.type === 10); - if (descComp) descComp.content = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + const modal = new ModalBuilder() + .setCustomId('edit_description_modal') + .setTitle('📄 Editar Descripción'); + + const descComp = blockState.components.find((c: any) => c.type === 10); + const currentDesc = descComp ? descComp.content : ''; + + const descInput = new TextInputBuilder() + .setCustomId('description_input') + .setLabel('Nueva Descripción') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('Escribe la nueva descripción aquí...') + .setValue(currentDesc || '') + .setMaxLength(2000) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(descInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); break; } case "edit_color": { - const prompt = await message.channel.send("Escribe el nuevo **color** en HEX (#RRGGBB)."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - const newValue = collected.content; - let parsed: number | null = null; - if (/^#?[0-9A-Fa-f]{6}$/.test(newValue)) { - parsed = parseInt(newValue.replace("#", ""), 16); - } - blockState.color = parsed; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + const modal = new ModalBuilder() + .setCustomId('edit_color_modal') + .setTitle('🎨 Editar Color del Block'); + + const currentColor = blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : ''; + + const colorInput = new TextInputBuilder() + .setCustomId('color_input') + .setLabel('Color en formato HEX') + .setStyle(TextInputStyle.Short) + .setPlaceholder('#FF5733 o FF5733') + .setValue(currentColor) + .setMaxLength(7) + .setRequired(false); + + const firstActionRow = new ActionRowBuilder().addComponents(colorInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); break; } case "add_content": { - const prompt = await message.channel.send("Escribe el nuevo **contenido**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.components.push({ type: 10, content: collected.content, thumbnail: null }); - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - break; - } - case "add_separator": { - //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, - content: "¿El separador debe ser visible?", - components: [ - { - type: 1, - components: [ - { type: 2, style: ButtonStyle.Success, label: "✅ Visible", custom_id: "separator_visible" }, - { type: 2, style: ButtonStyle.Secondary, label: "❌ Invisible", custom_id: "separator_invisible" } - ] - } - ], - fetchReply: true - }); + const modal = new ModalBuilder() + .setCustomId('add_content_modal') + .setTitle('➕ Agregar Nuevo Contenido'); + + const contentInput = new TextInputBuilder() + .setCustomId('content_input') + .setLabel('Contenido del Texto') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('Escribe el contenido aquí...') + .setMaxLength(2000) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(contentInput); + modal.addComponents(firstActionRow); //@ts-ignore - const sepCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.Button, - max: 1, - time: 60000, - filter: (b: any) => b.user.id === message.author.id - }); - - sepCollector.on("collect", async (b: any) => { - const isVisible = b.customId === "separator_visible"; - blockState.components.push({ type: 14, divider: isVisible, spacing: 1 }); - - await b.update({ - content: `✅ Separador ${isVisible ? 'visible' : 'invisible'} añadido.`, - components: [] - }); - - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - - sepCollector.stop(); - }); + await i.showModal(modal); break; } case "add_image": { - const prompt = await message.channel.send("Escribe la **URL de la imagen**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.components.push({ type: 12, url: collected.content }); - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + const modal = new ModalBuilder() + .setCustomId('add_image_modal') + .setTitle('🖼️ Agregar Nueva Imagen'); + + const imageUrlInput = new TextInputBuilder() + .setCustomId('image_url_input') + .setLabel('URL de la Imagen') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/imagen.png') + .setMaxLength(2000) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(imageUrlInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); break; } case "cover_image": { if (blockState.coverImage) { // Si ya tiene portada, preguntar si editar o eliminar //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, + const reply = await i.reply({ + flags: MessageFlags.Ephemeral, content: "Ya tienes una imagen de portada. ¿Qué quieres hacer?", components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: "✏️ Editar", custom_id: "edit_cover" }, + { type: 2, style: ButtonStyle.Primary, label: "✏️ Editar", custom_id: "edit_cover_modal" }, { type: 2, style: ButtonStyle.Danger, label: "🗑️ Eliminar", custom_id: "delete_cover" } ] } @@ -389,24 +404,26 @@ export const command: CommandMessage = { }); coverCollector.on("collect", async (b: any) => { - if (b.customId === "edit_cover") { - await b.update({ content: "Escribe la nueva **URL de la imagen de portada**:", components: [] }); + if (b.customId === "edit_cover_modal") { + // Crear modal para editar portada + const modal = new ModalBuilder() + .setCustomId('edit_cover_modal') + .setTitle('🖼️ Editar Imagen de Portada'); - const prompt = await message.channel.send("Nueva URL de portada:"); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); + const coverInput = new TextInputBuilder() + .setCustomId('cover_input') + .setLabel('URL de la Imagen de Portada') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/portada.png') + .setValue(blockState.coverImage || '') + .setMaxLength(2000) + .setRequired(true); - mc.on("collect", async (collected) => { - blockState.coverImage = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + const firstActionRow = new ActionRowBuilder().addComponents(coverInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await b.showModal(modal); } else if (b.customId === "delete_cover") { blockState.coverImage = null; await b.update({ content: "✅ Imagen de portada eliminada.", components: [] }); @@ -417,116 +434,89 @@ export const command: CommandMessage = { coverCollector.stop(); }); } else { - // No tiene portada, añadir nueva - const prompt = await message.channel.send("Escribe la **URL de la imagen de portada**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.coverImage = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + // No tiene portada, crear modal para añadir nueva + const modal = new ModalBuilder() + .setCustomId('add_cover_modal') + .setTitle('🖼️ Agregar Imagen de Portada'); + + const coverInput = new TextInputBuilder() + .setCustomId('cover_input') + .setLabel('URL de la Imagen de Portada') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/portada.png') + .setMaxLength(2000) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(coverInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); } break; } - case "edit_thumbnail": { - // Buscar componentes de texto para seleccionar cuál editar - const textComponents = blockState.components - .map((c: any, idx: number) => ({ component: c, index: idx })) - .filter(({ component }) => component.type === 10); + case "add_separator": { + const modal = new ModalBuilder() + .setCustomId('add_separator_modal') + .setTitle('➖ Agregar Separador'); - if (textComponents.length === 0) { + const visibleInput = new TextInputBuilder() + .setCustomId('separator_visible') + .setLabel('¿Separador visible? (true/false)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('true o false') + .setValue('true') + .setMaxLength(5) + .setRequired(true); + + const spacingInput = new TextInputBuilder() + .setCustomId('separator_spacing') + .setLabel('Espaciado (1-3)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('1, 2 o 3') + .setValue('1') + .setMaxLength(1) + .setRequired(false); + + const firstRow = new ActionRowBuilder().addComponents(visibleInput); + const secondRow = new ActionRowBuilder().addComponents(spacingInput); + modal.addComponents(firstRow, secondRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "edit_thumbnail": { + // Buscar el primer componente de texto para añadir/editar thumbnail + const textComp = blockState.components.find((c: any) => c.type === 10); + + if (!textComp) { + await i.deferReply({ flags: MessageFlags.Ephemeral }); //@ts-ignore - await i.followUp({ - content: "❌ No hay componentes de texto para añadir thumbnail.", - ephemeral: true + await i.editReply({ + content: "❌ Necesitas al menos un componente de texto para añadir thumbnail." }); break; } - if (textComponents.length === 1) { - // Solo un componente de texto, editarlo directamente - const prompt = await message.channel.send("Escribe la **URL del thumbnail**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - textComponents[0].component.thumbnail = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - } else { - // Múltiples componentes de texto, mostrar selector - const options = textComponents.map(({ component, index }) => ({ - label: `Texto: ${component.content?.slice(0, 30) || "..."}`, - value: index.toString(), - description: component.thumbnail ? "Ya tiene thumbnail" : "Sin thumbnail" - })); + const modal = new ModalBuilder() + .setCustomId('edit_thumbnail_modal') + .setTitle('📎 Editar Thumbnail'); - //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, - content: "Selecciona el texto al que quieres añadir/editar thumbnail:", - components: [ - { - type: 1, - components: [ - { - type: 3, - custom_id: "select_text_for_thumbnail", - placeholder: "Elige un texto", - options - } - ] - } - ], - fetchReply: true - }); + const thumbnailInput = new TextInputBuilder() + .setCustomId('thumbnail_input') + .setLabel('URL del Thumbnail') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/thumbnail.png o dejar vacío para eliminar') + .setValue(textComp.thumbnail || '') + .setMaxLength(2000) + .setRequired(false); - //@ts-ignore - const selCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (sel: any) => sel.user.id === message.author.id - }); + const firstRow = new ActionRowBuilder().addComponents(thumbnailInput); + modal.addComponents(firstRow); - selCollector.on("collect", async (sel: any) => { - const selectedIndex = parseInt(sel.values[0]); - - await sel.update({ - content: "Escribe la **URL del thumbnail**:", - components: [] - }); - - const prompt = await message.channel.send("URL del thumbnail:"); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - - mc.on("collect", async (collected) => { - blockState.components[selectedIndex].thumbnail = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - }); - } + //@ts-ignore + await i.showModal(modal); break; } case "move_block": { @@ -535,7 +525,7 @@ export const command: CommandMessage = { c.type === 10 ? `Texto: ${c.content?.slice(0, 30) || "..."}` : c.type === 14 - ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` + ? "Separador" : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}` : `Componente ${c.type}`, @@ -547,8 +537,8 @@ export const command: CommandMessage = { })); //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, + const reply = await i.reply({ + flags: MessageFlags.Ephemeral, content: "Selecciona el bloque que quieres mover:", components: [ { @@ -656,17 +646,17 @@ export const command: CommandMessage = { }); if (options.length === 0) { + await i.deferReply({ flags: MessageFlags.Ephemeral }); //@ts-ignore - await i.followUp({ - content: "❌ No hay elementos para eliminar.", - ephemeral: true + await i.editReply({ + content: "❌ No hay elementos para eliminar." }); break; } //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, + const reply = await i.reply({ + flags: MessageFlags.Ephemeral, content: "Selecciona el elemento que quieres eliminar:", components: [ { @@ -708,25 +698,333 @@ export const command: CommandMessage = { break; } - default: + case "show_variables": { + await i.deferReply({ flags: MessageFlags.Ephemeral }); + //@ts-ignore + await i.editReply({ + content: "📋 **Variables Disponibles:**\n\n" + + "**👤 Usuario:**\n" + + "`{user.name}` - Nombre del usuario\n" + + "`{user.id}` - ID del usuario\n" + + "`{user.mention}` - Mención del usuario\n" + + "`{user.avatar}` - Avatar del usuario\n\n" + + "**📊 Estadísticas:**\n" + + "`{user.pointsAll}` - Puntos totales\n" + + "`{user.pointsWeekly}` - Puntos semanales\n" + + "`{user.pointsMonthly}` - Puntos mensuales\n\n" + + "**🏠 Servidor:**\n" + + "`{guild.name}` - Nombre del servidor\n" + + "`{guild.icon}` - Ícono del servidor\n\n" + + "**🔗 Invitación:**\n" + + "`{invite.name}` - Nombre del servidor invitado\n" + + "`{invite.icon}` - Ícono del servidor invitado" + }); break; + } + case "duplicate_block": { + const options = blockState.components.map((c: any, idx: number) => ({ + label: c.type === 10 ? `Texto: ${c.content?.slice(0, 30) || "..."}` + : c.type === 14 ? "Separador" + : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}` + : `Componente ${c.type}`, + value: idx.toString(), + description: c.type === 10 && c.thumbnail ? "Con thumbnail" : undefined + })); + + if (options.length === 0) { + await i.deferReply({ flags: MessageFlags.Ephemeral }); + //@ts-ignore + await i.editReply({ content: "❌ No hay elementos para duplicar." }); + break; + } + + //@ts-ignore + const reply = await i.reply({ + flags: MessageFlags.Ephemeral, + content: "Selecciona el elemento que quieres duplicar:", + components: [{ + type: 1, + components: [{ + type: 3, + custom_id: "duplicate_select", + placeholder: "Elige un elemento", + options + }] + }], + fetchReply: true + }); + + //@ts-ignore + const selCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + max: 1, + time: 60000, + filter: (sel: any) => sel.user.id === message.author.id + }); + + selCollector.on("collect", async (sel: any) => { + const idx = parseInt(sel.values[0]); + const originalComponent = blockState.components[idx]; + const duplicatedComponent = JSON.parse(JSON.stringify(originalComponent)); + + blockState.components.splice(idx + 1, 0, duplicatedComponent); + + await sel.update({ content: "✅ Elemento duplicado.", components: [] }); + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + }); + break; + } + case "show_raw": { + const rawJson = JSON.stringify(blockState, null, 2); + const truncated = rawJson.length > 1900 ? rawJson.slice(0, 1900) + "..." : rawJson; + + //@ts-ignore + await i.reply({ + flags: MessageFlags.Ephemeral, + content: `\`\`\`json\n${truncated}\`\`\`` + }); + break; + } + case "import_json": { + const modal = new ModalBuilder() + .setCustomId('import_json_modal') + .setTitle('📥 Importar JSON'); + + const jsonInput = new TextInputBuilder() + .setCustomId('json_input') + .setLabel('Pega tu configuración JSON aquí') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('{"title": "...", "components": [...]}') + .setMaxLength(4000) + .setRequired(true); + + const firstRow = new ActionRowBuilder().addComponents(jsonInput); + modal.addComponents(firstRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "export_json": { + const exportJson = JSON.stringify(blockState, null, 2); + + // Truncar si es muy largo para evitar problemas con Discord + const truncatedJson = exportJson.length > 1800 ? exportJson.slice(0, 1800) + "\n..." : exportJson; + + //@ts-ignore + await i.reply({ + flags: MessageFlags.Ephemeral, + content: `📤 **JSON Exportado:**\n\`\`\`json\n${truncatedJson}\`\`\`\n\n💡 **Tip:** Copia el JSON de arriba manualmente y pégalo donde necesites.` + }); + break; + } } - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); + // Actualizar vista previa después de cada acción + try { + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + } catch (updateError: any) { + if (updateError.code === 10008) { + console.log('Mensaje del editor eliminado'); + } else { + console.error('Error actualizando vista previa:', updateError); + } + } } }); + // Manejo de modales mejorado con mejor gestión de errores + let modalHandlerActive = true; + + const modalHandler = async (interaction: any) => { + if (!interaction.isModalSubmit()) return; + if (interaction.user.id !== message.author.id) return; + if (!interaction.customId.endsWith('_modal')) return; + if (!modalHandlerActive) return; + + try { + switch (interaction.customId) { + case 'edit_title_modal': { + blockState.title = interaction.fields.getTextInputValue('title_input'); + await interaction.reply({ content: '✅ Título actualizado.', flags: MessageFlags.Ephemeral }); + break; + } + case 'edit_description_modal': { + const newDescription = interaction.fields.getTextInputValue('description_input'); + const descComp = blockState.components.find((c: any) => c.type === 10); + if (descComp) { + descComp.content = newDescription; + } else { + blockState.components.push({ type: 10, content: newDescription, thumbnail: null }); + } + await interaction.reply({ content: '✅ Descripción actualizada.', flags: MessageFlags.Ephemeral }); + break; + } + case 'edit_color_modal': { + const colorInput = interaction.fields.getTextInputValue('color_input'); + if (colorInput.trim() === '') { + blockState.color = null; + } else { + let hexColor = colorInput.replace('#', ''); + if (/^[0-9A-F]{6}$/i.test(hexColor)) { + blockState.color = parseInt(hexColor, 16); + } else { + await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', flags: MessageFlags.Ephemeral }); + return; + } + } + await interaction.reply({ content: '✅ Color actualizado.', flags: MessageFlags.Ephemeral }); + break; + } + case 'add_content_modal': { + const newContent = interaction.fields.getTextInputValue('content_input'); + blockState.components.push({ type: 10, content: newContent, thumbnail: null }); + await interaction.reply({ content: '✅ Contenido añadido.', flags: MessageFlags.Ephemeral }); + break; + } + case 'add_image_modal': { + const imageUrl = interaction.fields.getTextInputValue('image_url_input'); + if (isValidUrl(imageUrl)) { + blockState.components.push({ type: 12, url: imageUrl }); + await interaction.reply({ content: '✅ Imagen añadida.', flags: MessageFlags.Ephemeral }); + } else { + await interaction.reply({ content: '❌ URL de imagen inválida.', flags: MessageFlags.Ephemeral }); + return; + } + break; + } + case 'add_cover_modal': + case 'edit_cover_modal': { + const coverUrl = interaction.fields.getTextInputValue('cover_input'); + if (isValidUrl(coverUrl)) { + blockState.coverImage = coverUrl; + await interaction.reply({ content: '✅ Imagen de portada actualizada.', flags: MessageFlags.Ephemeral }); + } else { + await interaction.reply({ content: '❌ URL de portada inválida.', flags: MessageFlags.Ephemeral }); + return; + } + break; + } + case 'add_separator_modal': { + const visibleStr = interaction.fields.getTextInputValue('separator_visible').toLowerCase(); + const spacingStr = interaction.fields.getTextInputValue('separator_spacing') || '1'; + + const divider = visibleStr === 'true' || visibleStr === '1' || visibleStr === 'si' || visibleStr === 'sí'; + const spacing = Math.min(3, Math.max(1, parseInt(spacingStr) || 1)); + + blockState.components.push({ type: 14, divider, spacing }); + await interaction.reply({ content: '✅ Separador añadido.', flags: MessageFlags.Ephemeral }); + break; + } + case 'edit_thumbnail_modal': { + const thumbnailUrl = interaction.fields.getTextInputValue('thumbnail_input'); + const textComp = blockState.components.find((c: any) => c.type === 10); + + if (textComp) { + if (thumbnailUrl.trim() === '') { + // Si está vacío, eliminar thumbnail + textComp.thumbnail = null; + await interaction.reply({ content: '✅ Thumbnail eliminado.', flags: MessageFlags.Ephemeral }); + } else if (!isValidUrl(thumbnailUrl)) { + // Si no es una URL válida, mostrar error + await interaction.reply({ content: '❌ URL de thumbnail inválida.', flags: MessageFlags.Ephemeral }); + return; + } else { + // Si es una URL válida, añadir thumbnail + textComp.thumbnail = thumbnailUrl; + await interaction.reply({ content: '✅ Thumbnail actualizado.', flags: MessageFlags.Ephemeral }); + } + } + break; + } + case 'import_json_modal': { + try { + const jsonString = interaction.fields.getTextInputValue('json_input'); + const importedData = JSON.parse(jsonString); + + if (importedData && typeof importedData === 'object') { + blockState = { + title: importedData.title || blockState.title, + color: importedData.color || blockState.color, + coverImage: importedData.coverImage || blockState.coverImage, + components: Array.isArray(importedData.components) ? importedData.components : blockState.components + }; + + await interaction.reply({ content: '✅ JSON importado correctamente.', flags: MessageFlags.Ephemeral }); + } else { + await interaction.reply({ content: '❌ Estructura JSON inválida.', flags: MessageFlags.Ephemeral }); + return; + } + } catch (error) { + await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', flags: MessageFlags.Ephemeral }); + return; + } + break; + } + default: + return; + } + + // Actualizar la vista previa después de cada cambio en el modal + setTimeout(async () => { + if (!modalHandlerActive) return; + + try { + const messageExists = await editorMessage.fetch().catch(() => null); + if (!messageExists) return; + + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + } catch (error: any) { + if (error.code === 10008) { + console.log('Mensaje del editor eliminado'); + } else if (error.code === 10062) { + console.log('Interacción expirada'); + } else { + console.error('Error actualizando preview:', error.message || error); + } + } + }, 1000); + + } catch (error: any) { + console.error('Error en modal:', error); + try { + if (error.code !== 10062 && !interaction.replied && !interaction.deferred) { + await interaction.reply({ content: '❌ Error procesando el modal.', flags: MessageFlags.Ephemeral }); + } + } catch (replyError) { + console.log('No se pudo responder a la interacción (probablemente expirada)'); + } + } + }; + + // Registrar el manejador de modales + client.on('interactionCreate', modalHandler); + //@ts-ignore collector.on("end", async (_, reason) => { + modalHandlerActive = false; + client.off('interactionCreate', modalHandler); + if (reason === "time") { - await editorMessage.edit({ - components: [ - { type: 17, components: [{ type: 10, content: "⏰ Editor finalizado por inactividad." }] } - ] - }); + try { + const messageExists = await editorMessage.fetch().catch(() => null); + if (messageExists) { + await editorMessage.edit({ + components: [ + { type: 17, components: [{ type: 10, content: "⏰ Editor finalizado por inactividad." }] } + ] + }); + } + } catch (error) { + console.log('No se pudo actualizar el mensaje final'); + } } }); } -}; \ No newline at end of file +}; diff --git a/src/commands/messages/alliaces/embedDelete.ts b/src/commands/messages/alliaces/embedDelete.ts index 772edf2..413ee56 100644 --- a/src/commands/messages/alliaces/embedDelete.ts +++ b/src/commands/messages/alliaces/embedDelete.ts @@ -1,34 +1,217 @@ import { CommandMessage } from "../../../core/types/commands"; export const command: CommandMessage = { - name: "embeddelete", + name: "eliminar-embed", type: "message", - aliases: ["delembed", "removeembed"], + aliases: ["embed-eliminar", "borrar-embed", "embeddelete"], cooldown: 10, - //@ts-ignore - run: async (message, args, client) => { + run: async (message: any, args: string[], client: any) => { if (!message.member?.permissions.has("Administrator")) { - return message.reply("❌ No tienes permisos de Administrador."); + await message.reply("❌ No tienes permisos de Administrador."); + return; } - const embedName = args[0]; - if (!embedName) { - return message.reply("Debes proporcionar el nombre del embed a eliminar. Uso: `!embeddelete `"); - } + // Obtener todos los bloques del servidor + const blocks = await client.prisma.blockV2Config.findMany({ + where: { guildId: message.guildId! }, + select: { name: true, id: true } + }); - try { - await client.prisma.blockV2Config.delete({ - where: { - guildId_name: { - guildId: message.guildId!, - name: embedName, - }, - }, + if (blocks.length === 0) { + const noBlocksEmbed = { + color: 0xf04747, + title: "🗂️ Panel de Eliminación de Bloques", + description: "📭 **No hay bloques disponibles**\n\nNo se encontraron bloques para eliminar en este servidor.\n\nPuedes crear nuevos bloques usando `!blockcreate`.", + footer: { + text: "Sistema de gestión de bloques • Amayo Bot" + } + }; + + await message.reply({ + embeds: [noBlocksEmbed] }); - - return message.reply(`✅ El embed **${embedName}** fue eliminado con éxito.`); - } catch { - return message.reply("❌ No encontré un embed con ese nombre."); + return; } + + // Crear opciones para el select menu + const selectOptions = blocks.slice(0, 25).map((block: any, index: number) => ({ + label: block.name, + value: block.name, + description: `ID: ${block.id}`, + emoji: index < 10 ? { name: `${index + 1}️⃣` } : { name: "📄" } + })); + + // Crear embed principal de eliminación + const deleteEmbed = { + color: 0xff6b35, + title: "🗑️ Panel de Eliminación de Bloques", + description: `📊 **${blocks.length} bloque(s) encontrado(s)**\n\n⚠️ **ADVERTENCIA:** La eliminación es permanente e irreversible.\n\nSelecciona el bloque que deseas eliminar del menú de abajo:`, + footer: { + text: "Selecciona un bloque para eliminar • Timeout: 5 minutos" + } + }; + + const actionRow = { + type: 1, + components: [ + { + type: 3, // StringSelect + custom_id: "delete_block_select", + placeholder: "🗑️ Selecciona un bloque para eliminar...", + min_values: 1, + max_values: 1, + options: selectOptions + } + ] + }; + + const cancelRow = { + type: 1, + components: [ + { + type: 2, // Button + style: 4, // Danger + label: "❌ Cancelar", + custom_id: "cancel_delete" + } + ] + }; + + const panelMessage = await message.reply({ + embeds: [deleteEmbed], + components: [actionRow, cancelRow] + }); + + const collector = panelMessage.createMessageComponentCollector({ + time: 300000, // 5 minutos + filter: (i: any) => i.user.id === message.author.id + }); + + collector.on("collect", async (interaction: any) => { + if (interaction.customId === "cancel_delete") { + const canceledEmbed = { + color: 0x36393f, + title: "❌ Operación Cancelada", + description: "La eliminación de bloques ha sido cancelada.\nNingún bloque fue eliminado.", + footer: { text: "Operación cancelada por el usuario" } + }; + + await interaction.update({ + embeds: [canceledEmbed], + components: [] + }); + + collector.stop("cancelled"); + return; + } + + if (interaction.customId === "delete_block_select" && interaction.isStringSelectMenu()) { + const selectedBlock = interaction.values[0]; + + const confirmationEmbed = { + color: 0xf04747, + title: "⚠️ CONFIRMAR ELIMINACIÓN", + description: `🗑️ **Bloque a eliminar:** \`${selectedBlock}\`\n\n❗ **ESTA ACCIÓN ES IRREVERSIBLE**\n\nUna vez eliminado, no podrás recuperar:\n• Toda la configuración del bloque\n• Los componentes y contenido\n• Las imágenes y colores personalizados\n\n¿Estás seguro de que quieres continuar?`, + footer: { text: "⚠️ Acción irreversible - Piénsalo bien" } + }; + + const confirmationRow = { + type: 1, + components: [ + { + type: 2, + style: 4, // Danger + label: "🗑️ SÍ, ELIMINAR", + custom_id: `confirm_delete_${selectedBlock}` + }, + { + type: 2, + style: 2, // Secondary + label: "↩️ Volver Atrás", + custom_id: "back_to_selection" + } + ] + }; + + await interaction.update({ + embeds: [confirmationEmbed], + components: [confirmationRow] + }); + return; + } + + if (interaction.customId.startsWith("confirm_delete_")) { + const blockName = interaction.customId.replace("confirm_delete_", ""); + + try { + await client.prisma.blockV2Config.delete({ + where: { + guildId_name: { + guildId: message.guildId!, + name: blockName, + }, + }, + }); + + const successEmbed = { + color: 0x57f287, + title: "✅ Eliminación Exitosa", + description: `🗑️ **Bloque eliminado:** \`${blockName}\`\n\n✨ El bloque ha sido eliminado permanentemente de la base de datos.\n\n📋 Para ver los bloques restantes, usa: \`!embedlist\`\n📝 Para crear un nuevo bloque, usa: \`!blockcreate\``, + footer: { text: "Bloque eliminado exitosamente" } + }; + + await interaction.update({ + embeds: [successEmbed], + components: [] + }); + + collector.stop("success"); + + } catch (error) { + const errorEmbed = { + color: 0xf04747, + title: "❌ Error en la Eliminación", + description: `🔍 **Bloque no encontrado:** \`${blockName}\`\n\n💭 Posibles causas:\n• El bloque ya fue eliminado\n• Error de conexión con la base de datos\n• El nombre del bloque cambió\n\n🔄 Intenta refrescar la lista con \`!embedlist\``, + footer: { text: "Error de eliminación" } + }; + + await interaction.update({ + embeds: [errorEmbed], + components: [] + }); + + collector.stop("error"); + } + return; + } + + if (interaction.customId === "back_to_selection") { + await interaction.update({ + embeds: [deleteEmbed], + components: [actionRow, cancelRow] + }); + return; + } + }); + + collector.on("end", async (collected: any, reason: string) => { + if (reason === "time") { + const timeoutEmbed = { + color: 0x36393f, + title: "⏰ Tiempo Agotado", + description: "El panel de eliminación ha expirado por inactividad.\nUsa el comando nuevamente si necesitas eliminar bloques.", + footer: { text: "Panel expirado por inactividad" } + }; + + try { + await panelMessage.edit({ + embeds: [timeoutEmbed], + components: [] + }); + } catch (error) { + // Mensaje ya eliminado o error de edición + } + } + }); }, }; diff --git a/src/commands/messages/alliaces/embedList.ts b/src/commands/messages/alliaces/embedList.ts index 4e5ef7a..85e9a42 100644 --- a/src/commands/messages/alliaces/embedList.ts +++ b/src/commands/messages/alliaces/embedList.ts @@ -1,75 +1,402 @@ -import {CommandMessage} from "../../../core/types/commands"; -import { - //@ts-ignore - ChannelType, - ContainerBuilder, - //@ts-ignore - MessageFlags, - SectionBuilder, - SeparatorBuilder, - //@ts-ignore - SeparatorSpacingSize, - TextChannel, - TextDisplayBuilder -} from "discord.js"; +import { CommandMessage } from "../../../core/types/commands"; export const command: CommandMessage = { - name: "embedlist", + name: "lista-embeds", type: "message", - aliases: ["listembeds", "embeds"], + aliases: ["embeds", "ver-embeds", "embedlist"], cooldown: 10, - //@ts-ignore - run: async (message, args, client) => { + run: async (message: any, args: string[], client: any) => { if (!message.member?.permissions.has("Administrator")) { return message.reply("❌ No tienes permisos de Administrador."); } - const embeds = await client.prisma.blockV2Config.findMany({ + const blocks = await client.prisma.blockV2Config.findMany({ where: { guildId: message.guildId! }, + select: { + name: true, + id: true, + config: true + }, + orderBy: { name: 'asc' } }); - if (embeds.length === 0) { - return message.reply("📭 No hay ningún embed guardado en este servidor."); + if (blocks.length === 0) { + const emptyEmbed = { + color: 0x5865f2, + title: "📚 Centro de Gestión de Bloques", + description: "📭 **No hay bloques disponibles**\n\nEste servidor aún no tiene bloques configurados.\n\n🚀 **¿Quieres empezar?**\n• Usa `!crear-embed ` para crear tu primer bloque\n• Usa `!editar-embed ` para editar bloques existentes", + footer: { text: "Sistema de gestión de bloques • Amayo Bot" } + }; + + const createRow = { + type: 1, + components: [ + { + type: 2, + style: 3, + label: "📝 Crear Primer Bloque", + custom_id: "show_create_help" + } + ] + }; + + const helpMessage = await message.reply({ + embeds: [emptyEmbed], + components: [createRow] + }); + + const helpCollector = helpMessage.createMessageComponentCollector({ + time: 60000, + filter: (i: any) => i.user.id === message.author.id + }); + + helpCollector.on("collect", async (interaction: any) => { + if (interaction.customId === "show_create_help") { + const helpEmbed = { + color: 0x57f287, + title: "📖 Guía de Creación de Bloques", + description: "🔧 **Comandos disponibles:**\n\n• `!crear-embed ` - Crear nuevo bloque\n• `!editar-embed ` - Editar bloque existente\n• `!eliminar-embed ` - Eliminar bloque\n• `!lista-embeds` - Ver todos los bloques\n\n💡 **Tip:** Los bloques permiten crear interfaces modernas e interactivas.", + footer: { text: "Guía de comandos de creación" } + }; + + await interaction.update({ + embeds: [helpEmbed], + components: [] + }); + } + }); + + return; } - const title = new TextDisplayBuilder() - .setContent('﹒⌒    Embed List    ╰୧﹒'); + // Dividir bloques en páginas de 5 + const itemsPerPage = 5; + const totalPages = Math.ceil(blocks.length / itemsPerPage); + let currentPage = 0; - // Combina la lista de embeds en la misma sección que la miniatura - // para un mejor diseño. - //@ts-ignore - const embedListContent = embeds.map((e, i) => `**${i + 1}.** ${e.name}`).join("\n"); + const generateBlockListEmbed = (page: number) => { + const startIndex = page * itemsPerPage; + const endIndex = Math.min(startIndex + itemsPerPage, blocks.length); + const pageBlocks = blocks.slice(startIndex, endIndex); - // Obtenemos la URL del icono de forma segura - const guildIconURL = message.guild?.iconURL({ forceStatic: false }); + let blockListText = `📊 **Página ${page + 1} de ${totalPages}** (${blocks.length} total)\n\n`; - // Creamos la sección que contendrá el texto Y la miniatura - const mainSection = new SectionBuilder() - .addTextDisplayComponents(text => text.setContent(embedListContent)); // <--- Componente principal requerido + pageBlocks.forEach((block: any, index: number) => { + const globalIndex = startIndex + index + 1; + const componentsCount = Array.isArray(block.config?.components) ? block.config.components.length : 0; + const hasImage = block.config?.coverImage ? "🖼️" : ""; - // Solo añadimos la miniatura si la URL existe - if (guildIconURL) { - //@ts-ignore - mainSection.setThumbnailAccessory(thumbnail => thumbnail - .setURL(guildIconURL) - .setDescription('Icono del servidor') - ); - } + blockListText += `**${globalIndex}.** \`${block.name}\` ${hasImage}\n`; + blockListText += ` └ ${componentsCount} componente(s) • ID: ${block.id.slice(-8)}\n\n`; + }); - const separator = new SeparatorBuilder() - .setSpacing(SeparatorSpacingSize.Large) - .setDivider(false); + return { + color: 0x5865f2, + title: "📚 Centro de Gestión de Bloques", + description: blockListText, + footer: { text: `Página ${page + 1}/${totalPages} • ${blocks.length} bloques total` } + }; + }; - const container = new ContainerBuilder() - .setAccentColor(0x49225B) - .addTextDisplayComponents(title) - .addSeparatorComponents(separator) - .addSectionComponents(mainSection); // <--- Añadimos la sección ya completa + const generateActionRows = (page: number) => { + const rows = []; + // Select menu para acciones rápidas + const currentPageBlocks = blocks.slice(page * itemsPerPage, (page + 1) * itemsPerPage); + if (currentPageBlocks.length > 0) { + const selectOptions = currentPageBlocks.map((block: any) => ({ + label: block.name, + value: block.name, + description: `${Array.isArray(block.config?.components) ? block.config.components.length : 0} componente(s)`, + emoji: { name: "⚙️" } + })); - if (message.channel.type === ChannelType.GuildText) { - const channel = message.channel as TextChannel; - await channel.send({ components: [container], flags: MessageFlags.IsComponentsV2}); - } + rows.push({ + type: 1, + components: [ + { + type: 3, + custom_id: "block_actions_select", + placeholder: "⚙️ Selecciona un bloque para gestionar...", + options: selectOptions + } + ] + }); + } + + // Botones de navegación y acciones generales + const navigationRow: any = { + type: 1, + components: [] + }; + + // Navegación + if (totalPages > 1) { + navigationRow.components.push({ + type: 2, + style: 2, + label: "◀️ Anterior", + custom_id: "prev_page", + disabled: page === 0 + }); + + navigationRow.components.push({ + type: 2, + style: 2, + label: `${page + 1}/${totalPages}`, + custom_id: "page_info", + disabled: true + }); + + navigationRow.components.push({ + type: 2, + style: 2, + label: "▶️ Siguiente", + custom_id: "next_page", + disabled: page === totalPages - 1 + }); + } + + // Botón de refrescar + navigationRow.components.push({ + type: 2, + style: 1, + label: "🔄 Refrescar", + custom_id: "refresh_list" + }); + + rows.push(navigationRow); + + // Acciones principales + const actionsRow = { + type: 1, + components: [ + { + type: 2, + style: 3, + label: "📝 Crear Nuevo", + custom_id: "show_create_commands" + }, + { + type: 2, + style: 2, + label: "📋 Exportar Lista", + custom_id: "export_block_list" + }, + { + type: 2, + style: 4, + label: "🗑️ Eliminar", + custom_id: "show_delete_commands" + } + ] + }; + + rows.push(actionsRow); + + return rows; + }; + + const panelMessage = await message.reply({ + embeds: [generateBlockListEmbed(currentPage)], + components: generateActionRows(currentPage) + }); + + const collector = panelMessage.createMessageComponentCollector({ + time: 600000, + filter: (i: any) => i.user.id === message.author.id + }); + + collector.on("collect", async (interaction: any) => { + switch (interaction.customId) { + case "prev_page": + if (currentPage > 0) { + currentPage--; + await interaction.update({ + embeds: [generateBlockListEmbed(currentPage)], + components: generateActionRows(currentPage) + }); + } + break; + + case "next_page": + if (currentPage < totalPages - 1) { + currentPage++; + await interaction.update({ + embeds: [generateBlockListEmbed(currentPage)], + components: generateActionRows(currentPage) + }); + } + break; + + case "refresh_list": + // Recargar datos + const refreshedBlocks = await client.prisma.blockV2Config.findMany({ + where: { guildId: message.guildId! }, + select: { + name: true, + id: true, + config: true + }, + orderBy: { name: 'asc' } + }); + + blocks.length = 0; + blocks.push(...refreshedBlocks); + + const newTotalPages = Math.ceil(blocks.length / itemsPerPage); + if (currentPage >= newTotalPages) { + currentPage = Math.max(0, newTotalPages - 1); + } + + await interaction.update({ + embeds: [generateBlockListEmbed(currentPage)], + components: generateActionRows(currentPage) + }); + break; + + case "block_actions_select": + if (interaction.isStringSelectMenu()) { + const selectedBlock = interaction.values[0]; + + const blockActionEmbed = { + color: 0xff9500, + title: `⚙️ Gestión de Bloque: \`${selectedBlock}\``, + description: "Selecciona la acción que deseas realizar con este bloque:", + footer: { text: "Acciones disponibles para el bloque seleccionado" } + }; + + const blockActionsRow = { + type: 1, + components: [ + { + type: 2, + style: 1, + label: "✏️ Editar", + custom_id: `edit_block_${selectedBlock}` + }, + { + type: 2, + style: 2, + label: "👁️ Vista Previa", + custom_id: `preview_block_${selectedBlock}` + }, + { + type: 2, + style: 2, + label: "📋 Duplicar", + custom_id: `duplicate_block_${selectedBlock}` + }, + { + type: 2, + style: 4, + label: "🗑️ Eliminar", + custom_id: `delete_block_${selectedBlock}` + } + ] + }; + + const backRow = { + type: 1, + components: [ + { + type: 2, + style: 2, + label: "↩️ Volver a la Lista", + custom_id: "back_to_list" + } + ] + }; + + await interaction.update({ + embeds: [blockActionEmbed], + components: [blockActionsRow, backRow] + }); + } + break; + + case "back_to_list": + await interaction.update({ + embeds: [generateBlockListEmbed(currentPage)], + components: generateActionRows(currentPage) + }); + break; + + case "show_create_commands": + await interaction.reply({ + content: `🔧 **Crear nuevos bloques:**\n\n• \`!crear-embed \` - Crear bloque básico\n• \`!editar-embed \` - Editor avanzado\n\n💡 **Ejemplo:** \`!crear-embed bienvenida\`\n\n📖 **Guía completa:** Los bloques usan DisplayComponents para crear interfaces modernas e interactivas.`, + flags: 64 + }); + break; + + case "show_delete_commands": + await interaction.reply({ + content: `⚠️ **Eliminar bloques:**\n\n• \`!eliminar-embed\` - Panel interactivo de eliminación\n• \`!eliminar-embed \` - Eliminación directa\n\n❗ **Advertencia:** La eliminación es irreversible.`, + flags: 64 + }); + break; + + case "export_block_list": + const exportText = blocks.map((block: any, index: number) => { + const componentsCount = Array.isArray(block.config?.components) ? block.config.components.length : 0; + return `${index + 1}. ${block.name} (${componentsCount} componentes) - ID: ${block.id}`; + }).join('\n'); + + await interaction.reply({ + content: `📋 **Lista Exportada:**\n\`\`\`\n${exportText}\`\`\``, + flags: 64 + }); + break; + + default: + // Manejar acciones específicas de bloques + if (interaction.customId.startsWith("edit_block_")) { + const blockName = interaction.customId.replace("edit_block_", ""); + await interaction.reply({ + content: `Usa: \`!editar-embed ${blockName}\``, + flags: 64 + }); + } else if (interaction.customId.startsWith("delete_block_")) { + const blockName = interaction.customId.replace("delete_block_", ""); + await interaction.reply({ + content: `Usa: \`!eliminar-embed ${blockName}\` para eliminar este bloque de forma segura.`, + flags: 64 + }); + } else if (interaction.customId.startsWith("preview_block_")) { + const blockName = interaction.customId.replace("preview_block_", ""); + await interaction.reply({ + content: `Vista previa de \`${blockName}\` - Funcionalidad en desarrollo`, + flags: 64 + }); + } else if (interaction.customId.startsWith("duplicate_block_")) { + const blockName = interaction.customId.replace("duplicate_block_", ""); + await interaction.reply({ + content: `Funcionalidad de duplicación de \`${blockName}\` en desarrollo`, + flags: 64 + }); + } + break; + } + }); + + collector.on("end", async (collected: any, reason: string) => { + if (reason === "time") { + const timeoutEmbed = { + color: 0x36393f, + title: "⏰ Panel Expirado", + description: "El panel de gestión ha expirado por inactividad.\n\nUsa `!lista-embeds` para abrir un nuevo panel de gestión.", + footer: { text: "Panel expirado por inactividad" } + }; + + try { + await panelMessage.edit({ + embeds: [timeoutEmbed], + components: [] + }); + } catch (error) { + // Mensaje eliminado o error de edición + } + } + }); }, }; \ No newline at end of file diff --git a/src/commands/messages/alliaces/setupChannel.ts b/src/commands/messages/alliaces/setupChannel.ts index 6e14993..349129d 100644 --- a/src/commands/messages/alliaces/setupChannel.ts +++ b/src/commands/messages/alliaces/setupChannel.ts @@ -1,93 +1,662 @@ import { CommandMessage } from "../../../core/types/commands"; +// @ts-ignore +import { ComponentType, ButtonStyle, MessageFlags, ChannelType } from "discord.js"; export const command: CommandMessage = { name: "setchannel-alliance", type: "message", aliases: ["alchannel", "channelally"], cooldown: 10, - //@ts-ignore + // @ts-ignore run: async (message, args, client) => { if (!message.member?.permissions.has("Administrator")) { return message.reply("❌ No tienes permisos de Administrador."); } - // Validar argumentos - if (args.length < 2) { - return message.reply("❌ Uso correcto: `!setchannel-alliance <#canal|ID> `"); - } + // Obtener canales configurados existentes y bloques disponibles + const existingChannels = await client.prisma.allianceChannel.findMany({ + where: { guildId: message.guildId! }, + include: { blockConfig: true } + }); - const channelInput = args[0]; - const blockConfigName = args[1]; + const availableBlocks = await client.prisma.blockV2Config.findMany({ + where: { guildId: message.guildId! }, + select: { name: true, id: true, config: true } + }); - // Extraer ID del canal - let channelId: string; - - // Si es una mención de canal (#canal) - if (channelInput.startsWith('<#') && channelInput.endsWith('>')) { - channelId = channelInput.slice(2, -1); - } - // Si es solo un ID - else if (/^\d+$/.test(channelInput)) { - channelId = channelInput; - } - else { - return message.reply("❌ Formato de canal inválido. Usa `#canal` o el ID del canal."); - } - - try { - // Verificar que el canal existe en el servidor - const channel = await message.guild?.channels.fetch(channelId); - if (!channel) { - return message.reply("❌ El canal especificado no existe en este servidor."); - } - - // Verificar que el canal es un canal de texto - if (!channel.isTextBased()) { - return message.reply("❌ El canal debe ser un canal de texto."); - } - - // Verificar que existe el blockConfig - const blockConfig = await client.prisma.blockV2Config.findFirst({ - where: { - guildId: message.guildId, - name: blockConfigName + // Panel principal de configuración + const setupPanel = { + type: 17, + accent_color: 0x00ff88, // Verde alliance + components: [ + { + type: 10, + content: "# 🤝 **Centro de Configuración de Alianzas**" + }, + { + type: 14, + spacing: 2, + divider: true + }, + { + type: 10, + content: `📊 **Estado Actual:**\n` + + `• **${existingChannels.length}** canales configurados\n` + + `• **${availableBlocks.length}** bloques disponibles\n` + + `• **${existingChannels.filter((c: any) => c.isActive).length}** canales activos\n\n` + + `⚙️ Selecciona una acción para continuar:` } - }); + ] + }; - if (!blockConfig) { - return message.reply(`❌ No se encontró el bloque de configuración \`${blockConfigName}\`. Asegúrate de que exista.`); - } + const mainActionsRow = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Success, + label: "➕ Configurar Canal", + custom_id: "setup_new_channel", + emoji: { name: "🔧" } + }, + { + type: 2, + style: ButtonStyle.Primary, + label: "📋 Ver Configurados", + custom_id: "view_configured_channels", + emoji: { name: "📊" } + }, + { + type: 2, + style: ButtonStyle.Secondary, + label: "🧪 Crear Bloque", + custom_id: "help_create_block", + emoji: { name: "📝" } + } + ] + }; - // Configurar el canal de alianzas - const allianceChannel = await client.prisma.allianceChannel.upsert({ - where: { - guildId_channelId: { - guildId: message.guildId, - channelId: channelId + const managementRow = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Secondary, + label: "🔄 Refrescar", + custom_id: "refresh_status" + }, + { + type: 2, + style: ButtonStyle.Secondary, + label: "📖 Ayuda", + custom_id: "show_help" + }, + { + type: 2, + style: ButtonStyle.Danger, + label: "🗑️ Gestionar", + custom_id: "manage_channels", + disabled: existingChannels.length === 0 + } + ] + }; + + const panelMessage = await message.reply({ + flags: MessageFlags.SuppressEmbeds, + components: [setupPanel, mainActionsRow, managementRow] + }); + + const collector = panelMessage.createMessageComponentCollector({ + time: 600000, // 10 minutos + filter: (i) => i.user.id === message.author.id + }); + + collector.on("collect", async (interaction) => { + switch (interaction.customId) { + case "setup_new_channel": + // Obtener canales de texto disponibles + const textChannels = message.guild!.channels.cache + .filter(channel => + channel.type === ChannelType.GuildText && + // @ts-ignore + !existingChannels.some(ec => ec.channelId === channel.id) + ) + .map(channel => ({ + label: `#${channel.name}`, + value: channel.id, + description: `ID: ${channel.id}`, + emoji: { name: "💬" } + })) + .slice(0, 25); + + if (textChannels.length === 0) { + const noChannelsPanel = { + type: 17, + accent_color: 0xffa500, + components: [ + { + type: 10, + content: "⚠️ **Sin Canales Disponibles**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: "No hay canales de texto disponibles para configurar.\n\n**Posibles causas:**\n• Todos los canales ya están configurados\n• No hay canales de texto en el servidor\n• Faltan permisos para ver canales" + } + ] + }; + + await interaction.update({ components: [noChannelsPanel] }); + return; } - }, - create: { - guildId: message.guildId, - channelId: channelId, - blockConfigName: blockConfigName, - isActive: true - }, - update: { - blockConfigName: blockConfigName, - isActive: true, - updatedAt: new Date() + + const channelSelectPanel = { + type: 17, + accent_color: 0x5865f2, + components: [ + { + type: 10, + content: "📺 **Seleccionar Canal**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: `🎯 Selecciona el canal que quieres configurar para alianzas:\n\n💡 **Tip:** Solo se muestran canales de texto que aún no están configurados.` + } + ] + }; + + const channelSelectRow = { + type: 1, + components: [ + { + type: 3, + custom_id: "channel_select", + placeholder: "📺 Selecciona un canal...", + options: textChannels + } + ] + }; + + const backRow = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Secondary, + label: "↩️ Volver al Inicio", + custom_id: "back_to_main" + } + ] + }; + + await interaction.update({ + components: [channelSelectPanel, channelSelectRow, backRow] + }); + break; + + case "channel_select": + if (interaction.isStringSelectMenu()) { + const selectedChannelId = interaction.values[0]; + const selectedChannel = message.guild!.channels.cache.get(selectedChannelId); + + if (availableBlocks.length === 0) { + const noBlocksPanel = { + type: 17, + accent_color: 0xf04747, + components: [ + { + type: 10, + content: "❌ **Sin Bloques Disponibles**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: `📺 **Canal seleccionado:** #${selectedChannel?.name}\n\n⚠️ **Problema:** No hay bloques de configuración disponibles.\n\n🔧 **Solución:**\n• Crea un bloque usando: \`!blockcreate \`\n• Edita bloques usando: \`!blockeditv2 \`` + } + ] + }; + + const createBlockRow = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Success, + label: "📝 Ayuda Crear Bloque", + custom_id: "help_create_block" + }, + { + type: 2, + style: ButtonStyle.Secondary, + label: "↩️ Volver", + custom_id: "setup_new_channel" + } + ] + }; + + await interaction.update({ + components: [noBlocksPanel, createBlockRow] + }); + return; + } +// @ts-ignore + const blockOptions = availableBlocks.map(block => ({ + label: block.name, + value: `${selectedChannelId}_${block.name}`, + description: `ID: ${block.id}`, + emoji: { name: "🧩" } + })); + + const blockSelectPanel = { + type: 17, + accent_color: 0xff9500, + components: [ + { + type: 10, + content: "🧩 **Seleccionar Configuración**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: `📺 **Canal:** #${selectedChannel?.name}\n\n🎯 Selecciona qué bloque de configuración usar para este canal:\n\n💡 Los bloques definen cómo se verán los mensajes de alianza.` + } + ] + }; + + const blockSelectRow = { + type: 1, + components: [ + { + type: 3, + custom_id: "block_select", + placeholder: "🧩 Selecciona una configuración...", + options: blockOptions + } + ] + }; + + await interaction.update({ + // @ts-ignore + components: [blockSelectPanel, blockSelectRow, backRow] + }); + } + break; + + case "block_select": + if (interaction.isStringSelectMenu()) { + const [channelId, blockName] = interaction.values[0].split('_'); + const channel = message.guild!.channels.cache.get(channelId); + + try { + // Verificar que el bloque existe + const blockConfig = await client.prisma.blockV2Config.findFirst({ + where: { + guildId: message.guildId, + name: blockName + } + }); + + if (!blockConfig) { + throw new Error("Bloque no encontrado"); + } + + // Configurar el canal + await client.prisma.allianceChannel.upsert({ + where: { + guildId_channelId: { + guildId: message.guildId!, + channelId: channelId + } + }, + create: { + guildId: message.guildId!, + channelId: channelId, + blockConfigName: blockName, + isActive: true + }, + update: { + blockConfigName: blockName, + isActive: true, + updatedAt: new Date() + } + }); + + const successPanel = { + type: 17, + accent_color: 0x57f287, + components: [ + { + type: 10, + content: "✅ **Configuración Exitosa**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: `🎉 **Canal configurado correctamente:**\n\n` + + `📺 **Canal:** <#${channelId}>\n` + + `🧩 **Configuración:** \`${blockName}\`\n` + + `🟢 **Estado:** Activo\n\n` + + `🚀 **¡Listo!** Los enlaces de Discord válidos en este canal ahora otorgarán puntos de alianza usando la configuración especificada.` + } + ] + }; + + const successActionsRow = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Success, + label: "🏠 Volver al Inicio", + custom_id: "back_to_main" + }, + { + type: 2, + style: ButtonStyle.Primary, + label: "➕ Configurar Otro", + custom_id: "setup_new_channel" + }, + { + type: 2, + style: ButtonStyle.Secondary, + label: "📋 Ver Todos", + custom_id: "view_configured_channels" + } + ] + }; + + await interaction.update({ + components: [successPanel, successActionsRow] + }); + + } catch (error) { + const errorPanel = { + type: 17, + accent_color: 0xf04747, + components: [ + { + type: 10, + content: "❌ **Error de Configuración**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: `🔍 **Detalles del error:**\n\n` + + `📺 Canal: <#${channelId}>\n` + + `🧩 Bloque: \`${blockName}\`\n\n` + + `💭 **Posibles causas:**\n` + + `• El bloque fue eliminado\n` + + `• Error de base de datos\n` + + `• Permisos insuficientes\n\n` + + `🔄 Intenta nuevamente o contacta al soporte.` + } + ] + }; + + await interaction.update({ components: [errorPanel] }); + } + } + break; + + case "view_configured_channels": + if (existingChannels.length === 0) { + const emptyListPanel = { + type: 17, + accent_color: 0x36393f, + components: [ + { + type: 10, + content: "📋 **Canales Configurados**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: "🗂️ **Lista vacía**\n\nNo hay canales configurados para alianzas en este servidor.\n\n🚀 **¿Quieres empezar?**\n• Usa el botón \"Configurar Canal\" para añadir tu primer canal" + } + ] + }; + + await interaction.update({ components: [emptyListPanel] }); + return; + } + + let channelListText = `📊 **${existingChannels.length} canal(es) configurado(s)**\n\n`; + // @ts-ignore + existingChannels.forEach((config, index) => { + const channel = message.guild!.channels.cache.get(config.channelId); + const channelName = channel ? `#${channel.name}` : `Canal Eliminado`; + const status = config.isActive ? "🟢 Activo" : "🔴 Inactivo"; + + channelListText += `**${index + 1}.** ${channelName}\n`; + channelListText += ` └ \`${config.blockConfigName}\` • ${status}\n\n`; + }); + + const channelListPanel = { + type: 17, + accent_color: 0x5865f2, + components: [ + { + type: 10, + content: "📋 **Canales Configurados**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: channelListText + } + ] + }; + + const listActionsRow = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Secondary, + label: "🏠 Volver al Inicio", + custom_id: "back_to_main" + }, + { + type: 2, + style: ButtonStyle.Primary, + label: "➕ Configurar Más", + custom_id: "setup_new_channel" + }, + { + type: 2, + style: ButtonStyle.Danger, + label: "🔧 Gestionar", + custom_id: "manage_channels" + } + ] + }; + + await interaction.update({ + components: [channelListPanel, listActionsRow] + }); + break; + + case "help_create_block": + await interaction.reply({ + content: `📖 **Guía de Bloques**\n\n🧩 **¿Qué son los bloques?**\nLos bloques son configuraciones que definen cómo se ven los mensajes de alianza.\n\n🔧 **Comandos para gestionar bloques:**\n\n• \`!blockcreate \` - Crear nuevo bloque\n• \`!blockeditv2 \` - Editor completo\n• \`!embedlist\` - Ver todos los bloques\n• \`!embeddelete\` - Eliminar bloques\n\n💡 **Ejemplo:** \`!blockcreate alianza-general\``, + flags: 64 // Ephemeral + }); + break; + + case "show_help": + await interaction.reply({ + content: `📚 **Ayuda Completa**\n\n🤝 **Sistema de Alianzas:**\nConfigura canales donde los enlaces de Discord válidos otorgan puntos.\n\n🏗️ **Proceso de configuración:**\n1. Crear un bloque con \`!blockcreate\`\n2. Configurar canal con este comando\n3. ¡Los usuarios empezarán a ganar puntos!\n\n⚙️ **Gestión avanzada:**\n• Usar \`!embedlist\` para ver bloques\n• Usar \`!blockeditv2\` para personalizar\n• Este comando para gestionar canales`, + flags: 64 // Ephemeral + }); + break; + + case "back_to_main": + case "refresh_status": + // Recargar datos y volver al panel principal + const refreshedChannels = await client.prisma.allianceChannel.findMany({ + where: { guildId: message.guildId! }, + include: { blockConfig: true } + }); + + const refreshedBlocks = await client.prisma.blockV2Config.findMany({ + where: { guildId: message.guildId! }, + select: { name: true, id: true, config: true } + }); + + const refreshedPanel = { + type: 17, + accent_color: 0x00ff88, + components: [ + { + type: 10, + content: "🤝 **Centro de Configuración de Alianzas**" + }, + { + type: 14, + divider: true, + spacing: 2 + }, + { + type: 10, + content: `📊 **Estado Actual:**\n` + + `• **${refreshedChannels.length}** canales configurados\n` + + `• **${refreshedBlocks.length}** bloques disponibles\n` + + // @ts-ignore + `• **${refreshedChannels.filter(c => c.isActive).length}** canales activos\n\n` + + `⚙️ Selecciona una acción para continuar:` + } + ] + }; + + const refreshedMainActions = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Success, + label: "➕ Configurar Canal", + custom_id: "setup_new_channel", + emoji: { name: "🔧" } + }, + { + type: 2, + style: ButtonStyle.Primary, + label: "📋 Ver Configurados", + custom_id: "view_configured_channels", + emoji: { name: "📊" } + }, + { + type: 2, + style: ButtonStyle.Secondary, + label: "🧪 Crear Bloque", + custom_id: "help_create_block", + emoji: { name: "📝" } + } + ] + }; + + const refreshedManagement = { + type: 1, + components: [ + { + type: 2, + style: ButtonStyle.Secondary, + label: "🔄 Refrescar", + custom_id: "refresh_status" + }, + { + type: 2, + style: ButtonStyle.Secondary, + label: "📖 Ayuda", + custom_id: "show_help" + }, + { + type: 2, + style: ButtonStyle.Danger, + label: "🗑️ Gestionar", + custom_id: "manage_channels", + disabled: refreshedChannels.length === 0 + } + ] + }; + + await interaction.update({ + components: [refreshedPanel, refreshedMainActions, refreshedManagement] + }); + break; + + case "manage_channels": + await interaction.reply({ + content: `🔧 **Gestión Avanzada**\n\n⚠️ **Funciones de gestión avanzada:**\n\n🔄 Activar/desactivar canales\n🗑️ Eliminar configuraciones\n✏️ Cambiar bloques asignados\n\n💡 **Próximamente:** Panel interactivo completo`, + flags: 64 // Ephemeral + }); + break; + } + }); + + collector.on("end", async (collected, reason) => { + if (reason === "time") { + const timeoutPanel = { + type: 17, + accent_color: 0x36393f, + components: [ + { + type: 10, + content: "⏰ **Sesión Expirada**" + }, + { + type: 14, + divider: true, + spacing: 1 + }, + { + type: 10, + content: "El panel de configuración ha expirado.\nUsa el comando nuevamente para continuar." + } + ] + }; + + try { + await panelMessage.edit({ + components: [timeoutPanel] + }); + } catch (error) { + // Mensaje eliminado o error de edición } - }); - - return message.reply(`✅ Canal de alianzas configurado correctamente!\n\n` + - `**Canal:** <#${channelId}>\n` + - `**Configuración:** \`${blockConfigName}\`\n` + - `**Estado:** Activo\n\n` + - `Los enlaces de Discord válidos en este canal ahora otorgarán puntos de alianza.`); - - } catch (error) { - console.error('Error configurando canal de alianzas:', error); - return message.reply("❌ Ocurrió un error al configurar el canal de alianzas. Inténtalo de nuevo."); - } + } + }); } } diff --git a/src/commands/messages/help.ts b/src/commands/messages/help.ts new file mode 100644 index 0000000..dc147d3 --- /dev/null +++ b/src/commands/messages/help.ts @@ -0,0 +1,433 @@ +// @ts-ignore +import { CommandMessage } from "../../../core/types/commands"; + +export const command: CommandMessage = { + name: 'ayuda', + type: "message", + aliases: ['help', 'comandos', 'cmds'], + cooldown: 5, + run: async (message: any, args: string[], client: any) => { + // Obtener información del servidor para mostrar el prefix actual + const server = await client.prisma.guild.findFirst({ + where: { id: message.guild!.id } + }); + const prefix = server?.prefix || "!"; + + // Definir categorías de comandos con nombres modernos + const commandCategories = { + "Alianzas": [ + { + name: "crear-embed", + aliases: ["embed-crear", "nuevo-embed"], + description: "Crear nuevos embeds con DisplayComponents modernos", + usage: `${prefix}crear-embed ` + }, + { + name: "editar-embed", + aliases: ["embed-editar", "modificar-embed"], + description: "Editor avanzado de embeds con interfaz interactiva", + usage: `${prefix}editar-embed ` + }, + { + name: "lista-embeds", + aliases: ["embeds", "ver-embeds"], + description: "Centro de gestión de embeds con paginación", + usage: `${prefix}lista-embeds` + }, + { + name: "eliminar-embed", + aliases: ["embed-eliminar", "borrar-embed"], + description: "Panel interactivo para eliminar embeds", + usage: `${prefix}eliminar-embed [nombre]` + }, + { + name: "canal-alianza", + aliases: ["configurar-canal", "setup-canal"], + description: "Configurar canales para sistema de alianzas", + usage: `${prefix}canal-alianza` + }, + { + name: "demo-componentes", + aliases: ["demo", "prueba-componentes"], + description: "Demostración de DisplayComponents con accesorios", + usage: `${prefix}demo-componentes` + } + ], + "Red": [ + { + name: "ping", + aliases: ["latencia", "pong"], + description: "Verificar latencia y estado del bot", + usage: `${prefix}ping` + } + ], + "Configuracion": [ + { + name: "configuracion", + aliases: ["config", "ajustes", "settings"], + description: "Panel de configuración del servidor", + usage: `${prefix}configuracion` + } + ] + }; + + // Definir backRow una sola vez fuera de los casos + const backRow = { + type: 1, + components: [ + { + type: 2, + style: 2, + label: "↩️ Volver al Menú", + custom_id: "back_to_main" + } + ] + }; + + // Si se especifica un comando específico + if (args.length > 0) { + const searchCommand = args[0].toLowerCase(); + let foundCommand = null; + let foundCategory = null; + + // Buscar el comando en todas las categorías + for (const [category, commands] of Object.entries(commandCategories)) { + const command = commands.find(cmd => + cmd.name === searchCommand || cmd.aliases.includes(searchCommand) + ); + if (command) { + foundCommand = command; + foundCategory = category; + break; + } + } + + if (foundCommand) { + // Panel detallado del comando específico - SIMPLIFICADO + const commandDetailPanel = { + type: 17, + accent_color: 0x5865f2, + components: [ + { + type: 10, + content: `### 📖 **Ayuda: \`${foundCommand.name}\`**\n\n**Categoría:** ${foundCategory}\n**Descripción:** ${foundCommand.description}\n**Uso:** ${foundCommand.usage}\n\n**Aliases disponibles:**\n${foundCommand.aliases.map(alias => `\`${prefix}${alias}\``).join(", ")}` + } + ] + }; + + const detailActionsRow = { + type: 1, + components: [ + { + type: 2, + style: 2, + label: "📋 Ver Todos", + custom_id: "show_all_commands" + }, + { + type: 2, + style: 2, + label: "🔍 Buscar Otro", + custom_id: "search_command" + } + ] + }; + + await message.reply({ + flags: 32768, + components: [commandDetailPanel, detailActionsRow] + }); + return; + } else { + // Comando no encontrado - SIMPLIFICADO + const notFoundPanel = { + type: 17, + accent_color: 0xf04747, + components: [ + { + type: 10, + content: `### ❌ **Comando no encontrado: \`${searchCommand}\`**\n\nNo se encontró ningún comando con el nombre o alias \`${searchCommand}\`.\n\n🔍 **Sugerencias:**\n• Verifica la ortografía\n• Usa \`${prefix}ayuda\` para ver todos los comandos\n• Usa \`${prefix}ayuda \` para filtrar` + } + ] + }; + + const notFoundRow = { + type: 1, + components: [ + { + type: 2, + style: 1, + label: "📋 Ver Todos", + custom_id: "show_all_commands" + } + ] + }; + + await message.reply({ + flags: 32768, + components: [notFoundPanel, notFoundRow] + }); + return; + } + } + + // Panel principal de ayuda - OPTIMIZADO para no exceder límite de componentes + const helpPanel = { + type: 17, + accent_color: 0x5865f2, + components: [ + { + type: 10, + content: `### 📚 **Centro de Ayuda - ${message.guild!.name}**` + }, + { + type: 14, + spacing: 1, + divider: true + }, + { + type: 10, + content: `**Prefix actual:** \`${prefix}\`\n**Total de comandos:** ${Object.values(commandCategories).flat().length}\n**Categorías disponibles:** ${Object.keys(commandCategories).length}` + }, + { + type: 14, + spacing: 2, + divider: false + } + ] + }; + + // Agregar resumen de categorías de forma compacta + for (const [categoryName, commands] of Object.entries(commandCategories)) { + const commandsList = commands.map(cmd => `\`${cmd.name}\``).join(", "); + helpPanel.components.push({ + type: 10, + content: `🔹 **${categoryName}** (${commands.length})\n${commandsList}` + }); + } + + // Botones de navegación + const navigationRow = { + type: 1, + components: [ + { + type: 2, + style: 1, + label: "🤝 Alianzas", + custom_id: "category_alliances" + }, + { + type: 2, + style: 2, + label: "🌐 Red", + custom_id: "category_network" + }, + { + type: 2, + style: 2, + label: "⚙️ Config", + custom_id: "category_settings" + }, + { + type: 2, + style: 3, + label: "📋 Exportar", + custom_id: "export_commands" + } + ] + }; + + const panelMessage = await message.reply({ + flags: 32768, + components: [helpPanel, navigationRow] + }); + + const collector = panelMessage.createMessageComponentCollector({ + time: 600000, + filter: (i: any) => i.user.id === message.author.id + }); + + collector.on("collect", async (interaction: any) => { + // Manejar información específica de comandos + if (interaction.customId.startsWith("cmd_info_")) { + const commandName = interaction.customId.replace("cmd_info_", ""); + let foundCommand = null; + let foundCategory = null; + + for (const [category, commands] of Object.entries(commandCategories)) { + const command = commands.find(cmd => cmd.name === commandName); + if (command) { + foundCommand = command; + foundCategory = category; + break; + } + } + + if (foundCommand) { + await interaction.reply({ + content: `📖 **${foundCommand.name}**\n\n**Categoría:** ${foundCategory}\n**Descripción:** ${foundCommand.description}\n**Uso:** ${foundCommand.usage}\n**Aliases:** ${foundCommand.aliases.join(", ")}\n\n💡 **Tip:** Usa \`${foundCommand.usage}\` para ejecutar este comando.`, + flags: 64 + }); + } + return; + } + + // Manejar categorías específicas - VERSIÓN COMPACTA + switch (interaction.customId) { + case "category_alliances": + const alliancePanel = { + type: 17, + accent_color: 0x00ff88, + components: [ + { + type: 10, + content: "### 🤝 **Comandos de Alianzas**\n\nSistema completo para gestionar alianzas entre servidores:" + }, + { + type: 14, + spacing: 2, + divider: true + } + ] + }; + + // Agregar comandos de forma compacta + commandCategories["Alianzas"].forEach(cmd => { + alliancePanel.components.push({ + type: 10, + content: `**${cmd.name}**\n${cmd.description}\n\`${cmd.usage}\`` + }); + }); + + await interaction.update({ + components: [alliancePanel, backRow] + }); + break; + + case "category_network": + const networkPanel = { + type: 17, + accent_color: 0x5865f2, + components: [ + { + type: 10, + content: "### 🌐 **Comandos de Red**" + }, + { + type: 14, + spacing: 2, + divider: true + } + ] + }; + + commandCategories["Red"].forEach(cmd => { + networkPanel.components.push({ + type: 10, + content: `**${cmd.name}**\n${cmd.description}\n\`${cmd.usage}\`` + }); + }); + + await interaction.update({ + components: [networkPanel, backRow] + }); + break; + + case "category_settings": + const settingsPanel = { + type: 17, + accent_color: 0xff9500, + components: [ + { + type: 10, + content: "### ⚙️ **Comandos de Configuración**" + }, + { + type: 14, + spacing: 2, + divider: true + } + ] + }; + + commandCategories["Configuracion"].forEach(cmd => { + settingsPanel.components.push({ + type: 10, + content: `**${cmd.name}**\n${cmd.description}\n\`${cmd.usage}\`` + }); + }); + + await interaction.update({ + components: [settingsPanel, backRow] + }); + break; + + case "back_to_main": + await interaction.update({ + components: [helpPanel, navigationRow] + }); + break; + + case "export_commands": + let exportText = `📋 **Lista Completa de Comandos - ${message.guild!.name}**\n\n`; + exportText += `**Prefix:** ${prefix}\n\n`; + + for (const [category, commands] of Object.entries(commandCategories)) { + exportText += `**${category}**\n`; + commands.forEach(cmd => { + exportText += `• ${cmd.name} - ${cmd.description}\n`; + exportText += ` Uso: ${cmd.usage}\n`; + if (cmd.aliases.length > 0) { + exportText += ` Aliases: ${cmd.aliases.join(", ")}\n`; + } + exportText += `\n`; + }); + exportText += `\n`; + } + + await interaction.reply({ + content: `\`\`\`\n${exportText}\`\`\``, + flags: 64 + }); + break; + + default: + if (interaction.customId.startsWith("use_")) { + const commandName = interaction.customId.replace("use_", ""); + const foundCmd = Object.values(commandCategories).flat().find(cmd => cmd.name === commandName); + + if (foundCmd) { + await interaction.reply({ + content: `🚀 **Ejecutar: \`${foundCmd.name}\`**\n\nUsa: \`${foundCmd.usage}\`\n\n💡 **Tip:** Copia y pega el comando en el chat para usarlo.`, + flags: 64 + }); + } + } + break; + } + }); + + collector.on("end", async (collected: any, reason: string) => { + if (reason === "time") { + const timeoutPanel = { + type: 17, + accent_color: 0x36393f, + components: [ + { + type: 10, + content: `### ⏰ **Panel de Ayuda Expirado**\n\nEl panel de ayuda ha expirado por inactividad.\n\nUsa \`${prefix}ayuda\` para abrir un nuevo panel.` + } + ] + }; + + try { + await panelMessage.edit({ + components: [timeoutPanel] + }); + } catch (error) { + // Mensaje eliminado o error de edición + } + } + }); + } +}; diff --git a/src/commands/messages/settings-server/settings.ts b/src/commands/messages/settings-server/settings.ts index 4138df0..7dfa09a 100644 --- a/src/commands/messages/settings-server/settings.ts +++ b/src/commands/messages/settings-server/settings.ts @@ -1,51 +1,351 @@ -import {CommandMessage} from "../../../core/types/commands"; -//@ts-ignore -import { - ButtonStyle, ChannelType, - ContainerBuilder, - MessageFlags, - SectionBuilder, SeparatorBuilder, SeparatorSpacingSize, TextChannel, - TextDisplayBuilder, - UserSelectMenuBuilder -} from "discord.js"; +import { CommandMessage } from "../../../core/types/commands"; export const command: CommandMessage = { - name: 'settings', + name: 'configuracion', type: "message", - aliases: ['options', 'stts'], + aliases: ['config', 'ajustes', 'settings'], cooldown: 5, run: async (message, args, client) => { - const server = await client.prisma.guild.findFirst({ where: { id: message.guild!.id } }); - const title = new TextDisplayBuilder() - .setContent("## ﹒⌒    Settings Seɾveɾ    ╰୧﹒") - const description = new TextDisplayBuilder() - .setContent("Panel de Administracion del bot dentro del servidor.") - const sect = new TextDisplayBuilder() - .setContent("**Prefix del bot:** " + ` \`\`\`${server.prefix}\`\`\``) - - const section = new SectionBuilder() - .addTextDisplayComponents(sect) - //@ts-ignore - .setButtonAccessory(button => button - .setCustomId('prefixsettings') - .setLabel('Prefix') - .setStyle(ButtonStyle.Primary), - ) - - const separator = new SeparatorBuilder() - .setSpacing(SeparatorSpacingSize.Large) - .setDivider(false); - - const main = new ContainerBuilder() - .addTextDisplayComponents(title, description) - .addSeparatorComponents(separator) - .addSectionComponents(section) - - - //@ts-ignore - if (message.channel.type === ChannelType.GuildText) { - const channel = message.channel as TextChannel; - await channel.send({ components: [main], flags: MessageFlags.IsComponentsV2}); + if (!message.member?.permissions.has("Administrator")) { + await message.reply("❌ No tienes permisos de Administrador."); + return; } + + const server = await client.prisma.guild.findFirst({ + where: { id: message.guild!.id } + }); + + const currentPrefix = server?.prefix || "!"; + + // Panel de configuración usando DisplayComponents + const settingsPanel = { + type: 17, + accent_color: 6178018, // Color del ejemplo + components: [ + { + type: 10, + content: "### <:invisible:1418684224441028608> 梅,panel admin,📢\n" + }, + { + type: 14, + spacing: 1, + divider: false + }, + { + type: 10, + content: "Configuracion del Servidor:" + }, + { + type: 9, // Section + components: [ + { + type: 10, + content: `**Prefix:**<:invisible:1418684224441028608>\`${currentPrefix}\`` + } + ], + accessory: { + type: 2, // Button + style: 2, // Secondary + emoji: { + name: "⚙️" + }, + custom_id: "open_prefix_modal", + label: "Cambiar" + } + }, + { + type: 14, + divider: false + } + ] + }; + + const panelMessage = await message.reply({ + flags: 32768, // SuppressEmbeds + components: [settingsPanel] + }); + + const collector = panelMessage.createMessageComponentCollector({ + time: 300000, // 5 minutos + filter: (i: any) => i.user.id === message.author.id + }); + + collector.on("collect", async (interaction: any) => { + if (interaction.customId === "open_prefix_modal") { + // Crear y mostrar modal para cambiar prefix + const prefixModal = { + title: "⚙️ Configurar Prefix del Servidor", + custom_id: "prefix_settings_modal", + components: [ + { + type: 1, // ActionRow + components: [ + { + type: 4, // TextInput + custom_id: "new_prefix_input", + label: "Nuevo Prefix", + style: 1, // Short + placeholder: `Prefix actual: ${currentPrefix}`, + required: true, + max_length: 10, + min_length: 1, + value: currentPrefix + } + ] + }, + { + type: 1, + components: [ + { + type: 4, + custom_id: "prefix_description", + label: "¿Por qué cambiar el prefix? (Opcional)", + style: 2, // Paragraph + placeholder: "Ej: Evitar conflictos con otros bots...", + required: false, + max_length: 200 + } + ] + } + ] + }; + + await interaction.showModal(prefixModal); + + // Crear un collector específico para este modal + const modalCollector = interaction.awaitModalSubmit({ + time: 300000, // 5 minutos + filter: (modalInt: any) => modalInt.customId === "prefix_settings_modal" && modalInt.user.id === message.author.id + }); + + modalCollector.then(async (modalInteraction: any) => { + const newPrefix = modalInteraction.fields.getTextInputValue("new_prefix_input"); + const description = modalInteraction.fields.getTextInputValue("prefix_description") || "Sin descripción"; + + // Validar prefix + if (!newPrefix || newPrefix.length > 10) { + await modalInteraction.reply({ + content: "❌ **Error:** El prefix debe tener entre 1 y 10 caracteres.", + flags: 64 // Ephemeral + }); + return; + } + + try { + // Actualizar prefix en la base de datos + await client.prisma.guild.upsert({ + where: { id: message.guild!.id }, + create: { + id: message.guild!.id, + name: message.guild!.name, + prefix: newPrefix + }, + update: { + prefix: newPrefix, + name: message.guild!.name + } + }); + + // Panel de confirmación + const successPanel = { + type: 17, + accent_color: 3066993, // Verde + components: [ + { + type: 10, + content: "### ✅ **Prefix Actualizado Exitosamente**" + }, + { + type: 14, + spacing: 2, + divider: true + }, + { + type: 9, + components: [ + { + type: 10, + content: `**Prefix anterior:** \`${currentPrefix}\`\n**Prefix nuevo:** \`${newPrefix}\`\n\n**Motivo:** ${description}` + } + ], + accessory: { + type: 2, + style: 3, // Success + label: "✓ Listo", + custom_id: "prefix_confirmed", + emoji: { name: "✅" } + } + }, + { + type: 14, + spacing: 1, + divider: false + }, + { + type: 10, + content: "🚀 **¡Listo!** Ahora puedes usar los comandos con el nuevo prefix.\n\n💡 **Ejemplo:** `" + newPrefix + "help`, `" + newPrefix + "embedlist`" + } + ] + }; + + // Botón para volver al panel principal + const backToSettingsRow = { + type: 1, + components: [ + { + type: 2, + style: 2, // Secondary + label: "↩️ Volver a Configuración", + custom_id: "back_to_settings" + } + ] + }; + + // Actualizar el panel original + await modalInteraction.update({ + components: [successPanel, backToSettingsRow] + }); + + } catch (error) { + const errorPanel = { + type: 17, + accent_color: 15548997, // Rojo + components: [ + { + type: 10, + content: "### ❌ **Error al Actualizar Prefix**" + }, + { + type: 14, + spacing: 2, + divider: true + }, + { + type: 10, + content: `**Error:** No se pudo actualizar el prefix a \`${newPrefix}\`\n\n**Posibles causas:**\n• Error de conexión con la base de datos\n• Prefix contiene caracteres no válidos\n• Permisos insuficientes\n\n🔄 **Solución:** Intenta nuevamente con un prefix diferente.` + } + ] + }; + + const retryRow = { + type: 1, + components: [ + { + type: 2, + style: 2, + label: "🔄 Reintentar", + custom_id: "open_prefix_modal" + }, + { + type: 2, + style: 4, // Danger + label: "❌ Cancelar", + custom_id: "cancel_prefix_change" + } + ] + }; + + await modalInteraction.update({ + components: [errorPanel, retryRow] + }); + } + }).catch(async (error: any) => { + // Modal timeout o cancelado + console.log("Modal timeout o error:", error.message); + }); + } + + // Manejar botones adicionales + if (interaction.customId === "back_to_settings") { + // Volver al panel principal + const updatedServer = await client.prisma.guild.findFirst({ + where: { id: message.guild!.id } + }); + const newCurrentPrefix = updatedServer?.prefix || "!"; + + const updatedSettingsPanel = { + type: 17, + accent_color: 6178018, + components: [ + { + type: 10, + content: "### <:invisible:1418684224441028608> 梅,panel admin,📢\n" + }, + { + type: 14, + spacing: 1, + divider: false + }, + { + type: 10, + content: "Configuracion del Servidor:" + }, + { + type: 9, + components: [ + { + type: 10, + content: `**Prefix:** \`${newCurrentPrefix}\`` + } + ], + accessory: { + type: 2, + style: 2, + emoji: { name: "⚙️" }, + custom_id: "open_prefix_modal", + label: "Cambiar" + } + }, + { + type: 14, + divider: false + } + ] + }; + + await interaction.update({ + components: [updatedSettingsPanel] + }); + } + + if (interaction.customId === "cancel_prefix_change") { + // Volver al panel original sin cambios + await interaction.update({ + components: [settingsPanel] + }); + } + }); + + collector.on("end", async (collected: any, reason: string) => { + if (reason === "time") { + const timeoutPanel = { + type: 17, + accent_color: 6178018, + components: [ + { + type: 10, + content: "### ⏰ **Panel Expirado**" + }, + { + type: 14, + spacing: 1, + divider: true + }, + { + type: 10, + content: "El panel de configuración ha expirado por inactividad.\n\nUsa `!settings` para abrir un nuevo panel." + } + ] + }; + + try { + await panelMessage.edit({ + components: [timeoutPanel] + }); + } catch (error) { + // Mensaje eliminado o error de edición + } + } + }); } -} \ No newline at end of file +}; diff --git a/src/core/lib/vars.ts b/src/core/lib/vars.ts index 40005bd..775368e 100644 --- a/src/core/lib/vars.ts +++ b/src/core/lib/vars.ts @@ -6,10 +6,8 @@ export async function replaceVars(text: string, user: User | undefined, guild: G // Crear inviteObject solo si invite existe y tiene guild const inviteObject = invite?.guild ? { - guild: { - //@ts-ignore - icon: `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` - } + name: invite.guild.name, + icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : '' } : null; return text @@ -37,7 +35,6 @@ export async function replaceVars(text: string, user: User | undefined, guild: G /** * INVITE INFO */ - .replace(/(invite\.name)/g, invite?.guild?.name ?? "") - .replace(/(invite\.icon)/g, inviteObject?.guild.icon ?? '0') - + .replace(/(invite\.name)/g, inviteObject?.name ?? "") + .replace(/(invite\.icon)/g, inviteObject?.icon ?? '') } \ No newline at end of file diff --git a/src/events/extras/alliace.ts b/src/events/extras/alliace.ts index 28246d5..6ee5481 100644 --- a/src/events/extras/alliace.ts +++ b/src/events/extras/alliace.ts @@ -288,6 +288,7 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an // Añadir imagen de portada primero si existe if (config.coverImage && isValidUrl(config.coverImage)) { + // @ts-ignore const processedCoverUrl = await replaceVars(config.coverImage, user, guild); if (isValidUrl(processedCoverUrl)) { previewComponents.push({ @@ -301,6 +302,7 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an if (config.title) { previewComponents.push({ type: 10, + // @ts-ignore content: await replaceVars(config.title, user, guild) }); } @@ -310,6 +312,7 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an for (const c of config.components) { if (c.type === 10) { // Componente de texto con thumbnail opcional + // @ts-ignore const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, user, guild) : null; if (processedThumbnail && isValidUrl(processedThumbnail)) { @@ -319,6 +322,7 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an components: [ { type: 10, + // @ts-ignore content: await replaceVars(c.content || " ", user, guild) } ], @@ -331,6 +335,7 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an // Sin thumbnail o thumbnail inválido, componente normal previewComponents.push({ type: 10, + // @ts-ignore content: await replaceVars(c.content || " ", user, guild) }); } @@ -343,6 +348,7 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an }); } else if (c.type === 12) { // Imagen - validar URL también + // @ts-ignore const processedImageUrl = await replaceVars(c.url, user, guild); if (isValidUrl(processedImageUrl)) {