ni yo se que hice xd
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { CommandMessage } from "../../../core/types/commands";
|
import { CommandMessage } from "../../../core/types/commands";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags } from "discord.js";
|
import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags } from "discord.js";
|
||||||
import { replaceVars, isValidUrlOrVariable } from "../../../core/lib/vars";
|
import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Botones de edición - VERSIÓN MEJORADA
|
* Botones de edición - VERSIÓN MEJORADA
|
||||||
@@ -23,6 +23,7 @@ const btns = (disabled = false) => ([
|
|||||||
{ style: ButtonStyle.Secondary, type: 2, label: "🖼️ Imagen", disabled, custom_id: "add_image" },
|
{ style: ButtonStyle.Secondary, type: 2, label: "🖼️ Imagen", disabled, custom_id: "add_image" },
|
||||||
{ style: ButtonStyle.Secondary, type: 2, label: "🖼️ Portada", disabled, custom_id: "cover_image" },
|
{ style: ButtonStyle.Secondary, type: 2, label: "🖼️ Portada", disabled, custom_id: "cover_image" },
|
||||||
{ style: ButtonStyle.Secondary, type: 2, label: "📎 Thumbnail", disabled, custom_id: "edit_thumbnail" },
|
{ style: ButtonStyle.Secondary, type: 2, label: "📎 Thumbnail", disabled, custom_id: "edit_thumbnail" },
|
||||||
|
{ style: ButtonStyle.Secondary, type: 2, label: "🔗 Crear Botón Link", disabled, custom_id: "edit_link_button" },
|
||||||
{ style: ButtonStyle.Primary, type: 2, label: "🔄 Mover", disabled, custom_id: "move_block" },
|
{ style: ButtonStyle.Primary, type: 2, label: "🔄 Mover", disabled, custom_id: "move_block" },
|
||||||
{ style: ButtonStyle.Danger, type: 2, label: "🗑️ Eliminar", disabled, custom_id: "delete_block" }
|
{ style: ButtonStyle.Danger, type: 2, label: "🗑️ Eliminar", disabled, custom_id: "delete_block" }
|
||||||
]
|
]
|
||||||
@@ -73,6 +74,55 @@ const validateContent = (content: string): string => {
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Validación y parseo de emoji (unicode o personalizado <a:name:id> / <:name:id>)
|
||||||
|
const parseEmojiInput = (input?: string): any | null => {
|
||||||
|
if (!input) return null;
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
const match = trimmed.match(/^<(a?):(\w+):(\d+)>$/);
|
||||||
|
if (match) {
|
||||||
|
const animated = match[1] === 'a';
|
||||||
|
const name = match[2];
|
||||||
|
const id = match[3];
|
||||||
|
return { id, name, animated };
|
||||||
|
}
|
||||||
|
// Asumimos unicode si no es formato de emoji personalizado
|
||||||
|
return { name: trimmed };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye un accesorio de botón link para Display Components
|
||||||
|
*/
|
||||||
|
const buildLinkAccessory = async (link: any, member: any, guild: any) => {
|
||||||
|
if (!link || !link.url) return null;
|
||||||
|
// @ts-ignore
|
||||||
|
const processedUrl = await replaceVars(link.url, member, guild);
|
||||||
|
if (!isValidUrl(processedUrl)) return null;
|
||||||
|
|
||||||
|
const accessory: any = {
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Link,
|
||||||
|
url: processedUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
if (link.label && typeof link.label === 'string' && link.label.trim().length > 0) {
|
||||||
|
accessory.label = link.label.trim().slice(0, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link.emoji && typeof link.emoji === 'string') {
|
||||||
|
const parsed = parseEmojiInput(link.emoji);
|
||||||
|
if (parsed) accessory.emoji = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debe tener al menos label o emoji
|
||||||
|
if (!accessory.label && !accessory.emoji) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessory;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generar vista previa
|
* Generar vista previa
|
||||||
*/
|
*/
|
||||||
@@ -102,15 +152,26 @@ const renderPreview = async (blockState: any, member: any, guild: any) => {
|
|||||||
// Procesar componentes en orden
|
// Procesar componentes en orden
|
||||||
for (const c of blockState.components) {
|
for (const c of blockState.components) {
|
||||||
if (c.type === 10) {
|
if (c.type === 10) {
|
||||||
// Componente de texto con thumbnail opcional
|
// Componente de texto con accessory opcional (thumbnail o botón link)
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, member, guild) : null;
|
const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, member, guild) : null;
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const processedContent = await replaceVars(c.content || "Sin contenido", member, guild);
|
const processedContent = await replaceVars(c.content || "Sin contenido", member, guild);
|
||||||
const validatedContent = validateContent(processedContent);
|
const validatedContent = validateContent(processedContent);
|
||||||
|
|
||||||
if (processedThumbnail && isValidUrl(processedThumbnail)) {
|
// Construir accessory según prioridad: linkButton > thumbnail
|
||||||
// Si tiene thumbnail válido, usar contenedor tipo 9 con accessory
|
let accessory: any = null;
|
||||||
|
if (c.linkButton) {
|
||||||
|
accessory = await buildLinkAccessory(c.linkButton, member, guild);
|
||||||
|
}
|
||||||
|
if (!accessory && processedThumbnail && isValidUrl(processedThumbnail)) {
|
||||||
|
accessory = {
|
||||||
|
type: 11,
|
||||||
|
media: { url: processedThumbnail }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessory) {
|
||||||
previewComponents.push({
|
previewComponents.push({
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [
|
components: [
|
||||||
@@ -119,13 +180,10 @@ const renderPreview = async (blockState: any, member: any, guild: any) => {
|
|||||||
content: validatedContent
|
content: validatedContent
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
accessory: {
|
accessory
|
||||||
type: 11,
|
|
||||||
media: { url: processedThumbnail }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Sin thumbnail o thumbnail inválido, componente normal
|
// Sin accessory válido
|
||||||
previewComponents.push({
|
previewComponents.push({
|
||||||
type: 10,
|
type: 10,
|
||||||
content: validatedContent
|
content: validatedContent
|
||||||
@@ -213,7 +271,7 @@ export const command: CommandMessage = {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await editorMessage.edit({
|
await editorMessage.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 4096,
|
||||||
components: [
|
components: [
|
||||||
await renderPreview(blockState, message.member, message.guild),
|
await renderPreview(blockState, message.member, message.guild),
|
||||||
...btns(false)
|
...btns(false)
|
||||||
@@ -473,8 +531,8 @@ export const command: CommandMessage = {
|
|||||||
: `Componente ${c.type}`,
|
: `Componente ${c.type}`,
|
||||||
value: idx.toString(),
|
value: idx.toString(),
|
||||||
description:
|
description:
|
||||||
c.type === 10 && c.thumbnail
|
c.type === 10 && (c.thumbnail || c.linkButton)
|
||||||
? "Con thumbnail"
|
? (c.thumbnail ? "Con thumbnail" : "Con botón link")
|
||||||
: undefined
|
: undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -557,7 +615,7 @@ export const command: CommandMessage = {
|
|||||||
}
|
}
|
||||||
case "delete_block": {
|
case "delete_block": {
|
||||||
// Incluir portada en las opciones si existe
|
// Incluir portada en las opciones si existe
|
||||||
const options = [];
|
const options = [] as any[];
|
||||||
|
|
||||||
// Añadir portada como opción si existe
|
// Añadir portada como opción si existe
|
||||||
if (blockState.coverImage) {
|
if (blockState.coverImage) {
|
||||||
@@ -575,14 +633,14 @@ export const command: CommandMessage = {
|
|||||||
c.type === 10
|
c.type === 10
|
||||||
? `Texto: ${c.content?.slice(0, 30) || "..."}`
|
? `Texto: ${c.content?.slice(0, 30) || "..."}`
|
||||||
: c.type === 14
|
: c.type === 14
|
||||||
? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}`
|
? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` // <-- Arreglado aquí
|
||||||
: c.type === 12
|
: c.type === 12
|
||||||
? `Imagen: ${c.url?.slice(-30) || "..."}`
|
? `Imagen: ${c.url?.slice(-30) || "..."}`
|
||||||
: `Componente ${c.type}`,
|
: `Componente ${c.type}`,
|
||||||
value: idx.toString(),
|
value: idx.toString(),
|
||||||
description:
|
description:
|
||||||
c.type === 10 && c.thumbnail
|
c.type === 10 && (c.thumbnail || c.linkButton)
|
||||||
? "Con thumbnail"
|
? (c.thumbnail ? "Con thumbnail" : "Con botón link")
|
||||||
: undefined
|
: undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -641,27 +699,36 @@ export const command: CommandMessage = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "show_variables": {
|
case "show_variables": {
|
||||||
await i.deferReply({ flags: 64 }); // MessageFlags.Ephemeral
|
// Construir lista de variables dinámicamente desde var.ts
|
||||||
|
const vars = listVariables();
|
||||||
|
const chunked: string[] = [];
|
||||||
|
let current = "";
|
||||||
|
for (const v of vars) {
|
||||||
|
const line = `• ${v}\n`;
|
||||||
|
if ((current + line).length > 1800) {
|
||||||
|
chunked.push(current);
|
||||||
|
current = line;
|
||||||
|
} else {
|
||||||
|
current += line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current) chunked.push(current);
|
||||||
|
|
||||||
|
// Responder en uno o varios mensajes efímeros según el tamaño
|
||||||
|
if (chunked.length === 0) {
|
||||||
|
await i.deferReply({ flags: 64 });
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await i.editReply({
|
await i.editReply({ content: "No hay variables registradas." });
|
||||||
content: "📋 **Variables Disponibles:**\n\n" +
|
} else {
|
||||||
"**👤 Usuario:**\n" +
|
// Primer bloque
|
||||||
"`user.name` - Nombre del usuario\n" +
|
//@ts-ignore
|
||||||
"`user.id` - ID del usuario\n" +
|
await i.reply({ flags: 64, content: `📋 **Variables Disponibles:**\n\n${chunked[0]}` });
|
||||||
"`user.mention` - Mención del usuario\n" +
|
// Bloques adicionales si hiciera falta
|
||||||
"`user.avatar` - Avatar del usuario\n\n" +
|
for (let idx = 1; idx < chunked.length; idx++) {
|
||||||
"**📊 Estadísticas:**\n" +
|
//@ts-ignore
|
||||||
"`user.pointsAll` - Puntos totales\n" +
|
await i.followUp({ flags: 64, content: chunked[idx] });
|
||||||
"`user.pointsWeekly` - Puntos semanales\n" +
|
}
|
||||||
"`user.pointsMonthly` - Puntos mensuales\n\n" +
|
}
|
||||||
"**🏠 Servidor:**\n" +
|
|
||||||
"`guild.name` - Nombre del servidor\n" +
|
|
||||||
"`guild.icon` - Ícono del servidor\n\n" +
|
|
||||||
"**🔗 Invitación:**\n" +
|
|
||||||
"`invite.name` - Nombre del servidor invitado\n" +
|
|
||||||
"`invite.icon` - Ícono del servidor invitado\n\n" +
|
|
||||||
"💡 **Nota:** Las variables se usan SIN llaves `{}` en los campos de URL/imagen."
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "duplicate_block": {
|
case "duplicate_block": {
|
||||||
@@ -671,7 +738,7 @@ export const command: CommandMessage = {
|
|||||||
: c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}`
|
: c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}`
|
||||||
: `Componente ${c.type}`,
|
: `Componente ${c.type}`,
|
||||||
value: idx.toString(),
|
value: idx.toString(),
|
||||||
description: c.type === 10 && c.thumbnail ? "Con thumbnail" : undefined
|
description: c.type === 10 && (c.thumbnail || c.linkButton) ? (c.thumbnail ? "Con thumbnail" : "Con botón link") : undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
@@ -726,7 +793,7 @@ export const command: CommandMessage = {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await i.reply({
|
await i.reply({
|
||||||
flags: 64, // MessageFlags.Ephemeral
|
flags: 64, // MessageFlags.Ephemeral
|
||||||
content: `\`\`\`json\n${truncated}\`\`\``
|
content: `\`\`\`json\n${truncated}\n\`\`\``
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -759,7 +826,7 @@ export const command: CommandMessage = {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await i.reply({
|
await i.reply({
|
||||||
flags: 64, // MessageFlags.Ephemeral
|
flags: 64, // MessageFlags.Ephemeral
|
||||||
content: `📤 **JSON Exportado:**\n\`\`\`json\n${truncatedJson}\`\`\`\n\n💡 **Tip:** Copia el JSON de arriba manualmente y pégalo donde necesites.`
|
content: `📤 **JSON Exportado:**\n\`\`\`json\n${truncatedJson}\n\`\`\`\n\n💡 **Tip:** Copia el JSON de arriba manualmente y pégalo donde necesites.`
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -795,20 +862,48 @@ export const command: CommandMessage = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "edit_thumbnail": {
|
case "edit_thumbnail": {
|
||||||
// Buscar el primer componente de texto para añadir/editar thumbnail
|
// Construir listado de TextDisplays
|
||||||
const textComp = blockState.components.find((c: any) => c.type === 10);
|
const textDisplays = blockState.components
|
||||||
|
.map((c: any, idx: number) => ({ c, idx }))
|
||||||
|
.filter(({ c }: any) => c.type === 10);
|
||||||
|
|
||||||
if (!textComp) {
|
if (textDisplays.length === 0) {
|
||||||
await i.deferReply({ flags: 64 }); // MessageFlags.Ephemeral
|
await i.deferReply({ flags: 64 });
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await i.editReply({
|
await i.editReply({ content: "❌ No hay bloques de texto para editar thumbnail." });
|
||||||
content: "❌ Necesitas al menos un componente de texto para añadir thumbnail."
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const options = textDisplays.map(({ c, idx }: any) => ({
|
||||||
|
label: `Texto #${idx + 1}: ${c.content?.slice(0, 30) || '...'}`,
|
||||||
|
value: String(idx),
|
||||||
|
description: c.thumbnail ? 'Con thumbnail' : c.linkButton ? 'Con botón link' : 'Sin accesorio'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const reply = await i.reply({
|
||||||
|
flags: 64,
|
||||||
|
content: "Elige el TextDisplay a editar su thumbnail:",
|
||||||
|
components: [
|
||||||
|
{ type: 1, components: [ { type: 3, custom_id: 'choose_text_for_thumbnail', placeholder: 'Selecciona un bloque de texto', options } ] }
|
||||||
|
],
|
||||||
|
fetchReply: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const selCollector = reply.createMessageComponentCollector({
|
||||||
|
componentType: ComponentType.StringSelect,
|
||||||
|
max: 1,
|
||||||
|
time: 60000,
|
||||||
|
filter: (it: any) => it.user.id === message.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
selCollector.on('collect', async (sel: any) => {
|
||||||
|
const idx = parseInt(sel.values[0]);
|
||||||
|
const textComp = blockState.components[idx];
|
||||||
|
|
||||||
const modal = new ModalBuilder()
|
const modal = new ModalBuilder()
|
||||||
.setCustomId('edit_thumbnail_modal')
|
.setCustomId(`edit_thumbnail_modal_${idx}`)
|
||||||
.setTitle('📎 Editar Thumbnail');
|
.setTitle('📎 Editar Thumbnail');
|
||||||
|
|
||||||
const thumbnailInput = new TextInputBuilder()
|
const thumbnailInput = new TextInputBuilder()
|
||||||
@@ -816,15 +911,175 @@ export const command: CommandMessage = {
|
|||||||
.setLabel('URL del Thumbnail')
|
.setLabel('URL del Thumbnail')
|
||||||
.setStyle(TextInputStyle.Short)
|
.setStyle(TextInputStyle.Short)
|
||||||
.setPlaceholder('https://ejemplo.com/thumbnail.png o dejar vacío para eliminar')
|
.setPlaceholder('https://ejemplo.com/thumbnail.png o dejar vacío para eliminar')
|
||||||
.setValue(textComp.thumbnail || '')
|
.setValue(textComp?.thumbnail || '')
|
||||||
.setMaxLength(2000)
|
.setMaxLength(2000)
|
||||||
.setRequired(false);
|
.setRequired(false);
|
||||||
|
|
||||||
const firstRow = new ActionRowBuilder().addComponents(thumbnailInput);
|
const firstRow = new ActionRowBuilder().addComponents(thumbnailInput);
|
||||||
modal.addComponents(firstRow);
|
modal.addComponents(firstRow);
|
||||||
|
|
||||||
|
await sel.update({ content: 'Abriendo modal…', components: [] });
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await i.showModal(modal);
|
await i.showModal(modal);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "edit_link_button": {
|
||||||
|
// Elegir a qué TextDisplay aplicar
|
||||||
|
const textDisplays = blockState.components
|
||||||
|
.map((c: any, idx: number) => ({ c, idx }))
|
||||||
|
.filter(({ c }: any) => c.type === 10);
|
||||||
|
|
||||||
|
if (textDisplays.length === 0) {
|
||||||
|
await i.deferReply({ flags: 64 });
|
||||||
|
// @ts-ignore
|
||||||
|
await i.editReply({ content: "❌ Necesitas al menos un componente de texto para añadir un botón link." });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = textDisplays.map(({ c, idx }: any) => ({
|
||||||
|
label: `Texto #${idx + 1}: ${c.content?.slice(0, 30) || '...'}`,
|
||||||
|
value: String(idx),
|
||||||
|
description: c.linkButton ? 'Con botón link' : c.thumbnail ? 'Con thumbnail' : 'Sin accesorio'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const reply = await i.reply({
|
||||||
|
flags: 64,
|
||||||
|
content: "Elige el TextDisplay donde agregar/editar el botón link:",
|
||||||
|
components: [
|
||||||
|
{ type: 1, components: [ { type: 3, custom_id: 'choose_text_for_linkbtn', placeholder: 'Selecciona un bloque de texto', options } ] }
|
||||||
|
],
|
||||||
|
fetchReply: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const selCollector = reply.createMessageComponentCollector({
|
||||||
|
componentType: ComponentType.StringSelect,
|
||||||
|
max: 1,
|
||||||
|
time: 60000,
|
||||||
|
filter: (it: any) => it.user.id === message.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
selCollector.on('collect', async (sel: any) => {
|
||||||
|
const idx = parseInt(sel.values[0]);
|
||||||
|
const textComp = blockState.components[idx];
|
||||||
|
|
||||||
|
// Regla de exclusividad
|
||||||
|
if (textComp.thumbnail) {
|
||||||
|
await sel.update({ content: '❌ Este bloque ya tiene un thumbnail. Elimínalo antes de añadir un botón link.', components: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textComp.linkButton) {
|
||||||
|
// @ts-ignore
|
||||||
|
const sub = await i.followUp({
|
||||||
|
flags: 64,
|
||||||
|
content: `Texto #${idx + 1}: ya tiene botón link. ¿Qué deseas hacer?`,
|
||||||
|
components: [
|
||||||
|
{ type: 1, components: [
|
||||||
|
{ type: 2, style: ButtonStyle.Primary, label: '✏️ Editar', custom_id: `edit_link_button_modal_${idx}` },
|
||||||
|
{ type: 2, style: ButtonStyle.Danger, label: '🗑️ Eliminar', custom_id: `delete_link_button_${idx}` }
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
fetchReply: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const btnCollector = sub.createMessageComponentCollector({
|
||||||
|
componentType: ComponentType.Button,
|
||||||
|
max: 1,
|
||||||
|
time: 60000,
|
||||||
|
filter: (b: any) => b.user.id === message.author.id
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCollector.on('collect', async (b: any) => {
|
||||||
|
if (b.customId.startsWith('edit_link_button_modal_')) {
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId(`edit_link_button_modal_${idx}`)
|
||||||
|
.setTitle('🔗 Editar Botón Link');
|
||||||
|
|
||||||
|
const urlInput = new TextInputBuilder()
|
||||||
|
.setCustomId('link_url_input')
|
||||||
|
.setLabel('URL del botón (obligatoria)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('https://ejemplo.com')
|
||||||
|
.setValue(textComp.linkButton?.url || '')
|
||||||
|
.setMaxLength(2000)
|
||||||
|
.setRequired(true);
|
||||||
|
|
||||||
|
const labelInput = new TextInputBuilder()
|
||||||
|
.setCustomId('link_label_input')
|
||||||
|
.setLabel('Etiqueta (opcional)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('Texto del botón o vacío para usar solo emoji')
|
||||||
|
.setValue(textComp.linkButton?.label || '')
|
||||||
|
.setMaxLength(80)
|
||||||
|
.setRequired(false);
|
||||||
|
|
||||||
|
const emojiInput = new TextInputBuilder()
|
||||||
|
.setCustomId('link_emoji_input')
|
||||||
|
.setLabel('Emoji (opcional)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('Ej: 🔗 o <:name:id>')
|
||||||
|
.setValue(textComp.linkButton?.emoji || '')
|
||||||
|
.setMaxLength(64)
|
||||||
|
.setRequired(false);
|
||||||
|
|
||||||
|
const r1 = new ActionRowBuilder().addComponents(urlInput);
|
||||||
|
const r2 = new ActionRowBuilder().addComponents(labelInput);
|
||||||
|
const r3 = new ActionRowBuilder().addComponents(emojiInput);
|
||||||
|
modal.addComponents(r1, r2, r3);
|
||||||
|
|
||||||
|
await b.update({ content: 'Abriendo modal…', components: [] });
|
||||||
|
// @ts-ignore
|
||||||
|
await i.showModal(modal);
|
||||||
|
} else if (b.customId.startsWith('delete_link_button_')) {
|
||||||
|
delete textComp.linkButton;
|
||||||
|
await b.update({ content: '✅ Botón link eliminado.', components: [] });
|
||||||
|
await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const modal = new ModalBuilder()
|
||||||
|
.setCustomId(`create_link_button_modal_${idx}`)
|
||||||
|
.setTitle('🔗 Crear Botón Link');
|
||||||
|
|
||||||
|
const urlInput = new TextInputBuilder()
|
||||||
|
.setCustomId('link_url_input')
|
||||||
|
.setLabel('URL del botón (obligatoria)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('https://ejemplo.com')
|
||||||
|
.setMaxLength(2000)
|
||||||
|
.setRequired(true);
|
||||||
|
|
||||||
|
const labelInput = new TextInputBuilder()
|
||||||
|
.setCustomId('link_label_input')
|
||||||
|
.setLabel('Etiqueta (opcional)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('Texto del botón o vacío para usar solo emoji')
|
||||||
|
.setMaxLength(80)
|
||||||
|
.setRequired(false);
|
||||||
|
|
||||||
|
const emojiInput = new TextInputBuilder()
|
||||||
|
.setCustomId('link_emoji_input')
|
||||||
|
.setLabel('Emoji (opcional)')
|
||||||
|
.setStyle(TextInputStyle.Short)
|
||||||
|
.setPlaceholder('Ej: 🔗 o <:name:id>')
|
||||||
|
.setMaxLength(64)
|
||||||
|
.setRequired(false);
|
||||||
|
|
||||||
|
const r1 = new ActionRowBuilder().addComponents(urlInput);
|
||||||
|
const r2 = new ActionRowBuilder().addComponents(labelInput);
|
||||||
|
const r3 = new ActionRowBuilder().addComponents(emojiInput);
|
||||||
|
modal.addComponents(r1, r2, r3);
|
||||||
|
|
||||||
|
await sel.update({ content: 'Abriendo modal…', components: [] });
|
||||||
|
// @ts-ignore
|
||||||
|
await i.showModal(modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -841,28 +1096,20 @@ export const command: CommandMessage = {
|
|||||||
const modalHandler = async (interaction: any) => {
|
const modalHandler = async (interaction: any) => {
|
||||||
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 (!interaction.customId.endsWith('_modal')) return;
|
// Quitamos la restricción de endsWith('_modal') para permitir IDs dinámicos con índice
|
||||||
if (!modalHandlerActive) return; // Evitar procesar si ya no está activo
|
if (!modalHandlerActive) return; // Evitar procesar si ya no está activo
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (interaction.customId) {
|
const id = interaction.customId as string;
|
||||||
case 'edit_title_modal': {
|
if (id === 'edit_title_modal') {
|
||||||
blockState.title = interaction.fields.getTextInputValue('title_input');
|
blockState.title = interaction.fields.getTextInputValue('title_input');
|
||||||
await interaction.reply({ content: '✅ Título actualizado.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Título actualizado.', flags: 64 });
|
||||||
break;
|
} else if (id === 'edit_description_modal') {
|
||||||
}
|
|
||||||
case 'edit_description_modal': {
|
|
||||||
const newDescription = interaction.fields.getTextInputValue('description_input');
|
const newDescription = interaction.fields.getTextInputValue('description_input');
|
||||||
const descComp = blockState.components.find((c: any) => c.type === 10);
|
const firstText = blockState.components.find((c: any) => c.type === 10);
|
||||||
if (descComp) {
|
if (firstText) firstText.content = newDescription; else blockState.components.push({ type: 10, content: newDescription, thumbnail: null });
|
||||||
descComp.content = newDescription;
|
await interaction.reply({ content: '✅ Descripción actualizada.', flags: 64 });
|
||||||
} else {
|
} else if (id === 'edit_color_modal') {
|
||||||
blockState.components.push({ type: 10, content: newDescription, thumbnail: null });
|
|
||||||
}
|
|
||||||
await interaction.reply({ content: '✅ Descripción actualizada.', flags: MessageFlags.Ephemeral });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'edit_color_modal': {
|
|
||||||
const colorInput = interaction.fields.getTextInputValue('color_input');
|
const colorInput = interaction.fields.getTextInputValue('color_input');
|
||||||
if (colorInput.trim() === '') {
|
if (colorInput.trim() === '') {
|
||||||
blockState.color = null;
|
blockState.color = null;
|
||||||
@@ -871,80 +1118,96 @@ export const command: CommandMessage = {
|
|||||||
if (/^[0-9A-F]{6}$/i.test(hexColor)) {
|
if (/^[0-9A-F]{6}$/i.test(hexColor)) {
|
||||||
blockState.color = parseInt(hexColor, 16);
|
blockState.color = parseInt(hexColor, 16);
|
||||||
} else {
|
} else {
|
||||||
await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', flags: 64 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await interaction.reply({ content: '✅ Color actualizado.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Color actualizado.', flags: 64 });
|
||||||
break;
|
} else if (id === 'add_content_modal') {
|
||||||
}
|
|
||||||
case 'add_content_modal': {
|
|
||||||
const newContent = interaction.fields.getTextInputValue('content_input');
|
const newContent = interaction.fields.getTextInputValue('content_input');
|
||||||
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: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Contenido añadido.', flags: 64 });
|
||||||
break;
|
} else if (id === 'add_image_modal') {
|
||||||
}
|
|
||||||
case 'add_image_modal': {
|
|
||||||
const imageUrl = interaction.fields.getTextInputValue('image_url_input');
|
const imageUrl = interaction.fields.getTextInputValue('image_url_input');
|
||||||
if (isValidUrl(imageUrl)) {
|
if (isValidUrl(imageUrl)) {
|
||||||
blockState.components.push({ type: 12, url: imageUrl });
|
blockState.components.push({ type: 12, url: imageUrl });
|
||||||
await interaction.reply({ content: '✅ Imagen añadida.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Imagen añadida.', flags: 64 });
|
||||||
} else {
|
} else {
|
||||||
await interaction.reply({ content: '❌ URL de imagen inválida.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '❌ URL de imagen inválida.', flags: 64 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
} else if (id === 'add_cover_modal' || id === 'edit_cover_modal') {
|
||||||
}
|
|
||||||
case 'add_cover_modal':
|
|
||||||
case 'edit_cover_modal': {
|
|
||||||
const coverUrl = interaction.fields.getTextInputValue('cover_input');
|
const coverUrl = interaction.fields.getTextInputValue('cover_input');
|
||||||
if (isValidUrl(coverUrl)) {
|
if (isValidUrl(coverUrl)) {
|
||||||
blockState.coverImage = coverUrl;
|
blockState.coverImage = coverUrl;
|
||||||
await interaction.reply({ content: '✅ Imagen de portada actualizada.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Imagen de portada actualizada.', flags: 64 });
|
||||||
} else {
|
} else {
|
||||||
await interaction.reply({ content: '❌ URL de portada inválida.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '❌ URL de portada inválida.', flags: 64 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
} else if (id === 'add_separator_modal') {
|
||||||
}
|
|
||||||
case 'add_separator_modal': {
|
|
||||||
const visibleStr = interaction.fields.getTextInputValue('separator_visible').toLowerCase();
|
const visibleStr = interaction.fields.getTextInputValue('separator_visible').toLowerCase();
|
||||||
const spacingStr = interaction.fields.getTextInputValue('separator_spacing') || '1';
|
const spacingStr = interaction.fields.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: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Separador añadido.', flags: 64 });
|
||||||
break;
|
} else if (id.startsWith('edit_thumbnail_modal_')) {
|
||||||
}
|
const idx = parseInt(id.replace('edit_thumbnail_modal_', ''));
|
||||||
case 'edit_thumbnail_modal': {
|
const textComp = blockState.components[idx];
|
||||||
|
if (!textComp || textComp.type !== 10) return;
|
||||||
const thumbnailUrl = interaction.fields.getTextInputValue('thumbnail_input');
|
const thumbnailUrl = interaction.fields.getTextInputValue('thumbnail_input');
|
||||||
const textComp = blockState.components.find((c: any) => c.type === 10);
|
|
||||||
|
|
||||||
if (textComp) {
|
|
||||||
if (thumbnailUrl.trim() === '') {
|
if (thumbnailUrl.trim() === '') {
|
||||||
// Si está vacío, eliminar thumbnail
|
|
||||||
textComp.thumbnail = null;
|
textComp.thumbnail = null;
|
||||||
await interaction.reply({ content: '✅ Thumbnail eliminado.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Thumbnail eliminado.', flags: 64 });
|
||||||
} else if (!isValidUrl(thumbnailUrl)) {
|
} else if (!isValidUrl(thumbnailUrl)) {
|
||||||
// Si no es una URL válida, mostrar error
|
await interaction.reply({ content: '❌ URL de thumbnail inválida.', flags: 64 });
|
||||||
await interaction.reply({ content: '❌ URL de thumbnail inválida.', flags: MessageFlags.Ephemeral });
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Si es una URL válida, añadir thumbnail
|
if (textComp.linkButton) {
|
||||||
|
await interaction.reply({ content: '❌ Este bloque ya tiene un botón link. Elimina el botón antes de añadir thumbnail.', flags: 64 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
textComp.thumbnail = thumbnailUrl;
|
textComp.thumbnail = thumbnailUrl;
|
||||||
await interaction.reply({ content: '✅ Thumbnail actualizado.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '✅ Thumbnail actualizado.', flags: 64 });
|
||||||
}
|
}
|
||||||
|
} else if (id.startsWith('create_link_button_modal_') || id.startsWith('edit_link_button_modal_')) {
|
||||||
|
const idx = parseInt(id.replace('create_link_button_modal_', '').replace('edit_link_button_modal_', ''));
|
||||||
|
const textComp = blockState.components[idx];
|
||||||
|
if (!textComp || textComp.type !== 10) return;
|
||||||
|
|
||||||
|
const url = interaction.fields.getTextInputValue('link_url_input');
|
||||||
|
const label = (interaction.fields.getTextInputValue('link_label_input') || '').trim();
|
||||||
|
const emojiStr = (interaction.fields.getTextInputValue('link_emoji_input') || '').trim();
|
||||||
|
|
||||||
|
if (!isValidUrl(url)) {
|
||||||
|
await interaction.reply({ content: '❌ URL inválida para el botón.', flags: 64 });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
const parsedEmoji = parseEmojiInput(emojiStr || undefined);
|
||||||
|
if (!label && !parsedEmoji) {
|
||||||
|
await interaction.reply({ content: '❌ Debes proporcionar al menos una etiqueta o un emoji.', flags: 64 });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
case 'import_json_modal': {
|
|
||||||
|
if (textComp.thumbnail) {
|
||||||
|
await interaction.reply({ content: '❌ Este bloque tiene thumbnail. Elimínalo antes de añadir un botón link.', flags: 64 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textComp.linkButton = {
|
||||||
|
url,
|
||||||
|
label: label || undefined,
|
||||||
|
// Guardamos el string original; se parsea en render/build
|
||||||
|
emoji: emojiStr || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.reply({ content: '✅ Botón link actualizado.', flags: 64 });
|
||||||
|
} else if (id === 'import_json_modal') {
|
||||||
try {
|
try {
|
||||||
const jsonString = interaction.fields.getTextInputValue('json_input');
|
const jsonString = interaction.fields.getTextInputValue('json_input');
|
||||||
const importedData = JSON.parse(jsonString);
|
const importedData = JSON.parse(jsonString);
|
||||||
|
|
||||||
// Validar estructura básica
|
|
||||||
if (importedData && typeof importedData === 'object') {
|
if (importedData && typeof importedData === 'object') {
|
||||||
blockState = {
|
blockState = {
|
||||||
title: importedData.title || blockState.title,
|
title: importedData.title || blockState.title,
|
||||||
@@ -952,39 +1215,32 @@ export const command: CommandMessage = {
|
|||||||
coverImage: importedData.coverImage || blockState.coverImage,
|
coverImage: importedData.coverImage || blockState.coverImage,
|
||||||
components: Array.isArray(importedData.components) ? importedData.components : blockState.components
|
components: Array.isArray(importedData.components) ? importedData.components : blockState.components
|
||||||
};
|
};
|
||||||
|
for (const comp of blockState.components) {
|
||||||
await interaction.reply({ content: '✅ JSON importado correctamente.', flags: MessageFlags.Ephemeral });
|
if (comp?.type === 10 && comp.linkButton && comp.thumbnail) {
|
||||||
|
delete comp.thumbnail; // priorizamos linkButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await interaction.reply({ content: '✅ JSON importado correctamente.', flags: 64 });
|
||||||
} else {
|
} else {
|
||||||
await interaction.reply({ content: '❌ Estructura JSON inválida.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '❌ Estructura JSON inválida.', flags: 64 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', flags: 64 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
}
|
|
||||||
default:
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar la vista previa después de cada cambio en el modal con mejor manejo de errores
|
// Actualizar vista previa tras cada modal
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (!modalHandlerActive) return; // Evitar actualizar si ya no está activo
|
if (!modalHandlerActive) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verificar si el mensaje aún existe antes de intentar editarlo
|
|
||||||
const messageExists = await editorMessage.fetch().catch(() => null);
|
const messageExists = await editorMessage.fetch().catch(() => null);
|
||||||
if (!messageExists) {
|
if (!messageExists) return;
|
||||||
console.log('El mensaje del editor ya no existe');
|
await editorMessage.edit({ components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] });
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await editorMessage.edit({
|
|
||||||
components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)]
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Manejar diferentes tipos de errores
|
|
||||||
if (error.code === 10008) {
|
if (error.code === 10008) {
|
||||||
console.log('Mensaje del editor eliminado');
|
console.log('Mensaje del editor eliminado');
|
||||||
} else if (error.code === 10062) {
|
} else if (error.code === 10062) {
|
||||||
@@ -993,18 +1249,15 @@ export const command: CommandMessage = {
|
|||||||
console.error('Error actualizando preview:', error.message || error);
|
console.error('Error actualizando preview:', error.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 500);
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error en modal:', error);
|
console.error('Error en modal:', error);
|
||||||
try {
|
try {
|
||||||
// Solo intentar responder si la interacción no ha expirado
|
|
||||||
if (error.code !== 10062 && !interaction.replied && !interaction.deferred) {
|
if (error.code !== 10062 && !interaction.replied && !interaction.deferred) {
|
||||||
await interaction.reply({ content: '❌ Error procesando el modal.', flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: '❌ Error procesando el modal.', flags: 64 });
|
||||||
}
|
|
||||||
} catch (replyError) {
|
|
||||||
console.log('No se pudo responder a la interacción (probablemente expirada)');
|
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -144,7 +144,9 @@ export const command: CommandMessage = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Agrupar por estado
|
// Agrupar por estado
|
||||||
|
//@ts-ignore
|
||||||
const activeChannels = channelDetails.filter(c => c.status.includes("Activo"));
|
const activeChannels = channelDetails.filter(c => c.status.includes("Activo"));
|
||||||
|
//@ts-ignore
|
||||||
const inactiveChannels = channelDetails.filter(c => c.status.includes("Inactivo"));
|
const inactiveChannels = channelDetails.filter(c => c.status.includes("Inactivo"));
|
||||||
|
|
||||||
// Construir embed principal
|
// Construir embed principal
|
||||||
@@ -161,6 +163,7 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
// Añadir campos de canales activos
|
// Añadir campos de canales activos
|
||||||
if (activeChannels.length > 0) {
|
if (activeChannels.length > 0) {
|
||||||
|
//@ts-ignore
|
||||||
const activeList = activeChannels.slice(0, 10).map(c =>
|
const activeList = activeChannels.slice(0, 10).map(c =>
|
||||||
`**${c.index}.** ${c.channelName}\n` +
|
`**${c.index}.** ${c.channelName}\n` +
|
||||||
`└ \`${c.blockName}\` • ${c.blockStatus}\n` +
|
`└ \`${c.blockName}\` • ${c.blockStatus}\n` +
|
||||||
@@ -179,6 +182,7 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
// Añadir campos de canales inactivos (si los hay)
|
// Añadir campos de canales inactivos (si los hay)
|
||||||
if (inactiveChannels.length > 0) {
|
if (inactiveChannels.length > 0) {
|
||||||
|
//@ts-ignore
|
||||||
const inactiveList = inactiveChannels.slice(0, 5).map(c =>
|
const inactiveList = inactiveChannels.slice(0, 5).map(c =>
|
||||||
`**${c.index}.** ${c.channelName}\n` +
|
`**${c.index}.** ${c.channelName}\n` +
|
||||||
`└ \`${c.blockName}\` • ${c.blockStatus}`
|
`└ \`${c.blockName}\` • ${c.blockStatus}`
|
||||||
@@ -197,6 +201,7 @@ export const command: CommandMessage = {
|
|||||||
mainEmbed.addFields([
|
mainEmbed.addFields([
|
||||||
{
|
{
|
||||||
name: "📊 Estadísticas del Servidor",
|
name: "📊 Estadísticas del Servidor",
|
||||||
|
//@ts-ignore
|
||||||
value: `🧩 **Bloques disponibles:** ${availableBlocks}\n📈 **Total puntos otorgados:** ${totalPointsHistory}\n⚡ **Canales más activos:** ${channelDetails.sort((a, b) => b.pointsCount - a.pointsCount).slice(0, 3).map((c, i) => `${i + 1}. ${c.channelName.replace(/[#❌*]/g, '').trim()}`).join(', ') || 'N/A'}`,
|
value: `🧩 **Bloques disponibles:** ${availableBlocks}\n📈 **Total puntos otorgados:** ${totalPointsHistory}\n⚡ **Canales más activos:** ${channelDetails.sort((a, b) => b.pointsCount - a.pointsCount).slice(0, 3).map((c, i) => `${i + 1}. ${c.channelName.replace(/[#❌*]/g, '').trim()}`).join(', ') || 'N/A'}`,
|
||||||
inline: false
|
inline: false
|
||||||
}
|
}
|
||||||
@@ -292,6 +297,7 @@ export const command: CommandMessage = {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "show_stats":
|
case "show_stats":
|
||||||
|
//@ts-ignore
|
||||||
const detailedStats = channelDetails.map(c =>
|
const detailedStats = channelDetails.map(c =>
|
||||||
`• ${c.channelName}: **${c.pointsCount}** puntos`
|
`• ${c.channelName}: **${c.pointsCount}** puntos`
|
||||||
).join('\n');
|
).join('\n');
|
||||||
|
|||||||
@@ -1,71 +1,143 @@
|
|||||||
import {Guild, Invite, User} from "discord.js";
|
import { Guild, Invite, User, GuildMember } from "discord.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lista de variables válidas del sistema (sin llaves {})
|
* Registro central de variables -> resolutores
|
||||||
|
* Cada clave es el token que aparecerá en el texto (sin llaves),
|
||||||
|
* y su valor es una función que recibe el contexto y devuelve el string a insertar.
|
||||||
*/
|
*/
|
||||||
export const VALID_VARIABLES = [
|
type VarCtx = {
|
||||||
'user.name', 'user.id', 'user.mention', 'user.avatar',
|
user?: User | GuildMember;
|
||||||
'user.pointsAll', 'user.pointsWeekly', 'user.pointsMonthly',
|
guild?: Guild;
|
||||||
'guild.name', 'guild.icon',
|
stats?: any;
|
||||||
'invite.name', 'invite.icon'
|
invite?: Invite;
|
||||||
];
|
};
|
||||||
|
|
||||||
/**
|
type VarResolver = (ctx: VarCtx) => string | Promise<string>;
|
||||||
* Validar si una URL es válida o contiene variables del sistema
|
|
||||||
* @param url - La URL o texto a validar
|
|
||||||
* @returns boolean - true si es válida
|
|
||||||
*/
|
|
||||||
export function isValidUrlOrVariable(url: string): boolean {
|
|
||||||
if (!url) return false;
|
|
||||||
|
|
||||||
// Verificar si el texto contiene variables válidas
|
// Helpers seguros para leer datos de usuario/miembro y guild/invite
|
||||||
const hasValidVariables = VALID_VARIABLES.some(variable => url.includes(variable));
|
const getUserId = (u?: User | GuildMember) => (u as any)?.id || (u as any)?.user?.id || "";
|
||||||
if (hasValidVariables) return true;
|
const getUsername = (u?: User | GuildMember) => (u as any)?.username || (u as any)?.user?.username || "";
|
||||||
|
const getAvatar = (u?: User | GuildMember) => {
|
||||||
// Si no tiene variables, validar como URL normal
|
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
const fn = (u as any)?.displayAvatarURL || (u as any)?.user?.displayAvatarURL;
|
||||||
return url.startsWith('http://') || url.startsWith('https://');
|
return typeof fn === 'function' ? fn.call((u as any)?.user ?? u, { forceStatic: false }) : "";
|
||||||
|
} catch { return ""; }
|
||||||
|
};
|
||||||
|
const getGuildIcon = (g?: Guild) => {
|
||||||
|
try { return g?.iconURL({ forceStatic: false }) ?? ""; } catch { return ""; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construye datos de invite similares a la versión previa
|
||||||
|
const getInviteObject = (invite?: Invite) => invite?.guild ? {
|
||||||
|
name: invite.guild.name,
|
||||||
|
icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : ''
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
export const VARIABLES: Record<string, VarResolver> = {
|
||||||
|
// USER INFO
|
||||||
|
'user.name': ({ user }) => getUsername(user),
|
||||||
|
'user.id': ({ user }) => getUserId(user),
|
||||||
|
'user.mention': ({ user }) => {
|
||||||
|
const id = getUserId(user);
|
||||||
|
return id ? `<@${id}>` : '';
|
||||||
|
},
|
||||||
|
'user.avatar': ({ user }) => getAvatar(user),
|
||||||
|
|
||||||
|
// USER STATS
|
||||||
|
'user.pointsAll': ({ stats }) => stats?.totalPoints?.toString?.() ?? '0',
|
||||||
|
'user.pointsWeekly': ({ stats }) => stats?.weeklyPoints?.toString?.() ?? '0',
|
||||||
|
'user.pointsMonthly': ({ stats }) => stats?.monthlyPoints?.toString?.() ?? '0',
|
||||||
|
|
||||||
|
// GUILD INFO
|
||||||
|
'guild.name': ({ guild }) => guild?.name ?? '',
|
||||||
|
'guild.icon': ({ guild }) => getGuildIcon(guild),
|
||||||
|
|
||||||
|
// INVITE INFO
|
||||||
|
'invite.name': ({ invite }) => getInviteObject(invite)?.name ?? '',
|
||||||
|
'invite.icon': ({ invite }) => getInviteObject(invite)?.icon ?? ''
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista de variables válidas del sistema (derivada del registro)
|
||||||
|
* Exportada por compatibilidad y para UI.
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
export const VALID_VARIABLES: string[] = Object.freeze(Object.keys(VARIABLES));
|
||||||
|
|
||||||
|
/** Devuelve la lista actual de variables (no congelada) */
|
||||||
|
export function listVariables(): string[] {
|
||||||
|
return Object.keys(VARIABLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Escapa una cadena para uso literal dentro de una RegExp */
|
||||||
|
const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validar si un texto es una URL válida o contiene variables del sistema
|
||||||
|
* Mantiene la semántica previa: true si contiene cualquier token válido o si es http/https válido.
|
||||||
|
*/
|
||||||
|
export function isValidUrlOrVariable(text: string): boolean {
|
||||||
|
if (!text) return false;
|
||||||
|
|
||||||
|
// ¿Contiene alguna variable?
|
||||||
|
if (VALID_VARIABLES.some(v => text.includes(v))) return true;
|
||||||
|
|
||||||
|
// ¿Es URL http/https válida?
|
||||||
|
try {
|
||||||
|
const u = new URL(text);
|
||||||
|
return u.protocol === 'http:' || u.protocol === 'https:';
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//@ts-ignore
|
/**
|
||||||
export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any, invite: Invite | undefined): Promise<string> {
|
* Reemplaza variables en un texto usando el registro de VARIABLES.
|
||||||
|
* Compatible con llamadas existentes: acepta User o GuildMember en el primer parámetro (históricamente llamado "user").
|
||||||
|
*/
|
||||||
|
export async function replaceVars(
|
||||||
|
text: string,
|
||||||
|
userOrMember: User | GuildMember | undefined,
|
||||||
|
guild: Guild | undefined,
|
||||||
|
stats?: any,
|
||||||
|
invite?: Invite
|
||||||
|
): Promise<string> {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
|
|
||||||
// Crear inviteObject solo si invite existe y tiene guild
|
const ctx: VarCtx = { user: userOrMember, guild, stats, invite };
|
||||||
const inviteObject = invite?.guild ? {
|
|
||||||
name: invite.guild.name,
|
|
||||||
icon: invite.guild.icon ? `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` : ''
|
|
||||||
} : null;
|
|
||||||
|
|
||||||
return text
|
// Construimos una única RegExp que contenga todas las claves (sin anchors, para coincidir en cualquier parte)
|
||||||
/**
|
const keys = Object.keys(VARIABLES);
|
||||||
* USER INFO
|
if (keys.length === 0) return text;
|
||||||
*/
|
// Ordenar por longitud descendente para evitar falsas coincidencias de prefijos (defensivo)
|
||||||
.replace(/(user\.name)/g, user?.username ?? '')
|
const keysEscaped = keys.sort((a, b) => b.length - a.length).map(escapeRegex);
|
||||||
.replace(/(user\.id)/g, user?.id ?? '')
|
const pattern = new RegExp(`(${keysEscaped.join('|')})`, 'g');
|
||||||
.replace(/(user\.mention)/g, user ? `<@${user.id}>` : '')
|
|
||||||
.replace(/(user\.avatar)/g, user?.displayAvatarURL({ forceStatic: false }) ?? '')
|
|
||||||
|
|
||||||
/**
|
// Reemplazo asíncrono
|
||||||
* USER STATS
|
const parts: (string | Promise<string>)[] = [];
|
||||||
*/
|
let lastIndex = 0;
|
||||||
.replace(/(user\.pointsAll)/g, stats?.totalPoints?.toString() ?? '0')
|
let m: RegExpExecArray | null;
|
||||||
.replace(/(user\.pointsWeekly)/g, stats?.weeklyPoints?.toString() ?? '0')
|
while ((m = pattern.exec(text)) !== null) {
|
||||||
.replace(/(user\.pointsMonthly)/g, stats?.monthlyPoints?.toString() ?? '0')
|
const matchStart = m.index;
|
||||||
|
if (matchStart > lastIndex) parts.push(text.slice(lastIndex, matchStart));
|
||||||
/**
|
const token = m[1];
|
||||||
* GUILD INFO
|
const resolver = VARIABLES[token];
|
||||||
*/
|
if (resolver) {
|
||||||
.replace(/(guild\.name)/g, guild?.name ?? '')
|
try {
|
||||||
.replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? '')
|
const value = resolver(ctx);
|
||||||
|
parts.push(Promise.resolve(value).then(v => (v ?? '').toString()));
|
||||||
/**
|
} catch {
|
||||||
* INVITE INFO
|
parts.push('');
|
||||||
*/
|
}
|
||||||
.replace(/(invite\.name)/g, inviteObject?.name ?? "")
|
} else {
|
||||||
.replace(/(invite\.icon)/g, inviteObject?.icon ?? '')
|
// No debería ocurrir, pero añadimos el literal por seguridad
|
||||||
|
parts.push(token);
|
||||||
|
}
|
||||||
|
lastIndex = pattern.lastIndex;
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) parts.push(text.slice(lastIndex));
|
||||||
|
|
||||||
|
// Resolver todas las partes (las literales quedan tal cual)
|
||||||
|
const resolved = await Promise.all(parts.map(p => Promise.resolve(p as any)));
|
||||||
|
return resolved.join('');
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,6 @@ bot.on(Events.MessageCreate, async (message) => {
|
|||||||
if (!message.content.startsWith(PREFIX)) return;
|
if (!message.content.startsWith(PREFIX)) return;
|
||||||
|
|
||||||
const [cmdName, ...args] = message.content.slice(PREFIX.length).trim().split(/\s+/);
|
const [cmdName, ...args] = message.content.slice(PREFIX.length).trim().split(/\s+/);
|
||||||
console.log(cmdName);
|
|
||||||
const command = commands.get(cmdName);
|
const command = commands.get(cmdName);
|
||||||
if (!command) return;
|
if (!command) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user