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 0000000..8e532b6
Binary files /dev/null and b/prisma/dev.db differ
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