From 0eb5aa06913146d9fa19af184c9b7c29306822fe Mon Sep 17 00:00:00 2001 From: Shni Date: Tue, 14 Oct 2025 11:12:52 -0500 Subject: [PATCH] Refactor Amayo client initialization and enhance editor functionality - Updated the Amayo client to use environment variables for configuration defaults. - Improved cache and sweeper settings for message and user management. - Added detailed error handling and logging during database connection and Discord login. - Introduced a new interactive editor for creating and managing display components with modals. - Implemented various editor actions including adding content, editing titles/descriptions, and managing components. - Enhanced user feedback with ephemeral messages for actions taken in the editor. --- src/.backup/createDisplayComponent.ts | 1052 ++++++++ .../alliaces/createDisplayComponent.ts | 2174 ++++++++++------- src/core/client.ts | 151 +- 3 files changed, 2403 insertions(+), 974 deletions(-) create mode 100644 src/.backup/createDisplayComponent.ts diff --git a/src/.backup/createDisplayComponent.ts b/src/.backup/createDisplayComponent.ts new file mode 100644 index 0000000..3ff87fb --- /dev/null +++ b/src/.backup/createDisplayComponent.ts @@ -0,0 +1,1052 @@ +import { + ButtonInteraction, + Message, + MessageComponentInteraction, + MessageFlags, + ModalSubmitInteraction, + TextChannel, +} from "discord.js"; +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"; +import type Amayo from "../../../core/client"; +import { + BlockState, + DisplayComponentUtils, + EditorActionRow, + DESCRIPTION_PLACEHOLDER, + syncDescriptionComponent, + ensureDescriptionTextComponent, + normalizeDisplayContent +} from "../../../core/types/displayComponentEditor"; +import type {DisplayComponentContainer} from "../../../core/types/displayComponents"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; + +interface EditorData { + content?: string; + flags?: MessageFlags; + display?: DisplayComponentContainer; + components?: EditorActionRow[]; +} + +// --- Helpers (yald-style minimal generators) --------------------------------- +type ModalField = { + customId: string; + style: number; + placeholder?: string; + value?: string; + required?: boolean; + maxLength?: number; + label?: string; +}; + +function createModal(params: { title: string; customId: string; fields: ModalField[] }) { + const components = params.fields.map(f => ({ + type: ComponentType.Label, + label: f.label ?? "", + component: { + type: ComponentType.TextInput, + customId: f.customId, + style: f.style, + placeholder: f.placeholder, + value: f.value, + required: f.required ?? false, + maxLength: f.maxLength + } + })); + return { title: params.title, customId: params.customId, components } as const; +} + +function buildSelectOptionsFromComponents(components: any[]) { + return components.map((c: any, idx: number) => ({ + 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 + })); +} + + +// 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; + } + + // Si usamos Components V2, debemos limpiar explícitamente el content legado en el servidor + if (payload.flags === MessageFlags.IsComponentsV2) { + payload.content = null; + } + + await message.edit(payload); +} + +function stripLegacyDescriptionComponent(blockState: BlockState, match?: string | null): void { + if (!Array.isArray(blockState.components) || blockState.components.length === 0) return; + + const normalize = (value: string | undefined | null) => value?.replace(/\s+/g, " ").trim() ?? ""; + const target = normalize(match ?? blockState.description ?? undefined); + if (!target) return; + + const index = blockState.components.findIndex((component: any) => { + if (!component || component.type !== 10) return false; + if (component.thumbnail || component.linkButton) return false; + return normalize(component.content) === target; + }); + + if (index >= 0) { + blockState.components.splice(index, 1); + } +} + +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) => { + const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); + if (!allowed) { + await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); + 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: DESCRIPTION_PLACEHOLDER, 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: MessageComponentInteraction) => { + // Verificar que sea una interacción de botón + if (!interaction.isButton()) return; + + 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 "edit_thumbnail": { + ensureDescriptionTextComponent(blockState, { placeholder: DESCRIPTION_PLACEHOLDER }); + + const descriptionNormalized = normalizeDisplayContent(blockState.description); + const textDisplays = blockState.components + .map((component: any, idx: number) => ({ component, idx })) + .filter(({ component }) => component?.type === 10); + + if (textDisplays.length === 0) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); + await interaction.editReply({ content: '❌ No hay bloques de texto disponibles para añadir thumbnail.' }).catch(() => {}); + break; + } + + const options = textDisplays.map(({ component, idx }) => ({ + label: descriptionNormalized && normalizeDisplayContent(component.content) === descriptionNormalized + ? 'Descripción principal' + : `Texto #${idx + 1}: ${component.content?.slice(0, 30) || '...'}`, + value: String(idx), + description: component.thumbnail ? 'Con thumbnail' : component.linkButton ? 'Con botón link' : 'Sin accesorio' + })); + + try { + await interaction.reply({ + flags: MessageFlags.Ephemeral, + content: 'Selecciona el bloque de texto al que quieres editar el thumbnail:', + components: [ + { + type: 1, + components: [ + { + type: 3, + custom_id: 'choose_text_for_thumbnail', + placeholder: 'Selecciona un bloque de texto', + options + } + ] + } + ] + }); + } catch (error) { + logger.error({ err: error }, 'Error enviando selector de thumbnails'); + break; + } + + let replyMsg: Message | null = null; + try { + replyMsg = await interaction.fetchReply(); + } catch {} + + if (!replyMsg) break; + + 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) => { + selCollector.stop('collected'); + + const idx = parseInt(sel.values[0], 10); + if (Number.isNaN(idx)) { + try { + if (!sel.replied && !sel.deferred) { + await sel.reply({ content: '❌ Selección inválida.', flags: MessageFlags.Ephemeral }); + } + } catch {} + return; + } + + const textComp = blockState.components[idx]; + if (!textComp || textComp.type !== 10) { + try { + if (!sel.replied && !sel.deferred) { + await sel.reply({ content: '❌ El bloque seleccionado ya no existe.', flags: MessageFlags.Ephemeral }); + } + } catch {} + return; + } + + const modal = createModal({ + title: '📎 Editar Thumbnail', + customId: `edit_thumbnail_modal_${idx}`, + fields: [ + { + customId: 'thumbnail_input', + style: TextInputStyle.Short, + placeholder: 'https://ejemplo.com/thumbnail.png (vacío para eliminar)', + value: textComp.thumbnail || '', + maxLength: 512, + required: false, + label: 'URL del Thumbnail' + } + ] + }); + + try { + await sel.showModal(modal); + } catch (error) { + logger.error({ err: error }, 'No se pudo mostrar el modal de thumbnail'); + return; + } + + const modalInteraction = await awaitModalWithDeferredReply(sel); + if (!modalInteraction) return; + + const rawInput = modalInteraction.components.getTextInputValue('thumbnail_input').trim(); + + if (rawInput.length === 0) { + textComp.thumbnail = null; + await modalInteraction.editReply({ content: '✅ Thumbnail eliminado.' }).catch(() => {}); + } else if (!DisplayComponentUtils.isValidUrl(rawInput)) { + await modalInteraction.editReply({ content: '❌ URL de thumbnail inválida.' }).catch(() => {}); + return; + } else if (textComp.linkButton) { + await modalInteraction.editReply({ content: '❌ Este bloque tiene un botón link. Elimínalo antes de añadir un thumbnail.' }).catch(() => {}); + return; + } else { + textComp.thumbnail = rawInput; + await modalInteraction.editReply({ content: '✅ Thumbnail actualizado.' }).catch(() => {}); + } + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + }); + + selCollector.on('end', async () => { + try { + await replyMsg!.edit({ components: [] }); + } catch {} + }); + + break; + } + + case "cover_image": + await handleCoverImage(interaction, editorMessage, originalMessage, blockState); + break; + + case "move_block": { + const options = buildSelectOptionsFromComponents(blockState.components); + + 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' }); + options.push(...buildSelectOptionsFromComponents(blockState.components)); + + 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; + + case "show_raw": + await handleShowRaw(interaction, blockState); + break; + + case "save_block": + await handleSaveBlock(interaction, editorMessage, 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 awaitModalWithDeferredReply( + interaction: ButtonInteraction | MessageComponentInteraction, + options: Parameters[0] = { time: 300000 } +): Promise { + try { + const modalInteraction = await interaction.awaitModalSubmit(options); + if (!modalInteraction.deferred && !modalInteraction.replied) { + await modalInteraction.deferReply({ flags: MessageFlags.Ephemeral }); + } + return modalInteraction; + } catch (error) { + if (!(error instanceof Error) || !error.message.includes('Collector received no interactions')) { + logger.error({ err: error }, "Error esperando envío de modal en editor"); + } + return null; + } +} + +async function handleEditTitle( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = createModal({ + title: "Editar Título del Bloque", + customId: "edit_title_modal", + fields: [ + { + customId: "title_input", + style: TextInputStyle.Short, + required: true, + placeholder: "Escribe el título del bloque...", + value: blockState.title || "", + maxLength: 256, + label: 'Título' + } + ] + }); + + await interaction.showModal(modal as any); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const newTitle = modalInteraction.components.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.editReply({ + content: "✅ Título actualizado correctamente." + }); + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction.editReply({ + content: "❌ No se pudo actualizar el título. Inténtalo de nuevo." + }).catch(() => {}); + } + if (error instanceof Error && error.message.includes('Collector received no interactions')) { + return; + } + logger.error({ err: error }, "Error procesando modal de título"); + } +} + +async function handleEditDescription( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = { + title: "Editar Descripción del Bloque", + customId: "edit_description_modal", + components: [ + { + type: ComponentType.Label, + label: "Descripción", + component: { + type: ComponentType.TextInput, + customId: "description_input", + style: TextInputStyle.Paragraph, + required: false, + placeholder: "Escribe la descripción del bloque...", + value: blockState.description || "", + maxLength: 4000 + } + } + ] + } as const; + + await interaction.showModal(modal); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const rawDescription = modalInteraction.components.getTextInputValue("description_input"); + const previousDescription = typeof blockState.description === "string" ? blockState.description : null; + syncDescriptionComponent(blockState, rawDescription, { + previousDescription, + placeholder: DESCRIPTION_PLACEHOLDER + }); + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.editReply({ + content: "✅ Descripción actualizada correctamente." + }); + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction.editReply({ + content: "❌ No se pudo actualizar la descripción. Inténtalo de nuevo." + }).catch(() => {}); + } + if (error instanceof Error && error.message.includes('Collector received no interactions')) { + return; + } + logger.error({ err: error }, "Error procesando modal de descripción"); + } +} + +async function handleEditColor( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = { + title: "Editar Color del Bloque", + customId: "edit_color_modal", + components: [ + { + type: ComponentType.Label, + label: "Color (formato HEX)", + component: { + type: ComponentType.TextInput, + customId: "color_input", + style: TextInputStyle.Short, + required: false, + placeholder: "#FF5733 o FF5733", + value: blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : "", + maxLength: 7 + } + } + ] + } as const; + + await interaction.showModal(modal); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const colorValue = modalInteraction.components.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.editReply({ + content: "✅ Color actualizado correctamente." + }); + } else { + await modalInteraction.editReply({ + content: "❌ Color inválido. Usa formato HEX como #FF5733" + }); + } + } else { + blockState.color = undefined; + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), + components: DisplayComponentUtils.createEditorButtons(false) + }); + + await modalInteraction.editReply({ + content: "✅ Color removido." + }); + } + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction.editReply({ + content: "❌ No se pudo actualizar el color. Inténtalo de nuevo." + }).catch(() => {}); + } + if (error instanceof Error && error.message.includes('Collector received no interactions')) { + return; + } + logger.error({ err: error }, "Error procesando modal de color"); + } +} + +async function handleAddContent( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = { + title: "Añadir Contenido de Texto", + customId: "add_content_modal", + components: [ + { + type: ComponentType.Label, + label: "Contenido", + component: { + type: ComponentType.TextInput, + customId: "content_input", + style: TextInputStyle.Paragraph, + required: true, + placeholder: "Escribe el contenido de texto...", + maxLength: 4000 + } + } + ] + } as const; + + await interaction.showModal(modal); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const content = modalInteraction.components.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.editReply({ + content: "✅ Contenido añadido correctamente." + }); + } + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction.editReply({ + content: "❌ No se pudo añadir el contenido. Inténtalo de nuevo." + }).catch(() => {}); + } + if (error instanceof Error && error.message.includes('Collector received no interactions')) { + return; + } + logger.error({ err: error }, "Error procesando modal de contenido"); + } +} + +async function handleAddSeparator( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const wasAcknowledged = interaction.deferred || interaction.replied; + if (!wasAcknowledged) { + try { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + } catch (error) { + logger.warn({ err: error }, "No se pudo diferir respuesta al añadir separador"); + } + } + + 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) + }); + + const payload = { content: "✅ Separador añadido correctamente.", flags: MessageFlags.Ephemeral } as const; + + if (interaction.deferred) { + await interaction.editReply({ content: payload.content }).catch(() => {}); + } else if (interaction.replied) { + await interaction.followUp(payload).catch(() => {}); + } else { + await interaction.reply(payload).catch(() => {}); + } +} + +async function handleAddImage( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = createModal({ + title: "Añadir Imagen", + customId: "add_image_modal", + fields: [ + { + customId: "image_input", + style: TextInputStyle.Short, + required: true, + placeholder: "https://ejemplo.com/imagen.png", + maxLength: 512, + label: 'URL de la Imagen' + } + ] + }); + + await interaction.showModal(modal as any); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const imageUrl = modalInteraction.components.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.editReply({ + content: "✅ Imagen añadida correctamente." + }); + } else { + await modalInteraction.editReply({ + content: "❌ URL de imagen inválida." + }); + } + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction.editReply({ + content: "❌ No se pudo añadir la imagen. Inténtalo de nuevo." + }).catch(() => {}); + } + if (error instanceof Error && error.message.includes('Collector received no interactions')) { + return; + } + logger.error({ err: error }, "Error procesando modal de imagen"); + } +} + +async function handleCoverImage( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = { + title: "Imagen de Portada", + customId: "cover_image_modal", + components: [ + { + type: ComponentType.Label, + label: "URL de la Imagen de Portada", + component: { + type: ComponentType.TextInput, + customId: "cover_input", + style: TextInputStyle.Short, + required: false, + placeholder: "https://ejemplo.com/portada.png", + value: blockState.coverImage || "", + maxLength: 512 + } + } + ] + } as const; + + await interaction.showModal(modal); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const coverUrl = modalInteraction.components.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.editReply({ + content: coverUrl ? "✅ Imagen de portada actualizada." : "✅ Imagen de portada removida." + }); + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction.editReply({ + content: "❌ No se pudo actualizar la imagen de portada. Inténtalo de nuevo." + }).catch(() => {}); + } + if (error instanceof Error && error.message.includes('Collector received no interactions')) { + return; + } + logger.error({ err: error }, "Error procesando modal de portada"); + } +} + +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, + editorMessage: Message, + client: Amayo, + blockName: string, + blockState: BlockState, + guildId: string +): Promise { + try { + stripLegacyDescriptionComponent(blockState); + 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-embed ${blockName}\``, + flags: MessageFlags.Ephemeral + }); + + // Cerrar el editor eliminando el mensaje del editor + try { await editorMessage.delete(); } catch {} + } 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 { + try { + await interaction.deferUpdate(); + } catch {} + await updateEditor(editorMessage, { + display: { + type: 17, + components: [ + { type: 10, content: "❌ **Editor cancelado**" }, + { type: 10, content: "La creación del bloque ha sido cancelada." } + ] + } as any, + components: [] + }); +} + +async function handleEditorTimeout(editorMessage: Message): Promise { + try { + await updateEditor(editorMessage, { + display: { + type: 17, + components: [ + { type: 10, content: "⏰ **Editor expirado**" }, + { type: 10, content: "El editor ha expirado por inactividad. Usa el comando nuevamente para crear un bloque." } + ] + } as any, + components: [] + }); + } catch { + // message likely deleted + } +} diff --git a/src/commands/messages/alliaces/createDisplayComponent.ts b/src/commands/messages/alliaces/createDisplayComponent.ts index a351f81..58f43cb 100644 --- a/src/commands/messages/alliaces/createDisplayComponent.ts +++ b/src/commands/messages/alliaces/createDisplayComponent.ts @@ -1,1035 +1,1381 @@ import { - ButtonInteraction, - Message, - MessageComponentInteraction, - MessageFlags, - ModalSubmitInteraction, - TextChannel, + ButtonInteraction, + Message, + MessageComponentInteraction, + MessageFlags, + ModalSubmitInteraction, + TextChannel, } from "discord.js"; -import { ComponentType, TextInputStyle, ButtonStyle } 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"; +import { CommandMessage } from "../../../core/types/commands"; +import { listVariables } from "../../../core/lib/vars"; import type Amayo from "../../../core/client"; import { - BlockState, - DisplayComponentUtils, - EditorActionRow, - DESCRIPTION_PLACEHOLDER, - syncDescriptionComponent, - ensureDescriptionTextComponent, - normalizeDisplayContent + BlockState, + DisplayComponentUtils, + EditorActionRow, + DESCRIPTION_PLACEHOLDER, + syncDescriptionComponent, + ensureDescriptionTextComponent, + normalizeDisplayContent, } from "../../../core/types/displayComponentEditor"; -import type {DisplayComponentContainer} from "../../../core/types/displayComponents"; +import type { DisplayComponentContainer } from "../../../core/types/displayComponents"; import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; interface EditorData { - content?: string; - flags?: MessageFlags; - display?: DisplayComponentContainer; - components?: EditorActionRow[]; + content?: string; + flags?: MessageFlags; + display?: DisplayComponentContainer; + components?: EditorActionRow[]; +} + +// --- Helpers (yald-style minimal generators) --------------------------------- +type ModalField = { + customId: string; + style: number; + placeholder?: string; + value?: string; + required?: boolean; + maxLength?: number; + label?: string; +}; + +function createModal(params: { + title: string; + customId: string; + fields: ModalField[]; +}) { + const components = params.fields.map((f) => ({ + type: ComponentType.Label, + label: f.label ?? "", + component: { + type: ComponentType.TextInput, + customId: f.customId, + style: f.style, + placeholder: f.placeholder, + value: f.value, + required: f.required ?? false, + maxLength: f.maxLength, + }, + })); + return { + title: params.title, + customId: params.customId, + components, + } as const; +} + +function buildSelectOptionsFromComponents(components: any[]) { + return components.map((c: any, idx: number) => ({ + 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, + })); } // 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 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; + const payload: any = { ...data }; + delete payload.display; + payload.components = components; - if (payload.flags === undefined) { - payload.flags = MessageFlags.IsComponentsV2; - } + if (payload.flags === undefined) { + payload.flags = MessageFlags.IsComponentsV2; + } - // Si usamos Components V2, debemos limpiar explícitamente el content legado en el servidor - if (payload.flags === MessageFlags.IsComponentsV2) { - payload.content = null; - } + // Si usamos Components V2, debemos limpiar explícitamente el content legado en el servidor + if (payload.flags === MessageFlags.IsComponentsV2) { + payload.content = null; + } - await message.edit(payload); + await message.edit(payload); } -function stripLegacyDescriptionComponent(blockState: BlockState, match?: string | null): void { - if (!Array.isArray(blockState.components) || blockState.components.length === 0) return; +function stripLegacyDescriptionComponent( + blockState: BlockState, + match?: string | null +): void { + if ( + !Array.isArray(blockState.components) || + blockState.components.length === 0 + ) + return; - const normalize = (value: string | undefined | null) => value?.replace(/\s+/g, " ").trim() ?? ""; - const target = normalize(match ?? blockState.description ?? undefined); - if (!target) return; + const normalize = (value: string | undefined | null) => + value?.replace(/\s+/g, " ").trim() ?? ""; + const target = normalize(match ?? blockState.description ?? undefined); + if (!target) return; - const index = blockState.components.findIndex((component: any) => { - if (!component || component.type !== 10) return false; - if (component.thumbnail || component.linkButton) return false; - return normalize(component.content) === target; - }); + const index = blockState.components.findIndex((component: any) => { + if (!component || component.type !== 10) return false; + if (component.thumbnail || component.linkButton) return false; + return normalize(component.content) === target; + }); - if (index >= 0) { - blockState.components.splice(index, 1); - } + if (index >= 0) { + blockState.components.splice(index, 1); + } } 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) => { - const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); - if (!allowed) { - await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); - return; - } + 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) => { + const allowed = await hasManageGuildOrStaff( + message.member, + message.guild!.id, + client.prisma + ); + if (!allowed) { + await message.reply( + "❌ No tienes permisos de ManageGuild ni rol de staff." + ); + return; + } - const blockName = args[0]?.trim(); - if (!blockName) { - await message.reply("Debes proporcionar un nombre. Uso: `!crear-embed `"); - 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 - } - }); + // 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; - } + 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: DESCRIPTION_PLACEHOLDER, thumbnail: null } - ] - }; + // Estado inicial + let blockState: BlockState = { + title: `Editor de Block: ${blockName}`, + color: 0x5865f2, + coverImage: undefined, + components: [ + { type: 14, divider: false, spacing: 1 }, + { type: 10, content: DESCRIPTION_PLACEHOLDER, 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; - } + //@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...*" - }); + 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)); + // 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) - }); + // 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); - }, + await handleEditorInteractions( + editorMessage, + message, + client, + blockName, + blockState + ); + }, }; async function handleEditorInteractions( - editorMessage: Message, - originalMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState + 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 - }); + const collector = editorMessage.createMessageComponentCollector({ + time: 3600000, // 1 hour + filter: (interaction: MessageComponentInteraction) => + interaction.user.id === originalMessage.author.id, + }); - collector.on("collect", async (interaction: MessageComponentInteraction) => { - // Verificar que sea una interacción de botón - if (!interaction.isButton()) return; + collector.on("collect", async (interaction: MessageComponentInteraction) => { + // Verificar que sea una interacción de botón + if (!interaction.isButton()) return; - 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 - }); - } - } - }); + 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); - } - }); + 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 + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + client: Amayo, + blockName: string, + blockState: BlockState ): Promise { - const { customId } = interaction; + const { customId } = interaction; - switch (customId) { - case "edit_title": - await handleEditTitle(interaction, editorMessage, originalMessage, blockState); - break; + switch (customId) { + case "edit_title": + await handleEditTitle( + interaction, + editorMessage, + originalMessage, + blockState + ); + break; - case "edit_description": - await handleEditDescription(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 "edit_color": + await handleEditColor( + interaction, + editorMessage, + originalMessage, + blockState + ); + break; - case "add_content": - await handleAddContent(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_separator": + await handleAddSeparator( + interaction, + editorMessage, + originalMessage, + blockState + ); + break; - case "add_image": - await handleAddImage(interaction, editorMessage, originalMessage, blockState); - break; + case "add_image": + await handleAddImage( + interaction, + editorMessage, + originalMessage, + blockState + ); + break; - case "edit_thumbnail": { - ensureDescriptionTextComponent(blockState, { placeholder: DESCRIPTION_PLACEHOLDER }); + case "edit_thumbnail": { + ensureDescriptionTextComponent(blockState, { + placeholder: DESCRIPTION_PLACEHOLDER, + }); - const descriptionNormalized = normalizeDisplayContent(blockState.description); - const textDisplays = blockState.components - .map((component: any, idx: number) => ({ component, idx })) - .filter(({ component }) => component?.type === 10); + const descriptionNormalized = normalizeDisplayContent( + blockState.description + ); + const textDisplays = blockState.components + .map((component: any, idx: number) => ({ component, idx })) + .filter(({ component }) => component?.type === 10); - if (textDisplays.length === 0) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); - await interaction.editReply({ content: '❌ No hay bloques de texto disponibles para añadir thumbnail.' }).catch(() => {}); - break; + if (textDisplays.length === 0) { + await interaction + .deferReply({ flags: MessageFlags.Ephemeral }) + .catch(() => {}); + await interaction + .editReply({ + content: + "❌ No hay bloques de texto disponibles para añadir thumbnail.", + }) + .catch(() => {}); + break; + } + + const options = textDisplays.map(({ component, idx }) => ({ + label: + descriptionNormalized && + normalizeDisplayContent(component.content) === descriptionNormalized + ? "Descripción principal" + : `Texto #${idx + 1}: ${component.content?.slice(0, 30) || "..."}`, + value: String(idx), + description: component.thumbnail + ? "Con thumbnail" + : component.linkButton + ? "Con botón link" + : "Sin accesorio", + })); + + try { + await interaction.reply({ + flags: MessageFlags.Ephemeral, + content: + "Selecciona el bloque de texto al que quieres editar el thumbnail:", + components: [ + { + type: 1, + components: [ + { + type: 3, + custom_id: "choose_text_for_thumbnail", + placeholder: "Selecciona un bloque de texto", + options, + }, + ], + }, + ], + }); + } catch (error) { + logger.error({ err: error }, "Error enviando selector de thumbnails"); + break; + } + + let replyMsg: Message | null = null; + try { + replyMsg = await interaction.fetchReply(); + } catch {} + + if (!replyMsg) break; + + 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) => { + selCollector.stop("collected"); + + const idx = parseInt(sel.values[0], 10); + if (Number.isNaN(idx)) { + try { + if (!sel.replied && !sel.deferred) { + await sel.reply({ + content: "❌ Selección inválida.", + flags: MessageFlags.Ephemeral, + }); } - - const options = textDisplays.map(({ component, idx }) => ({ - label: descriptionNormalized && normalizeDisplayContent(component.content) === descriptionNormalized - ? 'Descripción principal' - : `Texto #${idx + 1}: ${component.content?.slice(0, 30) || '...'}`, - value: String(idx), - description: component.thumbnail ? 'Con thumbnail' : component.linkButton ? 'Con botón link' : 'Sin accesorio' - })); - - try { - await interaction.reply({ - flags: MessageFlags.Ephemeral, - content: 'Selecciona el bloque de texto al que quieres editar el thumbnail:', - components: [ - { - type: 1, - components: [ - { - type: 3, - custom_id: 'choose_text_for_thumbnail', - placeholder: 'Selecciona un bloque de texto', - options - } - ] - } - ] - }); - } catch (error) { - logger.error({ err: error }, 'Error enviando selector de thumbnails'); - break; - } - - let replyMsg: Message | null = null; - try { - replyMsg = await interaction.fetchReply(); - } catch {} - - if (!replyMsg) break; - - 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) => { - selCollector.stop('collected'); - - const idx = parseInt(sel.values[0], 10); - if (Number.isNaN(idx)) { - try { - if (!sel.replied && !sel.deferred) { - await sel.reply({ content: '❌ Selección inválida.', flags: MessageFlags.Ephemeral }); - } - } catch {} - return; - } - - const textComp = blockState.components[idx]; - if (!textComp || textComp.type !== 10) { - try { - if (!sel.replied && !sel.deferred) { - await sel.reply({ content: '❌ El bloque seleccionado ya no existe.', flags: MessageFlags.Ephemeral }); - } - } catch {} - return; - } - - const modal = { - title: '📎 Editar Thumbnail', - customId: `edit_thumbnail_modal_${idx}`, - components: [ - { - type: ComponentType.Label, - label: 'URL del Thumbnail', - component: { - type: ComponentType.TextInput, - customId: 'thumbnail_input', - style: TextInputStyle.Short, - placeholder: 'https://ejemplo.com/thumbnail.png (vacío para eliminar)', - value: textComp.thumbnail || '', - maxLength: 512, - required: false - } - } - ] - } as const; - - try { - await sel.showModal(modal); - } catch (error) { - logger.error({ err: error }, 'No se pudo mostrar el modal de thumbnail'); - return; - } - - const modalInteraction = await awaitModalWithDeferredReply(sel); - if (!modalInteraction) return; - - const rawInput = modalInteraction.components.getTextInputValue('thumbnail_input').trim(); - - if (rawInput.length === 0) { - textComp.thumbnail = null; - await modalInteraction.editReply({ content: '✅ Thumbnail eliminado.' }).catch(() => {}); - } else if (!DisplayComponentUtils.isValidUrl(rawInput)) { - await modalInteraction.editReply({ content: '❌ URL de thumbnail inválida.' }).catch(() => {}); - return; - } else if (textComp.linkButton) { - await modalInteraction.editReply({ content: '❌ Este bloque tiene un botón link. Elimínalo antes de añadir un thumbnail.' }).catch(() => {}); - return; - } else { - textComp.thumbnail = rawInput; - await modalInteraction.editReply({ content: '✅ Thumbnail actualizado.' }).catch(() => {}); - } - - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - }); - - selCollector.on('end', async () => { - try { - await replyMsg!.edit({ components: [] }); - } catch {} - }); - - break; + } catch {} + return; } - case "cover_image": - 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({ + const textComp = blockState.components[idx]; + if (!textComp || textComp.type !== 10) { + try { + if (!sel.replied && !sel.deferred) { + await sel.reply({ + content: "❌ El bloque seleccionado ya no existe.", 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; + } catch {} + return; } - case "show_variables": - await handleShowVariables(interaction); - break; + const modal = createModal({ + title: "📎 Editar Thumbnail", + customId: `edit_thumbnail_modal_${idx}`, + fields: [ + { + customId: "thumbnail_input", + style: TextInputStyle.Short, + placeholder: + "https://ejemplo.com/thumbnail.png (vacío para eliminar)", + value: textComp.thumbnail || "", + maxLength: 512, + required: false, + label: "URL del Thumbnail", + }, + ], + }); - case "show_raw": - await handleShowRaw(interaction, blockState); - break; + try { + await sel.showModal(modal); + } catch (error) { + logger.error( + { err: error }, + "No se pudo mostrar el modal de thumbnail" + ); + return; + } - case "save_block": - await handleSaveBlock(interaction, editorMessage, client, blockName, blockState, originalMessage.guildId!); - break; + const modalInteraction = await awaitModalWithDeferredReply(sel); + if (!modalInteraction) return; - case "cancel_block": - await handleCancelBlock(interaction, editorMessage); - break; + const rawInput = modalInteraction.components + .getTextInputValue("thumbnail_input") + .trim(); - default: - await interaction.reply({ - content: `⚠️ Funcionalidad \`${customId}\` en desarrollo.`, - flags: MessageFlags.Ephemeral, - }); - break; + if (rawInput.length === 0) { + textComp.thumbnail = null; + await modalInteraction + .editReply({ content: "✅ Thumbnail eliminado." }) + .catch(() => {}); + } else if (!DisplayComponentUtils.isValidUrl(rawInput)) { + await modalInteraction + .editReply({ content: "❌ URL de thumbnail inválida." }) + .catch(() => {}); + return; + } else if (textComp.linkButton) { + await modalInteraction + .editReply({ + content: + "❌ Este bloque tiene un botón link. Elimínalo antes de añadir un thumbnail.", + }) + .catch(() => {}); + return; + } else { + textComp.thumbnail = rawInput; + await modalInteraction + .editReply({ content: "✅ Thumbnail actualizado." }) + .catch(() => {}); + } + + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview( + blockState, + originalMessage.member!, + originalMessage.guild! + ), + components: DisplayComponentUtils.createEditorButtons(false), + }); + }); + + selCollector.on("end", async () => { + try { + await replyMsg!.edit({ components: [] }); + } catch {} + }); + + break; } + + case "cover_image": + await handleCoverImage( + interaction, + editorMessage, + originalMessage, + blockState + ); + break; + + case "move_block": { + const options = buildSelectOptionsFromComponents(blockState.components); + + 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", + }); + options.push(...buildSelectOptionsFromComponents(blockState.components)); + + 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; + + case "show_raw": + await handleShowRaw(interaction, blockState); + break; + + case "save_block": + await handleSaveBlock( + interaction, + editorMessage, + 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 awaitModalWithDeferredReply( - interaction: ButtonInteraction | MessageComponentInteraction, - options: Parameters[0] = { time: 300000 } + interaction: ButtonInteraction | MessageComponentInteraction, + options: Parameters[0] = { + time: 300000, + } ): Promise { - try { - const modalInteraction = await interaction.awaitModalSubmit(options); - if (!modalInteraction.deferred && !modalInteraction.replied) { - await modalInteraction.deferReply({ flags: MessageFlags.Ephemeral }); - } - return modalInteraction; - } catch (error) { - if (!(error instanceof Error) || !error.message.includes('Collector received no interactions')) { - logger.error({ err: error }, "Error esperando envío de modal en editor"); - } - return null; + try { + const modalInteraction = await interaction.awaitModalSubmit(options); + if (!modalInteraction.deferred && !modalInteraction.replied) { + await modalInteraction.deferReply({ flags: MessageFlags.Ephemeral }); } + return modalInteraction; + } catch (error) { + if ( + !(error instanceof Error) || + !error.message.includes("Collector received no interactions") + ) { + logger.error({ err: error }, "Error esperando envío de modal en editor"); + } + return null; + } } async function handleEditTitle( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState ): Promise { - const modal = { - title: "Editar Título del Bloque", - customId: "edit_title_modal", - components: [ - { - type: ComponentType.Label, - label: "Título", - component: { - type: ComponentType.TextInput, - customId: "title_input", - style: TextInputStyle.Short, - required: true, - placeholder: "Escribe el título del bloque...", - value: blockState.title || "", - maxLength: 256 - } - } - ] - } as const; + const modal = createModal({ + title: "Editar Título del Bloque", + customId: "edit_title_modal", + fields: [ + { + customId: "title_input", + style: TextInputStyle.Short, + required: true, + placeholder: "Escribe el título del bloque...", + value: blockState.title || "", + maxLength: 256, + label: "Título", + }, + ], + }); - await interaction.showModal(modal); + await interaction.showModal(modal as any); - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; - const newTitle = modalInteraction.components.getTextInputValue("title_input").trim(); + const newTitle = modalInteraction.components + .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.editReply({ - content: "✅ Título actualizado correctamente." - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar el título. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de título"); + if (newTitle) { + blockState.title = newTitle; + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview( + blockState, + originalMessage.member!, + originalMessage.guild! + ), + components: DisplayComponentUtils.createEditorButtons(false), + }); } + + await modalInteraction.editReply({ + content: "✅ Título actualizado correctamente.", + }); + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction + .editReply({ + content: "❌ No se pudo actualizar el título. Inténtalo de nuevo.", + }) + .catch(() => {}); + } + if ( + error instanceof Error && + error.message.includes("Collector received no interactions") + ) { + return; + } + logger.error({ err: error }, "Error procesando modal de título"); + } } async function handleEditDescription( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState ): Promise { - const modal = { - title: "Editar Descripción del Bloque", - customId: "edit_description_modal", - components: [ - { - type: ComponentType.Label, - label: "Descripción", - component: { - type: ComponentType.TextInput, - customId: "description_input", - style: TextInputStyle.Paragraph, - required: false, - placeholder: "Escribe la descripción del bloque...", - value: blockState.description || "", - maxLength: 4000 - } - } - ] - } as const; + const modal = { + title: "Editar Descripción del Bloque", + customId: "edit_description_modal", + components: [ + { + type: ComponentType.Label, + label: "Descripción", + component: { + type: ComponentType.TextInput, + customId: "description_input", + style: TextInputStyle.Paragraph, + required: false, + placeholder: "Escribe la descripción del bloque...", + value: blockState.description || "", + maxLength: 4000, + }, + }, + ], + } as const; - await interaction.showModal(modal); + await interaction.showModal(modal); - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; - const rawDescription = modalInteraction.components.getTextInputValue("description_input"); - const previousDescription = typeof blockState.description === "string" ? blockState.description : null; - syncDescriptionComponent(blockState, rawDescription, { - previousDescription, - placeholder: DESCRIPTION_PLACEHOLDER - }); + const rawDescription = + modalInteraction.components.getTextInputValue("description_input"); + const previousDescription = + typeof blockState.description === "string" + ? blockState.description + : null; + syncDescriptionComponent(blockState, rawDescription, { + previousDescription, + placeholder: DESCRIPTION_PLACEHOLDER, + }); - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview( + blockState, + originalMessage.member!, + originalMessage.guild! + ), + components: DisplayComponentUtils.createEditorButtons(false), + }); - await modalInteraction.editReply({ - content: "✅ Descripción actualizada correctamente." - }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar la descripción. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de descripción"); + await modalInteraction.editReply({ + content: "✅ Descripción actualizada correctamente.", + }); + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction + .editReply({ + content: + "❌ No se pudo actualizar la descripción. Inténtalo de nuevo.", + }) + .catch(() => {}); } + if ( + error instanceof Error && + error.message.includes("Collector received no interactions") + ) { + return; + } + logger.error({ err: error }, "Error procesando modal de descripción"); + } } async function handleEditColor( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState ): Promise { - const modal = { - title: "Editar Color del Bloque", - customId: "edit_color_modal", - components: [ - { - type: ComponentType.Label, - label: "Color (formato HEX)", - component: { - type: ComponentType.TextInput, - customId: "color_input", - style: TextInputStyle.Short, - required: false, - placeholder: "#FF5733 o FF5733", - value: blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : "", - maxLength: 7 - } - } - ] - } as const; + const modal = { + title: "Editar Color del Bloque", + customId: "edit_color_modal", + components: [ + { + type: ComponentType.Label, + label: "Color (formato HEX)", + component: { + type: ComponentType.TextInput, + customId: "color_input", + style: TextInputStyle.Short, + required: false, + placeholder: "#FF5733 o FF5733", + value: blockState.color + ? `#${blockState.color.toString(16).padStart(6, "0")}` + : "", + maxLength: 7, + }, + }, + ], + } as const; - await interaction.showModal(modal); + await interaction.showModal(modal); - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; - const colorValue = modalInteraction.components.getTextInputValue("color_input").trim(); + const colorValue = modalInteraction.components + .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.editReply({ - content: "✅ Color actualizado correctamente." - }); - } else { - await modalInteraction.editReply({ - content: "❌ Color inválido. Usa formato HEX como #FF5733" - }); - } - } else { - blockState.color = undefined; - await updateEditor(editorMessage, { - display: await DisplayComponentUtils.renderPreview(blockState, originalMessage.member!, originalMessage.guild!), - components: DisplayComponentUtils.createEditorButtons(false) - }); - - await modalInteraction.editReply({ - content: "✅ Color removido." - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar el color. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de color"); - } -} - -async function handleAddContent( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Añadir Contenido de Texto", - customId: "add_content_modal", - components: [ - { - type: ComponentType.Label, - label: "Contenido", - component: { - type: ComponentType.TextInput, - customId: "content_input", - style: TextInputStyle.Paragraph, - required: true, - placeholder: "Escribe el contenido de texto...", - maxLength: 4000 - } - } - ] - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const content = modalInteraction.components.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.editReply({ - content: "✅ Contenido añadido correctamente." - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo añadir el contenido. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de contenido"); - } -} - -async function handleAddSeparator( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const wasAcknowledged = interaction.deferred || interaction.replied; - if (!wasAcknowledged) { - try { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - } catch (error) { - logger.warn({ err: error }, "No se pudo diferir respuesta al añadir separador"); - } - } - - 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) - }); - - const payload = { content: "✅ Separador añadido correctamente.", flags: MessageFlags.Ephemeral } as const; - - if (interaction.deferred) { - await interaction.editReply({ content: payload.content }).catch(() => {}); - } else if (interaction.replied) { - await interaction.followUp(payload).catch(() => {}); - } else { - await interaction.reply(payload).catch(() => {}); - } -} - -async function handleAddImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Añadir Imagen", - customId: "add_image_modal", - components: [ - { - type: ComponentType.Label, - label: "URL de la Imagen", - component: { - type: ComponentType.TextInput, - customId: "image_input", - style: TextInputStyle.Short, - required: true, - placeholder: "https://ejemplo.com/imagen.png", - maxLength: 512 - } - } - ] - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const imageUrl = modalInteraction.components.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.editReply({ - content: "✅ Imagen añadida correctamente." - }); - } else { - await modalInteraction.editReply({ - content: "❌ URL de imagen inválida." - }); - } - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo añadir la imagen. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de imagen"); - } -} - -async function handleCoverImage( - interaction: ButtonInteraction, - editorMessage: Message, - originalMessage: Message, - blockState: BlockState -): Promise { - const modal = { - title: "Imagen de Portada", - customId: "cover_image_modal", - components: [ - { - type: ComponentType.Label, - label: "URL de la Imagen de Portada", - component: { - type: ComponentType.TextInput, - customId: "cover_input", - style: TextInputStyle.Short, - required: false, - placeholder: "https://ejemplo.com/portada.png", - value: blockState.coverImage || "", - maxLength: 512 - } - } - ] - } as const; - - await interaction.showModal(modal); - - let modalInteraction: ModalSubmitInteraction | null = null; - try { - modalInteraction = await awaitModalWithDeferredReply(interaction); - if (!modalInteraction) return; - - const coverUrl = modalInteraction.components.getTextInputValue("cover_input").trim(); - - if (coverUrl && DisplayComponentUtils.isValidUrl(coverUrl)) { - blockState.coverImage = coverUrl; - } else { - blockState.coverImage = undefined; - } + 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) + display: await DisplayComponentUtils.renderPreview( + blockState, + originalMessage.member!, + originalMessage.guild! + ), + components: DisplayComponentUtils.createEditorButtons(false), }); await modalInteraction.editReply({ - content: coverUrl ? "✅ Imagen de portada actualizada." : "✅ Imagen de portada removida." + content: "✅ Color actualizado correctamente.", }); - } catch (error) { - if (modalInteraction?.deferred && !modalInteraction.replied) { - await modalInteraction.editReply({ - content: "❌ No se pudo actualizar la imagen de portada. Inténtalo de nuevo." - }).catch(() => {}); - } - if (error instanceof Error && error.message.includes('Collector received no interactions')) { - return; - } - logger.error({ err: error }, "Error procesando modal de portada"); + } else { + await modalInteraction.editReply({ + content: "❌ Color inválido. Usa formato HEX como #FF5733", + }); + } + } else { + blockState.color = undefined; + await updateEditor(editorMessage, { + display: await DisplayComponentUtils.renderPreview( + blockState, + originalMessage.member!, + originalMessage.guild! + ), + components: DisplayComponentUtils.createEditorButtons(false), + }); + + await modalInteraction.editReply({ + content: "✅ Color removido.", + }); } + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction + .editReply({ + content: "❌ No se pudo actualizar el color. Inténtalo de nuevo.", + }) + .catch(() => {}); + } + if ( + error instanceof Error && + error.message.includes("Collector received no interactions") + ) { + return; + } + logger.error({ err: error }, "Error procesando modal de color"); + } } -async function handleShowVariables(interaction: ButtonInteraction): Promise { - const variables = listVariables(); - await interaction.reply({ - content: `📋 **Variables disponibles:**\n\`\`\`\n${variables}\`\`\``, - flags: MessageFlags.Ephemeral - }); +async function handleAddContent( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = { + title: "Añadir Contenido de Texto", + customId: "add_content_modal", + components: [ + { + type: ComponentType.Label, + label: "Contenido", + component: { + type: ComponentType.TextInput, + customId: "content_input", + style: TextInputStyle.Paragraph, + required: true, + placeholder: "Escribe el contenido de texto...", + maxLength: 4000, + }, + }, + ], + } as const; + + await interaction.showModal(modal); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const content = modalInteraction.components + .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.editReply({ + content: "✅ Contenido añadido correctamente.", + }); + } + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction + .editReply({ + content: "❌ No se pudo añadir el contenido. Inténtalo de nuevo.", + }) + .catch(() => {}); + } + if ( + error instanceof Error && + error.message.includes("Collector received no interactions") + ) { + return; + } + logger.error({ err: error }, "Error procesando modal de contenido"); + } } -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 handleAddSeparator( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const wasAcknowledged = interaction.deferred || interaction.replied; + if (!wasAcknowledged) { + try { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + } catch (error) { + logger.warn( + { err: error }, + "No se pudo diferir respuesta al añadir separador" + ); + } + } + + 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), + }); + + const payload = { + content: "✅ Separador añadido correctamente.", + flags: MessageFlags.Ephemeral, + } as const; + + if (interaction.deferred) { + await interaction.editReply({ content: payload.content }).catch(() => {}); + } else if (interaction.replied) { + await interaction.followUp(payload).catch(() => {}); + } else { + await interaction.reply(payload).catch(() => {}); + } +} + +async function handleAddImage( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = createModal({ + title: "Añadir Imagen", + customId: "add_image_modal", + fields: [ + { + customId: "image_input", + style: TextInputStyle.Short, + required: true, + placeholder: "https://ejemplo.com/imagen.png", + maxLength: 512, + label: "URL de la Imagen", + }, + ], + }); + + await interaction.showModal(modal as any); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const imageUrl = modalInteraction.components + .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.editReply({ + content: "✅ Imagen añadida correctamente.", + }); + } else { + await modalInteraction.editReply({ + content: "❌ URL de imagen inválida.", + }); + } + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction + .editReply({ + content: "❌ No se pudo añadir la imagen. Inténtalo de nuevo.", + }) + .catch(() => {}); + } + if ( + error instanceof Error && + error.message.includes("Collector received no interactions") + ) { + return; + } + logger.error({ err: error }, "Error procesando modal de imagen"); + } +} + +async function handleCoverImage( + interaction: ButtonInteraction, + editorMessage: Message, + originalMessage: Message, + blockState: BlockState +): Promise { + const modal = { + title: "Imagen de Portada", + customId: "cover_image_modal", + components: [ + { + type: ComponentType.Label, + label: "URL de la Imagen de Portada", + component: { + type: ComponentType.TextInput, + customId: "cover_input", + style: TextInputStyle.Short, + required: false, + placeholder: "https://ejemplo.com/portada.png", + value: blockState.coverImage || "", + maxLength: 512, + }, + }, + ], + } as const; + + await interaction.showModal(modal); + + let modalInteraction: ModalSubmitInteraction | null = null; + try { + modalInteraction = await awaitModalWithDeferredReply(interaction); + if (!modalInteraction) return; + + const coverUrl = modalInteraction.components + .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.editReply({ + content: coverUrl + ? "✅ Imagen de portada actualizada." + : "✅ Imagen de portada removida.", + }); + } catch (error) { + if (modalInteraction?.deferred && !modalInteraction.replied) { + await modalInteraction + .editReply({ + content: + "❌ No se pudo actualizar la imagen de portada. Inténtalo de nuevo.", + }) + .catch(() => {}); + } + if ( + error instanceof Error && + error.message.includes("Collector received no interactions") + ) { + return; + } + logger.error({ err: error }, "Error procesando modal de portada"); + } +} + +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, - editorMessage: Message, - client: Amayo, - blockName: string, - blockState: BlockState, - guildId: string + interaction: ButtonInteraction, + editorMessage: Message, + client: Amayo, + blockName: string, + blockState: BlockState, + guildId: string ): Promise { + try { + stripLegacyDescriptionComponent(blockState); + 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-embed ${blockName}\``, + flags: MessageFlags.Ephemeral, + }); + + // Cerrar el editor eliminando el mensaje del editor try { - stripLegacyDescriptionComponent(blockState); - 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-embed ${blockName}\``, - flags: MessageFlags.Ephemeral - }); - - // Cerrar el editor eliminando el mensaje del editor - try { await editorMessage.delete(); } catch {} - } 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 - }); - } + await editorMessage.delete(); + } catch {} + } 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 { - try { - await interaction.deferUpdate(); - } catch {} - await updateEditor(editorMessage, { - display: { - type: 17, - components: [ - { type: 10, content: "❌ **Editor cancelado**" }, - { type: 10, content: "La creación del bloque ha sido cancelada." } - ] - } as any, - components: [] - }); +async function handleCancelBlock( + interaction: ButtonInteraction, + editorMessage: Message +): Promise { + try { + await interaction.deferUpdate(); + } catch {} + await updateEditor(editorMessage, { + display: { + type: 17, + components: [ + { type: 10, content: "❌ **Editor cancelado**" }, + { type: 10, content: "La creación del bloque ha sido cancelada." }, + ], + } as any, + components: [], + }); } async function handleEditorTimeout(editorMessage: Message): Promise { - try { - await updateEditor(editorMessage, { - display: { - type: 17, - components: [ - { type: 10, content: "⏰ **Editor expirado**" }, - { type: 10, content: "El editor ha expirado por inactividad. Usa el comando nuevamente para crear un bloque." } - ] - } as any, - components: [] - }); - } catch { - // message likely deleted - } + try { + await updateEditor(editorMessage, { + display: { + type: 17, + components: [ + { type: 10, content: "⏰ **Editor expirado**" }, + { + type: 10, + content: + "El editor ha expirado por inactividad. Usa el comando nuevamente para crear un bloque.", + }, + ], + } as any, + components: [], + }); + } catch { + // message likely deleted + } } diff --git a/src/core/client.ts b/src/core/client.ts index 9083069..0310fa4 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -1,64 +1,95 @@ -import { Client, GatewayIntentBits, Options, Partials } from 'discord.js'; -import { prisma, ensurePrismaConnection } from './database/prisma'; -import logger from './lib/logger'; +import { + Client, + GatewayIntentBits, + Options, + Partials, + ClientOptions, +} from "discord.js"; +import { prisma, ensurePrismaConnection } from "./database/prisma"; +import logger from "./lib/logger"; -class Amayo extends Client { - public key: string; - public prisma = prisma; - public mode: string; +const DEFAULTS = { + CACHE_MESSAGES_LIMIT: 50, + CACHE_MEMBERS_LIMIT: 100, + SWEEP_MESSAGES_INTERVAL_SECONDS: 300, + SWEEP_MESSAGES_LIFETIME_SECONDS: 900, + USERS_SWEEP_INTERVAL_SECONDS: 60 * 30, + REST_RETRIES: 5, +} as const; - constructor() { - super({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent - ], - partials: [Partials.Channel, Partials.Message], - makeCache: Options.cacheWithLimits({ - MessageManager: parseInt(process.env.CACHE_MESSAGES_LIMIT || '50', 10), - GuildMemberManager: parseInt(process.env.CACHE_MEMBERS_LIMIT || '100', 10), - ThreadManager: 10, - ReactionManager: 0, - GuildInviteManager: 0, - StageInstanceManager: 0, - PresenceManager: 0 - }), - sweepers: { - messages: { - interval: parseInt(process.env.SWEEP_MESSAGES_INTERVAL_SECONDS || '300', 10), - lifetime: parseInt(process.env.SWEEP_MESSAGES_LIFETIME_SECONDS || '900', 10) - }, - users: { - interval: 60 * 30, - filter: () => (user) => user.bot && user.id !== this.user?.id - } - }, - rest: { - retries: 5 - } - }); - - this.key = process.env.TOKEN ?? ''; - this.mode = process.env.MODE ?? 'Normal'; - } - - async play() { - if (!this.key) { - logger.error('No key provided'); - throw new Error('Missing DISCORD TOKEN'); - } else { - try { - await ensurePrismaConnection(); - logger.info('Successfully connected to the database (singleton).'); - await this.login(this.key); - } catch (error) { - logger.error({ err: error }, 'Failed to connect to DB or login to Discord'); - throw error; - } - } - } +function intEnv(name: keyof typeof DEFAULTS, fallback?: number): number { + const raw = process.env[name]; + const val = raw ? parseInt(raw, 10) : NaN; + return Number.isFinite(val) ? val : fallback ?? DEFAULTS[name]; } -export default Amayo; \ No newline at end of file +class Amayo extends Client { + public key: string; + public prisma = prisma; + public mode: string; + + constructor() { + // Build options here so `this` can be referenced in the users.sweep filter + const options: ClientOptions = { + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], + partials: [Partials.Channel, Partials.Message], + makeCache: Options.cacheWithLimits({ + MessageManager: intEnv("CACHE_MESSAGES_LIMIT"), + GuildMemberManager: intEnv("CACHE_MEMBERS_LIMIT"), + ThreadManager: 10, + ReactionManager: 0, + GuildInviteManager: 0, + StageInstanceManager: 0, + PresenceManager: 0, + }), + sweepers: { + messages: { + interval: intEnv("SWEEP_MESSAGES_INTERVAL_SECONDS"), + lifetime: intEnv("SWEEP_MESSAGES_LIFETIME_SECONDS"), + }, + users: { + interval: DEFAULTS.USERS_SWEEP_INTERVAL_SECONDS, + filter: () => (user) => user.bot && user.id !== this.user?.id, + }, + }, + rest: { + retries: DEFAULTS.REST_RETRIES, + }, + }; + + super(options); + + this.key = process.env.TOKEN ?? ""; + this.mode = process.env.MODE ?? "Normal"; + } + + /** + * Inicia la conexión a la base de datos y al gateway de Discord. + * Lanza si falta la clave o si falla la conexión/login. + */ + public async play(): Promise { + if (!this.key) { + logger.error("No key provided"); + throw new Error("Missing DISCORD TOKEN"); + } + + try { + await ensurePrismaConnection(); + logger.info("Successfully connected to the database (singleton)."); + await this.login(this.key); + } catch (error) { + logger.error( + { err: error }, + "Failed to connect to DB or login to Discord" + ); + throw error; + } + } +} + +export default Amayo;