Refactor modal handling for editDisplayComponent: improve error handling, logging, and input validation

This commit is contained in:
2025-10-05 16:02:16 -05:00
parent 4190e8ef89
commit 14c898fbec

View File

@@ -3,6 +3,7 @@ import { MessageFlags } from "discord.js";
import { ComponentType, ButtonStyle, TextInputStyle } from "discord-api-types/v10"; import { ComponentType, ButtonStyle, TextInputStyle } from "discord-api-types/v10";
import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars"; import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
import logger from "../../../core/lib/logger";
// Botones de edición (máx 5 por fila) // Botones de edición (máx 5 por fila)
const btns = (disabled = false) => ([ const btns = (disabled = false) => ([
@@ -824,69 +825,123 @@ export const command: CommandMessage = {
if (!interaction.isModalSubmit()) return; if (!interaction.isModalSubmit()) return;
if (interaction.user.id !== message.author.id) return; if (interaction.user.id !== message.author.id) return;
if (!modalHandlerActive) return; if (!modalHandlerActive) return;
const sendResponse = async (content: string) => {
try {
if (!interaction.deferred && !interaction.replied) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
}
if (interaction.deferred) {
await interaction.editReply({ content });
} else {
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
}
} catch (err) {
logger.error({ err }, 'Fallo al enviar respuesta del modal en editar-embed');
}
};
try { try {
const id = interaction.customId as string; const id = interaction.customId as string;
if (id === 'edit_title_modal') { if (id === 'edit_title_modal') {
blockState.title = interaction.fields.getTextInputValue('title_input'); const newTitle = interaction.components.getTextInputValue('title_input').trim();
await interaction.reply({ content: '✅ Título actualizado.', flags: 64 }); blockState.title = newTitle.length > 0 ? newTitle : blockState.title;
logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id }, 'Título actualizado mediante modal.');
await sendResponse('✅ Título actualizado.');
} else if (id === 'edit_description_modal') { } else if (id === 'edit_description_modal') {
const newDescription = interaction.fields.getTextInputValue('description_input').trim(); const newDescription = interaction.components.getTextInputValue('description_input').trim();
blockState.description = newDescription.length > 0 ? newDescription : undefined; blockState.description = newDescription.length > 0 ? newDescription : undefined;
await interaction.reply({ content: 'Descripción actualizada.', flags: 64 }); logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id }, 'Descripción actualizada mediante modal.');
await sendResponse('✅ Descripción actualizada.');
} else if (id === 'edit_color_modal') { } else if (id === 'edit_color_modal') {
const colorInput = interaction.fields.getTextInputValue('color_input'); const colorInput = interaction.components.getTextInputValue('color_input').trim();
if (colorInput.trim() === '') blockState.color = null; else { if (colorInput === '') {
blockState.color = null;
await sendResponse('✅ Color eliminado.');
} else {
const hexColor = colorInput.replace('#', ''); const hexColor = colorInput.replace('#', '');
if (/^[0-9A-F]{6}$/i.test(hexColor)) blockState.color = parseInt(hexColor, 16); else { if (/^[0-9A-F]{6}$/i.test(hexColor)) {
await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', flags: 64 }); blockState.color = parseInt(hexColor, 16);
logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id, color: blockState.color }, 'Color actualizado mediante modal.');
await sendResponse('✅ Color actualizado.');
} else {
await sendResponse('❌ Color inválido. Usa formato HEX (#FF5733)');
return; return;
} }
} }
await interaction.reply({ content: '✅ Color actualizado.', flags: 64 });
} else if (id === 'add_content_modal') { } else if (id === 'add_content_modal') {
const newContent = interaction.fields.getTextInputValue('content_input'); const newContent = interaction.components.getTextInputValue('content_input').trim();
if (!newContent) {
await sendResponse('❌ Debes ingresar contenido para añadir.');
return;
}
blockState.components.push({ type: 10, content: newContent, thumbnail: null }); blockState.components.push({ type: 10, content: newContent, thumbnail: null });
await interaction.reply({ content: 'Contenido añadido.', flags: 64 }); logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id }, 'Contenido añadido mediante modal.');
await sendResponse('✅ Contenido añadido.');
} else if (id === 'add_image_modal') { } else if (id === 'add_image_modal') {
const imageUrl = interaction.fields.getTextInputValue('image_url_input'); const imageUrl = interaction.components.getTextInputValue('image_url_input').trim();
if (isValidUrl(imageUrl)) { blockState.components.push({ type: 12, url: imageUrl }); await interaction.reply({ content: '✅ Imagen añadida.', flags: 64 }); } if (!isValidUrl(imageUrl)) {
else { await interaction.reply({ content: '❌ URL de imagen inválida.', flags: 64 }); return; } await sendResponse('❌ URL de imagen inválida.');
return;
}
blockState.components.push({ type: 12, url: imageUrl });
logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id }, 'Imagen añadida mediante modal.');
await sendResponse('✅ Imagen añadida.');
} else if (id === 'add_cover_modal' || id === 'edit_cover_modal') { } else if (id === 'add_cover_modal' || id === 'edit_cover_modal') {
const coverUrl = interaction.fields.getTextInputValue('cover_input'); const coverUrl = interaction.components.getTextInputValue('cover_input').trim();
if (isValidUrl(coverUrl)) { blockState.coverImage = coverUrl; await interaction.reply({ content: '✅ Imagen de portada actualizada.', flags: 64 }); } if (!isValidUrl(coverUrl)) {
else { await interaction.reply({ content: '❌ URL de portada inválida.', flags: 64 }); return; } await sendResponse('❌ URL de portada inválida.');
return;
}
blockState.coverImage = coverUrl;
logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id }, 'Portada actualizada mediante modal.');
await sendResponse('✅ Imagen de portada actualizada.');
} else if (id === 'add_separator_modal') { } else if (id === 'add_separator_modal') {
const visibleStr = interaction.fields.getTextInputValue('separator_visible').toLowerCase(); const visibleStr = interaction.components.getTextInputValue('separator_visible').toLowerCase();
const spacingStr = interaction.fields.getTextInputValue('separator_spacing') || '1'; const spacingStr = interaction.components.getTextInputValue('separator_spacing') || '1';
const divider = visibleStr === 'true' || visibleStr === '1' || visibleStr === 'si' || visibleStr === 'sí'; const divider = visibleStr === 'true' || visibleStr === '1' || visibleStr === 'si' || visibleStr === 'sí';
const spacing = Math.min(3, Math.max(1, parseInt(spacingStr) || 1)); const spacing = Math.min(3, Math.max(1, parseInt(spacingStr) || 1));
blockState.components.push({ type: 14, divider, spacing }); blockState.components.push({ type: 14, divider, spacing });
await interaction.reply({ content: 'Separador añadido.', flags: 64 }); logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id, divider, spacing }, 'Separador añadido mediante modal.');
await sendResponse('✅ Separador añadido.');
} else if (id.startsWith('edit_thumbnail_modal_')) { } else if (id.startsWith('edit_thumbnail_modal_')) {
const idx = parseInt(id.replace('edit_thumbnail_modal_', '')); const idx = parseInt(id.replace('edit_thumbnail_modal_', ''));
const textComp = blockState.components[idx]; const textComp = blockState.components[idx];
if (!textComp || textComp.type !== 10) return; if (!textComp || textComp.type !== 10) return;
const thumbnailUrl = interaction.fields.getTextInputValue('thumbnail_input'); const thumbnailUrl = interaction.components.getTextInputValue('thumbnail_input').trim();
if (thumbnailUrl.trim() === '') { textComp.thumbnail = null; await interaction.reply({ content: '✅ Thumbnail eliminado.', flags: 64 }); } if (thumbnailUrl === '') {
else if (!isValidUrl(thumbnailUrl)) { await interaction.reply({ content: '❌ URL de thumbnail inválida.', flags: 64 }); return; } textComp.thumbnail = null;
else { await sendResponse('✅ Thumbnail eliminado.');
if (textComp.linkButton) { await interaction.reply({ content: '❌ Este bloque ya tiene un botón link. Elimina el botón antes de añadir thumbnail.', flags: 64 }); return; } } else if (!isValidUrl(thumbnailUrl)) {
textComp.thumbnail = thumbnailUrl; await interaction.reply({ content: '✅ Thumbnail actualizado.', flags: 64 }); await sendResponse('❌ URL de thumbnail inválida.');
return;
} else {
if (textComp.linkButton) {
await sendResponse('❌ Este bloque ya tiene un botón link. Elimínalo antes de añadir thumbnail.');
return;
}
textComp.thumbnail = thumbnailUrl;
logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id }, 'Thumbnail actualizado mediante modal.');
await sendResponse('✅ Thumbnail actualizado.');
} }
} else if (id.startsWith('create_link_button_modal_') || id.startsWith('edit_link_button_modal_')) { } else if (id.startsWith('create_link_button_modal_') || id.startsWith('edit_link_button_modal_')) {
const idx = parseInt(id.replace('create_link_button_modal_', '').replace('edit_link_button_modal_', '')); const idx = parseInt(id.replace('create_link_button_modal_', '').replace('edit_link_button_modal_', ''));
const textComp = blockState.components[idx]; const textComp = blockState.components[idx];
if (!textComp || textComp.type !== 10) return; if (!textComp || textComp.type !== 10) return;
const url = interaction.fields.getTextInputValue('link_url_input'); const url = interaction.components.getTextInputValue('link_url_input').trim();
const label = (interaction.fields.getTextInputValue('link_label_input') || '').trim(); const label = (interaction.components.getTextInputValue('link_label_input') || '').trim();
const emojiStr = (interaction.fields.getTextInputValue('link_emoji_input') || '').trim(); const emojiStr = (interaction.components.getTextInputValue('link_emoji_input') || '').trim();
if (!isValidUrl(url)) { await interaction.reply({ content: '❌ URL inválida para el botón.', flags: 64 }); return; } if (!isValidUrl(url)) { await sendResponse('❌ URL inválida para el botón.'); return; }
const parsedEmoji = parseEmojiInput(emojiStr || undefined); const parsedEmoji = parseEmojiInput(emojiStr || undefined);
if (!label && !parsedEmoji) { await interaction.reply({ content: '❌ Debes proporcionar al menos una etiqueta o un emoji.', flags: 64 }); return; } if (!label && !parsedEmoji) { await sendResponse('❌ Debes proporcionar al menos una etiqueta o un emoji.'); return; }
if (textComp.thumbnail) { await interaction.reply({ content: '❌ Este bloque tiene thumbnail. Elimínalo antes de añadir un botón link.', flags: 64 }); return; } if (textComp.thumbnail) { await sendResponse('❌ Este bloque tiene thumbnail. Elimínalo antes de añadir un botón link.'); return; }
textComp.linkButton = { url, label: label || undefined, emoji: emojiStr || undefined }; textComp.linkButton = { url, label: label || undefined, emoji: emojiStr || undefined };
await interaction.reply({ content: 'Botón link actualizado.', flags: 64 }); logger.info({ modalId: id, guildId: message.guildId, userId: interaction.user.id }, 'Botón link actualizado mediante modal.');
} else { return; } await sendResponse('✅ Botón link actualizado.');
} else {
return;
}
setTimeout(async () => { setTimeout(async () => {
if (!modalHandlerActive) return; if (!modalHandlerActive) return;
@@ -897,9 +952,20 @@ export const command: CommandMessage = {
display: await renderPreview(blockState, message.member, message.guild), display: await renderPreview(blockState, message.member, message.guild),
components: btns(false) components: btns(false)
}); });
} catch {} } catch (err) {
logger.error({ err }, 'Error actualizando editor tras procesar modal.');
}
}, 400); }, 400);
} catch {} } catch (error) {
logger.error({ err: error, userId: interaction.user.id, modalId: interaction.customId }, 'Error procesando modal en editar-embed');
if (!interaction.deferred && !interaction.replied) {
try {
await interaction.reply({ content: '❌ Error procesando el modal. Revisa los logs.', flags: MessageFlags.Ephemeral });
} catch (err) {
logger.error({ err }, 'Fallo al responder con error tras excepción en modal.');
}
}
}
}; };
client.on('interactionCreate', modalHandler); client.on('interactionCreate', modalHandler);