diff --git a/prisma/dev.db b/prisma/dev.db deleted file mode 100644 index e160da7..0000000 Binary files a/prisma/dev.db and /dev/null differ diff --git a/prisma/migrations/20250917001309_modals/migration.sql b/prisma/migrations/20250917001309_modals/migration.sql deleted file mode 100644 index 9e01ff0..0000000 --- a/prisma/migrations/20250917001309_modals/migration.sql +++ /dev/null @@ -1,62 +0,0 @@ --- CreateTable -CREATE TABLE "Guild" ( - "id" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL -); - --- 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, - "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 -); - --- CreateIndex -CREATE UNIQUE INDEX "Alliance_messageId_key" ON "Alliance"("messageId"); - --- CreateIndex -CREATE UNIQUE INDEX "EmbedConfig_guildId_key" ON "EmbedConfig"("guildId"); diff --git a/prisma/migrations/20250917002041_modals/migration.sql b/prisma/migrations/20250917002041_modals/migration.sql deleted file mode 100644 index f4aefc0..0000000 --- a/prisma/migrations/20250917002041_modals/migration.sql +++ /dev/null @@ -1,33 +0,0 @@ -/* - Warnings: - - - Added the required column `name` to the `EmbedConfig` table without a default value. This is not possible if the table is not empty. - -*/ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_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 -); -INSERT INTO "new_EmbedConfig" ("authorIconURL", "authorName", "authorURL", "color", "description", "fields", "footerIconURL", "footerText", "guildId", "id", "imageURL", "thumbnailURL", "title", "url") SELECT "authorIconURL", "authorName", "authorURL", "color", "description", "fields", "footerIconURL", "footerText", "guildId", "id", "imageURL", "thumbnailURL", "title", "url" FROM "EmbedConfig"; -DROP TABLE "EmbedConfig"; -ALTER TABLE "new_EmbedConfig" RENAME TO "EmbedConfig"; -CREATE UNIQUE INDEX "EmbedConfig_guildId_name_key" ON "EmbedConfig"("guildId", "name"); -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/20250917044008_modals/migration.sql b/prisma/migrations/20250917044008_modals/migration.sql deleted file mode 100644 index da36991..0000000 --- a/prisma/migrations/20250917044008_modals/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ --- RedefineTables -PRAGMA defer_foreign_keys=ON; -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_Guild" ( - "id" TEXT NOT NULL PRIMARY KEY, - "name" TEXT NOT NULL, - "prefix" TEXT NOT NULL DEFAULT '!' -); -INSERT INTO "new_Guild" ("id", "name") SELECT "id", "name" FROM "Guild"; -DROP TABLE "Guild"; -ALTER TABLE "new_Guild" RENAME TO "Guild"; -PRAGMA foreign_keys=ON; -PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index 2a5a444..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# 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 bab82db..00ad8a9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,6 +29,7 @@ model Guild { // ✅ CAMBIO: Ahora un Guild puede tener MÚLTIPLES configuraciones de embed. embedConfigs EmbedConfig[] + BlockV2Config BlockV2Config[] } /* * ----------------------------------------------------------------------------- @@ -130,4 +131,27 @@ model EmbedConfig { // ✅ NUEVO: Asegura que el 'name' sea único por cada servidor. // No puedes tener dos embeds llamados "alianza" en el mismo servidor. @@unique([guildId, name]) -} \ No newline at end of file +} + +/* + * ----------------------------------------------------------------------------- + * Modelo para la Configuración de Bloques V2 + * ----------------------------------------------------------------------------- +*/ +model BlockV2Config { + id String @id @default(cuid()) + + // ✅ Nombre único dentro de cada servidor + name String + + // Configuración en JSON (embed + componentes, botones, etc.) + config Json + + + // Relación con el servidor + guild Guild @relation(fields: [guildId], references: [id]) + guildId String + + // 🔒 Asegura que un nombre no se repita dentro del mismo servidor + @@unique([guildId, name]) +} diff --git a/src/commands/messages/alliaces/createEmbed.ts b/src/.backup/createEmbed.backup.ts similarity index 56% rename from src/commands/messages/alliaces/createEmbed.ts rename to src/.backup/createEmbed.backup.ts index 92a0ea7..132a119 100644 --- a/src/commands/messages/alliaces/createEmbed.ts +++ b/src/.backup/createEmbed.backup.ts @@ -1,9 +1,66 @@ -import { CommandMessage } from "../../../core/types/commands"; -// @ts-ignore -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, TextChannel, ChannelType } from "discord.js"; -//@ts-ignore -import { ButtonStyle, ComponentType } from "discord.js"; -import { replaceVars } from "../../../core/lib/vars"; +import {CommandMessage} from "../core/types/commands"; + +import { + ActionRowBuilder, + ButtonBuilder, + //@ts-ignore + ButtonStyle, + //@ts-ignore + ChannelType, + //@ts-ignore + ComponentType, + EmbedBuilder, + TextChannel +} from "discord.js"; +import {replaceVars} from "../core/lib/vars"; + + + +/** + * VARIABLES COMPONENTS V2 + */ + + const variables_text = { + "flags": 32768, + "components": [ + { + "type": 17, + "components": [ + { + "type": 10, + "content": "﹒⌒    Variables Text    ╰୧﹒" + }, + { + "type": 14, + "spacing": 1, + "divider": false + }, + { + "type": 10, + "content": "**✿ ૮ ․ ․ ྀིა User Var**" + }, + { + "type": 10, + "content": "\n(user.id) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑖𝑑𝑒𝑛𝑡𝑖𝑓𝑖𝑐𝑎𝑑𝑜𝑟 𝑑𝑒 𝑢𝑛 𝑢𝑠𝑢𝑎𝑟𝑖𝑜.\n(user.name) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑛𝑜𝑚𝑏𝑟𝑒 𝑑𝑒 𝑢𝑛 𝑢𝑠𝑢𝑎𝑟𝑖𝑜\n(user.avatar) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑙𝑎 𝑢𝑟𝑙 𝑑𝑒𝑙 𝑎𝑣𝑎𝑡𝑎𝑟 𝑑𝑒𝑙 𝑢𝑠𝑢𝑎𝑟𝑖𝑜.\n(user.mention) **-** 𝑀𝑒𝑛𝑐𝑖𝑜𝑛𝑎 𝑎 𝑢𝑛 𝑢𝑠𝑢𝑎𝑟𝑖𝑜 𝑐𝑜𝑛 𝑠𝑢 @" + }, + { + "type": 10, + "content": "**✿ ૮ ․ ․ ྀིა Guild Var**" + }, + { + "type": 10, + "content": "(guild.icon) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑖𝑐𝑜𝑛𝑜 𝑑𝑒𝑙 𝑠𝑒𝑟𝑣𝑖𝑑𝑜𝑟.\n(guild.name) **-** 𝑀𝑢𝑒𝑠𝑡𝑟𝑎 𝑒𝑙 𝑛𝑜𝑚𝑏𝑟𝑒 𝑑𝑒𝑙 𝑠𝑒𝑟𝑣𝑖𝑑𝑜𝑟.\n" + } + ], + "accent_color": null, + "spoiler": true + } + ] +} + +/** + * COMMAND EXECUTE + */ export const command: CommandMessage = { name: "embedcreate", @@ -35,6 +92,8 @@ export const command: CommandMessage = { title?: string; description?: string; color?: number; + imageUrl?: string; + thumbnail?: string; footer?: string; } = { title: `Editor de Embed: ${embedName}`, @@ -42,6 +101,8 @@ export const command: CommandMessage = { "Usa los botones de abajo para configurar este embed.\n\n_Ejemplo de variable: `{user.name}`_", color: 0x5865f2, footer: "Haz clic en Guardar cuando termines.", + thumbnail: `${message.guild!.iconURL()}`, + imageUrl: `https://i.pinimg.com/originals/d2/c3/79/d2c3798684709cef3ed532b59c59bad4.gif` }; // 📌 Función para construir un embed a partir del estado @@ -52,18 +113,28 @@ export const command: CommandMessage = { if (embedState.title) preview.setTitle( //@ts-ignore - await replaceVars(embedState.title, message.member) + await replaceVars(embedState.title, message.member, message.guild) ); if (embedState.description) preview.setDescription( //@ts-ignore - await replaceVars(embedState.description, message.member) + await replaceVars(embedState.description, message.member, message.guild) ); if (embedState.footer) preview.setFooter({ //@ts-ignore - text: await replaceVars(embedState.footer, message.member), + text: await replaceVars(embedState.footer, message.member, message.guild), }); + if (embedState.imageUrl) + preview.setImage( + //@ts-ignore + await replaceVars(embedState.imageUrl, message.member, message.guild) + ); + if (embedState.thumbnail) + preview.setThumbnail( + //@ts-ignore + await replaceVars(embedState.thumbnail, message.member, message.guild) + ) return preview; }; @@ -85,6 +156,16 @@ export const command: CommandMessage = { .setCustomId("edit_color") .setLabel("Color") .setStyle(ButtonStyle.Primary) + .setDisabled(disabled), + new ButtonBuilder() + .setCustomId('edit_imageurl') + .setLabel('Image') + .setStyle(ButtonStyle.Secondary) + .setDisabled(disabled), + new ButtonBuilder() + .setCustomId('edit_thumbnail') + .setLabel('Thumbnail') + .setStyle(ButtonStyle.Secondary) .setDisabled(disabled) ); @@ -112,12 +193,109 @@ export const command: CommandMessage = { return [primaryRow, secondaryRow, controlRow]; }; + /** + * Botones Custom + */ + + const btns = (disabled = false) => ({ + flags: 32768, + components: [ + { + type: 17, + components: [ + { + type: 10, + content: "﹒⌒    Options    ╰୧﹒" + }, + { + type: 14, + divider: true + }, + { + type: 1, + components: [ + { + style: 2, + type: 2, + label: "Titulo", + disabled: disabled, // 👈 aquí ya funciona + custom_id: "edit_title" + }, + { + style: 2, + type: 2, + label: "Descripción", + disabled: disabled, + custom_id: "edit_description" + }, + { + style: 2, + type: 2, + label: "Color", + disabled: disabled, + custom_id: "edit_color" + }, + { + style: 2, + type: 2, + label: "Imagen", + disabled: disabled, + custom_id: 'edit_imageurl' + }, + { + style: 2, + type: 2, + label: "Thumbnail", + disabled: disabled, + custom_id: 'edit_thumbnail' + } + ] + }, + { + type: 1, + components: [ + { + style: 2, + type: 2, + label: "Footer", + disabled: disabled, + custom_id: "edit_footer" + } + ] + }, + { + type: 1, + components: [ + { + style: 3, + type: 2, + label: "Guardar", + disabled: disabled, + custom_id: "save_embed" + }, + { + style: 4, + type: 2, + label: "Eliminar", + disabled: disabled, + custom_id: "cancel_embed" + } + ] + } + ], + accent_color: null + } + ] + }); + + + if (message.channel.type === ChannelType.GuildText) { const channel = message.channel as TextChannel; const editorMessage = await channel.send({ embeds: [await renderPreview()], - components: generateButtonRows(), + //components: generateButtonRows(), }); const collector = editorMessage.createMessageComponentCollector({ @@ -203,32 +381,51 @@ export const command: CommandMessage = { // Edición let promptContent = ""; - let fieldToEdit: "title" | "description" | "color" | "footer" | null = + let variableContent; + let fieldToEdit: "title" | "description" | "color" | "footer" | "image" | "thumbnail" | null = null; switch (i.customId) { case "edit_title": promptContent = - "Escribe el nuevo **título** (puedes usar variables como `{user.name}`)."; + "Escribe el nuevo **título** (puedes usar variables como `(guild.name)`)."; + variableContent = variables_text fieldToEdit = "title"; break; case "edit_description": promptContent = "Escribe la nueva **descripción** (puedes usar variables)."; + variableContent = variables_text fieldToEdit = "description"; break; case "edit_color": promptContent = "Escribe el nuevo **color** en formato hexadecimal (ej: `#FF0000`)."; + variableContent = variables_text fieldToEdit = "color"; break; case "edit_footer": promptContent = "Escribe el nuevo **texto del footer** (puedes usar variables)."; + variableContent = variables_text fieldToEdit = "footer"; break; + case "edit_imageurl": + promptContent = + "Pega el url **de la imagen** (puedes usar variables)."; + variableContent = variables_text + fieldToEdit = "image"; + break; + case "edit_thumbnail": + promptContent = + "Pega el url **del thumbnail** (puedes usar variables)."; + variableContent = variables_text + fieldToEdit = "thumbnail"; + break; } + //@ts-ignore + const variableMessage = await i.channel.send(variableContent) //@ts-ignore const promptMessage = await i.channel.send(promptContent); @@ -246,6 +443,9 @@ export const command: CommandMessage = { if (fieldToEdit === "title") embedState.title = newValue; if (fieldToEdit === "description") embedState.description = newValue; if (fieldToEdit === "footer") embedState.footer = newValue; + // added v0.0.1.1 + if (fieldToEdit === "image") embedState.imageUrl = newValue; + if (fieldToEdit === "thumbnail") embedState.thumbnail = newValue; if (fieldToEdit === "color") { try { @@ -258,6 +458,7 @@ export const command: CommandMessage = { await collectedMessage.delete(); await promptMessage.delete(); + await variableMessage.delete(); await editorMessage.edit({ embeds: [await renderPreview()], @@ -268,6 +469,7 @@ export const command: CommandMessage = { messageCollector.on("end", async (collected) => { if (collected.size === 0) { await promptMessage.delete(); + await variableMessage.delete(); await editorMessage.edit({ components: generateButtonRows(false), }); diff --git a/src/commands/messages/alliaces/editEmbed.ts b/src/.backup/editEmbed.backup.ts similarity index 98% rename from src/commands/messages/alliaces/editEmbed.ts rename to src/.backup/editEmbed.backup.ts index ea2d5d2..3b2e3af 100644 --- a/src/commands/messages/alliaces/editEmbed.ts +++ b/src/.backup/editEmbed.backup.ts @@ -1,9 +1,9 @@ -import { CommandMessage } from "../../../core/types/commands"; +import { CommandMessage } from "../core/types/commands"; // @ts-ignore import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, TextChannel, ChannelType } from "discord.js"; //@ts-ignore import { ButtonStyle, ComponentType } from "discord.js"; -import { replaceVars } from "../../../core/lib/vars"; +import { replaceVars } from "../core/lib/vars"; export const command: CommandMessage = { name: "editembed", diff --git a/src/commands/messages/alliaces/createEmbedv2.ts b/src/commands/messages/alliaces/createEmbedv2.ts new file mode 100644 index 0000000..e88370b --- /dev/null +++ b/src/commands/messages/alliaces/createEmbedv2.ts @@ -0,0 +1,729 @@ +import { CommandMessage } from "../../../core/types/commands"; +// @ts-ignore +import { ComponentType, ButtonStyle } from "discord.js"; +import { replaceVars } from "../../../core/lib/vars"; + +/** + * Botones de edición + */ +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" } + ] + }, + { + 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" } + ] + }, + { + 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" } + ] + } +]); + +/** + * Validar si una URL es válida + */ +const 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; + } +}; + +/** + * Generar vista previa + */ +const renderPreview = async (blockState: any, member: any, guild: any) => { + const previewComponents = []; + + // Añadir imagen de portada primero si existe + if (blockState.coverImage && isValidUrl(blockState.coverImage)) { + //@ts-ignore + const processedCoverUrl = await replaceVars(blockState.coverImage, member, guild); + if (isValidUrl(processedCoverUrl)) { + previewComponents.push({ + type: 12, + items: [{ media: { url: processedCoverUrl } }] + }); + } + } + + // Añadir título después de la portada + previewComponents.push({ + type: 10, + //@ts-ignore + content: await replaceVars(blockState.title ?? "Sin título", member, guild) + }); + + // Procesar componentes en orden + for (const c of blockState.components) { + if (c.type === 10) { + // Componente de texto con thumbnail opcional + //@ts-ignore + const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, member, guild) : null; + + if (processedThumbnail && isValidUrl(processedThumbnail)) { + // Si tiene thumbnail válido, usar contenedor tipo 9 con accessory + previewComponents.push({ + type: 9, + components: [ + { + type: 10, + //@ts-ignore + content: await replaceVars(c.content || " ", member, guild) + } + ], + accessory: { + type: 11, + media: { url: processedThumbnail } + } + }); + } else { + // Sin thumbnail o thumbnail inválido, componente normal + previewComponents.push({ + type: 10, + //@ts-ignore + content: await replaceVars(c.content || " ", member, 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 + //@ts-ignore + const processedImageUrl = await replaceVars(c.url, member, guild); + + if (isValidUrl(processedImageUrl)) { + previewComponents.push({ + type: 12, + items: [{ media: { url: processedImageUrl } }] + }); + } + } + } + + return { + type: 17, + accent_color: blockState.color ?? null, + components: previewComponents + }; +}; + +export const command: CommandMessage = { + name: "blockcreatev2", + type: "message", + cooldown: 20, + run: async (message, args, client) => { + if (!message.member?.permissions.has("Administrator")) { + return message.reply("❌ No tienes permisos de Administrador."); + } + + const blockName: string | null = args[0] ?? null; + if (!blockName) { + return message.reply("Debes proporcionar un nombre. Uso: `!blockcreatev2 `"); + } + + const nameIsValid = await client.prisma.blockV2Config.findFirst({ + where: { guildId: message.guild!.id, name: blockName } + }); + if (nameIsValid) return message.reply("❌ Nombre ya usado!"); + + // Estado inicial + let blockState: any = { + title: `Editor de Block: ${blockName}`, + color: null, + coverImage: null, // Nueva propiedad para imagen de portada + components: [ + { type: 14, divider: false }, + { type: 10, content: "Usa los botones para configurar.", thumbnail: null } + ] + }; + + //@ts-ignore + const editorMessage = await message.channel.send({ + flags: 32768, + components: [ + await renderPreview(blockState, message.member, message.guild), + ...btns(false) + ] + }); + + const collector = editorMessage.createMessageComponentCollector({ + time: 300000 + }); + + collector.on("collect", async (i) => { + if (i.user.id !== message.author.id) { + await i.reply({ content: "No puedes usar este menú.", ephemeral: true }); + return; + } + + // --- BOTONES --- + if (i.isButton()) { + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(true)] + }); + await i.deferUpdate(); + + switch (i.customId) { + case "save_block": { + await client.prisma.blockV2Config.upsert({ + where: { guildId_name: { guildId: message.guildId!, name: blockName } }, + update: { config: blockState }, + create: { + name: blockName, + config: blockState, + guild: { + connectOrCreate: { + where: { id: message.guildId! }, + create: { id: message.guildId!, name: message.guild!.name } + } + } + } + }); + await editorMessage.edit({ + components: [ + { + type: 17, + accent_color: blockState.color ?? null, + components: [ + { type: 10, content: `✅ Guardado: ${blockName}` }, + { type: 10, content: "Configuración guardada en la base de datos (JSON)." } + ] + } + ] + }); + collector.stop(); + return; + } + case "cancel_block": { + 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)] + }); + }); + 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, + 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.Danger, label: "🗑️ Eliminar", custom_id: "delete_cover" } + ] + } + ], + fetchReply: true + }); + + //@ts-ignore + const coverCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + max: 1, + time: 60000, + filter: (b: any) => b.user.id === message.author.id + }); + + 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: [] }); + + 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 + }); + + 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)] + }); + }); + } else if (b.customId === "delete_cover") { + blockState.coverImage = null; + await b.update({ content: "✅ Imagen de portada eliminada.", components: [] }); + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + } + 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 + }); + + //@ts-ignore + const sepCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + max: 1, + time: 60000, + filter: (b: any) => b.user.id === message.author.id + }); + + 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" + })); + + //@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)] + }); + }); + }); + } + break; + } + case "move_block": { + const options = blockState.components.map((c: any, idx: number) => ({ + label: + c.type === 10 + ? `Texto: ${c.content?.slice(0, 30) || "..."}` + : c.type === 14 + ? "Separador" + : c.type === 12 + ? `Imagen: ${c.url?.slice(-30) || "..."}` + : `Componente ${c.type}`, + value: idx.toString(), + description: + c.type === 10 && c.thumbnail + ? "Con thumbnail" + : undefined + })); + + //@ts-ignore + const reply = await i.followUp({ + ephemeral: true, + content: "Selecciona el bloque que quieres mover:", + components: [ + { + type: 1, + components: [ + { type: 3, custom_id: "move_block_select", placeholder: "Elige un bloque", 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]); + + await sel.update({ + content: "¿Quieres mover este bloque?", + components: [ + { + type: 1, + components: [ + { type: 2, style: ButtonStyle.Secondary, label: "⬆️ Subir", custom_id: `move_up_${idx}`, disabled: idx === 0 }, + { type: 2, style: ButtonStyle.Secondary, label: "⬇️ Bajar", custom_id: `move_down_${idx}`, disabled: idx === blockState.components.length - 1 } + ] + } + ] + }); + + //@ts-ignore + const btnCollector = reply.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("move_up_")) { + const i2 = parseInt(b.customId.replace("move_up_", "")); + if (i2 > 0) { + const item = blockState.components[i2]; + blockState.components.splice(i2, 1); + blockState.components.splice(i2 - 1, 0, item); + } + await b.update({ content: "✅ Bloque movido arriba.", components: [] }); + } else if (b.customId.startsWith("move_down_")) { + const i2 = parseInt(b.customId.replace("move_down_", "")); + if (i2 < blockState.components.length - 1) { + const item = blockState.components[i2]; + blockState.components.splice(i2, 1); + blockState.components.splice(i2 + 1, 0, item); + } + await b.update({ content: "✅ Bloque movido abajo.", components: [] }); + } + + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + + btnCollector.stop(); + selCollector.stop(); + }); + }); + + break; + } + case "delete_block": { + // Incluir portada en las opciones si existe + const options = []; + + // Añadir portada como opción si existe + if (blockState.coverImage) { + options.push({ + label: "🖼️ Imagen de Portada", + value: "cover_image", + description: "Imagen principal del bloque" + }); + } + + // Añadir componentes regulares + blockState.components.forEach((c: any, idx: number) => { + options.push({ + label: + c.type === 10 + ? `Texto: ${c.content?.slice(0, 30) || "..."}` + : c.type === 14 + ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` + : c.type === 12 + ? `Imagen: ${c.url?.slice(-30) || "..."}` + : `Componente ${c.type}`, + value: idx.toString(), + description: + c.type === 10 && c.thumbnail + ? "Con thumbnail" + : undefined + }); + }); + + if (options.length === 0) { + //@ts-ignore + await i.followUp({ + content: "❌ No hay elementos para eliminar.", + ephemeral: true + }); + break; + } + + //@ts-ignore + const reply = await i.followUp({ + ephemeral: true, + content: "Selecciona el elemento que quieres eliminar:", + components: [ + { + type: 1, + components: [ + { type: 3, custom_id: "delete_block_select", placeholder: "Elige un elemento", 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 selectedValue = sel.values[0]; + + if (selectedValue === "cover_image") { + blockState.coverImage = null; + await sel.update({ content: "✅ Imagen de portada eliminada.", components: [] }); + } else { + const idx = parseInt(selectedValue); + blockState.components.splice(idx, 1); + await sel.update({ content: "✅ Elemento eliminado.", components: [] }); + } + + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + + selCollector.stop(); + }); + + break; + } + default: + break; + } + + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + } + }); + + //@ts-ignore + collector.on("end", async (_, reason) => { + if (reason === "time") { + await editorMessage.edit({ + components: [ + { type: 17, components: [{ type: 10, content: "⏰ Editor finalizado por inactividad." }] } + ] + }); + } + }); + } +}; \ No newline at end of file diff --git a/src/commands/messages/alliaces/editEmbedv2.ts b/src/commands/messages/alliaces/editEmbedv2.ts new file mode 100644 index 0000000..091ea22 --- /dev/null +++ b/src/commands/messages/alliaces/editEmbedv2.ts @@ -0,0 +1,732 @@ +import { CommandMessage } from "../../../core/types/commands"; +// @ts-ignore +import { ComponentType, ButtonStyle } from "discord.js"; +import { replaceVars } from "../../../core/lib/vars"; + +/** + * Validar si una URL es válida + */ +const 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; + } +}; + +/** + * Botones de edición + */ +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" } + ] + }, + { + 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" } + ] + }, + { + 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" } + ] + } +]); + +/** + * Generar vista previa + */ +const renderPreview = async (blockState: any, member: any, guild: any) => { + const previewComponents = []; + + // Añadir imagen de portada primero si existe + if (blockState.coverImage && isValidUrl(blockState.coverImage)) { + //@ts-ignore + const processedCoverUrl = await replaceVars(blockState.coverImage, member, guild); + if (isValidUrl(processedCoverUrl)) { + previewComponents.push({ + type: 12, + items: [{ media: { url: processedCoverUrl } }] + }); + } + } + + // Añadir título después de la portada + previewComponents.push({ + type: 10, + //@ts-ignore + content: await replaceVars(blockState.title ?? "Sin título", member, guild) + }); + + // Procesar componentes en orden + for (const c of blockState.components) { + if (c.type === 10) { + // Componente de texto con thumbnail opcional + //@ts-ignore + const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, member, guild) : null; + + if (processedThumbnail && isValidUrl(processedThumbnail)) { + // Si tiene thumbnail válido, usar contenedor tipo 9 con accessory + previewComponents.push({ + type: 9, + components: [ + { + type: 10, + //@ts-ignore + content: await replaceVars(c.content || " ", member, guild) + } + ], + accessory: { + type: 11, + media: { url: processedThumbnail } + } + }); + } else { + // Sin thumbnail o thumbnail inválido, componente normal + previewComponents.push({ + type: 10, + //@ts-ignore + content: await replaceVars(c.content || " ", member, 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 + //@ts-ignore + const processedImageUrl = await replaceVars(c.url, member, guild); + + if (isValidUrl(processedImageUrl)) { + previewComponents.push({ + type: 12, + items: [{ media: { url: processedImageUrl } }] + }); + } + } + } + + return { + type: 17, + accent_color: blockState.color ?? null, + components: previewComponents + }; +}; + +export const command: CommandMessage = { + name: "blockedit", + type: "message", + cooldown: 20, + run: async (message, args, client) => { + if (!message.member?.permissions.has("Administrator")) { + return message.reply("❌ No tienes permisos de Administrador."); + } + + const blockName: string | null = args[0] ?? null; + if (!blockName) { + return message.reply("Debes proporcionar un nombre. Uso: `!blockedit `"); + } + + // Buscar el block existente + const existingBlock = await client.prisma.blockV2Config.findFirst({ + where: { guildId: message.guild!.id, name: blockName } + }); + + if (!existingBlock) { + return message.reply(`❌ No se encontró un block con el nombre: **${blockName}**`); + } + + // Cargar configuración existente + let blockState: any = { + ...existingBlock.config, + // Asegurar que las propiedades necesarias existan + title: existingBlock.config.title || `Block: ${blockName}`, + color: existingBlock.config.color || null, + coverImage: existingBlock.config.coverImage || null, + components: existingBlock.config.components || [] + }; + + //@ts-ignore + const editorMessage = await message.channel.send({ + flags: 32768, + components: [ + await renderPreview(blockState, message.member, message.guild), + ...btns(false) + ] + }); + + const collector = editorMessage.createMessageComponentCollector({ + time: 300000 + }); + + collector.on("collect", async (i) => { + if (i.user.id !== message.author.id) { + await i.reply({ content: "No puedes usar este menú.", ephemeral: true }); + return; + } + + // --- BOTONES --- + if (i.isButton()) { + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(true)] + }); + await i.deferUpdate(); + + switch (i.customId) { + case "save_block": { + await client.prisma.blockV2Config.update({ + where: { guildId_name: { guildId: message.guildId!, name: blockName } }, + data: { config: blockState } + }); + await editorMessage.edit({ + components: [ + { + type: 17, + accent_color: blockState.color ?? null, + components: [ + { type: 10, content: `✅ Block actualizado: ${blockName}` }, + { type: 10, content: "Configuración guardada exitosamente." } + ] + } + ] + }); + collector.stop(); + return; + } + case "cancel_block": { + await editorMessage.edit({ + components: [ + { + type: 17, + components: [ + { type: 10, content: "❌ Edición cancelada." }, + { type: 10, content: "No se guardaron los cambios." } + ] + } + ] + }); + 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)] + }); + }); + 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 + }); + + //@ts-ignore + const sepCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + max: 1, + time: 60000, + filter: (b: any) => b.user.id === message.author.id + }); + + 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 "cover_image": { + if (blockState.coverImage) { + // Si ya tiene portada, preguntar si editar o eliminar + //@ts-ignore + const reply = await i.followUp({ + ephemeral: true, + 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.Danger, label: "🗑️ Eliminar", custom_id: "delete_cover" } + ] + } + ], + fetchReply: true + }); + + //@ts-ignore + const coverCollector = reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + max: 1, + time: 60000, + filter: (b: any) => b.user.id === message.author.id + }); + + 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: [] }); + + 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 + }); + + 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)] + }); + }); + } else if (b.customId === "delete_cover") { + blockState.coverImage = null; + await b.update({ content: "✅ Imagen de portada eliminada.", components: [] }); + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + } + 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_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" + })); + + //@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)] + }); + }); + }); + } + break; + } + case "move_block": { + const options = blockState.components.map((c: any, idx: number) => ({ + label: + c.type === 10 + ? `Texto: ${c.content?.slice(0, 30) || "..."}` + : c.type === 14 + ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` + : c.type === 12 + ? `Imagen: ${c.url?.slice(-30) || "..."}` + : `Componente ${c.type}`, + value: idx.toString(), + description: + c.type === 10 && c.thumbnail + ? "Con thumbnail" + : undefined + })); + + //@ts-ignore + const reply = await i.followUp({ + ephemeral: true, + content: "Selecciona el bloque que quieres mover:", + components: [ + { + type: 1, + components: [ + { type: 3, custom_id: "move_block_select", placeholder: "Elige un bloque", 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]); + + await sel.update({ + content: "¿Quieres mover este bloque?", + components: [ + { + type: 1, + components: [ + { type: 2, style: ButtonStyle.Secondary, label: "⬆️ Subir", custom_id: `move_up_${idx}`, disabled: idx === 0 }, + { type: 2, style: ButtonStyle.Secondary, label: "⬇️ Bajar", custom_id: `move_down_${idx}`, disabled: idx === blockState.components.length - 1 } + ] + } + ] + }); + + //@ts-ignore + const btnCollector = reply.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("move_up_")) { + const i2 = parseInt(b.customId.replace("move_up_", "")); + if (i2 > 0) { + const item = blockState.components[i2]; + blockState.components.splice(i2, 1); + blockState.components.splice(i2 - 1, 0, item); + } + await b.update({ content: "✅ Bloque movido arriba.", components: [] }); + } else if (b.customId.startsWith("move_down_")) { + const i2 = parseInt(b.customId.replace("move_down_", "")); + if (i2 < blockState.components.length - 1) { + const item = blockState.components[i2]; + blockState.components.splice(i2, 1); + blockState.components.splice(i2 + 1, 0, item); + } + await b.update({ content: "✅ Bloque movido abajo.", components: [] }); + } + + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + + btnCollector.stop(); + selCollector.stop(); + }); + }); + + break; + } + case "delete_block": { + // Incluir portada en las opciones si existe + const options = []; + + // Añadir portada como opción si existe + if (blockState.coverImage) { + options.push({ + label: "🖼️ Imagen de Portada", + value: "cover_image", + description: "Imagen principal del bloque" + }); + } + + // Añadir componentes regulares + blockState.components.forEach((c: any, idx: number) => { + options.push({ + label: + c.type === 10 + ? `Texto: ${c.content?.slice(0, 30) || "..."}` + : c.type === 14 + ? `Separador ${c.divider ? '(Visible)' : '(Invisible)'}` + : c.type === 12 + ? `Imagen: ${c.url?.slice(-30) || "..."}` + : `Componente ${c.type}`, + value: idx.toString(), + description: + c.type === 10 && c.thumbnail + ? "Con thumbnail" + : undefined + }); + }); + + if (options.length === 0) { + //@ts-ignore + await i.followUp({ + content: "❌ No hay elementos para eliminar.", + ephemeral: true + }); + break; + } + + //@ts-ignore + const reply = await i.followUp({ + ephemeral: true, + content: "Selecciona el elemento que quieres eliminar:", + components: [ + { + type: 1, + components: [ + { type: 3, custom_id: "delete_block_select", placeholder: "Elige un elemento", 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 selectedValue = sel.values[0]; + + if (selectedValue === "cover_image") { + blockState.coverImage = null; + await sel.update({ content: "✅ Imagen de portada eliminada.", components: [] }); + } else { + const idx = parseInt(selectedValue); + blockState.components.splice(idx, 1); + await sel.update({ content: "✅ Elemento eliminado.", components: [] }); + } + + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + + selCollector.stop(); + }); + + break; + } + default: + break; + } + + await editorMessage.edit({ + components: [await renderPreview(blockState, message.member, message.guild), ...btns(false)] + }); + } + }); + + //@ts-ignore + collector.on("end", async (_, reason) => { + if (reason === "time") { + await editorMessage.edit({ + components: [ + { type: 17, components: [{ type: 10, content: "⏰ Editor finalizado por inactividad." }] } + ] + }); + } + }); + } +}; \ No newline at end of file diff --git a/src/commands/messages/alliaces/embedDelete.ts b/src/commands/messages/alliaces/embedDelete.ts index c033abd..772edf2 100644 --- a/src/commands/messages/alliaces/embedDelete.ts +++ b/src/commands/messages/alliaces/embedDelete.ts @@ -17,7 +17,7 @@ export const command: CommandMessage = { } try { - await client.prisma.embedConfig.delete({ + await client.prisma.blockV2Config.delete({ where: { guildId_name: { guildId: message.guildId!, diff --git a/src/commands/messages/alliaces/embedList.ts b/src/commands/messages/alliaces/embedList.ts index e4d13cb..4e5ef7a 100644 --- a/src/commands/messages/alliaces/embedList.ts +++ b/src/commands/messages/alliaces/embedList.ts @@ -24,7 +24,7 @@ export const command: CommandMessage = { return message.reply("❌ No tienes permisos de Administrador."); } - const embeds = await client.prisma.embedConfig.findMany({ + const embeds = await client.prisma.blockV2Config.findMany({ where: { guildId: message.guildId! }, }); diff --git a/src/core/lib/vars.ts b/src/core/lib/vars.ts index 7578e36..ab0cc69 100644 --- a/src/core/lib/vars.ts +++ b/src/core/lib/vars.ts @@ -1,12 +1,20 @@ import {Guild, User} from "discord.js"; -export async function replaceVars(text: string, user: User | undefined, guild: -Guild | undefined, stats: any) { - if(!text) return; +export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any): Promise { + if(!text) return ''; return text - .replace(/(user.name)/g, user!.username ?? '') - .replace(/(user.id)/g, user!.id ?? '') - .replace(/(user.mention)/g, `<@${user!.id}>`) - .replace(/(user.avatar)/g, user!.displayAvatarURL({ forceStatic: false })) + /** + * USER INFO + */ + .replace(/(user\.name)/g, user?.username ?? '') + .replace(/(user\.id)/g, user?.id ?? '') + .replace(/(user\.mention)/g, user ? `<@${user.id}>` : '') + .replace(/(user\.avatar)/g, user?.displayAvatarURL({ forceStatic: false }) ?? '') + + /** + * GUILD INFO + */ + .replace(/(guild\.name)/g, guild?.name ?? '') + .replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? ''); } \ No newline at end of file diff --git a/src/events/extras/alliace.ts b/src/events/extras/alliace.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index f64d63f..4311b64 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -6,8 +6,17 @@ import {commands} from "../core/loader"; bot.on(Events.MessageCreate, async (message) => { if (message.author.bot) return; - const server = await bot.prisma.guild.findFirst({ where: { id: message.guild!.id } }) || "!"; - const PREFIX = server.prefix + const server = await bot.prisma.guild.upsert({ + where: { + id: message.guildId + }, + create: { + id: message.guildId, + name: message.guild!.name + }, + update: {} + }) + const PREFIX = server.prefix || "!" if (!message.content.startsWith(PREFIX)) return; const [cmdName, ...args] = message.content.slice(PREFIX.length).trim().split(/\s+/);