From 6707760e839306ed5e1107b6ba4cd410a9832c56 Mon Sep 17 00:00:00 2001 From: shnimlz Date: Fri, 19 Sep 2025 21:56:39 -0500 Subject: [PATCH] Arreglando errores garrafales como bugs a la hora de crear la alianza o errores del displayComponent. --- .idea/dataSources.xml | 12 + prisma/dev.db | Bin 0 -> 106496 bytes .../20250918123305_blockv2/migration.sql | 76 ++ .../migration.sql | 31 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 61 ++ .../messages/alliaces/createEmbedv2.ts | 858 ++++++++++++------ .../messages/alliaces/setupChannel.ts | 93 ++ src/core/lib/vars.ts | 29 +- src/events/extras/alliace.ts | 424 +++++++++ src/events/messageCreate.ts | 2 + 11 files changed, 1292 insertions(+), 297 deletions(-) create mode 100644 .idea/dataSources.xml create mode 100644 prisma/dev.db create mode 100644 prisma/migrations/20250918123305_blockv2/migration.sql create mode 100644 prisma/migrations/20250918165856_add_alliance_channels/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/commands/messages/alliaces/setupChannel.ts diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..d43ccdb --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/prisma/dev.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..8e532b6da7c167b1a7446e2000e58719bb0f2f49 GIT binary patch literal 106496 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 F4*;6BreOd8 literal 0 HcmV?d00001 diff --git a/prisma/migrations/20250918123305_blockv2/migration.sql b/prisma/migrations/20250918123305_blockv2/migration.sql new file mode 100644 index 0000000..6267ad8 --- /dev/null +++ b/prisma/migrations/20250918123305_blockv2/migration.sql @@ -0,0 +1,76 @@ +-- CreateTable +CREATE TABLE "Guild" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "prefix" TEXT NOT NULL DEFAULT '!' +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL PRIMARY KEY +); + +-- CreateTable +CREATE TABLE "PartnershipStats" ( + "totalPoints" INTEGER NOT NULL DEFAULT 0, + "weeklyPoints" INTEGER NOT NULL DEFAULT 0, + "monthlyPoints" INTEGER NOT NULL DEFAULT 0, + "lastWeeklyReset" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastMonthlyReset" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT NOT NULL, + "guildId" TEXT NOT NULL, + + PRIMARY KEY ("userId", "guildId"), + CONSTRAINT "PartnershipStats_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "PartnershipStats_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Alliance" ( + "id" TEXT NOT NULL PRIMARY KEY, + "channelId" TEXT NOT NULL, + "messageId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guildId" TEXT NOT NULL, + "creatorId" TEXT NOT NULL, + CONSTRAINT "Alliance_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Alliance_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "EmbedConfig" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "color" TEXT, + "title" TEXT, + "url" TEXT, + "authorName" TEXT, + "authorIconURL" TEXT, + "authorURL" TEXT, + "description" TEXT, + "thumbnailURL" TEXT, + "imageURL" TEXT, + "footerText" TEXT, + "footerIconURL" TEXT, + "fields" TEXT DEFAULT '[]', + "guildId" TEXT NOT NULL, + CONSTRAINT "EmbedConfig_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "BlockV2Config" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "config" JSONB NOT NULL, + "guildId" TEXT NOT NULL, + CONSTRAINT "BlockV2Config_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Alliance_messageId_key" ON "Alliance"("messageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "EmbedConfig_guildId_name_key" ON "EmbedConfig"("guildId", "name"); + +-- CreateIndex +CREATE UNIQUE INDEX "BlockV2Config_guildId_name_key" ON "BlockV2Config"("guildId", "name"); diff --git a/prisma/migrations/20250918165856_add_alliance_channels/migration.sql b/prisma/migrations/20250918165856_add_alliance_channels/migration.sql new file mode 100644 index 0000000..8d00131 --- /dev/null +++ b/prisma/migrations/20250918165856_add_alliance_channels/migration.sql @@ -0,0 +1,31 @@ +-- CreateTable +CREATE TABLE "AllianceChannel" ( + "id" TEXT NOT NULL PRIMARY KEY, + "channelId" TEXT NOT NULL, + "blockConfigName" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "guildId" TEXT NOT NULL, + CONSTRAINT "AllianceChannel_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "PointHistory" ( + "id" TEXT NOT NULL PRIMARY KEY, + "points" INTEGER NOT NULL DEFAULT 1, + "timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "messageId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "guildId" TEXT NOT NULL, + "channelId" TEXT NOT NULL, + CONSTRAINT "PointHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "PointHistory_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "PointHistory_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "AllianceChannel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "AllianceChannel_channelId_key" ON "AllianceChannel"("channelId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AllianceChannel_guildId_channelId_key" ON "AllianceChannel"("guildId", "channelId"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 00ad8a9..547ea94 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,6 +30,9 @@ model Guild { // ✅ CAMBIO: Ahora un Guild puede tener MÚLTIPLES configuraciones de embed. embedConfigs EmbedConfig[] BlockV2Config BlockV2Config[] + // ✅ NUEVAS RELACIONES + allianceChannels AllianceChannel[] + pointsHistory PointHistory[] } /* * ----------------------------------------------------------------------------- @@ -43,6 +46,8 @@ model User { // Relaciones partnerStats PartnershipStats[] createdAlliances Alliance[] + // ✅ NUEVA RELACIÓN + pointsHistory PointHistory[] } /* @@ -96,6 +101,62 @@ model Alliance { creatorId String } + +/* + * ----------------------------------------------------------------------------- + * Modelo para Canales de Alianza + * ----------------------------------------------------------------------------- + * Gestiona qué canales están configurados para otorgar puntos y qué bloque enviar +*/ +model AllianceChannel { + id String @id @default(cuid()) + channelId String @unique // ID del canal de Discord + + // Configuración del canal + blockConfigName String // Nombre del BlockV2Config a enviar + isActive Boolean @default(true) + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // --- Relaciones --- + guild Guild @relation(fields: [guildId], references: [id]) + guildId String + + // Historial de puntos otorgados en este canal + pointsHistory PointHistory[] + + // Un canal solo puede estar en un servidor + @@unique([guildId, channelId]) +} + +/* + * ----------------------------------------------------------------------------- + * Modelo para Historial de Puntos + * ----------------------------------------------------------------------------- + * Registra cada vez que un usuario gana puntos con fecha y hora +*/ +model PointHistory { + id String @id @default(cuid()) + + // Información del punto otorgado + points Int @default(1) + timestamp DateTime @default(now()) + messageId String // ID del mensaje que generó el punto + + // --- Relaciones --- + user User @relation(fields: [userId], references: [id]) + userId String + + guild Guild @relation(fields: [guildId], references: [id]) + guildId String + + allianceChannel AllianceChannel @relation(fields: [channelId], references: [id]) + channelId String +} + + /* * ----------------------------------------------------------------------------- * Modelo para la Configuración del Embed diff --git a/src/commands/messages/alliaces/createEmbedv2.ts b/src/commands/messages/alliaces/createEmbedv2.ts index e88370b..25d7052 100644 --- a/src/commands/messages/alliaces/createEmbedv2.ts +++ b/src/commands/messages/alliaces/createEmbedv2.ts @@ -1,37 +1,47 @@ import { CommandMessage } from "../../../core/types/commands"; // @ts-ignore -import { ComponentType, ButtonStyle } from "discord.js"; +import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message } from "discord.js"; import { replaceVars } from "../../../core/lib/vars"; /** - * Botones de edición + * Botones de edición - VERSIÓN MEJORADA */ const btns = (disabled = false) => ([ { type: 1, components: [ - { style: ButtonStyle.Secondary, type: 2, label: "Editar Título", disabled, custom_id: "edit_title" }, - { style: ButtonStyle.Secondary, type: 2, label: "Editar Descripción", disabled, custom_id: "edit_description" }, - { style: ButtonStyle.Secondary, type: 2, label: "Editar Color", disabled, custom_id: "edit_color" }, - { style: ButtonStyle.Secondary, type: 2, label: "Añadir Contenido", disabled, custom_id: "add_content" }, - { style: ButtonStyle.Secondary, type: 2, label: "Añadir Separador", disabled, custom_id: "add_separator" } + { style: ButtonStyle.Secondary, type: 2, label: "📝 Título", disabled, custom_id: "edit_title" }, + { style: ButtonStyle.Secondary, type: 2, label: "📄 Descripción", disabled, custom_id: "edit_description" }, + { style: ButtonStyle.Secondary, type: 2, label: "🎨 Color", disabled, custom_id: "edit_color" }, + { style: ButtonStyle.Secondary, type: 2, label: "➕ Contenido", disabled, custom_id: "add_content" }, + { style: ButtonStyle.Secondary, type: 2, label: "➖ Separador", disabled, custom_id: "add_separator" } ] }, { type: 1, components: [ - { style: ButtonStyle.Secondary, type: 2, label: "Añadir Imagen", disabled, custom_id: "add_image" }, - { style: ButtonStyle.Secondary, type: 2, label: "Imagen Portada", disabled, custom_id: "cover_image" }, - { style: ButtonStyle.Primary, type: 2, label: "Mover Bloque", disabled, custom_id: "move_block" }, - { style: ButtonStyle.Danger, type: 2, label: "Eliminar Bloque", disabled, custom_id: "delete_block" }, - { style: ButtonStyle.Secondary, type: 2, label: "Editar Thumbnail", disabled, custom_id: "edit_thumbnail" } + { style: ButtonStyle.Secondary, type: 2, label: "🖼️ Imagen", disabled, custom_id: "add_image" }, + { style: ButtonStyle.Secondary, type: 2, label: "🖼️ Portada", disabled, custom_id: "cover_image" }, + { style: ButtonStyle.Secondary, type: 2, label: "📎 Thumbnail", disabled, custom_id: "edit_thumbnail" }, + { style: ButtonStyle.Primary, type: 2, label: "🔄 Mover", disabled, custom_id: "move_block" }, + { style: ButtonStyle.Danger, type: 2, label: "🗑️ Eliminar", disabled, custom_id: "delete_block" } ] }, { type: 1, components: [ - { style: ButtonStyle.Success, type: 2, label: "Guardar", disabled, custom_id: "save_block" }, - { style: ButtonStyle.Danger, type: 2, label: "Cancelar", disabled, custom_id: "cancel_block" } + { style: ButtonStyle.Secondary, type: 2, label: "🎯 Variables", disabled, custom_id: "show_variables" }, + { style: ButtonStyle.Secondary, type: 2, label: "📋 Duplicar", disabled, custom_id: "duplicate_block" }, + { style: ButtonStyle.Secondary, type: 2, label: "📊 Vista Raw", disabled, custom_id: "show_raw" }, + { style: ButtonStyle.Secondary, type: 2, label: "📥 Importar", disabled, custom_id: "import_json" }, + { style: ButtonStyle.Secondary, type: 2, label: "📤 Exportar", disabled, custom_id: "export_json" } + ] + }, + { + type: 1, + components: [ + { style: ButtonStyle.Success, type: 2, label: "💾 Guardar", disabled, custom_id: "save_block" }, + { style: ButtonStyle.Danger, type: 2, label: "❌ Cancelar", disabled, custom_id: "cancel_block" } ] } ]); @@ -40,7 +50,7 @@ const btns = (disabled = false) => ([ * Validar si una URL es válida */ const isValidUrl = (url: string): boolean => { - if (!url || typeof url !== 'string') return false; + if (!url) return false; try { new URL(url); return url.startsWith('http://') || url.startsWith('https://'); @@ -49,6 +59,28 @@ const isValidUrl = (url: string): boolean => { } }; +/** + * Validar y limpiar contenido para Discord + */ +const validateContent = (content: string): string => { + if (!content || typeof content !== 'string') { + return "Sin contenido"; // Contenido por defecto + } + + // Limpiar contenido y asegurar que tenga al menos 1 carácter + const cleaned = content.trim(); + if (cleaned.length === 0) { + return "Sin contenido"; + } + + // Truncar si excede el límite de Discord (4000 caracteres) + if (cleaned.length > 4000) { + return cleaned.substring(0, 3997) + "..."; + } + + return cleaned; +}; + /** * Generar vista previa */ @@ -67,11 +99,12 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { } } - // Añadir título después de la portada + // Añadir título después de la portada - VALIDAR CONTENIDO + //@ts-ignore + const processedTitle = await replaceVars(blockState.title ?? "Sin título", member, guild); previewComponents.push({ type: 10, - //@ts-ignore - content: await replaceVars(blockState.title ?? "Sin título", member, guild) + content: validateContent(processedTitle) }); // Procesar componentes en orden @@ -80,6 +113,9 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { // Componente de texto con thumbnail opcional //@ts-ignore const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, member, guild) : null; + //@ts-ignore + const processedContent = await replaceVars(c.content || "Sin contenido", member, guild); + const validatedContent = validateContent(processedContent); if (processedThumbnail && isValidUrl(processedThumbnail)) { // Si tiene thumbnail válido, usar contenedor tipo 9 con accessory @@ -88,8 +124,7 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { components: [ { type: 10, - //@ts-ignore - content: await replaceVars(c.content || " ", member, guild) + content: validatedContent } ], accessory: { @@ -101,8 +136,7 @@ const renderPreview = async (blockState: any, member: any, guild: any) => { // Sin thumbnail o thumbnail inválido, componente normal previewComponents.push({ type: 10, - //@ts-ignore - content: await replaceVars(c.content || " ", member, guild) + content: validatedContent }); } } else if (c.type === 14) { @@ -139,18 +173,23 @@ export const command: CommandMessage = { cooldown: 20, run: async (message, args, client) => { if (!message.member?.permissions.has("Administrator")) { - return message.reply("❌ No tienes permisos de Administrador."); + await message.reply("❌ No tienes permisos de Administrador."); + return; } const blockName: string | null = args[0] ?? null; if (!blockName) { - return message.reply("Debes proporcionar un nombre. Uso: `!blockcreatev2 `"); + await message.reply("Debes proporcionar un nombre. Uso: `!blockcreatev2 `"); + return; } const nameIsValid = await client.prisma.blockV2Config.findFirst({ where: { guildId: message.guild!.id, name: blockName } }); - if (nameIsValid) return message.reply("❌ Nombre ya usado!"); + if (nameIsValid) { + await message.reply("❌ Nombre ya usado!"); + return; + } // Estado inicial let blockState: any = { @@ -165,6 +204,22 @@ export const command: CommandMessage = { //@ts-ignore const editorMessage = await message.channel.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)); + + //@ts-ignore + await editorMessage.edit({ + content: null, flags: 32768, components: [ await renderPreview(blockState, message.member, message.guild), @@ -173,10 +228,10 @@ export const command: CommandMessage = { }); const collector = editorMessage.createMessageComponentCollector({ - time: 300000 + time: 3600000 // 1 hora (60 minutos * 60 segundos * 1000 ms) }); - collector.on("collect", async (i) => { + collector.on("collect", async (i: any) => { if (i.user.id !== message.author.id) { await i.reply({ content: "No puedes usar este menú.", ephemeral: true }); return; @@ -184,13 +239,12 @@ export const command: CommandMessage = { // --- BOTONES --- if (i.isButton()) { - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(true)] - }); - await i.deferUpdate(); + // NO hacer deferUpdate antes de showModal + // await i.deferUpdate(); // <-- Esto causaba el error switch (i.customId) { case "save_block": { + await i.deferUpdate(); await client.prisma.blockV2Config.upsert({ where: { guildId_name: { guildId: message.guildId!, name: blockName } }, update: { config: blockState }, @@ -221,39 +275,132 @@ export const command: CommandMessage = { return; } case "cancel_block": { + await i.deferUpdate(); await editorMessage.delete(); collector.stop(); return; } case "edit_title": { - const prompt = await message.channel.send("Escribe el nuevo **título**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.title = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + // Crear modal para editar título + const modal = new ModalBuilder() + .setCustomId('edit_title_modal') + .setTitle('📝 Editar Título del Block'); + + const titleInput = new TextInputBuilder() + .setCustomId('title_input') + .setLabel('Nuevo Título') + .setStyle(TextInputStyle.Short) + .setPlaceholder('Escribe el nuevo título aquí...') + .setValue(blockState.title || '') + .setMaxLength(256) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(titleInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "edit_description": { + const modal = new ModalBuilder() + .setCustomId('edit_description_modal') + .setTitle('📄 Editar Descripción'); + + const descComp = blockState.components.find((c: any) => c.type === 10); + const currentDesc = descComp ? descComp.content : ''; + + const descInput = new TextInputBuilder() + .setCustomId('description_input') + .setLabel('Nueva Descripción') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('Escribe la nueva descripción aquí...') + .setValue(currentDesc || '') + .setMaxLength(2000) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(descInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "edit_color": { + const modal = new ModalBuilder() + .setCustomId('edit_color_modal') + .setTitle('🎨 Editar Color del Block'); + + const currentColor = blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : ''; + + const colorInput = new TextInputBuilder() + .setCustomId('color_input') + .setLabel('Color en formato HEX') + .setStyle(TextInputStyle.Short) + .setPlaceholder('#FF5733 o FF5733') + .setValue(currentColor) + .setMaxLength(7) + .setRequired(false); + + const firstActionRow = new ActionRowBuilder().addComponents(colorInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "add_content": { + const modal = new ModalBuilder() + .setCustomId('add_content_modal') + .setTitle('➕ Agregar Nuevo Contenido'); + + const contentInput = new TextInputBuilder() + .setCustomId('content_input') + .setLabel('Contenido del Texto') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('Escribe el contenido aquí...') + .setMaxLength(2000) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(contentInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "add_image": { + const modal = new ModalBuilder() + .setCustomId('add_image_modal') + .setTitle('🖼️ Agregar Nueva Imagen'); + + const imageUrlInput = new TextInputBuilder() + .setCustomId('image_url_input') + .setLabel('URL de la Imagen') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/imagen.png') + .setMaxLength(2000) + .setRequired(true); + + const firstActionRow = new ActionRowBuilder().addComponents(imageUrlInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await i.showModal(modal); break; } case "cover_image": { if (blockState.coverImage) { // Si ya tiene portada, preguntar si editar o eliminar //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, + const reply = await i.reply({ + flags: 64, // MessageFlags.Ephemeral content: "Ya tienes una imagen de portada. ¿Qué quieres hacer?", components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: "✏️ Editar", custom_id: "edit_cover" }, + { type: 2, style: ButtonStyle.Primary, label: "✏️ Editar", custom_id: "edit_cover_modal" }, { type: 2, style: ButtonStyle.Danger, label: "🗑️ Eliminar", custom_id: "delete_cover" } ] } @@ -270,24 +417,26 @@ export const command: CommandMessage = { }); coverCollector.on("collect", async (b: any) => { - if (b.customId === "edit_cover") { - await b.update({ content: "Escribe la nueva **URL de la imagen de portada**:", components: [] }); + if (b.customId === "edit_cover_modal") { + // Crear modal para editar portada + const modal = new ModalBuilder() + .setCustomId('edit_cover_modal') + .setTitle('🖼️ Editar Imagen de Portada'); - const prompt = await message.channel.send("Nueva URL de portada:"); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); + const coverInput = new TextInputBuilder() + .setCustomId('cover_input') + .setLabel('URL de la Imagen de Portada') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/portada.png') + .setValue(blockState.coverImage || '') + .setMaxLength(2000) + .setRequired(true); - mc.on("collect", async (collected) => { - blockState.coverImage = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); + const firstActionRow = new ActionRowBuilder().addComponents(coverInput); + modal.addComponents(firstActionRow); + + //@ts-ignore + await b.showModal(modal); } else if (b.customId === "delete_cover") { blockState.coverImage = null; await b.update({ content: "✅ Imagen de portada eliminada.", components: [] }); @@ -298,231 +447,24 @@ export const command: CommandMessage = { coverCollector.stop(); }); } else { - // No tiene portada, añadir nueva - const prompt = await message.channel.send("Escribe la **URL de la imagen de portada**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.coverImage = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - } - break; - } - case "edit_description": { - const prompt = await message.channel.send("Escribe la nueva **descripción**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - const descComp = blockState.components.find((c: any) => c.type === 10); - if (descComp) descComp.content = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - break; - } - case "edit_color": { - const prompt = await message.channel.send("Escribe el nuevo **color** en HEX (#RRGGBB)."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - const newValue = collected.content; - let parsed: number | null = null; - if (/^#?[0-9A-Fa-f]{6}$/.test(newValue)) { - parsed = parseInt(newValue.replace("#", ""), 16); - } - blockState.color = parsed; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - break; - } - case "add_content": { - const prompt = await message.channel.send("Escribe el nuevo **contenido**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.components.push({ type: 10, content: collected.content, thumbnail: null }); - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - break; - } - case "add_separator": { - //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, - content: "¿El separador debe ser visible?", - components: [ - { - type: 1, - components: [ - { type: 2, style: ButtonStyle.Success, label: "✅ Visible", custom_id: "separator_visible" }, - { type: 2, style: ButtonStyle.Secondary, label: "❌ Invisible", custom_id: "separator_invisible" } - ] - } - ], - fetchReply: true - }); + // No tiene portada, crear modal para añadir nueva + const modal = new ModalBuilder() + .setCustomId('add_cover_modal') + .setTitle('🖼️ Agregar Imagen de Portada'); - //@ts-ignore - const sepCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.Button, - max: 1, - time: 60000, - filter: (b: any) => b.user.id === message.author.id - }); + const coverInput = new TextInputBuilder() + .setCustomId('cover_input') + .setLabel('URL de la Imagen de Portada') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/portada.png') + .setMaxLength(2000) + .setRequired(true); - sepCollector.on("collect", async (b: any) => { - const isVisible = b.customId === "separator_visible"; - blockState.components.push({ type: 14, divider: isVisible, spacing: 1 }); - - await b.update({ - content: `✅ Separador ${isVisible ? 'visible' : 'invisible'} añadido.`, - components: [] - }); - - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - - sepCollector.stop(); - }); - break; - } - case "add_image": { - const prompt = await message.channel.send("Escribe la **URL de la imagen**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - blockState.components.push({ type: 12, url: collected.content }); - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - break; - } - case "edit_thumbnail": { - // Buscar componentes de texto para seleccionar cuál editar - const textComponents = blockState.components - .map((c: any, idx: number) => ({ component: c, index: idx })) - .filter(({ component }) => component.type === 10); - - if (textComponents.length === 0) { - //@ts-ignore - await i.followUp({ - content: "❌ No hay componentes de texto para añadir thumbnail.", - ephemeral: true - }); - break; - } - - if (textComponents.length === 1) { - // Solo un componente de texto, editarlo directamente - const prompt = await message.channel.send("Escribe la **URL del thumbnail**."); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - mc.on("collect", async (collected) => { - textComponents[0].component.thumbnail = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - } else { - // Múltiples componentes de texto, mostrar selector - const options = textComponents.map(({ component, index }) => ({ - label: `Texto: ${component.content?.slice(0, 30) || "..."}`, - value: index.toString(), - description: component.thumbnail ? "Ya tiene thumbnail" : "Sin thumbnail" - })); + const firstActionRow = new ActionRowBuilder().addComponents(coverInput); + modal.addComponents(firstActionRow); //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, - content: "Selecciona el texto al que quieres añadir/editar thumbnail:", - components: [ - { - type: 1, - components: [ - { - type: 3, - custom_id: "select_text_for_thumbnail", - placeholder: "Elige un texto", - options - } - ] - } - ], - fetchReply: true - }); - - //@ts-ignore - const selCollector = reply.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - max: 1, - time: 60000, - filter: (sel: any) => sel.user.id === message.author.id - }); - - selCollector.on("collect", async (sel: any) => { - const selectedIndex = parseInt(sel.values[0]); - - await sel.update({ - content: "Escribe la **URL del thumbnail**:", - components: [] - }); - - const prompt = await message.channel.send("URL del thumbnail:"); - const mc = message.channel.createMessageCollector({ - filter: (m) => m.author.id === message.author.id, - max: 1, - time: 60000 - }); - - mc.on("collect", async (collected) => { - blockState.components[selectedIndex].thumbnail = collected.content; - await collected.delete(); - await prompt.delete(); - await editorMessage.edit({ - components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] - }); - }); - }); + await i.showModal(modal); } break; } @@ -544,8 +486,8 @@ export const command: CommandMessage = { })); //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, + const reply = await i.reply({ + flags: 64, // MessageFlags.Ephemeral content: "Selecciona el bloque que quieres mover:", components: [ { @@ -653,17 +595,17 @@ export const command: CommandMessage = { }); if (options.length === 0) { + await i.deferReply({ flags: 64 }); // MessageFlags.Ephemeral //@ts-ignore - await i.followUp({ - content: "❌ No hay elementos para eliminar.", - ephemeral: true + await i.editReply({ + content: "❌ No hay elementos para eliminar." }); break; } //@ts-ignore - const reply = await i.followUp({ - ephemeral: true, + const reply = await i.reply({ + flags: 64, // MessageFlags.Ephemeral content: "Selecciona el elemento que quieres eliminar:", components: [ { @@ -705,8 +647,192 @@ export const command: CommandMessage = { break; } - default: + case "show_variables": { + await i.deferReply({ flags: 64 }); // MessageFlags.Ephemeral + //@ts-ignore + await i.editReply({ + content: "📋 **Variables Disponibles:**\n\n" + + "**👤 Usuario:**\n" + + "`{user.name}` - Nombre del usuario\n" + + "`{user.id}` - ID del usuario\n" + + "`{user.mention}` - Mención del usuario\n" + + "`{user.avatar}` - Avatar del usuario\n\n" + + "**📊 Estadísticas:**\n" + + "`{user.pointsAll}` - Puntos totales\n" + + "`{user.pointsWeekly}` - Puntos semanales\n" + + "`{user.pointsMonthly}` - Puntos mensuales\n\n" + + "**🏠 Servidor:**\n" + + "`{guild.name}` - Nombre del servidor\n" + + "`{guild.icon}` - Ícono del servidor\n\n" + + "**🔗 Invitación:**\n" + + "`{invite.name}` - Nombre del servidor invitado\n" + + "`{invite.icon}` - Ícono del servidor invitado" + }); break; + } + case "duplicate_block": { + const options = blockState.components.map((c: any, idx: number) => ({ + label: c.type === 10 ? `Texto: ${c.content?.slice(0, 30) || "..."}` + : c.type === 14 ? "Separador" + : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || "..."}` + : `Componente ${c.type}`, + value: idx.toString(), + description: c.type === 10 && c.thumbnail ? "Con thumbnail" : undefined + })); + + if (options.length === 0) { + await i.deferReply({ flags: 64 }); // MessageFlags.Ephemeral + //@ts-ignore + await i.editReply({ content: "❌ No hay elementos para duplicar." }); + break; + } + + //@ts-ignore + const reply = await i.reply({ + flags: 64, // MessageFlags.Ephemeral + content: "Selecciona el elemento que quieres duplicar:", + components: [{ + type: 1, + components: [{ + type: 3, + custom_id: "duplicate_select", + placeholder: "Elige un elemento", + options + }] + }], + fetchReply: true + }); + + //@ts-ignore + const selCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + max: 1, + time: 60000, + filter: (sel: any) => sel.user.id === message.author.id + }); + + selCollector.on("collect", async (sel: any) => { + const idx = parseInt(sel.values[0]); + const originalComponent = blockState.components[idx]; + const duplicatedComponent = JSON.parse(JSON.stringify(originalComponent)); + + blockState.components.splice(idx + 1, 0, duplicatedComponent); + + await sel.update({ content: "✅ Elemento duplicado.", components: [] }); + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + }); + break; + } + case "show_raw": { + const rawJson = JSON.stringify(blockState, null, 2); + const truncated = rawJson.length > 1900 ? rawJson.slice(0, 1900) + "..." : rawJson; + + //@ts-ignore + await i.reply({ + flags: 64, // MessageFlags.Ephemeral + content: `\`\`\`json\n${truncated}\`\`\`` + }); + break; + } + case "import_json": { + const modal = new ModalBuilder() + .setCustomId('import_json_modal') + .setTitle('📥 Importar JSON'); + + const jsonInput = new TextInputBuilder() + .setCustomId('json_input') + .setLabel('Pega tu configuración JSON aquí') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('{"title": "...", "components": [...]}') + .setMaxLength(4000) + .setRequired(true); + + const firstRow = new ActionRowBuilder().addComponents(jsonInput); + modal.addComponents(firstRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "export_json": { + const exportJson = JSON.stringify(blockState, null, 2); + + // Truncar si es muy largo para evitar problemas con Discord + const truncatedJson = exportJson.length > 1800 ? exportJson.slice(0, 1800) + "\n..." : exportJson; + + //@ts-ignore + await i.reply({ + flags: 64, // MessageFlags.Ephemeral + content: `📤 **JSON Exportado:**\n\`\`\`json\n${truncatedJson}\`\`\`\n\n💡 **Tip:** Copia el JSON de arriba manualmente y pégalo donde necesites.` + }); + break; + } + case "add_separator": { + const modal = new ModalBuilder() + .setCustomId('add_separator_modal') + .setTitle('➖ Agregar Separador'); + + const visibleInput = new TextInputBuilder() + .setCustomId('separator_visible') + .setLabel('¿Separador visible? (true/false)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('true o false') + .setValue('true') + .setMaxLength(5) + .setRequired(true); + + const spacingInput = new TextInputBuilder() + .setCustomId('separator_spacing') + .setLabel('Espaciado (1-3)') + .setStyle(TextInputStyle.Short) + .setPlaceholder('1, 2 o 3') + .setValue('1') + .setMaxLength(1) + .setRequired(false); + + const firstRow = new ActionRowBuilder().addComponents(visibleInput); + const secondRow = new ActionRowBuilder().addComponents(spacingInput); + modal.addComponents(firstRow, secondRow); + + //@ts-ignore + await i.showModal(modal); + break; + } + case "edit_thumbnail": { + // Buscar el primer componente de texto para añadir/editar thumbnail + const textComp = blockState.components.find((c: any) => c.type === 10); + + if (!textComp) { + await i.deferReply({ flags: 64 }); // MessageFlags.Ephemeral + //@ts-ignore + await i.editReply({ + content: "❌ Necesitas al menos un componente de texto para añadir thumbnail." + }); + break; + } + + const modal = new ModalBuilder() + .setCustomId('edit_thumbnail_modal') + .setTitle('📎 Editar Thumbnail'); + + const thumbnailInput = new TextInputBuilder() + .setCustomId('thumbnail_input') + .setLabel('URL del Thumbnail') + .setStyle(TextInputStyle.Short) + .setPlaceholder('https://ejemplo.com/thumbnail.png o dejar vacío para eliminar') + .setValue(textComp.thumbnail || '') + .setMaxLength(2000) + .setRequired(false); + + const firstRow = new ActionRowBuilder().addComponents(thumbnailInput); + modal.addComponents(firstRow); + + //@ts-ignore + await i.showModal(modal); + break; + } } await editorMessage.edit({ @@ -715,6 +841,150 @@ export const command: CommandMessage = { } }); + // Agregar manejo de modales + //@ts-ignore + client.on('interactionCreate', async (interaction) => { + if (!interaction.isModalSubmit()) return; + if (interaction.user.id !== message.author.id) return; + if (!interaction.customId.endsWith('_modal')) return; + + try { + switch (interaction.customId) { + case 'edit_title_modal': { + blockState.title = interaction.fields.getTextInputValue('title_input'); + await interaction.reply({ content: '✅ Título actualizado.', ephemeral: true }); + break; + } + case 'edit_description_modal': { + const newDescription = interaction.fields.getTextInputValue('description_input'); + const descComp = blockState.components.find((c: any) => c.type === 10); + if (descComp) { + descComp.content = newDescription; + } else { + blockState.components.push({ type: 10, content: newDescription, thumbnail: null }); + } + await interaction.reply({ content: '✅ Descripción actualizada.', ephemeral: true }); + break; + } + case 'edit_color_modal': { + const colorInput = interaction.fields.getTextInputValue('color_input'); + if (colorInput.trim() === '') { + blockState.color = null; + } else { + let hexColor = colorInput.replace('#', ''); + if (/^[0-9A-F]{6}$/i.test(hexColor)) { + blockState.color = parseInt(hexColor, 16); + } else { + await interaction.reply({ content: '❌ Color inválido. Usa formato HEX (#FF5733)', ephemeral: true }); + return; + } + } + await interaction.reply({ content: '✅ Color actualizado.', ephemeral: true }); + break; + } + case 'add_content_modal': { + const newContent = interaction.fields.getTextInputValue('content_input'); + blockState.components.push({ type: 10, content: newContent, thumbnail: null }); + await interaction.reply({ content: '✅ Contenido añadido.', ephemeral: true }); + break; + } + case 'add_image_modal': { + const imageUrl = interaction.fields.getTextInputValue('image_url_input'); + if (isValidUrl(imageUrl)) { + blockState.components.push({ type: 12, url: imageUrl }); + await interaction.reply({ content: '✅ Imagen añadida.', ephemeral: true }); + } else { + await interaction.reply({ content: '❌ URL de imagen inválida.', ephemeral: true }); + return; + } + break; + } + case 'add_cover_modal': + case 'edit_cover_modal': { + const coverUrl = interaction.fields.getTextInputValue('cover_input'); + if (isValidUrl(coverUrl)) { + blockState.coverImage = coverUrl; + await interaction.reply({ content: '✅ Imagen de portada actualizada.', ephemeral: true }); + } else { + await interaction.reply({ content: '❌ URL de portada inválida.', ephemeral: true }); + return; + } + break; + } + case 'add_separator_modal': { + const visibleStr = interaction.fields.getTextInputValue('separator_visible').toLowerCase(); + const spacingStr = interaction.fields.getTextInputValue('separator_spacing') || '1'; + + const divider = visibleStr === 'true' || visibleStr === '1' || visibleStr === 'si' || visibleStr === 'sí'; + const spacing = Math.min(3, Math.max(1, parseInt(spacingStr) || 1)); + + blockState.components.push({ type: 14, divider, spacing }); + await interaction.reply({ content: '✅ Separador añadido.', ephemeral: true }); + break; + } + case 'edit_thumbnail_modal': { + const thumbnailUrl = interaction.fields.getTextInputValue('thumbnail_input'); + const textComp = blockState.components.find((c: any) => c.type === 10); + + if (textComp) { + if (thumbnailUrl.trim() === '' || !isValidUrl(thumbnailUrl)) { + textComp.thumbnail = null; + await interaction.reply({ content: '✅ Thumbnail eliminado.', ephemeral: true }); + } else { + textComp.thumbnail = thumbnailUrl; + await interaction.reply({ content: '✅ Thumbnail actualizado.', ephemeral: true }); + } + } + break; + } + case 'import_json_modal': { + try { + const jsonString = interaction.fields.getTextInputValue('json_input'); + const importedData = JSON.parse(jsonString); + + // Validar estructura básica + if (importedData && typeof importedData === 'object') { + blockState = { + title: importedData.title || blockState.title, + color: importedData.color || blockState.color, + coverImage: importedData.coverImage || blockState.coverImage, + components: Array.isArray(importedData.components) ? importedData.components : blockState.components + }; + + await interaction.reply({ content: '✅ JSON importado correctamente.', ephemeral: true }); + } else { + await interaction.reply({ content: '❌ Estructura JSON inválida.', ephemeral: true }); + return; + } + } catch (error) { + await interaction.reply({ content: '❌ JSON inválido. Verifica el formato.', ephemeral: true }); + return; + } + break; + } + default: + return; + } + + // Actualizar la vista previa después de cada cambio en el modal + setTimeout(async () => { + try { + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + } catch (error) { + console.error('Error actualizando preview:', error); + } + }, 1000); + + } catch (error) { + console.error('Error en modal:', error); + try { + await interaction.reply({ content: '❌ Error procesando el modal.', ephemeral: true }); + } catch {} + } + }); + //@ts-ignore collector.on("end", async (_, reason) => { if (reason === "time") { @@ -726,4 +996,4 @@ export const command: CommandMessage = { } }); } -}; \ No newline at end of file +}; diff --git a/src/commands/messages/alliaces/setupChannel.ts b/src/commands/messages/alliaces/setupChannel.ts new file mode 100644 index 0000000..6e14993 --- /dev/null +++ b/src/commands/messages/alliaces/setupChannel.ts @@ -0,0 +1,93 @@ +import { CommandMessage } from "../../../core/types/commands"; + +export const command: CommandMessage = { + name: "setchannel-alliance", + type: "message", + aliases: ["alchannel", "channelally"], + cooldown: 10, + //@ts-ignore + run: async (message, args, client) => { + if (!message.member?.permissions.has("Administrator")) { + return message.reply("❌ No tienes permisos de Administrador."); + } + + // Validar argumentos + if (args.length < 2) { + return message.reply("❌ Uso correcto: `!setchannel-alliance <#canal|ID> `"); + } + + const channelInput = args[0]; + const blockConfigName = args[1]; + + // Extraer ID del canal + let channelId: string; + + // Si es una mención de canal (#canal) + if (channelInput.startsWith('<#') && channelInput.endsWith('>')) { + channelId = channelInput.slice(2, -1); + } + // Si es solo un ID + else if (/^\d+$/.test(channelInput)) { + channelId = channelInput; + } + else { + return message.reply("❌ Formato de canal inválido. Usa `#canal` o el ID del canal."); + } + + try { + // Verificar que el canal existe en el servidor + const channel = await message.guild?.channels.fetch(channelId); + if (!channel) { + return message.reply("❌ El canal especificado no existe en este servidor."); + } + + // Verificar que el canal es un canal de texto + if (!channel.isTextBased()) { + return message.reply("❌ El canal debe ser un canal de texto."); + } + + // Verificar que existe el blockConfig + const blockConfig = await client.prisma.blockV2Config.findFirst({ + where: { + guildId: message.guildId, + name: blockConfigName + } + }); + + if (!blockConfig) { + return message.reply(`❌ No se encontró el bloque de configuración \`${blockConfigName}\`. Asegúrate de que exista.`); + } + + // Configurar el canal de alianzas + const allianceChannel = await client.prisma.allianceChannel.upsert({ + where: { + guildId_channelId: { + guildId: message.guildId, + channelId: channelId + } + }, + create: { + guildId: message.guildId, + channelId: channelId, + blockConfigName: blockConfigName, + isActive: true + }, + update: { + blockConfigName: blockConfigName, + isActive: true, + updatedAt: new Date() + } + }); + + return message.reply(`✅ Canal de alianzas configurado correctamente!\n\n` + + `**Canal:** <#${channelId}>\n` + + `**Configuración:** \`${blockConfigName}\`\n` + + `**Estado:** Activo\n\n` + + `Los enlaces de Discord válidos en este canal ahora otorgarán puntos de alianza.`); + + } catch (error) { + console.error('Error configurando canal de alianzas:', error); + return message.reply("❌ Ocurrió un error al configurar el canal de alianzas. Inténtalo de nuevo."); + } + } +} diff --git a/src/core/lib/vars.ts b/src/core/lib/vars.ts index ab0cc69..40005bd 100644 --- a/src/core/lib/vars.ts +++ b/src/core/lib/vars.ts @@ -1,8 +1,17 @@ -import {Guild, User} from "discord.js"; +import {Guild, Invite, User} from "discord.js"; -export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any): Promise { +//@ts-ignore +export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any, invite: Invite | undefined): Promise { if(!text) return ''; + // Crear inviteObject solo si invite existe y tiene guild + const inviteObject = invite?.guild ? { + guild: { + //@ts-ignore + icon: `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256` + } + } : null; + return text /** * USER INFO @@ -12,9 +21,23 @@ export async function replaceVars(text: string, user: User | undefined, guild: G .replace(/(user\.mention)/g, user ? `<@${user.id}>` : '') .replace(/(user\.avatar)/g, user?.displayAvatarURL({ forceStatic: false }) ?? '') + /** + * USER STATS + */ + .replace(/(user\.pointsAll)/g, stats?.totalPoints?.toString() ?? '0') + .replace(/(user\.pointsWeekly)/g, stats?.weeklyPoints?.toString() ?? '0') + .replace(/(user\.pointsMonthly)/g, stats?.monthlyPoints?.toString() ?? '0') + /** * GUILD INFO */ .replace(/(guild\.name)/g, guild?.name ?? '') - .replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? ''); + .replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? '') + + /** + * INVITE INFO + */ + .replace(/(invite\.name)/g, invite?.guild?.name ?? "") + .replace(/(invite\.icon)/g, inviteObject?.guild.icon ?? '0') + } \ No newline at end of file diff --git a/src/events/extras/alliace.ts b/src/events/extras/alliace.ts index e69de29..28246d5 100644 --- a/src/events/extras/alliace.ts +++ b/src/events/extras/alliace.ts @@ -0,0 +1,424 @@ +import { + Message +} from "discord.js"; +// Se agrega ts +//@ts-ignore +import { PrismaClient } from "@prisma/client"; +import { replaceVars } from "../../core/lib/vars"; + +const prisma = new PrismaClient(); + +// Regex para detectar URLs válidas (corregido) +const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/gi; + +// Dominios de Discord válidos para invitaciones +const DISCORD_DOMAINS = [ + 'discord.gg', + 'discord.com/invite', + 'discordapp.com/invite' +]; + +export async function alliance(message: Message) { + try { + // Verificar que el mensaje tenga contenido + if (!message.content || message.content.trim() === '') { + return; + } + + // Buscar enlaces en el mensaje + const links = extractValidLinks(message.content); + + if (links.length === 0) { + return; // No hay enlaces válidos + } + + // Verificar si el canal está configurado para alianzas + const allianceChannel = await prisma.allianceChannel.findFirst({ + where: { + guildId: message.guild!.id, + channelId: message.channel.id, + isActive: true + } + }); + + if (!allianceChannel) { + return; // Canal no configurado para alianzas + } + + // Verificar permisos del usuario (corregido para evitar errores con tipos de canal) + const member = await message.guild!.members.fetch(message.author.id); + + // Verificar que es un canal de texto antes de verificar permisos + if (!message.channel.isTextBased()) { + return; // No es un canal de texto + } + //@ts-ignore + const permissions = message.channel.permissionsFor(member); + if (!permissions?.has('SendMessages')) { + return; // Usuario sin permisos + } + + // Validar que los enlaces sean de Discord (invitaciones) + const validDiscordLinks = validateDiscordLinks(links); + + if (validDiscordLinks.length === 0) { + return; // No hay enlaces válidos de Discord + } + + // Procesar cada enlace válido + for (const link of validDiscordLinks) { + await processValidLink(message, allianceChannel, link); + } + + } catch (error) { + console.error('Error en función alliance:', error); + } +} + +function extractValidLinks(content: string): string[] { + const matches = content.match(URL_REGEX); + return matches || []; +} + +function validateDiscordLinks(links: string[]): string[] { + return links.filter(link => { + return DISCORD_DOMAINS.some(domain => link.includes(domain)); + }); +} + +async function processValidLink(message: Message, allianceChannel: any, link: string) { + try { + // Verificar si el enlace de Discord es válido (opcional: hacer fetch) + const inviteData = await validateDiscordInvite(link); + + if (!inviteData) { + return; // Enlace inválido o expirado + } + + // Asegurar que el usuario existe en la base de datos + await prisma.user.upsert({ + where: { id: message.author.id }, + update: {}, + create: { id: message.author.id } + }); + + // Asegurar que el guild existe en la base de datos + await prisma.guild.upsert({ + where: { id: message.guild!.id }, + update: {}, + create: { + id: message.guild!.id, + name: message.guild!.name + } + }); + + // Registrar el punto en el historial + await prisma.pointHistory.create({ + data: { + userId: message.author.id, + guildId: message.guild!.id, + channelId: allianceChannel.id, + messageId: message.id, + points: 1 + } + }); + + // Actualizar estadísticas del usuario + await updateUserStats(message.author.id, message.guild!.id); + + // Obtener estadísticas para reemplazar variables + const userStats = await getUserAllianceStats(message.author.id, message.guild!.id); + + // Enviar el bloque configurado usando Display Components + await sendBlockConfigV2(message, allianceChannel.blockConfigName, message.guild!.id, link, userStats, inviteData); + + console.log(`✅ Punto otorgado a ${message.author.tag} por enlace válido: ${link}`); + + } catch (error) { + console.error('Error procesando enlace válido:', error); + } +} + +async function validateDiscordInvite(link: string): Promise { + try { + // Extraer el código de invitación del enlace + const inviteCode = extractInviteCode(link); + if (!inviteCode) return null; + + // Hacer una solicitud a la API de Discord para validar la invitación + const response = await fetch(`https://discord.com/api/v10/invites/${inviteCode}?with_counts=true`, { + method: 'GET', + headers: { + 'User-Agent': 'DiscordBot (https://github.com/discord/discord-api-docs, 1.0)' + } + }); + + if (response.status === 200) { + const inviteData = await response.json(); + // Verificar que la invitación tenga un servidor válido + if (inviteData.guild && inviteData.guild.id) { + return inviteData; // Retornar datos completos de la invitación + } + } + + return null; + } catch (error) { + console.error('Error validando invitación de Discord:', error); + return null; // En caso de error, considerar como inválido + } +} + +function extractInviteCode(link: string): string | null { + // Patrones para extraer códigos de invitación + const patterns = [ + /discord\.gg\/([a-zA-Z0-9]+)/, + /discord\.com\/invite\/([a-zA-Z0-9]+)/, + /discordapp\.com\/invite\/([a-zA-Z0-9]+)/ + ]; + + for (const pattern of patterns) { + const match = link.match(pattern); + if (match && match[1]) { + return match[1]; + } + } + + return null; +} + +async function updateUserStats(userId: string, guildId: string) { + const now = new Date(); + + // Obtener o crear las estadísticas del usuario + let userStats = await prisma.partnershipStats.findFirst({ + where: { + userId: userId, + guildId: guildId + } + }); + + if (!userStats) { + await prisma.partnershipStats.create({ + data: { + userId: userId, + guildId: guildId, + totalPoints: 1, + weeklyPoints: 1, + monthlyPoints: 1, + lastWeeklyReset: now, + lastMonthlyReset: now + } + }); + return; + } + + // Verificar si necesita reset semanal (7 días) + const weeksPassed = Math.floor((now.getTime() - userStats.lastWeeklyReset.getTime()) / (7 * 24 * 60 * 60 * 1000)); + const needsWeeklyReset = weeksPassed >= 1; + + // Verificar si necesita reset mensual (30 días) + const daysPassed = Math.floor((now.getTime() - userStats.lastMonthlyReset.getTime()) / (24 * 60 * 60 * 1000)); + const needsMonthlyReset = daysPassed >= 30; + + // Actualizar estadísticas + await prisma.partnershipStats.update({ + where: { + userId_guildId: { + userId: userId, + guildId: guildId + } + }, + data: { + totalPoints: { increment: 1 }, + weeklyPoints: needsWeeklyReset ? 1 : { increment: 1 }, + monthlyPoints: needsMonthlyReset ? 1 : { increment: 1 }, + lastWeeklyReset: needsWeeklyReset ? now : userStats.lastWeeklyReset, + lastMonthlyReset: needsMonthlyReset ? now : userStats.lastMonthlyReset + } + }); +} + +async function sendBlockConfigV2(message: Message, blockConfigName: string, guildId: string, validLink: string, userStats?: any, inviteObject?: any) { + try { + // Obtener la configuración del bloque + const blockConfig = await prisma.blockV2Config.findFirst({ + where: { + guildId: guildId, + name: blockConfigName + } + }); + + if (!blockConfig) { + console.error(`❌ Bloque "${blockConfigName}" no encontrado para guild ${guildId}`); + return; + } + + // Procesar las variables en la configuración usando la función unificada + const processedConfig = await processConfigVariables(blockConfig.config, message.author, message.guild!, userStats, inviteObject); + + // Convertir el JSON plano a la estructura de Display Components correcta + const displayComponent = await convertConfigToDisplayComponent(processedConfig, message.author, message.guild!); + + // Enviar usando Display Components con la flag correcta + // Usar la misma estructura que el editor: flag 32768 y type 17 + //@ts-ignore + await message.reply({ + flags: 32768, // Equivalente a MessageFlags.IsComponentsV2 + components: [displayComponent] + }); + + } catch (error) { + console.error('❌ Error enviando bloque de configuración V2:', error); + console.log('Detalles del error:', error); + + // Fallback: usar mensaje simple + try { + await message.reply({ + content: '✅ ¡Enlace de alianza procesado correctamente!' + }); + } catch (fallbackError) { + console.error('❌ Error en fallback:', fallbackError); + } + } +} + +async function convertConfigToDisplayComponent(config: any, user: any, guild: any): Promise { + try { + const previewComponents = []; + + // Añadir imagen de portada primero si existe + if (config.coverImage && isValidUrl(config.coverImage)) { + const processedCoverUrl = await replaceVars(config.coverImage, user, guild); + if (isValidUrl(processedCoverUrl)) { + previewComponents.push({ + type: 12, + items: [{ media: { url: processedCoverUrl } }] + }); + } + } + + // Añadir título después de la portada + if (config.title) { + previewComponents.push({ + type: 10, + content: await replaceVars(config.title, user, guild) + }); + } + + // Procesar componentes en orden (igual que el editor) + if (config.components && Array.isArray(config.components)) { + for (const c of config.components) { + if (c.type === 10) { + // Componente de texto con thumbnail opcional + const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, user, guild) : null; + + if (processedThumbnail && isValidUrl(processedThumbnail)) { + // Si tiene thumbnail válido, usar contenedor tipo 9 con accessory + previewComponents.push({ + type: 9, + components: [ + { + type: 10, + content: await replaceVars(c.content || " ", user, guild) + } + ], + accessory: { + type: 11, + media: { url: processedThumbnail } + } + }); + } else { + // Sin thumbnail o thumbnail inválido, componente normal + previewComponents.push({ + type: 10, + content: await replaceVars(c.content || " ", user, guild) + }); + } + } else if (c.type === 14) { + // Separador + previewComponents.push({ + type: 14, + divider: c.divider ?? true, + spacing: c.spacing ?? 1 + }); + } else if (c.type === 12) { + // Imagen - validar URL también + const processedImageUrl = await replaceVars(c.url, user, guild); + + if (isValidUrl(processedImageUrl)) { + previewComponents.push({ + type: 12, + items: [{ media: { url: processedImageUrl } }] + }); + } + } + } + } + + // Retornar la estructura exacta que usa el editor + return { + type: 17, // Container type + accent_color: config.color ?? null, + components: previewComponents + }; + + } catch (error) { + console.error('Error convirtiendo configuración a Display Component:', error); + + // Fallback: crear un componente básico + return { + type: 17, + accent_color: null, + components: [ + { type: 10, content: 'Error al procesar la configuración del bloque.' } + ] + }; + } +} + +// Función helper para validar URLs +function isValidUrl(url: string): boolean { + if (!url || typeof url !== 'string') return false; + try { + new URL(url); + return url.startsWith('http://') || url.startsWith('https://'); + } catch { + return false; + } +} + +async function processConfigVariables(config: any, user: any, guild: any, userStats?: any, inviteObject?: any): Promise { + if (typeof config === 'string') { + // Usar la función unificada replaceVars con todos los parámetros + return await replaceVars(config, user, guild, userStats, inviteObject); + } + + if (Array.isArray(config)) { + const processedArray = []; + for (const item of config) { + processedArray.push(await processConfigVariables(item, user, guild, userStats, inviteObject)); + } + return processedArray; + } + + if (config && typeof config === 'object') { + const processedObject: any = {}; + for (const [key, value] of Object.entries(config)) { + processedObject[key] = await processConfigVariables(value, user, guild, userStats, inviteObject); + } + return processedObject; + } + + return config; +} + + +// Función auxiliar para obtener estadísticas +export async function getUserAllianceStats(userId: string, guildId: string) { + return prisma.partnershipStats.findFirst({ + where: { + userId: userId, + guildId: guildId + } + }); +} \ No newline at end of file diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 4311b64..7da1a79 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -2,10 +2,12 @@ import {bot} from "../main"; import {Events} from "discord.js"; import {redis} from "../core/redis"; import {commands} from "../core/loader"; +import {alliance} from "./extras/alliace"; bot.on(Events.MessageCreate, async (message) => { if (message.author.bot) return; + await alliance(message); const server = await bot.prisma.guild.upsert({ where: { id: message.guildId