2025-09-19 21:56:39 -05:00
|
|
|
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<any> {
|
|
|
|
|
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<any> {
|
|
|
|
|
try {
|
|
|
|
|
const previewComponents = [];
|
|
|
|
|
|
|
|
|
|
// Añadir imagen de portada primero si existe
|
|
|
|
|
if (config.coverImage && isValidUrl(config.coverImage)) {
|
2025-09-20 00:00:39 -05:00
|
|
|
// @ts-ignore
|
2025-09-19 21:56:39 -05:00
|
|
|
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,
|
2025-09-20 00:00:39 -05:00
|
|
|
// @ts-ignore
|
2025-09-19 21:56:39 -05:00
|
|
|
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
|
2025-09-20 00:00:39 -05:00
|
|
|
// @ts-ignore
|
2025-09-19 21:56:39 -05:00
|
|
|
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,
|
2025-09-20 00:00:39 -05:00
|
|
|
// @ts-ignore
|
2025-09-19 21:56:39 -05:00
|
|
|
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,
|
2025-09-20 00:00:39 -05:00
|
|
|
// @ts-ignore
|
2025-09-19 21:56:39 -05:00
|
|
|
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
|
2025-09-20 00:00:39 -05:00
|
|
|
// @ts-ignore
|
2025-09-19 21:56:39 -05:00
|
|
|
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<any> {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|