From 8afca0c0ea23a20cac55a1721848bd873ba4d38b Mon Sep 17 00:00:00 2001 From: shnimlz Date: Sat, 20 Sep 2025 15:19:41 -0500 Subject: [PATCH] ni yo se que hice xd --- .../messages/alliaces/createEmbedv2.ts | 651 +++++++++---- src/commands/messages/alliaces/editEmbedv2.ts | 920 +++++++----------- .../messages/alliaces/listChannels.ts | 6 + src/core/lib/vars.ts | 182 ++-- src/events/messageCreate.ts | 1 - 5 files changed, 940 insertions(+), 820 deletions(-) diff --git a/src/commands/messages/alliaces/createEmbedv2.ts b/src/commands/messages/alliaces/createEmbedv2.ts index d517921..6a23227 100644 --- a/src/commands/messages/alliaces/createEmbedv2.ts +++ b/src/commands/messages/alliaces/createEmbedv2.ts @@ -1,7 +1,7 @@ import { CommandMessage } from "../../../core/types/commands"; // @ts-ignore import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags } from "discord.js"; -import { replaceVars, isValidUrlOrVariable } from "../../../core/lib/vars"; +import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars"; /** * Botones de edición - VERSIÓN MEJORADA @@ -23,6 +23,7 @@ const btns = (disabled = false) => ([ { 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.Secondary, type: 2, label: "🔗 Crear Botón Link", disabled, custom_id: "edit_link_button" }, { style: ButtonStyle.Primary, type: 2, label: "🔄 Mover", disabled, custom_id: "move_block" }, { style: ButtonStyle.Danger, type: 2, label: "🗑️ Eliminar", disabled, custom_id: "delete_block" } ] @@ -73,6 +74,55 @@ const validateContent = (content: string): string => { return cleaned; }; +// Validación y parseo de emoji (unicode o personalizado / <:name:id>) +const parseEmojiInput = (input?: string): any | null => { + if (!input) return null; + const trimmed = input.trim(); + if (!trimmed) return null; + + const match = trimmed.match(/^<(a?):(\w+):(\d+)>$/); + if (match) { + const animated = match[1] === 'a'; + const name = match[2]; + const id = match[3]; + return { id, name, animated }; + } + // Asumimos unicode si no es formato de emoji personalizado + return { name: trimmed }; +}; + +/** + * Construye un accesorio de botón link para Display Components + */ +const buildLinkAccessory = async (link: any, member: any, guild: any) => { + if (!link || !link.url) return null; + // @ts-ignore + const processedUrl = await replaceVars(link.url, member, guild); + if (!isValidUrl(processedUrl)) return null; + + const accessory: any = { + type: 2, + style: ButtonStyle.Link, + url: processedUrl + }; + + if (link.label && typeof link.label === 'string' && link.label.trim().length > 0) { + accessory.label = link.label.trim().slice(0, 80); + } + + if (link.emoji && typeof link.emoji === 'string') { + const parsed = parseEmojiInput(link.emoji); + if (parsed) accessory.emoji = parsed; + } + + // Debe tener al menos label o emoji + if (!accessory.label && !accessory.emoji) { + return null; + } + + return accessory; +}; + /** * Generar vista previa */ @@ -102,15 +152,26 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { // Procesar componentes en orden for (const c of blockState.components) { if (c.type === 10) { - // Componente de texto con thumbnail opcional + // Componente de texto con accessory opcional (thumbnail o botón link) //@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 + // Construir accessory según prioridad: linkButton > thumbnail + let accessory: any = null; + if (c.linkButton) { + accessory = await buildLinkAccessory(c.linkButton, member, guild); + } + if (!accessory && processedThumbnail && isValidUrl(processedThumbnail)) { + accessory = { + type: 11, + media: { url: processedThumbnail } + }; + } + + if (accessory) { previewComponents.push({ type: 9, components: [ @@ -119,13 +180,10 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { content: validatedContent } ], - accessory: { - type: 11, - media: { url: processedThumbnail } - } + accessory }); } else { - // Sin thumbnail o thumbnail inválido, componente normal + // Sin accessory válido previewComponents.push({ type: 10, content: validatedContent @@ -213,7 +271,7 @@ export const command: CommandMessage = { //@ts-ignore await editorMessage.edit({ content: null, - flags: 32768, + flags: 4096, components: [ await renderPreview(blockState, message.member, message.guild), ...btns(false) @@ -473,8 +531,8 @@ export const command: CommandMessage = { : `Componente ${c.type}`, value: idx.toString(), description: - c.type === 10 && c.thumbnail - ? "Con thumbnail" + c.type === 10 && (c.thumbnail || c.linkButton) + ? (c.thumbnail ? "Con thumbnail" : "Con botón link") : undefined })); @@ -557,7 +615,7 @@ export const command: CommandMessage = { } case "delete_block": { // Incluir portada en las opciones si existe - const options = []; + const options = [] as any[]; // Añadir portada como opción si existe if (blockState.coverImage) { @@ -575,14 +633,14 @@ export const command: CommandMessage = { c.type === 10 ? `Texto: ${c.content?.slice(0, 30) || "..."}` : c.type === 14 - ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` + ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` // <-- Arreglado aquí : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}` : `Componente ${c.type}`, value: idx.toString(), description: - c.type === 10 && c.thumbnail - ? "Con thumbnail" + c.type === 10 && (c.thumbnail || c.linkButton) + ? (c.thumbnail ? "Con thumbnail" : "Con botón link") : undefined }); }); @@ -641,27 +699,36 @@ export const command: CommandMessage = { break; } case "show_variables": { - await i.deferReply({ flags: 64 }); // 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\n\n" + - "💡 **Nota:** Las variables se usan SIN llaves `{}` en los campos de URL/imagen." - }); + // Construir lista de variables dinámicamente desde var.ts + const vars = listVariables(); + const chunked: string[] = []; + let current = ""; + for (const v of vars) { + const line = `• ${v}\n`; + if ((current + line).length > 1800) { + chunked.push(current); + current = line; + } else { + current += line; + } + } + if (current) chunked.push(current); + + // Responder en uno o varios mensajes efímeros según el tamaño + if (chunked.length === 0) { + await i.deferReply({ flags: 64 }); + //@ts-ignore + await i.editReply({ content: "No hay variables registradas." }); + } else { + // Primer bloque + //@ts-ignore + await i.reply({ flags: 64, content: `📋 **Variables Disponibles:**\n\n${chunked[0]}` }); + // Bloques adicionales si hiciera falta + for (let idx = 1; idx < chunked.length; idx++) { + //@ts-ignore + await i.followUp({ flags: 64, content: chunked[idx] }); + } + } break; } case "duplicate_block": { @@ -671,7 +738,7 @@ export const command: CommandMessage = { : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}` : `Componente ${c.type}`, value: idx.toString(), - description: c.type === 10 && c.thumbnail ? "Con thumbnail" : undefined + description: c.type === 10 && (c.thumbnail || c.linkButton) ? (c.thumbnail ? "Con thumbnail" : "Con botón link") : undefined })); if (options.length === 0) { @@ -726,7 +793,7 @@ export const command: CommandMessage = { //@ts-ignore await i.reply({ flags: 64, // MessageFlags.Ephemeral - content: `\`\`\`json\n${truncated}\`\`\`` + content: `\`\`\`json\n${truncated}\n\`\`\`` }); break; } @@ -759,7 +826,7 @@ export const command: CommandMessage = { //@ts-ignore await i.reply({ flags: 64, // MessageFlags.Ephemeral - content: `📤 **JSON Exportado:**\n\`\`\`json\n${truncatedJson}\`\`\`\n\n💡 **Tip:** Copia el JSON de arriba manualmente y pégalo donde necesites.` + content: `📤 **JSON Exportado:**\n\`\`\`json\n${truncatedJson}\n\`\`\`\n\n💡 **Tip:** Copia el JSON de arriba manualmente y pégalo donde necesites.` }); break; } @@ -795,36 +862,224 @@ export const command: CommandMessage = { 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); + // Construir listado de TextDisplays + const textDisplays = blockState.components + .map((c: any, idx: number) => ({ c, idx })) + .filter(({ c }: any) => c.type === 10); - if (!textComp) { - await i.deferReply({ flags: 64 }); // MessageFlags.Ephemeral - //@ts-ignore - await i.editReply({ - content: "❌ Necesitas al menos un componente de texto para añadir thumbnail." - }); + if (textDisplays.length === 0) { + await i.deferReply({ flags: 64 }); + // @ts-ignore + await i.editReply({ content: "❌ No hay bloques de texto para editar thumbnail." }); break; } - const modal = new ModalBuilder() - .setCustomId('edit_thumbnail_modal') - .setTitle('📎 Editar Thumbnail'); + const options = textDisplays.map(({ c, idx }: any) => ({ + label: `Texto #${idx + 1}: ${c.content?.slice(0, 30) || '...'}`, + value: String(idx), + description: c.thumbnail ? 'Con thumbnail' : c.linkButton ? 'Con botón link' : 'Sin accesorio' + })); - 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 reply = await i.reply({ + flags: 64, + content: "Elige el TextDisplay a editar su thumbnail:", + components: [ + { type: 1, components: [ { type: 3, custom_id: 'choose_text_for_thumbnail', placeholder: 'Selecciona un bloque de texto', options } ] } + ], + fetchReply: true + }); - const firstRow = new ActionRowBuilder().addComponents(thumbnailInput); - modal.addComponents(firstRow); + // @ts-ignore + const selCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + max: 1, + time: 60000, + filter: (it: any) => it.user.id === message.author.id + }); + + selCollector.on('collect', async (sel: any) => { + const idx = parseInt(sel.values[0]); + const textComp = blockState.components[idx]; + + const modal = new ModalBuilder() + .setCustomId(`edit_thumbnail_modal_${idx}`) + .setTitle('📎 Editar Thumbnail'); + + 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); + + const firstRow = new ActionRowBuilder().addComponents(thumbnailInput); + modal.addComponents(firstRow); + + await sel.update({ content: 'Abriendo modal…', components: [] }); + // @ts-ignore + await i.showModal(modal); + }); + break; + } + case "edit_link_button": { + // Elegir a qué TextDisplay aplicar + const textDisplays = blockState.components + .map((c: any, idx: number) => ({ c, idx })) + .filter(({ c }: any) => c.type === 10); + + if (textDisplays.length === 0) { + await i.deferReply({ flags: 64 }); + // @ts-ignore + await i.editReply({ content: "❌ Necesitas al menos un componente de texto para añadir un botón link." }); + break; + } + + const options = textDisplays.map(({ c, idx }: any) => ({ + label: `Texto #${idx + 1}: ${c.content?.slice(0, 30) || '...'}`, + value: String(idx), + description: c.linkButton ? 'Con botón link' : c.thumbnail ? 'Con thumbnail' : 'Sin accesorio' + })); + + // @ts-ignore + const reply = await i.reply({ + flags: 64, + content: "Elige el TextDisplay donde agregar/editar el botón link:", + components: [ + { type: 1, components: [ { type: 3, custom_id: 'choose_text_for_linkbtn', placeholder: 'Selecciona un bloque de texto', options } ] } + ], + fetchReply: true + }); + + // @ts-ignore + const selCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + max: 1, + time: 60000, + filter: (it: any) => it.user.id === message.author.id + }); + + selCollector.on('collect', async (sel: any) => { + const idx = parseInt(sel.values[0]); + const textComp = blockState.components[idx]; + + // Regla de exclusividad + if (textComp.thumbnail) { + await sel.update({ content: '❌ Este bloque ya tiene un thumbnail. Elimínalo antes de añadir un botón link.', components: [] }); + return; + } + + if (textComp.linkButton) { + // @ts-ignore + const sub = await i.followUp({ + flags: 64, + content: `Texto #${idx + 1}: ya tiene botón link. ¿Qué deseas hacer?`, + components: [ + { type: 1, components: [ + { type: 2, style: ButtonStyle.Primary, label: '✏️ Editar', custom_id: `edit_link_button_modal_${idx}` }, + { type: 2, style: ButtonStyle.Danger, label: '🗑️ Eliminar', custom_id: `delete_link_button_${idx}` } + ]} + ], + fetchReply: true + }); + + // @ts-ignore + const btnCollector = sub.createMessageComponentCollector({ + componentType: ComponentType.Button, + max: 1, + time: 60000, + filter: (b: any) => b.user.id === message.author.id + }); + + btnCollector.on('collect', async (b: any) => { + if (b.customId.startsWith('edit_link_button_modal_')) { + const modal = new ModalBuilder() + .setCustomId(`edit_link_button_modal_${idx}`) + .setTitle('🔗 Editar Botón Link'); + + const urlInput = new TextInputBuilder() + .setCustomId('link_url_input') + .setLabel('URL del botón (obligatoria)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com') + .setValue(textComp.linkButton?.url || '') + .setMaxLength(2000) + .setRequired(true); + + const labelInput = new TextInputBuilder() + .setCustomId('link_label_input') + .setLabel('Etiqueta (opcional)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('Texto del botón o vacío para usar solo emoji') + .setValue(textComp.linkButton?.label || '') + .setMaxLength(80) + .setRequired(false); + + const emojiInput = new TextInputBuilder() + .setCustomId('link_emoji_input') + .setLabel('Emoji (opcional)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('Ej: 🔗 o <:name:id>') + .setValue(textComp.linkButton?.emoji || '') + .setMaxLength(64) + .setRequired(false); + + const r1 = new ActionRowBuilder().addComponents(urlInput); + const r2 = new ActionRowBuilder().addComponents(labelInput); + const r3 = new ActionRowBuilder().addComponents(emojiInput); + modal.addComponents(r1, r2, r3); + + await b.update({ content: 'Abriendo modal…', components: [] }); + // @ts-ignore + await i.showModal(modal); + } else if (b.customId.startsWith('delete_link_button_')) { + delete textComp.linkButton; + await b.update({ content: '✅ Botón link eliminado.', components: [] }); + await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] }); + } + }); + } else { + const modal = new ModalBuilder() + .setCustomId(`create_link_button_modal_${idx}`) + .setTitle('🔗 Crear Botón Link'); + + const urlInput = new TextInputBuilder() + .setCustomId('link_url_input') + .setLabel('URL del botón (obligatoria)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com') + .setMaxLength(2000) + .setRequired(true); + + const labelInput = new TextInputBuilder() + .setCustomId('link_label_input') + .setLabel('Etiqueta (opcional)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('Texto del botón o vacío para usar solo emoji') + .setMaxLength(80) + .setRequired(false); + + const emojiInput = new TextInputBuilder() + .setCustomId('link_emoji_input') + .setLabel('Emoji (opcional)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('Ej: 🔗 o <:name:id>') + .setMaxLength(64) + .setRequired(false); + + const r1 = new ActionRowBuilder().addComponents(urlInput); + const r2 = new ActionRowBuilder().addComponents(labelInput); + const r3 = new ActionRowBuilder().addComponents(emojiInput); + modal.addComponents(r1, r2, r3); + + await sel.update({ content: 'Abriendo modal…', components: [] }); + // @ts-ignore + await i.showModal(modal); + } + }); - //@ts-ignore - await i.showModal(modal); break; } } @@ -841,150 +1096,151 @@ export const command: CommandMessage = { const modalHandler = async (interaction: any) => { if (!interaction.isModalSubmit()) return; if (interaction.user.id !== message.author.id) return; - if (!interaction.customId.endsWith('_modal')) return; + // Quitamos la restricción de endsWith('_modal') para permitir IDs dinámicos con índice 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.', 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; + const id = interaction.customId as string; + if (id === 'edit_title_modal') { + blockState.title = interaction.fields.getTextInputValue('title_input'); + await interaction.reply({ content: '✅ Título actualizado.', flags: 64 }); + } else if (id === 'edit_description_modal') { + const newDescription = interaction.fields.getTextInputValue('description_input'); + const firstText = blockState.components.find((c: any) => c.type === 10); + if (firstText) firstText.content = newDescription; else blockState.components.push({ type: 10, content: newDescription, thumbnail: null }); + await interaction.reply({ content: '✅ Descripción actualizada.', flags: 64 }); + } else if (id === '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 { - 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 }); + await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', flags: 64 }); 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); - - // Validar estructura básica - 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: + await interaction.reply({ content: '✅ Color actualizado.', flags: 64 }); + } else if (id === '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: 64 }); + } else if (id === '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: 64 }); + } else { + await interaction.reply({ content: '❌ URL de imagen inválida.', flags: 64 }); return; + } + } else if (id === 'add_cover_modal' || id === '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: 64 }); + } else { + await interaction.reply({ content: '❌ URL de portada inválida.', flags: 64 }); + return; + } + } else if (id === '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: 64 }); + } else if (id.startsWith('edit_thumbnail_modal_')) { + const idx = parseInt(id.replace('edit_thumbnail_modal_', '')); + const textComp = blockState.components[idx]; + if (!textComp || textComp.type !== 10) return; + const thumbnailUrl = interaction.fields.getTextInputValue('thumbnail_input'); + if (thumbnailUrl.trim() === '') { + textComp.thumbnail = null; + await interaction.reply({ content: '✅ Thumbnail eliminado.', flags: 64 }); + } else if (!isValidUrl(thumbnailUrl)) { + await interaction.reply({ content: '❌ URL de thumbnail inválida.', flags: 64 }); + return; + } else { + if (textComp.linkButton) { + await interaction.reply({ content: '❌ Este bloque ya tiene un botón link. Elimina el botón antes de añadir thumbnail.', flags: 64 }); + return; + } + textComp.thumbnail = thumbnailUrl; + await interaction.reply({ content: '✅ Thumbnail actualizado.', flags: 64 }); + } + } else if (id.startsWith('create_link_button_modal_') || id.startsWith('edit_link_button_modal_')) { + const idx = parseInt(id.replace('create_link_button_modal_', '').replace('edit_link_button_modal_', '')); + const textComp = blockState.components[idx]; + if (!textComp || textComp.type !== 10) return; + + const url = interaction.fields.getTextInputValue('link_url_input'); + const label = (interaction.fields.getTextInputValue('link_label_input') || '').trim(); + const emojiStr = (interaction.fields.getTextInputValue('link_emoji_input') || '').trim(); + + if (!isValidUrl(url)) { + await interaction.reply({ content: '❌ URL inválida para el botón.', flags: 64 }); + return; + } + + const parsedEmoji = parseEmojiInput(emojiStr || undefined); + if (!label && !parsedEmoji) { + await interaction.reply({ content: '❌ Debes proporcionar al menos una etiqueta o un emoji.', flags: 64 }); + return; + } + + if (textComp.thumbnail) { + await interaction.reply({ content: '❌ Este bloque tiene thumbnail. Elimínalo antes de añadir un botón link.', flags: 64 }); + return; + } + + textComp.linkButton = { + url, + label: label || undefined, + // Guardamos el string original; se parsea en render/build + emoji: emojiStr || undefined + }; + + await interaction.reply({ content: '✅ Botón link actualizado.', flags: 64 }); + } else if (id === '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 + }; + for (const comp of blockState.components) { + if (comp?.type === 10 && comp.linkButton && comp.thumbnail) { + delete comp.thumbnail; // priorizamos linkButton + } + } + await interaction.reply({ content: '✅ JSON importado correctamente.', flags: 64 }); + } else { + await interaction.reply({ content: '❌ Estructura JSON inválida.', flags: 64 }); + return; + } + } catch { + await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', flags: 64 }); + return; + } + } else { + return; } - // Actualizar la vista previa después de cada cambio en el modal con mejor manejo de errores + // Actualizar vista previa tras cada modal setTimeout(async () => { - if (!modalHandlerActive) return; // Evitar actualizar si ya no está activo - + if (!modalHandlerActive) return; 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)] - }); + if (!messageExists) return; + await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] }); } catch (error: any) { - // Manejar diferentes tipos de errores if (error.code === 10008) { console.log('Mensaje del editor eliminado'); } else if (error.code === 10062) { @@ -993,18 +1249,15 @@ export const command: CommandMessage = { console.error('Error actualizando preview:', error.message || error); } } - }, 1000); + }, 500); } catch (error: any) { console.error('Error en modal:', error); try { - // 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 }); + await interaction.reply({ content: '❌ Error procesando el modal.', flags: 64 }); } - } catch (replyError) { - console.log('No se pudo responder a la interacción (probablemente expirada)'); - } + } catch {} } }; diff --git a/src/commands/messages/alliaces/editEmbedv2.ts b/src/commands/messages/alliaces/editEmbedv2.ts index 7719037..80a9c13 100644 --- a/src/commands/messages/alliaces/editEmbedv2.ts +++ b/src/commands/messages/alliaces/editEmbedv2.ts @@ -1,7 +1,7 @@ import { CommandMessage } from "../../../core/types/commands"; // @ts-ignore import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags } from "discord.js"; -import { replaceVars, isValidUrlOrVariable } from "../../../core/lib/vars"; +import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars"; /** * Botones de edición - VERSIÓN MEJORADA @@ -23,6 +23,7 @@ const btns = (disabled = false) => ([ { 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.Secondary, type: 2, label: "🔗 Crear Botón Link", disabled, custom_id: "edit_link_button" }, { style: ButtonStyle.Primary, type: 2, label: "🔄 Mover", disabled, custom_id: "move_block" }, { style: ButtonStyle.Danger, type: 2, label: "🗑️ Eliminar", disabled, custom_id: "delete_block" } ] @@ -56,107 +57,93 @@ const isValidUrl = isValidUrlOrVariable; */ 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) + "..."; - } - + const cleaned = content.trim(); + if (cleaned.length === 0) return "Sin contenido"; + if (cleaned.length > 4000) return cleaned.substring(0, 3997) + "..."; return cleaned; }; +// Parseo de emoji (Unicode o personalizado /<:name:id>) +const parseEmojiInput = (input?: string): any | null => { + if (!input) return null; + const trimmed = input.trim(); + if (!trimmed) return null; + const match = trimmed.match(/^<(a?):(\w+):(\d+)>$/); + if (match) { + const animated = match[1] === 'a'; + const name = match[2]; + const id = match[3]; + return { id, name, animated }; + } + return { name: trimmed }; +}; + +// Construcción de accesorio botón link +const buildLinkAccessory = async (link: any, member: any, guild: any) => { + if (!link || !link.url) return null; + // @ts-ignore + const processedUrl = await replaceVars(link.url, member, guild); + if (!isValidUrl(processedUrl)) return null; + + const accessory: any = { type: 2, style: ButtonStyle.Link, url: processedUrl }; + if (link.label && typeof link.label === 'string' && link.label.trim()) accessory.label = link.label.trim().slice(0, 80); + if (link.emoji && typeof link.emoji === 'string') { + const parsed = parseEmojiInput(link.emoji); + if (parsed) accessory.emoji = parsed; + } + if (!accessory.label && !accessory.emoji) return null; + return accessory; +}; + /** * Generar vista previa */ const renderPreview = async (blockState: any, member: any, guild: any) => { - const previewComponents = []; + const previewComponents = [] as any[]; - // Añadir imagen de portada primero si existe if (blockState.coverImage && isValidUrl(blockState.coverImage)) { //@ts-ignore const processedCoverUrl = await replaceVars(blockState.coverImage, member, guild); if (isValidUrl(processedCoverUrl)) { - previewComponents.push({ - type: 12, - items: [{ media: { url: processedCoverUrl } }] - }); + previewComponents.push({ type: 12, items: [{ media: { url: processedCoverUrl } }] }); } } - // 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, - content: validateContent(processedTitle) - }); + previewComponents.push({ type: 10, content: validateContent(processedTitle) }); - // Procesar componentes en orden for (const c of blockState.components) { if (c.type === 10) { - // 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 - previewComponents.push({ - type: 9, - components: [ - { - type: 10, - content: validatedContent - } - ], - accessory: { - type: 11, - media: { url: processedThumbnail } - } - }); + let accessory: any = null; + if (c.linkButton) accessory = await buildLinkAccessory(c.linkButton, member, guild); + if (!accessory && processedThumbnail && isValidUrl(processedThumbnail)) { + accessory = { type: 11, media: { url: processedThumbnail } }; + } + + if (accessory) { + previewComponents.push({ type: 9, components: [{ type: 10, content: validatedContent }], accessory }); } else { - // Sin thumbnail o thumbnail inválido, componente normal - previewComponents.push({ - type: 10, - content: validatedContent - }); + previewComponents.push({ type: 10, content: validatedContent }); } } else if (c.type === 14) { - // Separador - previewComponents.push({ - type: 14, - divider: c.divider ?? true, - spacing: c.spacing ?? 1 - }); + previewComponents.push({ type: 14, divider: c.divider ?? true, spacing: c.spacing ?? 1 }); } else if (c.type === 12) { - // Imagen - validar URL también //@ts-ignore const processedImageUrl = await replaceVars(c.url, member, guild); - - if (isValidUrl(processedImageUrl)) { - previewComponents.push({ - type: 12, - items: [{ media: { url: processedImageUrl } }] - }); - } + if (isValidUrl(processedImageUrl)) previewComponents.push({ type: 12, items: [{ media: { url: processedImageUrl } }] }); } } - return { - type: 17, - accent_color: blockState.color ?? null, - components: previewComponents - }; + return { type: 17, accent_color: blockState.color ?? null, components: previewComponents }; }; export const command: CommandMessage = { @@ -176,7 +163,6 @@ export const command: CommandMessage = { return; } - // Buscar el bloque existente const existingBlock = await client.prisma.blockV2Config.findFirst({ where: { guildId: message.guild!.id, name: blockName } }); @@ -186,7 +172,6 @@ export const command: CommandMessage = { return; } - // Estado inicial basado en el bloque existente let blockState: any = { title: existingBlock.config.title || `Block: ${blockName}`, color: existingBlock.config.color || null, @@ -206,30 +191,26 @@ export const command: CommandMessage = { "*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, + flags: 4096, components: [ await renderPreview(blockState, message.member, message.guild), ...btns(false) ] }); - const collector = editorMessage.createMessageComponentCollector({ - time: 3600000 // 1 hora - }); + const collector = editorMessage.createMessageComponentCollector({ time: 3600000 }); collector.on("collect", async (i: any) => { if (i.user.id !== message.author.id) { - await i.reply({ content: "No puedes usar este menú.", flags: MessageFlags.Ephemeral }); + await i.reply({ content: "No puedes usar este menú.", flags: 64 }); return; } - // --- BOTONES --- if (i.isButton()) { switch (i.customId) { case "save_block": { @@ -260,325 +241,196 @@ export const command: CommandMessage = { return; } case "edit_title": { - // Crear modal para editar título - const modal = new ModalBuilder() - .setCustomId('edit_title_modal') - .setTitle('📝 Editar Título del Block'); - + 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); - + .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 modal = new ModalBuilder() - .setCustomId('edit_description_modal') - .setTitle('📄 Editar Descripción'); - + 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); - + .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 modal = new ModalBuilder() - .setCustomId('edit_color_modal') - .setTitle('🎨 Editar Color del Block'); - + 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); - + .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 modal = new ModalBuilder() - .setCustomId('add_content_modal') - .setTitle('➕ Agregar Nuevo Contenido'); - + 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); - + .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 await i.showModal(modal); break; } case "add_image": { - const modal = new ModalBuilder() - .setCustomId('add_image_modal') - .setTitle('🖼️ Agregar Nueva Imagen'); - + 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); - + .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.reply({ - flags: MessageFlags.Ephemeral, + flags: 64, 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_modal" }, - { type: 2, style: ButtonStyle.Danger, label: "🗑️ Eliminar", custom_id: "delete_cover" } - ] - } - ], + components: [{ type: 1, components: [ + { type: 2, style: ButtonStyle.Primary, label: "✏️ Editar", custom_id: "edit_cover_modal" }, + { type: 2, style: ButtonStyle.Danger, label: "🗑️ Eliminar", custom_id: "delete_cover" } + ]}], fetchReply: true }); - //@ts-ignore - const coverCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.Button, - max: 1, - time: 60000, - filter: (b: any) => b.user.id === message.author.id - }); - + const coverCollector = reply.createMessageComponentCollector({ componentType: ComponentType.Button, max: 1, time: 60000, filter: (b: any) => b.user.id === message.author.id }); coverCollector.on("collect", async (b: any) => { 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 modal = new ModalBuilder().setCustomId('edit_cover_modal').setTitle('🖼️ Editar 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') - .setValue(blockState.coverImage || '') - .setMaxLength(2000) - .setRequired(true); - + .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); 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: [] }); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); + await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] }); } coverCollector.stop(); }); } else { - // No tiene portada, crear modal para añadir nueva - const modal = new ModalBuilder() - .setCustomId('add_cover_modal') - .setTitle('🖼️ Agregar Imagen de Portada'); - + 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); - + .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 "add_separator": { - const modal = new ModalBuilder() - .setCustomId('add_separator_modal') - .setTitle('➖ Agregar Separador'); - + const modal = new ModalBuilder().setCustomId('add_separator_modal').setTitle('➖ Agregar Separador'); 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); - + .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); - + .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 }); + // Listado de TextDisplays para elegir + const textDisplays = blockState.components.map((c: any, idx: number) => ({ c, idx })).filter(({ c }: any) => c.type === 10); + if (textDisplays.length === 0) { + await i.deferReply({ flags: 64 }); //@ts-ignore - await i.editReply({ - content: "❌ Necesitas al menos un componente de texto para añadir thumbnail." - }); + await i.editReply({ content: "❌ No hay bloques de texto para editar thumbnail." }); break; } - - const modal = new ModalBuilder() - .setCustomId('edit_thumbnail_modal') - .setTitle('📎 Editar Thumbnail'); - - 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); - - const firstRow = new ActionRowBuilder().addComponents(thumbnailInput); - modal.addComponents(firstRow); - + const options = textDisplays.map(({ c, idx }: any) => ({ + label: `Texto #${idx + 1}: ${c.content?.slice(0, 30) || '...'}`, + value: String(idx), + description: c.thumbnail ? 'Con thumbnail' : c.linkButton ? 'Con botón link' : 'Sin accesorio' + })); //@ts-ignore - await i.showModal(modal); + const reply = await i.reply({ + flags: 64, + content: "Elige el TextDisplay a editar su thumbnail:", + components: [{ type: 1, components: [{ type: 3, custom_id: 'choose_text_for_thumbnail', placeholder: 'Selecciona un bloque de texto', options }] }], + fetchReply: true + }); + //@ts-ignore + const selCollector = reply.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === message.author.id }); + selCollector.on('collect', async (sel: any) => { + const idx = parseInt(sel.values[0]); + const textComp = blockState.components[idx]; + const modal = new ModalBuilder().setCustomId(`edit_thumbnail_modal_${idx}`).setTitle('📎 Editar Thumbnail'); + 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); + const firstRow = new ActionRowBuilder().addComponents(thumbnailInput); + modal.addComponents(firstRow); + await sel.update({ content: 'Abriendo modal…', components: [] }); + //@ts-ignore + await i.showModal(modal); + }); break; } case "move_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}`, + 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" + c.type === 10 && (c.thumbnail || c.linkButton) + ? (c.thumbnail ? "Con thumbnail" : "Con botón link") : undefined })); - //@ts-ignore const reply = await i.reply({ - flags: MessageFlags.Ephemeral, + flags: 64, content: "Selecciona el bloque que quieres mover:", - components: [ - { - type: 1, - components: [ - { type: 3, custom_id: "move_block_select", placeholder: "Elige un bloque", options } - ] - } - ], + components: [{ type: 1, components: [{ type: 3, custom_id: "move_block_select", placeholder: "Elige un bloque", options }] }], fetchReply: true }); - //@ts-ignore - const selCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (it: any) => it.user.id === message.author.id - }); - + const selCollector = reply.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === message.author.id }); selCollector.on("collect", async (sel: any) => { const idx = parseInt(sel.values[0]); - await sel.update({ content: "¿Quieres mover este bloque?", - components: [ - { - type: 1, - components: [ - { type: 2, style: ButtonStyle.Secondary, label: "⬆️ Subir", custom_id: `move_up_${idx}`, disabled: idx === 0 }, - { type: 2, style: ButtonStyle.Secondary, label: "⬇️ Bajar", custom_id: `move_down_${idx}`, disabled: idx === blockState.components.length - 1 } - ] - } - ] + components: [{ type: 1, components: [ + { type: 2, style: ButtonStyle.Secondary, label: "⬆️ Subir", custom_id: `move_up_${idx}`, disabled: idx === 0 }, + { type: 2, style: ButtonStyle.Secondary, label: "⬇️ Bajar", custom_id: `move_down_${idx}`, disabled: idx === blockState.components.length - 1 } + ] }] }); - //@ts-ignore - const btnCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.Button, - max: 1, - time: 60000, - filter: (b: any) => b.user.id === message.author.id - }); - + const btnCollector = reply.createMessageComponentCollector({ componentType: ComponentType.Button, max: 1, time: 60000, filter: (b: any) => b.user.id === message.author.id }); btnCollector.on("collect", async (b: any) => { if (b.customId.startsWith("move_up_")) { const i2 = parseInt(b.customId.replace("move_up_", "")); - if (i2 > 0) { + if (!(i2 <= 0)) { const item = blockState.components[i2]; blockState.components.splice(i2, 1); blockState.components.splice(i2 - 1, 0, item); @@ -586,92 +438,51 @@ export const command: CommandMessage = { await b.update({ content: "✅ Bloque movido arriba.", components: [] }); } else if (b.customId.startsWith("move_down_")) { const i2 = parseInt(b.customId.replace("move_down_", "")); - if (i2 < blockState.components.length - 1) { + if (!(i2 >= blockState.components.length - 1)) { const item = blockState.components[i2]; blockState.components.splice(i2, 1); blockState.components.splice(i2 + 1, 0, item); } await b.update({ content: "✅ Bloque movido abajo.", components: [] }); } - - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - + await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] }); btnCollector.stop(); selCollector.stop(); }); }); - break; } case "delete_block": { - // Incluir portada en las opciones si existe - const options = []; - - // Añadir portada como opción si existe - if (blockState.coverImage) { - options.push({ - label: "🖼️ Imagen de Portada", - value: "cover_image", - description: "Imagen principal del bloque" - }); - } - - // Añadir componentes regulares + const options = [] as any[]; + if (blockState.coverImage) options.push({ label: "🖼️ Imagen de Portada", value: "cover_image", description: "Imagen principal del bloque" }); blockState.components.forEach((c: any, idx: number) => { options.push({ label: - c.type === 10 - ? `Texto: ${c.content?.slice(0, 30) || "..."}` - : c.type === 14 - ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` - : c.type === 12 - ? `Imagen: ${c.url?.slice(-30) || "..."}` - : `Componente ${c.type}`, + c.type === 10 ? `Texto: ${c.content?.slice(0, 30) || "..."}` + : c.type === 14 ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` + : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}` + : `Componente ${c.type}`, value: idx.toString(), - description: - c.type === 10 && c.thumbnail - ? "Con thumbnail" - : undefined + description: c.type === 10 && (c.thumbnail || c.linkButton) ? (c.thumbnail ? "Con thumbnail" : "Con botón link") : undefined }); }); - if (options.length === 0) { - await i.deferReply({ flags: MessageFlags.Ephemeral }); + await i.deferReply({ flags: 64 }); //@ts-ignore - await i.editReply({ - content: "❌ No hay elementos para eliminar." - }); + await i.editReply({ content: "❌ No hay elementos para eliminar." }); break; } - //@ts-ignore const reply = await i.reply({ - flags: MessageFlags.Ephemeral, + flags: 64, content: "Selecciona el elemento que quieres eliminar:", - components: [ - { - type: 1, - components: [ - { type: 3, custom_id: "delete_block_select", placeholder: "Elige un elemento", options } - ] - } - ], + components: [{ type: 1, components: [{ type: 3, custom_id: "delete_block_select", placeholder: "Elige un elemento", options }] }], fetchReply: true }); - //@ts-ignore - const selCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (it: any) => it.user.id === message.author.id - }); - + const selCollector = reply.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === message.author.id }); selCollector.on("collect", async (sel: any) => { const selectedValue = sel.values[0]; - if (selectedValue === "cover_image") { blockState.coverImage = null; await sel.update({ content: "✅ Imagen de portada eliminada.", components: [] }); @@ -680,146 +491,187 @@ export const command: CommandMessage = { blockState.components.splice(idx, 1); await sel.update({ content: "✅ Elemento eliminado.", components: [] }); } - - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - + await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] }); selCollector.stop(); }); - break; } case "show_variables": { - await i.deferReply({ flags: MessageFlags.Ephemeral }); + await i.deferReply({ flags: 64 }); + const vars = listVariables(); + if (vars.length === 0) { + //@ts-ignore + await i.editReply({ content: "No hay variables registradas." }); + break; + } + const chunks: string[] = []; + let buf = ""; + for (const v of vars) { + const line = `• ${v}\n`; + if ((buf + line).length > 1800) { chunks.push(buf); buf = line; } else { buf += line; } + } + if (buf) chunks.push(buf); //@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\n\n" + - "💡 **Nota:** Las variables se usan SIN llaves `{}` en los campos de URL/imagen." - }); + await i.editReply({ content: `📋 **Variables Disponibles:**\n\n${chunks[0]}` }); + for (let k = 1; k < chunks.length; k++) { + //@ts-ignore + await i.followUp({ flags: 64, content: chunks[k] }); + } 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}`, + : 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 + description: c.type === 10 && (c.thumbnail || c.linkButton) ? (c.thumbnail ? "Con thumbnail" : "Con botón link") : undefined })); - if (options.length === 0) { - await i.deferReply({ flags: MessageFlags.Ephemeral }); + await i.deferReply({ flags: 64 }); //@ts-ignore await i.editReply({ content: "❌ No hay elementos para duplicar." }); break; } - //@ts-ignore const reply = await i.reply({ - flags: MessageFlags.Ephemeral, + flags: 64, content: "Selecciona el elemento que quieres duplicar:", - components: [{ - type: 1, - components: [{ - type: 3, - custom_id: "duplicate_select", - placeholder: "Elige un elemento", - options - }] - }], + 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 - }); - + 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)] - }); + 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}\`\`\`` - }); + await i.reply({ flags: 64, content: `\`\`\`json\n${truncated}\n\`\`\`` }); break; } case "import_json": { - const modal = new ModalBuilder() - .setCustomId('import_json_modal') - .setTitle('📥 Importar 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); - + .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.` + await i.reply({ flags: 64, content: `📤 **JSON Exportado:**\n\`\`\`json\n${truncatedJson}\n\`\`\`\n\n💡 **Tip:** Copia el JSON de arriba manualmente y pégalo donde necesites.` }); + break; + } + case "edit_link_button": { + // Elegir TextDisplay para botón link + const textDisplays = blockState.components.map((c: any, idx: number) => ({ c, idx })).filter(({ c }: any) => c.type === 10); + if (textDisplays.length === 0) { + await i.deferReply({ flags: 64 }); + //@ts-ignore + await i.editReply({ content: "❌ Necesitas al menos un componente de texto para añadir un botón link." }); + break; + } + const options = textDisplays.map(({ c, idx }: any) => ({ + label: `Texto #${idx + 1}: ${c.content?.slice(0, 30) || '...'}`, + value: String(idx), + description: c.linkButton ? 'Con botón link' : c.thumbnail ? 'Con thumbnail' : 'Sin accesorio' + })); + //@ts-ignore + const reply = await i.reply({ + flags: 64, + content: "Elige el TextDisplay donde agregar/editar el botón link:", + components: [{ type: 1, components: [{ type: 3, custom_id: 'choose_text_for_linkbtn', placeholder: 'Selecciona un bloque de texto', options }] }], + fetchReply: true + }); + //@ts-ignore + const selCollector = reply.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === message.author.id }); + selCollector.on('collect', async (sel: any) => { + const idx = parseInt(sel.values[0]); + const textComp = blockState.components[idx]; + if (textComp.thumbnail) { + await sel.update({ content: '❌ Este bloque ya tiene un thumbnail. Elimínalo antes de añadir un botón link.', components: [] }); + return; + } + if (textComp.linkButton) { + //@ts-ignore + const sub = await i.followUp({ + flags: 64, + content: `Texto #${idx + 1}: ya tiene botón link. ¿Qué deseas hacer?`, + components: [{ type: 1, components: [ + { type: 2, style: ButtonStyle.Primary, label: '✏️ Editar', custom_id: `edit_link_button_modal_${idx}` }, + { type: 2, style: ButtonStyle.Danger, label: '🗑️ Eliminar', custom_id: `delete_link_button_${idx}` } + ] }], + fetchReply: true + }); + //@ts-ignore + const btnCollector = sub.createMessageComponentCollector({ componentType: ComponentType.Button, max: 1, time: 60000, filter: (b: any) => b.user.id === message.author.id }); + btnCollector.on('collect', async (b: any) => { + if (b.customId.startsWith('edit_link_button_modal_')) { + const modal = new ModalBuilder().setCustomId(`edit_link_button_modal_${idx}`).setTitle('🔗 Editar Botón Link'); + const urlInput = new TextInputBuilder() + .setCustomId('link_url_input').setLabel('URL del botón (obligatoria)').setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com').setValue(textComp.linkButton?.url || '').setMaxLength(2000).setRequired(true); + const labelInput = new TextInputBuilder() + .setCustomId('link_label_input').setLabel('Etiqueta (opcional)').setStyle(TextInputStyle.Short) + .setPlaceholder('Texto del botón o vacío para usar solo emoji').setValue(textComp.linkButton?.label || '').setMaxLength(80).setRequired(false); + const emojiInput = new TextInputBuilder() + .setCustomId('link_emoji_input').setLabel('Emoji (opcional)').setStyle(TextInputStyle.Short) + .setPlaceholder('Ej: 🔗 o <:name:id>').setValue(textComp.linkButton?.emoji || '').setMaxLength(64).setRequired(false); + const r1 = new ActionRowBuilder().addComponents(urlInput); + const r2 = new ActionRowBuilder().addComponents(labelInput); + const r3 = new ActionRowBuilder().addComponents(emojiInput); + modal.addComponents(r1, r2, r3); + await b.update({ content: 'Abriendo modal…', components: [] }); + //@ts-ignore + await i.showModal(modal); + } else if (b.customId.startsWith('delete_link_button_')) { + delete textComp.linkButton; + await b.update({ content: '✅ Botón link eliminado.', components: [] }); + await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] }); + } + }); + } else { + const modal = new ModalBuilder().setCustomId(`create_link_button_modal_${idx}`).setTitle('🔗 Crear Botón Link'); + const urlInput = new TextInputBuilder() + .setCustomId('link_url_input').setLabel('URL del botón (obligatoria)').setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com').setMaxLength(2000).setRequired(true); + const labelInput = new TextInputBuilder() + .setCustomId('link_label_input').setLabel('Etiqueta (opcional)').setStyle(TextInputStyle.Short) + .setPlaceholder('Texto del botón o vacío para usar solo emoji').setMaxLength(80).setRequired(false); + const emojiInput = new TextInputBuilder() + .setCustomId('link_emoji_input').setLabel('Emoji (opcional)').setStyle(TextInputStyle.Short) + .setPlaceholder('Ej: 🔗 o <:name:id>').setMaxLength(64).setRequired(false); + const r1 = new ActionRowBuilder().addComponents(urlInput); + const r2 = new ActionRowBuilder().addComponents(labelInput); + const r3 = new ActionRowBuilder().addComponents(emojiInput); + modal.addComponents(r1, r2, r3); + await sel.update({ content: 'Abriendo modal…', components: [] }); + //@ts-ignore + await i.showModal(modal); + } }); break; } } - // Actualizar vista previa después de cada acción try { - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); + 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'); @@ -830,189 +682,127 @@ export const command: CommandMessage = { } }); - // 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; + const id = interaction.customId as string; + if (id === 'edit_title_modal') { + blockState.title = interaction.fields.getTextInputValue('title_input'); + await interaction.reply({ content: '✅ Título actualizado.', flags: 64 }); + } else if (id === '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: 64 }); + } else if (id === '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: 64 }); return; } } - 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; + await interaction.reply({ content: '✅ Color actualizado.', flags: 64 }); + } else if (id === '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: 64 }); + } else if (id === '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: 64 }); } + else { await interaction.reply({ content: '❌ URL de imagen inválida.', flags: 64 }); return; } + } else if (id === 'add_cover_modal' || id === '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: 64 }); } + else { await interaction.reply({ content: '❌ URL de portada inválida.', flags: 64 }); return; } + } else if (id === '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: 64 }); + } else if (id.startsWith('edit_thumbnail_modal_')) { + const idx = parseInt(id.replace('edit_thumbnail_modal_', '')); + const textComp = blockState.components[idx]; + if (!textComp || textComp.type !== 10) return; + const thumbnailUrl = interaction.fields.getTextInputValue('thumbnail_input'); + if (thumbnailUrl.trim() === '') { textComp.thumbnail = null; await interaction.reply({ content: '✅ Thumbnail eliminado.', flags: 64 }); } + else if (!isValidUrl(thumbnailUrl)) { await interaction.reply({ content: '❌ URL de thumbnail inválida.', flags: 64 }); return; } + else { + if (textComp.linkButton) { await interaction.reply({ content: '❌ Este bloque ya tiene un botón link. Elimínalo antes de añadir thumbnail.', flags: 64 }); return; } + textComp.thumbnail = thumbnailUrl; await interaction.reply({ content: '✅ Thumbnail actualizado.', flags: 64 }); } - 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; + } else if (id.startsWith('create_link_button_modal_') || id.startsWith('edit_link_button_modal_')) { + const idx = parseInt(id.replace('create_link_button_modal_', '').replace('edit_link_button_modal_', '')); + const textComp = blockState.components[idx]; + if (!textComp || textComp.type !== 10) return; + const url = interaction.fields.getTextInputValue('link_url_input'); + const label = (interaction.fields.getTextInputValue('link_label_input') || '').trim(); + const emojiStr = (interaction.fields.getTextInputValue('link_emoji_input') || '').trim(); + if (!isValidUrl(url)) { await interaction.reply({ content: '❌ URL inválida para el botón.', flags: 64 }); return; } + const parsedEmoji = parseEmojiInput(emojiStr || undefined); + if (!label && !parsedEmoji) { await interaction.reply({ content: '❌ Debes proporcionar al menos una etiqueta o un emoji.', flags: 64 }); return; } + if (textComp.thumbnail) { await interaction.reply({ content: '❌ Este bloque tiene thumbnail. Elimínalo antes de añadir un botón link.', flags: 64 }); return; } + textComp.linkButton = { url, label: label || undefined, emoji: emojiStr || undefined }; + await interaction.reply({ content: '✅ Botón link actualizado.', flags: 64 }); + } else if (id === '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 + }; + for (const comp of blockState.components) { + if (comp?.type === 10 && comp.linkButton && comp.thumbnail) delete comp.thumbnail; } - } - 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'; + await interaction.reply({ content: '✅ JSON importado correctamente.', flags: 64 }); + } else { await interaction.reply({ content: '❌ Estructura JSON inválida.', flags: 64 }); return; } + } catch { await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', flags: 64 }); return; } + } else { return; } - 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)] - }); + 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); - } + 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); + }, 500); } 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 }); + await interaction.reply({ content: '❌ Error procesando el modal.', flags: 64 }); } - } catch (replyError) { - console.log('No se pudo responder a la interacción (probablemente expirada)'); - } + } catch {} } }; - // 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") { try { const messageExists = await editorMessage.fetch().catch(() => null); if (messageExists) { - await editorMessage.edit({ - components: [ - { type: 17, components: [{ type: 10, content: "⏰ Editor finalizado por inactividad." }] } - ] - }); + 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/listChannels.ts b/src/commands/messages/alliaces/listChannels.ts index 8591edf..f1218d8 100644 --- a/src/commands/messages/alliaces/listChannels.ts +++ b/src/commands/messages/alliaces/listChannels.ts @@ -144,7 +144,9 @@ export const command: CommandMessage = { ); // Agrupar por estado + //@ts-ignore const activeChannels = channelDetails.filter(c => c.status.includes("Activo")); + //@ts-ignore const inactiveChannels = channelDetails.filter(c => c.status.includes("Inactivo")); // Construir embed principal @@ -161,6 +163,7 @@ export const command: CommandMessage = { // Añadir campos de canales activos if (activeChannels.length > 0) { + //@ts-ignore const activeList = activeChannels.slice(0, 10).map(c => `**${c.index}.** ${c.channelName}\n` + `└ \`${c.blockName}\` • ${c.blockStatus}\n` + @@ -179,6 +182,7 @@ export const command: CommandMessage = { // Añadir campos de canales inactivos (si los hay) if (inactiveChannels.length > 0) { + //@ts-ignore const inactiveList = inactiveChannels.slice(0, 5).map(c => `**${c.index}.** ${c.channelName}\n` + `└ \`${c.blockName}\` • ${c.blockStatus}` @@ -197,6 +201,7 @@ export const command: CommandMessage = { mainEmbed.addFields([ { name: "📊 Estadísticas del Servidor", + //@ts-ignore value: `🧩 **Bloques disponibles:** ${availableBlocks}\n📈 **Total puntos otorgados:** ${totalPointsHistory}\n⚡ **Canales más activos:** ${channelDetails.sort((a, b) => b.pointsCount - a.pointsCount).slice(0, 3).map((c, i) => `${i + 1}. ${c.channelName.replace(/[#❌*]/g, '').trim()}`).join(', ') || 'N/A'}`, inline: false } @@ -292,6 +297,7 @@ export const command: CommandMessage = { break; case "show_stats": + //@ts-ignore const detailedStats = channelDetails.map(c => `• ${c.channelName}: **${c.pointsCount}** puntos` ).join('\n'); diff --git a/src/core/lib/vars.ts b/src/core/lib/vars.ts index 484df46..e4ee720 100644 --- a/src/core/lib/vars.ts +++ b/src/core/lib/vars.ts @@ -1,71 +1,143 @@ -import {Guild, Invite, User} from "discord.js"; +import { Guild, Invite, User, GuildMember } from "discord.js"; /** - * Lista de variables válidas del sistema (sin llaves {}) + * Registro central de variables -> resolutores + * Cada clave es el token que aparecerá en el texto (sin llaves), + * y su valor es una función que recibe el contexto y devuelve el string a insertar. */ -export const VALID_VARIABLES = [ - 'user.name', 'user.id', 'user.mention', 'user.avatar', - 'user.pointsAll', 'user.pointsWeekly', 'user.pointsMonthly', - 'guild.name', 'guild.icon', - 'invite.name', 'invite.icon' -]; +type VarCtx = { + user?: User | GuildMember; + guild?: Guild; + stats?: any; + invite?: Invite; +}; -/** - * Validar si una URL es válida o contiene variables del sistema - * @param url - La URL o texto a validar - * @returns boolean - true si es válida - */ -export function isValidUrlOrVariable(url: string): boolean { - if (!url) return false; +type VarResolver = (ctx: VarCtx) => string | Promise; - // Verificar si el texto contiene variables válidas - const hasValidVariables = VALID_VARIABLES.some(variable => url.includes(variable)); - if (hasValidVariables) return true; - - // Si no tiene variables, validar como URL normal +// Helpers seguros para leer datos de usuario/miembro y guild/invite +const getUserId = (u?: User | GuildMember) => (u as any)?.id || (u as any)?.user?.id || ""; +const getUsername = (u?: User | GuildMember) => (u as any)?.username || (u as any)?.user?.username || ""; +const getAvatar = (u?: User | GuildMember) => { try { - new URL(url); - return url.startsWith('http://') || url.startsWith('https://'); + const fn = (u as any)?.displayAvatarURL || (u as any)?.user?.displayAvatarURL; + return typeof fn === 'function' ? fn.call((u as any)?.user ?? u, { forceStatic: false }) : ""; + } catch { return ""; } +}; +const getGuildIcon = (g?: Guild) => { + try { return g?.iconURL({ forceStatic: false }) ?? ""; } catch { return ""; } +}; + +// Construye datos de invite similares a la versión previa +const getInviteObject = (invite?: Invite) => invite?.guild ? { + name: invite.guild.name, + icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : '' +} : null; + +export const VARIABLES: Record = { + // USER INFO + 'user.name': ({ user }) => getUsername(user), + 'user.id': ({ user }) => getUserId(user), + 'user.mention': ({ user }) => { + const id = getUserId(user); + return id ? `<@${id}>` : ''; + }, + 'user.avatar': ({ user }) => getAvatar(user), + + // USER STATS + 'user.pointsAll': ({ stats }) => stats?.totalPoints?.toString?.() ?? '0', + 'user.pointsWeekly': ({ stats }) => stats?.weeklyPoints?.toString?.() ?? '0', + 'user.pointsMonthly': ({ stats }) => stats?.monthlyPoints?.toString?.() ?? '0', + + // GUILD INFO + 'guild.name': ({ guild }) => guild?.name ?? '', + 'guild.icon': ({ guild }) => getGuildIcon(guild), + + // INVITE INFO + 'invite.name': ({ invite }) => getInviteObject(invite)?.name ?? '', + 'invite.icon': ({ invite }) => getInviteObject(invite)?.icon ?? '' +}; + +/** + * Lista de variables válidas del sistema (derivada del registro) + * Exportada por compatibilidad y para UI. + */ +// @ts-ignore + export const VALID_VARIABLES: string[] = Object.freeze(Object.keys(VARIABLES)); + +/** Devuelve la lista actual de variables (no congelada) */ +export function listVariables(): string[] { + return Object.keys(VARIABLES); +} + +/** Escapa una cadena para uso literal dentro de una RegExp */ +const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** + * Validar si un texto es una URL válida o contiene variables del sistema + * Mantiene la semántica previa: true si contiene cualquier token válido o si es http/https válido. + */ +export function isValidUrlOrVariable(text: string): boolean { + if (!text) return false; + + // ¿Contiene alguna variable? + if (VALID_VARIABLES.some(v => text.includes(v))) return true; + + // ¿Es URL http/https válida? + try { + const u = new URL(text); + return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; } } -//@ts-ignore -export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any, invite: Invite | undefined): Promise { - if(!text) return ''; +/** + * Reemplaza variables en un texto usando el registro de VARIABLES. + * Compatible con llamadas existentes: acepta User o GuildMember en el primer parámetro (históricamente llamado "user"). + */ +export async function replaceVars( + text: string, + userOrMember: User | GuildMember | undefined, + guild: Guild | undefined, + stats?: any, + invite?: Invite +): Promise { + if (!text) return ''; - // Crear inviteObject solo si invite existe y tiene guild - const inviteObject = invite?.guild ? { - name: invite.guild.name, - icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : '' - } : null; + const ctx: VarCtx = { user: userOrMember, guild, stats, invite }; - return text - /** - * USER INFO - */ - .replace(/(user\.name)/g, user?.username ?? '') - .replace(/(user\.id)/g, user?.id ?? '') - .replace(/(user\.mention)/g, user ? `<@${user.id}>` : '') - .replace(/(user\.avatar)/g, user?.displayAvatarURL({ forceStatic: false }) ?? '') + // Construimos una única RegExp que contenga todas las claves (sin anchors, para coincidir en cualquier parte) + const keys = Object.keys(VARIABLES); + if (keys.length === 0) return text; + // Ordenar por longitud descendente para evitar falsas coincidencias de prefijos (defensivo) + const keysEscaped = keys.sort((a, b) => b.length - a.length).map(escapeRegex); + const pattern = new RegExp(`(${keysEscaped.join('|')})`, 'g'); - /** - * USER STATS - */ - .replace(/(user\.pointsAll)/g, stats?.totalPoints?.toString() ?? '0') - .replace(/(user\.pointsWeekly)/g, stats?.weeklyPoints?.toString() ?? '0') - .replace(/(user\.pointsMonthly)/g, stats?.monthlyPoints?.toString() ?? '0') + // Reemplazo asíncrono + const parts: (string | Promise)[] = []; + let lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = pattern.exec(text)) !== null) { + const matchStart = m.index; + if (matchStart > lastIndex) parts.push(text.slice(lastIndex, matchStart)); + const token = m[1]; + const resolver = VARIABLES[token]; + if (resolver) { + try { + const value = resolver(ctx); + parts.push(Promise.resolve(value).then(v => (v ?? '').toString())); + } catch { + parts.push(''); + } + } else { + // No debería ocurrir, pero añadimos el literal por seguridad + parts.push(token); + } + lastIndex = pattern.lastIndex; + } + if (lastIndex < text.length) parts.push(text.slice(lastIndex)); - /** - * GUILD INFO - */ - .replace(/(guild\.name)/g, guild?.name ?? '') - .replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? '') - - /** - * INVITE INFO - */ - .replace(/(invite\.name)/g, inviteObject?.name ?? "") - .replace(/(invite\.icon)/g, inviteObject?.icon ?? '') + // Resolver todas las partes (las literales quedan tal cual) + const resolved = await Promise.all(parts.map(p => Promise.resolve(p as any))); + return resolved.join(''); } \ No newline at end of file diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 7da1a79..f588ffd 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -22,7 +22,6 @@ bot.on(Events.MessageCreate, async (message) => { if (!message.content.startsWith(PREFIX)) return; const [cmdName, ...args] = message.content.slice(PREFIX.length).trim().split(/\s+/); - console.log(cmdName); const command = commands.get(cmdName); if (!command) return;