Elimino los embedBuilder y los cambio por Components Message V2, algo tardado.

This commit is contained in:
2025-09-18 11:43:47 -05:00
parent 286b724eef
commit 58bb3844ad
15 changed files with 1730 additions and 137 deletions

Binary file not shown.

View File

@@ -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");

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"

View File

@@ -29,6 +29,7 @@ model Guild {
// ✅ CAMBIO: Ahora un Guild puede tener MÚLTIPLES configuraciones de embed.
embedConfigs EmbedConfig[]
BlockV2Config BlockV2Config[]
}
/*
* -----------------------------------------------------------------------------
@@ -131,3 +132,26 @@ model EmbedConfig {
// No puedes tener dos embeds llamados "alianza" en el mismo servidor.
@@unique([guildId, name])
}
/*
* -----------------------------------------------------------------------------
* 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])
}

View File

@@ -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),
});

View File

@@ -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",

View File

@@ -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 <nombre>`");
}
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." }] }
]
});
}
});
}
};

View File

@@ -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 <nombre>`");
}
// 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." }] }
]
});
}
});
}
};

View File

@@ -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!,

View File

@@ -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! },
});

View File

@@ -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<string> {
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 }) ?? '');
}

View File

View File

@@ -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+/);