diff --git a/src/.backup/createDisplayComponent.backup.ts b/src/.backup/createDisplayComponent.backup.ts new file mode 100644 index 0000000..bf77e06 --- /dev/null +++ b/src/.backup/createDisplayComponent.backup.ts @@ -0,0 +1,618 @@ +import { + ActionRowBuilder, + ButtonInteraction, + Message, + MessageComponentInteraction, + MessageFlags, + ModalBuilder, TextChannel, + TextInputBuilder, + TextInputStyle, +} from "discord.js"; +import logger from "../../../core/lib/logger"; +import {CommandMessage} from "../../../core/types/commands"; +import {listVariables} from "../../../core/lib/vars"; +import type Amayo from "../../../core/client"; +import {BlockState, DisplayComponentUtils, EditorActionRow} from "../../../core/types/displayComponentEditor"; +import type {DisplayComponentContainer} from "../../../core/types/displayComponents"; + +interface EditorData { + content?: string; + flags?: MessageFlags; + display?: DisplayComponentContainer; + components?: EditorActionRow[]; +} + +// Helper para actualizar el editor combinando Display Container dentro de components (tipado) +async function updateEditor(message: Message, data: EditorData): Promise { + const container = data.display; + const rows = Array.isArray(data.components) ? data.components : []; + const components = container ? [container, ...rows] : rows; + + const payload: any = { ...data }; + delete payload.display; + payload.components = components; + + if (payload.flags === undefined) { + payload.flags = MessageFlags.IsComponentsV2; + } + + await message.edit(payload); +} + +export const command: CommandMessage = { + name: "crear-embed", + type: "message", + aliases: ["embed-crear", "nuevo-embed", "blockcreatev2"], + cooldown: 20, + description: "Crea un nuevo bloque/embedded con editor interactivo (DisplayComponents).", + category: "Alianzas", + usage: "crear-embed ", + run: async (message, args, client) => { + if (!message.member?.permissions.has("Administrator")) { + await message.reply("❌ No tienes permisos de Administrador."); + return; + } + + const blockName = args[0]?.trim(); + if (!blockName) { + await message.reply("Debes proporcionar un nombre. Uso: `!crear-embed `"); + return; + } + + // Check if block name already exists + const existingBlock = await client.prisma.blockV2Config.findFirst({ + where: { + guildId: message.guild!.id, + name: blockName + } + }); + + if (existingBlock) { + await message.reply("❌ Ya existe un bloque con ese nombre!"); + return; + } + + // Estado inicial + let blockState: BlockState = { + title: `Editor de Block: ${blockName}`, + color: 0x5865f2, + coverImage: undefined, + components: [ + { type: 14, divider: false, spacing: 1 }, + { type: 10, content: "Usa los botones para configurar.", thumbnail: null } + ] + }; + + //@ts-ignore + const channelSend: If = message.channel; + if (!channelSend?.isTextBased()) { + await message.reply("❌ This command can only be used in a text-based channel."); + return; + } + + const editorMessage = await channelSend.send({ + content: "⚠️ **IMPORTANTE:** Prepara tus títulos, descripciones y URLs antes de empezar.\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 5 segundos...*" + }); + + // Esperar 5 segundos para que lean el mensaje + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Actualizar para mostrar el editor + await updateEditor(editorMessage, { + content: undefined, + flags: MessageFlags.IsComponentsV2, + display: await DisplayComponentUtils.renderPreview(blockState, message.member!, message.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await handleEditorInteractions(editorMessage, message, client, blockName, blockState); + }, +}; + +async function handleEditorInteractions( + editorMessage: Message, + originalMessage: Message, + client: Amayo, + blockName: string, + blockState: BlockState +): Promise { + const collector = editorMessage.createMessageComponentCollector({ + time: 3600000, // 1 hour + filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id + }); + + collector.on("collect", async (interaction: ButtonInteraction) => { + try { + await handleButtonInteraction( + interaction, + editorMessage, + originalMessage, + client, + blockName, + blockState + ); + } catch (error) { + //@ts-ignore + logger.error("Error handling editor interaction:", error); + if (!interaction.replied && !interaction.deferred) { + await interaction.reply({ + content: "❌ Ocurrió un error al procesar la interacción.", + flags: MessageFlags.Ephemeral + }); + } + } + }); + + collector.on("end", async (_collected, reason) => { + if (reason === "time") { + await handleEditorTimeout(editorMessage); + } + }); +} + +async function handleButtonInteraction( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + client: Amayo, + blockName: string, + blockState: BlockState +): Promise { + const { customId } = interaction; + + switch (customId) { + case "edit_title": + await handleEditTitle(interaction, editorMessage, originalMessage, blockState); + break; + + case "edit_description": + await handleEditDescription(interaction, editorMessage, originalMessage, blockState); + break; + + case "edit_color": + await handleEditColor(interaction, editorMessage, originalMessage, blockState); + break; + + case "add_content": + await handleAddContent(interaction, editorMessage, originalMessage, blockState); + break; + + case "add_separator": + await handleAddSeparator(interaction, editorMessage, originalMessage, blockState); + break; + + case "add_image": + await handleAddImage(interaction, editorMessage, originalMessage, blockState); + break; + + case "cover_image": + await handleCoverImage(interaction, editorMessage, originalMessage, blockState); + break; + + case "show_variables": + await handleShowVariables(interaction); + break; + + case "show_raw": + await handleShowRaw(interaction, blockState); + break; + + case "save_block": + await handleSaveBlock(interaction, client, blockName, blockState, originalMessage.guildId!); + break; + + case "cancel_block": + await handleCancelBlock(interaction, editorMessage); + break; + + default: + await interaction.reply({ + content: `⚠️ Funcionalidad \`${customId}\` en desarrollo.`, + flags: MessageFlags.Ephemeral + }); + break; + } +} + +async function handleEditTitle( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = new ModalBuilder() + .setCustomId("edit_title_modal") + .setTitle("Editar Título del Bloque"); + + const titleInput = new TextInputBuilder() + .setCustomId("title_input") + .setLabel("Título") + .setStyle(TextInputStyle.Short) + .setPlaceholder("Escribe el título del bloque...") + .setValue(blockState.title || "") + .setRequired(true) + .setMaxLength(256); + + const actionRow = new ActionRowBuilder().addComponents(titleInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + + try { + const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); + const newTitle = modalInteraction.fields.getTextInputValue("title_input").trim(); + + if (newTitle) { + blockState.title = newTitle; + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + } + + await modalInteraction.reply({ + content: "✅ Título actualizado correctamente.", + flags: MessageFlags.Ephemeral + }); + } catch { + // Modal timed out or error occurred + // no-op + } +} + +async function handleEditDescription( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = new ModalBuilder() + .setCustomId("edit_description_modal") + .setTitle("Editar Descripción del Bloque"); + + const descriptionInput = new TextInputBuilder() + .setCustomId("description_input") + .setLabel("Descripción") + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder("Escribe la descripción del bloque...") + .setValue(blockState.description || "") + .setRequired(false) + .setMaxLength(4000); + + const actionRow = new ActionRowBuilder().addComponents(descriptionInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + + try { + const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); + const newDescription = modalInteraction.fields.getTextInputValue("description_input").trim(); + + blockState.description = newDescription || undefined; + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.reply({ + content: "✅ Descripción actualizada correctamente.", + flags: MessageFlags.Ephemeral + }); + } catch { + // ignore + } +} + +async function handleEditColor( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = new ModalBuilder() + .setCustomId("edit_color_modal") + .setTitle("Editar Color del Bloque"); + + const colorInput = new TextInputBuilder() + .setCustomId("color_input") + .setLabel("Color (formato HEX)") + .setStyle(TextInputStyle.Short) + .setPlaceholder("#FF5733 o FF5733") + .setValue(blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : "") + .setRequired(false) + .setMaxLength(7); + + const actionRow = new ActionRowBuilder().addComponents(colorInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + + try { + const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); + const colorValue = modalInteraction.fields.getTextInputValue("color_input").trim(); + + if (colorValue) { + const cleanColor = colorValue.replace('#', ''); + const colorNumber = parseInt(cleanColor, 16); + + if (!isNaN(colorNumber) && cleanColor.length === 6) { + blockState.color = colorNumber; + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.reply({ + content: "✅ Color actualizado correctamente.", + flags: MessageFlags.Ephemeral + }); + } else { + await modalInteraction.reply({ + content: "❌ Color inválido. Usa formato HEX como #FF5733", + flags: MessageFlags.Ephemeral + }); + } + } else { + blockState.color = undefined; + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.reply({ + content: "✅ Color removido.", + flags: MessageFlags.Ephemeral + }); + } + } catch { + // ignore + } +} + +async function handleAddContent( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = new ModalBuilder() + .setCustomId("add_content_modal") + .setTitle("Añadir Contenido de Texto"); + + const contentInput = new TextInputBuilder() + .setCustomId("content_input") + .setLabel("Contenido") + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder("Escribe el contenido de texto...") + .setRequired(true) + .setMaxLength(4000); + + const actionRow = new ActionRowBuilder().addComponents(contentInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + + try { + const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); + const content = modalInteraction.fields.getTextInputValue("content_input").trim(); + + if (content) { + blockState.components.push({ + type: 10, + content, + thumbnail: null + }); + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.reply({ + content: "✅ Contenido añadido correctamente.", + flags: MessageFlags.Ephemeral + }); + } + } catch { + // ignore + } +} + +async function handleAddSeparator( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + blockState.components.push({ + type: 14, + divider: true, + spacing: 1 + }); + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await interaction.reply({ + content: "✅ Separador añadido correctamente.", + flags: MessageFlags.Ephemeral + }); +} + +async function handleAddImage( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = new ModalBuilder() + .setCustomId("add_image_modal") + .setTitle("Añadir Imagen"); + + const imageInput = new TextInputBuilder() + .setCustomId("image_input") + .setLabel("URL de la Imagen") + .setStyle(TextInputStyle.Short) + .setPlaceholder("https://ejemplo.com/imagen.png") + .setRequired(true) + .setMaxLength(512); + + const actionRow = new ActionRowBuilder().addComponents(imageInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + + try { + const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); + const imageUrl = modalInteraction.fields.getTextInputValue("image_input").trim(); + + if (imageUrl && DisplayComponentUtils.isValidUrl(imageUrl)) { + blockState.components.push({ + type: 12, + url: imageUrl + }); + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.reply({ + content: "✅ Imagen añadida correctamente.", + ephemeral: true + }); + } else { + await modalInteraction.reply({ + content: "❌ URL de imagen inválida.", + ephemeral: true + }); + } + } catch { + // ignore + } +} + +async function handleCoverImage( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = new ModalBuilder() + .setCustomId("cover_image_modal") + .setTitle("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 || "") + .setRequired(false) + .setMaxLength(512); + + const actionRow = new ActionRowBuilder().addComponents(coverInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + + try { + const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); + const coverUrl = modalInteraction.fields.getTextInputValue("cover_input").trim(); + + if (coverUrl && DisplayComponentUtils.isValidUrl(coverUrl)) { + blockState.coverImage = coverUrl; + } else { + blockState.coverImage = undefined; + } + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.reply({ + content: coverUrl ? "✅ Imagen de portada actualizada." : "✅ Imagen de portada removida.", + ephemeral: true + }); + } catch { + // ignore + } +} + +async function handleShowVariables(interaction: ButtonInteraction): Promise { + const variables = listVariables(); + await interaction.reply({ + content: `📋 **Variables disponibles:**\n\`\`\`\n${variables}\`\`\``, + flags: MessageFlags.Ephemeral + }); +} + +async function handleShowRaw(interaction: ButtonInteraction, blockState: BlockState): Promise { + const rawData = JSON.stringify(blockState, null, 2); + await interaction.reply({ + content: `📊 **Datos del bloque:**\n\`\`\`json\n${rawData.slice(0, 1800)}\`\`\``, + flags: MessageFlags.Ephemeral + }); +} + +async function handleSaveBlock( + interaction: ButtonInteraction, + client: Amayo, + blockName: string, + blockState: BlockState, + guildId: string +): Promise { + try { + await client.prisma.blockV2Config.create({ + data: { + guildId, + name: blockName, + config: blockState as any + } + }); + + await interaction.reply({ + content: `✅ **Bloque guardado exitosamente!**\n\n📄 **Nombre:** \`${blockName}\`\n🎨 **Componentes:** ${blockState.components.length}\n\n🎯 **Uso:** \`!send ${blockName}\``, + flags: MessageFlags.Ephemeral + }); + + logger.info(`Block created: ${blockName} in guild ${guildId}`); + } catch (error) { + //@ts-ignore + logger.error("Error saving block:", error); + await interaction.reply({ + content: "❌ Error al guardar el bloque. Inténtalo de nuevo.", + flags: MessageFlags.Ephemeral + }); + } +} + +async function handleCancelBlock(interaction: ButtonInteraction, editorMessage: Message): Promise { + await interaction.update({ + content: "❌ **Editor cancelado**\n\nLa creación del bloque ha sido cancelada.", + components: [], + embeds: [] + }); +} + +async function handleEditorTimeout(editorMessage: Message): Promise { + try { + await editorMessage.edit({ + content: "⏰ **Editor expirado**\n\nEl editor ha expirado por inactividad. Usa el comando nuevamente para crear un bloque.", + components: [], + embeds: [] + }); + } catch { + // message likely deleted + } +} diff --git a/src/commands/messages/alliaces/createDisplayComponent.ts b/src/commands/messages/alliaces/createDisplayComponent.ts index a371b36..9488a78 100644 --- a/src/commands/messages/alliaces/createDisplayComponent.ts +++ b/src/commands/messages/alliaces/createDisplayComponent.ts @@ -5,7 +5,7 @@ import { MessageFlags, TextChannel, } from "discord.js"; -import { ComponentType, TextInputStyle } from "discord-api-types/v10"; +import { ComponentType, TextInputStyle, ButtonStyle } from "discord-api-types/v10"; import logger from "../../../core/lib/logger"; import {CommandMessage} from "../../../core/types/commands"; import {listVariables} from "../../../core/lib/vars"; @@ -202,6 +202,113 @@ async function handleButtonInteraction( await handleCoverImage(interaction, editorMessage, originalMessage, blockState); 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}`, + value: String(idx), + description: c.type === 10 && (c.thumbnail || c.linkButton) ? (c.thumbnail ? 'Con thumbnail' : 'Con botón link') : undefined, + })); + + await interaction.reply({ + flags: MessageFlags.Ephemeral, + content: 'Selecciona el bloque que quieres mover:', + components: [ + { type: 1, components: [ { type: 3, custom_id: 'move_block_select', placeholder: 'Elige un bloque', options } ] }, + ], + }); + const replyMsg = await interaction.fetchReply(); + // @ts-ignore + const selCollector = replyMsg.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === originalMessage.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 }, + ]}, + ], + }); + // @ts-ignore + const btnCollector = replyMsg.createMessageComponentCollector({ componentType: ComponentType.Button, max: 1, time: 60000, filter: (b: any) => b.user.id === originalMessage.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) { + const item = blockState.components[i2]; + blockState.components.splice(i2, 1); + blockState.components.splice(i2 - 1, 0, item); + } + 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) { + 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 updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false), + }); + btnCollector.stop(); + selCollector.stop(); + }); + }); + break; + } + + case "delete_block": { + const options: 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}`, + value: String(idx), + description: c.type === 10 && (c.thumbnail || c.linkButton) ? (c.thumbnail ? 'Con thumbnail' : 'Con botón link') : undefined, + })); + + if (options.length === 0) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + // @ts-ignore + await interaction.editReply({ content: '❌ No hay elementos para eliminar.' }); + break; + } + + await interaction.reply({ + flags: MessageFlags.Ephemeral, + content: 'Selecciona el elemento que quieres eliminar:', + components: [ + { type: 1, components: [ { type: 3, custom_id: 'delete_block_select', placeholder: 'Elige un elemento', options } ] }, + ], + }); + const replyMsg = await interaction.fetchReply(); + // @ts-ignore + const selCollector = replyMsg.createMessageComponentCollector({ componentType: ComponentType.StringSelect, max: 1, time: 60000, filter: (it: any) => it.user.id === originalMessage.author.id }); + selCollector.on('collect', async (sel: any) => { + const selectedValue = sel.values[0]; + if (selectedValue === 'cover_image') { + // @ts-ignore + blockState.coverImage = null; + await sel.update({ content: '✅ Imagen de portada eliminada.', components: [] }); + } else { + const idx = parseInt(selectedValue); + blockState.components.splice(idx, 1); + await sel.update({ content: '✅ Elemento eliminado.', components: [] }); + } + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false), + }); + selCollector.stop(); + }); + break; + } + case "show_variables": await handleShowVariables(interaction); break; @@ -221,7 +328,7 @@ async function handleButtonInteraction( default: await interaction.reply({ content: `⚠️ Funcionalidad \`${customId}\` en desarrollo.`, - flags: MessageFlags.Ephemeral + flags: MessageFlags.Ephemeral, }); break; }