Merge pull request #25 from Shnimlz/feature/mi-cambio
Refactor y mejora de la función 'alliance' para validar enlaces de Di…
This commit is contained in:
@@ -1,27 +1,25 @@
|
|||||||
import {
|
import { Message } from "discord.js";
|
||||||
Message
|
|
||||||
} from "discord.js";
|
|
||||||
// Reemplaza instancia local -> usa singleton
|
// Reemplaza instancia local -> usa singleton
|
||||||
import { prisma } from "../../core/database/prisma";
|
import { prisma } from "../../core/database/prisma";
|
||||||
import { replaceVars } from "../../core/lib/vars";
|
import { replaceVars } from "../../core/lib/vars";
|
||||||
import logger from "../../core/lib/logger";
|
import logger from "../../core/lib/logger";
|
||||||
import { sendComponentsV2Message } from "../../core/api/discordAPI";
|
import { sendComponentsV2Message } from "../../core/api/discordAPI";
|
||||||
|
|
||||||
|
|
||||||
// Regex para detectar URLs válidas (corregido)
|
// 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;
|
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
|
// Dominios de Discord válidos para invitaciones
|
||||||
const DISCORD_DOMAINS = [
|
const DISCORD_DOMAINS = [
|
||||||
'discord.gg',
|
"discord.gg",
|
||||||
'discord.com/invite',
|
"discord.com/invite",
|
||||||
'discordapp.com/invite'
|
"discordapp.com/invite",
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function alliance(message: Message) {
|
export async function alliance(message: Message) {
|
||||||
try {
|
try {
|
||||||
// Verificar que el mensaje tenga contenido
|
// Verificar que el mensaje tenga contenido
|
||||||
if (!message.content || message.content.trim() === '') {
|
if (!message.content || message.content.trim() === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +35,8 @@ export async function alliance(message: Message) {
|
|||||||
where: {
|
where: {
|
||||||
guildId: message.guild!.id,
|
guildId: message.guild!.id,
|
||||||
channelId: message.channel.id,
|
channelId: message.channel.id,
|
||||||
isActive: true
|
isActive: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!allianceChannel) {
|
if (!allianceChannel) {
|
||||||
@@ -54,7 +52,7 @@ export async function alliance(message: Message) {
|
|||||||
}
|
}
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const permissions = message.channel.permissionsFor(member);
|
const permissions = message.channel.permissionsFor(member);
|
||||||
if (!permissions?.has('SendMessages')) {
|
if (!permissions?.has("SendMessages")) {
|
||||||
return; // Usuario sin permisos
|
return; // Usuario sin permisos
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,24 +67,66 @@ export async function alliance(message: Message) {
|
|||||||
for (const link of validDiscordLinks) {
|
for (const link of validDiscordLinks) {
|
||||||
await processValidLink(message, allianceChannel, link);
|
await processValidLink(message, allianceChannel, link);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error en función alliance');
|
logger.error({ err: error }, "Error en función alliance");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractValidLinks(content: string): string[] {
|
function extractValidLinks(content: string): string[] {
|
||||||
const matches = content.match(URL_REGEX);
|
const matches = content.match(URL_REGEX);
|
||||||
return matches || [];
|
if (!matches) return [];
|
||||||
}
|
|
||||||
|
|
||||||
|
const results: string[] = [];
|
||||||
|
for (const raw of matches) {
|
||||||
|
try {
|
||||||
|
const url = new URL(raw);
|
||||||
|
|
||||||
|
// Sólo http/https
|
||||||
|
if (url.protocol !== "http:" && url.protocol !== "https:") continue;
|
||||||
|
|
||||||
|
// Rechazar URLs con query string o fragment (ej: ?event=...)
|
||||||
|
if (url.search || url.hash) continue;
|
||||||
|
|
||||||
|
// Normalizar hostname y pathname
|
||||||
|
const host = url.hostname.toLowerCase();
|
||||||
|
const pathname = url.pathname.replace(/\/+/g, "/");
|
||||||
|
|
||||||
|
// Validar formatos de invitación de Discord estrictos
|
||||||
|
if (host === "discord.gg") {
|
||||||
|
// debe ser /<codigo>
|
||||||
|
if (!/^\/[A-Za-z0-9]+$/.test(pathname)) continue;
|
||||||
|
} else if (
|
||||||
|
host === "discord.com" ||
|
||||||
|
host === "www.discord.com" ||
|
||||||
|
host === "discordapp.com" ||
|
||||||
|
host === "www.discordapp.com"
|
||||||
|
) {
|
||||||
|
// debe ser /invite/<codigo>
|
||||||
|
if (!/^\/invite\/[A-Za-z0-9]+$/.test(pathname)) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si llegó hasta aquí, es aceptable
|
||||||
|
results.push(url.toString());
|
||||||
|
} catch {
|
||||||
|
// ignorar coincidencias inválidas
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
// ...existing code...
|
||||||
function validateDiscordLinks(links: string[]): string[] {
|
function validateDiscordLinks(links: string[]): string[] {
|
||||||
return links.filter(link => {
|
return links.filter((link) => {
|
||||||
return DISCORD_DOMAINS.some(domain => link.includes(domain));
|
return DISCORD_DOMAINS.some((domain) => link.includes(domain));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processValidLink(message: Message, allianceChannel: any, link: string) {
|
async function processValidLink(
|
||||||
|
message: Message,
|
||||||
|
allianceChannel: any,
|
||||||
|
link: string
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// Verificar si el enlace de Discord es válido (opcional: hacer fetch)
|
// Verificar si el enlace de Discord es válido (opcional: hacer fetch)
|
||||||
const inviteData = await validateDiscordInvite(link);
|
const inviteData = await validateDiscordInvite(link);
|
||||||
@@ -99,7 +139,7 @@ async function processValidLink(message: Message, allianceChannel: any, link: st
|
|||||||
await prisma.user.upsert({
|
await prisma.user.upsert({
|
||||||
where: { id: message.author.id },
|
where: { id: message.author.id },
|
||||||
update: {},
|
update: {},
|
||||||
create: { id: message.author.id }
|
create: { id: message.author.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Asegurar que el guild existe en la base de datos
|
// Asegurar que el guild existe en la base de datos
|
||||||
@@ -108,8 +148,8 @@ async function processValidLink(message: Message, allianceChannel: any, link: st
|
|||||||
update: {},
|
update: {},
|
||||||
create: {
|
create: {
|
||||||
id: message.guild!.id,
|
id: message.guild!.id,
|
||||||
name: message.guild!.name
|
name: message.guild!.name,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Registrar el punto en el historial
|
// Registrar el punto en el historial
|
||||||
@@ -119,23 +159,34 @@ async function processValidLink(message: Message, allianceChannel: any, link: st
|
|||||||
guildId: message.guild!.id,
|
guildId: message.guild!.id,
|
||||||
channelId: allianceChannel.id,
|
channelId: allianceChannel.id,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
points: 1
|
points: 1,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actualizar estadísticas del usuario
|
// Actualizar estadísticas del usuario
|
||||||
await updateUserStats(message.author.id, message.guild!.id);
|
await updateUserStats(message.author.id, message.guild!.id);
|
||||||
|
|
||||||
// Obtener estadísticas para reemplazar variables
|
// Obtener estadísticas para reemplazar variables
|
||||||
const userStats = await getUserAllianceStats(message.author.id, message.guild!.id);
|
const userStats = await getUserAllianceStats(
|
||||||
|
message.author.id,
|
||||||
|
message.guild!.id
|
||||||
|
);
|
||||||
|
|
||||||
// Enviar el bloque configurado usando Display Components
|
// Enviar el bloque configurado usando Display Components
|
||||||
await sendBlockConfigV2(message, allianceChannel.blockConfigName, message.guild!.id, link, userStats, inviteData);
|
await sendBlockConfigV2(
|
||||||
|
message,
|
||||||
logger.info(`✅ Punto otorgado a ${message.author.tag} por enlace válido: ${link}`);
|
allianceChannel.blockConfigName,
|
||||||
|
message.guild!.id,
|
||||||
|
link,
|
||||||
|
userStats,
|
||||||
|
inviteData
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`✅ Punto otorgado a ${message.author.tag} por enlace válido: ${link}`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error procesando enlace válido');
|
logger.error({ err: error }, "Error procesando enlace válido");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +197,16 @@ async function validateDiscordInvite(link: string): Promise<any> {
|
|||||||
if (!inviteCode) return null;
|
if (!inviteCode) return null;
|
||||||
|
|
||||||
// Hacer una solicitud a la API de Discord para validar la invitación
|
// 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`, {
|
const response = await fetch(
|
||||||
method: 'GET',
|
`https://discord.com/api/v10/invites/${inviteCode}?with_counts=true`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'DiscordBot (https://github.com/discord/discord-api-docs, 1.0)'
|
"User-Agent":
|
||||||
|
"DiscordBot (https://github.com/discord/discord-api-docs, 1.0)",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const inviteData = await response.json();
|
const inviteData = await response.json();
|
||||||
@@ -163,7 +218,7 @@ async function validateDiscordInvite(link: string): Promise<any> {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error validando invitación de Discord');
|
logger.error({ err: error }, "Error validando invitación de Discord");
|
||||||
return null; // En caso de error, considerar como inválido
|
return null; // En caso de error, considerar como inválido
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +228,7 @@ function extractInviteCode(link: string): string | null {
|
|||||||
const patterns = [
|
const patterns = [
|
||||||
/discord\.gg\/([a-zA-Z0-9]+)/,
|
/discord\.gg\/([a-zA-Z0-9]+)/,
|
||||||
/discord\.com\/invite\/([a-zA-Z0-9]+)/,
|
/discord\.com\/invite\/([a-zA-Z0-9]+)/,
|
||||||
/discordapp\.com\/invite\/([a-zA-Z0-9]+)/
|
/discordapp\.com\/invite\/([a-zA-Z0-9]+)/,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
@@ -193,8 +248,8 @@ async function updateUserStats(userId: string, guildId: string) {
|
|||||||
let userStats = await prisma.partnershipStats.findFirst({
|
let userStats = await prisma.partnershipStats.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
guildId: guildId
|
guildId: guildId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userStats) {
|
if (!userStats) {
|
||||||
@@ -206,18 +261,24 @@ async function updateUserStats(userId: string, guildId: string) {
|
|||||||
weeklyPoints: 1,
|
weeklyPoints: 1,
|
||||||
monthlyPoints: 1,
|
monthlyPoints: 1,
|
||||||
lastWeeklyReset: now,
|
lastWeeklyReset: now,
|
||||||
lastMonthlyReset: now
|
lastMonthlyReset: now,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar si necesita reset semanal (7 días)
|
// Verificar si necesita reset semanal (7 días)
|
||||||
const weeksPassed = Math.floor((now.getTime() - userStats.lastWeeklyReset.getTime()) / (7 * 24 * 60 * 60 * 1000));
|
const weeksPassed = Math.floor(
|
||||||
|
(now.getTime() - userStats.lastWeeklyReset.getTime()) /
|
||||||
|
(7 * 24 * 60 * 60 * 1000)
|
||||||
|
);
|
||||||
const needsWeeklyReset = weeksPassed >= 1;
|
const needsWeeklyReset = weeksPassed >= 1;
|
||||||
|
|
||||||
// Verificar si necesita reset mensual (30 días)
|
// Verificar si necesita reset mensual (30 días)
|
||||||
const daysPassed = Math.floor((now.getTime() - userStats.lastMonthlyReset.getTime()) / (24 * 60 * 60 * 1000));
|
const daysPassed = Math.floor(
|
||||||
|
(now.getTime() - userStats.lastMonthlyReset.getTime()) /
|
||||||
|
(24 * 60 * 60 * 1000)
|
||||||
|
);
|
||||||
const needsMonthlyReset = daysPassed >= 30;
|
const needsMonthlyReset = daysPassed >= 30;
|
||||||
|
|
||||||
// Actualizar estadísticas
|
// Actualizar estadísticas
|
||||||
@@ -225,39 +286,58 @@ async function updateUserStats(userId: string, guildId: string) {
|
|||||||
where: {
|
where: {
|
||||||
userId_guildId: {
|
userId_guildId: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
guildId: guildId
|
guildId: guildId,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
totalPoints: { increment: 1 },
|
totalPoints: { increment: 1 },
|
||||||
weeklyPoints: needsWeeklyReset ? 1 : { increment: 1 },
|
weeklyPoints: needsWeeklyReset ? 1 : { increment: 1 },
|
||||||
monthlyPoints: needsMonthlyReset ? 1 : { increment: 1 },
|
monthlyPoints: needsMonthlyReset ? 1 : { increment: 1 },
|
||||||
lastWeeklyReset: needsWeeklyReset ? now : userStats.lastWeeklyReset,
|
lastWeeklyReset: needsWeeklyReset ? now : userStats.lastWeeklyReset,
|
||||||
lastMonthlyReset: needsMonthlyReset ? now : userStats.lastMonthlyReset
|
lastMonthlyReset: needsMonthlyReset ? now : userStats.lastMonthlyReset,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendBlockConfigV2(message: Message, blockConfigName: string, guildId: string, validLink: string, userStats?: any, inviteObject?: any) {
|
async function sendBlockConfigV2(
|
||||||
|
message: Message,
|
||||||
|
blockConfigName: string,
|
||||||
|
guildId: string,
|
||||||
|
validLink: string,
|
||||||
|
userStats?: any,
|
||||||
|
inviteObject?: any
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
// Obtener la configuración del bloque
|
// Obtener la configuración del bloque
|
||||||
const blockConfig = await prisma.blockV2Config.findFirst({
|
const blockConfig = await prisma.blockV2Config.findFirst({
|
||||||
where: {
|
where: {
|
||||||
guildId: guildId,
|
guildId: guildId,
|
||||||
name: blockConfigName
|
name: blockConfigName,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!blockConfig) {
|
if (!blockConfig) {
|
||||||
logger.error(`❌ Bloque "${blockConfigName}" no encontrado para guild ${guildId}`);
|
logger.error(
|
||||||
|
`❌ Bloque "${blockConfigName}" no encontrado para guild ${guildId}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Procesar las variables en la configuración usando la función unificada
|
// Procesar las variables en la configuración usando la función unificada
|
||||||
const processedConfig = await processConfigVariables(blockConfig.config, message.author, message.guild!, userStats, inviteObject);
|
const processedConfig = await processConfigVariables(
|
||||||
|
blockConfig.config,
|
||||||
|
message.author,
|
||||||
|
message.guild!,
|
||||||
|
userStats,
|
||||||
|
inviteObject
|
||||||
|
);
|
||||||
|
|
||||||
// Convertir el JSON plano a la estructura de Display Components correcta
|
// Convertir el JSON plano a la estructura de Display Components correcta
|
||||||
const displayComponent = await convertConfigToDisplayComponent(processedConfig, message.author, message.guild!);
|
const displayComponent = await convertConfigToDisplayComponent(
|
||||||
|
processedConfig,
|
||||||
|
message.author,
|
||||||
|
message.guild!
|
||||||
|
);
|
||||||
|
|
||||||
// Construir adjuntos desde la config si existen
|
// Construir adjuntos desde la config si existen
|
||||||
const attachments = buildAttachmentsFromConfig(processedConfig);
|
const attachments = buildAttachmentsFromConfig(processedConfig);
|
||||||
@@ -268,33 +348,49 @@ async function sendBlockConfigV2(message: Message, blockConfigName: string, guil
|
|||||||
replyToMessageId: message.id,
|
replyToMessageId: message.id,
|
||||||
attachments: attachments.length ? attachments : undefined,
|
attachments: attachments.length ? attachments : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, '❌ Error enviando bloque de configuración V2');
|
logger.error(
|
||||||
|
{ err: error },
|
||||||
|
"❌ Error enviando bloque de configuración V2"
|
||||||
|
);
|
||||||
|
|
||||||
// Fallback: usar mensaje simple
|
// Fallback: usar mensaje simple
|
||||||
try {
|
try {
|
||||||
await message.reply({
|
await message.reply({
|
||||||
content: '✅ ¡Enlace de alianza procesado correctamente!'
|
content: "✅ ¡Enlace de alianza procesado correctamente!",
|
||||||
});
|
});
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
logger.error({ err: fallbackError }, '❌ Error en fallback');
|
logger.error({ err: fallbackError }, "❌ Error en fallback");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extrae adjuntos desde la config (base64) para usar attachment://<filename>
|
// Extrae adjuntos desde la config (base64) para usar attachment://<filename>
|
||||||
function buildAttachmentsFromConfig(config: any) {
|
function buildAttachmentsFromConfig(config: any) {
|
||||||
const results: { name: string; data: Buffer; description?: string; spoiler?: boolean }[] = [];
|
const results: {
|
||||||
if (!config || typeof config !== 'object') return results;
|
name: string;
|
||||||
|
data: Buffer;
|
||||||
|
description?: string;
|
||||||
|
spoiler?: boolean;
|
||||||
|
}[] = [];
|
||||||
|
if (!config || typeof config !== "object") return results;
|
||||||
|
|
||||||
const arr = Array.isArray(config.attachments) ? config.attachments : [];
|
const arr = Array.isArray(config.attachments) ? config.attachments : [];
|
||||||
for (const item of arr) {
|
for (const item of arr) {
|
||||||
if (!item || typeof item !== 'object') continue;
|
if (!item || typeof item !== "object") continue;
|
||||||
const name = typeof item.name === 'string' && item.name.trim() ? item.name.trim() : null;
|
const name =
|
||||||
const description = typeof item.description === 'string' ? item.description : undefined;
|
typeof item.name === "string" && item.name.trim()
|
||||||
|
? item.name.trim()
|
||||||
|
: null;
|
||||||
|
const description =
|
||||||
|
typeof item.description === "string" ? item.description : undefined;
|
||||||
const spoiler = Boolean(item.spoiler);
|
const spoiler = Boolean(item.spoiler);
|
||||||
const raw = typeof item.dataBase64 === 'string' ? item.dataBase64 : (typeof item.data === 'string' ? item.data : null);
|
const raw =
|
||||||
|
typeof item.dataBase64 === "string"
|
||||||
|
? item.dataBase64
|
||||||
|
: typeof item.data === "string"
|
||||||
|
? item.data
|
||||||
|
: null;
|
||||||
if (!name || !raw) continue;
|
if (!name || !raw) continue;
|
||||||
const buf = decodeBase64Payload(raw);
|
const buf = decodeBase64Payload(raw);
|
||||||
if (!buf) continue;
|
if (!buf) continue;
|
||||||
@@ -307,13 +403,13 @@ function decodeBase64Payload(raw: string): Buffer | null {
|
|||||||
try {
|
try {
|
||||||
let base64 = raw.trim();
|
let base64 = raw.trim();
|
||||||
// Soportar formatos: "base64:..." o data URLs "data:mime/type;base64,...."
|
// Soportar formatos: "base64:..." o data URLs "data:mime/type;base64,...."
|
||||||
if (base64.startsWith('base64:')) {
|
if (base64.startsWith("base64:")) {
|
||||||
base64 = base64.slice('base64:'.length);
|
base64 = base64.slice("base64:".length);
|
||||||
} else if (base64.startsWith('data:')) {
|
} else if (base64.startsWith("data:")) {
|
||||||
const comma = base64.indexOf(',');
|
const comma = base64.indexOf(",");
|
||||||
if (comma !== -1) base64 = base64.slice(comma + 1);
|
if (comma !== -1) base64 = base64.slice(comma + 1);
|
||||||
}
|
}
|
||||||
return Buffer.from(base64, 'base64');
|
return Buffer.from(base64, "base64");
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -321,10 +417,10 @@ function decodeBase64Payload(raw: string): Buffer | null {
|
|||||||
|
|
||||||
// Helper: URLs http/https únicamente
|
// Helper: URLs http/https únicamente
|
||||||
function isHttpUrl(url: unknown): url is string {
|
function isHttpUrl(url: unknown): url is string {
|
||||||
if (typeof url !== 'string' || !url) return false;
|
if (typeof url !== "string" || !url) return false;
|
||||||
try {
|
try {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
return u.protocol === 'http:' || u.protocol === 'https:';
|
return u.protocol === "http:" || u.protocol === "https:";
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -332,10 +428,10 @@ function isHttpUrl(url: unknown): url is string {
|
|||||||
|
|
||||||
// Helper: permitir http/https y attachment:// para medios (thumbnail/media/file)
|
// Helper: permitir http/https y attachment:// para medios (thumbnail/media/file)
|
||||||
function isMediaUrl(url: unknown): boolean {
|
function isMediaUrl(url: unknown): boolean {
|
||||||
if (typeof url !== 'string' || !url) return false;
|
if (typeof url !== "string" || !url) return false;
|
||||||
if (isHttpUrl(url)) return true;
|
if (isHttpUrl(url)) return true;
|
||||||
const s = url as string;
|
const s = url as string;
|
||||||
return s.startsWith('attachment://');
|
return s.startsWith("attachment://");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: construir accessory de Link Button para Display Components
|
// Helper: construir accessory de Link Button para Display Components
|
||||||
@@ -347,10 +443,10 @@ async function buildLinkAccessory(link: any, user: any, guild: any) {
|
|||||||
// En botones de enlace solo se permite http/https
|
// En botones de enlace solo se permite http/https
|
||||||
if (!isHttpUrl(processedUrl)) return null;
|
if (!isHttpUrl(processedUrl)) return null;
|
||||||
const accessory: any = { type: 2, style: 5, url: processedUrl };
|
const accessory: any = { type: 2, style: 5, url: processedUrl };
|
||||||
if (link.label && typeof link.label === 'string' && link.label.trim()) {
|
if (link.label && typeof link.label === "string" && link.label.trim()) {
|
||||||
accessory.label = link.label.trim().slice(0, 80);
|
accessory.label = link.label.trim().slice(0, 80);
|
||||||
}
|
}
|
||||||
if (link.emoji && typeof link.emoji === 'string') {
|
if (link.emoji && typeof link.emoji === "string") {
|
||||||
const parsed = parseEmojiInput(link.emoji);
|
const parsed = parseEmojiInput(link.emoji);
|
||||||
if (parsed) accessory.emoji = parsed;
|
if (parsed) accessory.emoji = parsed;
|
||||||
}
|
}
|
||||||
@@ -362,16 +458,27 @@ async function buildLinkAccessory(link: any, user: any, guild: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertConfigToDisplayComponent(config: any, user: any, guild: any): Promise<any> {
|
async function convertConfigToDisplayComponent(
|
||||||
|
config: any,
|
||||||
|
user: any,
|
||||||
|
guild: any
|
||||||
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const previewComponents: any[] = [];
|
const previewComponents: any[] = [];
|
||||||
|
|
||||||
// Añadir imagen de portada primero si existe
|
// Añadir imagen de portada primero si existe
|
||||||
if (config.coverImage) {
|
if (config.coverImage) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processedCoverUrl = await replaceVars(config.coverImage, user, guild);
|
const processedCoverUrl = await replaceVars(
|
||||||
|
config.coverImage,
|
||||||
|
user,
|
||||||
|
guild
|
||||||
|
);
|
||||||
if (isMediaUrl(processedCoverUrl)) {
|
if (isMediaUrl(processedCoverUrl)) {
|
||||||
previewComponents.push({ type: 12, items: [{ media: { url: processedCoverUrl } }] });
|
previewComponents.push({
|
||||||
|
type: 12,
|
||||||
|
items: [{ media: { url: processedCoverUrl } }],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +487,7 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an
|
|||||||
previewComponents.push({
|
previewComponents.push({
|
||||||
type: 10,
|
type: 10,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
content: await replaceVars(config.title, user, guild)
|
content: await replaceVars(config.title, user, guild),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,42 +497,75 @@ async function convertConfigToDisplayComponent(config: any, user: any, guild: an
|
|||||||
if (c.type === 10) {
|
if (c.type === 10) {
|
||||||
// Texto con accessory opcional: priorizar linkButton > thumbnail
|
// Texto con accessory opcional: priorizar linkButton > thumbnail
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processedContent = await replaceVars(c.content || " ", user, guild);
|
const processedContent = await replaceVars(
|
||||||
|
c.content || " ",
|
||||||
|
user,
|
||||||
|
guild
|
||||||
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, user, guild) : null;
|
const processedThumbnail = c.thumbnail
|
||||||
|
? await replaceVars(c.thumbnail, user, guild)
|
||||||
|
: null;
|
||||||
|
|
||||||
let accessory: any = null;
|
let accessory: any = null;
|
||||||
if (c.linkButton) {
|
if (c.linkButton) {
|
||||||
accessory = await buildLinkAccessory(c.linkButton, user, guild);
|
accessory = await buildLinkAccessory(c.linkButton, user, guild);
|
||||||
}
|
}
|
||||||
if (!accessory && processedThumbnail && isMediaUrl(processedThumbnail)) {
|
if (
|
||||||
|
!accessory &&
|
||||||
|
processedThumbnail &&
|
||||||
|
isMediaUrl(processedThumbnail)
|
||||||
|
) {
|
||||||
accessory = { type: 11, media: { url: processedThumbnail } };
|
accessory = { type: 11, media: { url: processedThumbnail } };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessory) {
|
if (accessory) {
|
||||||
previewComponents.push({ type: 9, components: [{ type: 10, content: processedContent }], accessory });
|
previewComponents.push({
|
||||||
|
type: 9,
|
||||||
|
components: [{ type: 10, content: processedContent }],
|
||||||
|
accessory,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
previewComponents.push({ type: 10, content: processedContent });
|
previewComponents.push({ type: 10, content: processedContent });
|
||||||
}
|
}
|
||||||
} else if (c.type === 14) {
|
} else if (c.type === 14) {
|
||||||
previewComponents.push({ type: 14, divider: c.divider ?? true, spacing: c.spacing ?? 1 });
|
previewComponents.push({
|
||||||
|
type: 14,
|
||||||
|
divider: c.divider ?? true,
|
||||||
|
spacing: c.spacing ?? 1,
|
||||||
|
});
|
||||||
} else if (c.type === 12) {
|
} else if (c.type === 12) {
|
||||||
// Imagen - validar http/https o attachment://
|
// Imagen - validar http/https o attachment://
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processedImageUrl = await replaceVars(c.url, user, guild);
|
const processedImageUrl = await replaceVars(c.url, user, guild);
|
||||||
if (isMediaUrl(processedImageUrl)) {
|
if (isMediaUrl(processedImageUrl)) {
|
||||||
previewComponents.push({ type: 12, items: [{ media: { url: processedImageUrl } }] });
|
previewComponents.push({
|
||||||
|
type: 12,
|
||||||
|
items: [{ media: { url: processedImageUrl } }],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retornar la estructura exacta que usa el editor
|
// Retornar la estructura exacta que usa el editor
|
||||||
return { type: 17, accent_color: config.color ?? null, components: previewComponents };
|
return {
|
||||||
|
type: 17,
|
||||||
|
accent_color: config.color ?? null,
|
||||||
|
components: previewComponents,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, 'Error convirtiendo configuración a Display Component');
|
logger.error(
|
||||||
return { type: 17, accent_color: null, components: [ { type: 10, content: 'Error al procesar la configuración del bloque.' } ] };
|
{ err: error },
|
||||||
|
"Error convirtiendo configuración a Display Component"
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
type: 17,
|
||||||
|
accent_color: null,
|
||||||
|
components: [
|
||||||
|
{ type: 10, content: "Error al procesar la configuración del bloque." },
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +576,7 @@ function parseEmojiInput(input?: string): any | null {
|
|||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
const match = trimmed.match(/^<(a?):(\w+):(\d+)>$/);
|
const match = trimmed.match(/^<(a?):(\w+):(\d+)>$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const animated = match[1] === 'a';
|
const animated = match[1] === "a";
|
||||||
const name = match[2];
|
const name = match[2];
|
||||||
const id = match[3];
|
const id = match[3];
|
||||||
return { id, name, animated };
|
return { id, name, animated };
|
||||||
@@ -447,25 +587,39 @@ function parseEmojiInput(input?: string): any | null {
|
|||||||
|
|
||||||
// Función helper para validar URLs (http/https y attachment:// para medios)
|
// Función helper para validar URLs (http/https y attachment:// para medios)
|
||||||
|
|
||||||
async function processConfigVariables(config: any, user: any, guild: any, userStats?: any, inviteObject?: any): Promise<any> {
|
async function processConfigVariables(
|
||||||
if (typeof config === 'string') {
|
config: any,
|
||||||
|
user: any,
|
||||||
|
guild: any,
|
||||||
|
userStats?: any,
|
||||||
|
inviteObject?: any
|
||||||
|
): Promise<any> {
|
||||||
|
if (typeof config === "string") {
|
||||||
// Usar la función unificada replaceVars con todos los parámetros
|
// Usar la función unificada replaceVars con todos los parámetros
|
||||||
return await replaceVars(config, user, guild, userStats, inviteObject);
|
return await replaceVars(config, user, guild, userStats, inviteObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(config)) {
|
if (Array.isArray(config)) {
|
||||||
const processedArray = [];
|
const processedArray: any[] = [];
|
||||||
for (const item of config) {
|
for (const item of config) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
processedArray.push(await processConfigVariables(item, user, guild, userStats, inviteObject));
|
processedArray.push(
|
||||||
|
await processConfigVariables(item, user, guild, userStats, inviteObject)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return processedArray;
|
return processedArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config && typeof config === 'object') {
|
if (config && typeof config === "object") {
|
||||||
const processedObject: any = {};
|
const processedObject: any = {};
|
||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
processedObject[key] = await processConfigVariables(value, user, guild, userStats, inviteObject);
|
processedObject[key] = await processConfigVariables(
|
||||||
|
value,
|
||||||
|
user,
|
||||||
|
guild,
|
||||||
|
userStats,
|
||||||
|
inviteObject
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return processedObject;
|
return processedObject;
|
||||||
}
|
}
|
||||||
@@ -473,13 +627,12 @@ async function processConfigVariables(config: any, user: any, guild: any, userSt
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Función auxiliar para obtener estadísticas
|
// Función auxiliar para obtener estadísticas
|
||||||
export async function getUserAllianceStats(userId: string, guildId: string) {
|
export async function getUserAllianceStats(userId: string, guildId: string) {
|
||||||
return prisma.partnershipStats.findFirst({
|
return prisma.partnershipStats.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
guildId: guildId
|
guildId: guildId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user