Arreglando errores garrafales como bugs a la hora de crear la alianza o errores del displayComponent.
This commit is contained in:
12
.idea/dataSources.xml
generated
Normal file
12
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="dev" uuid="ed4d8df0-abeb-4f93-a766-8c8fe5abee6c">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/prisma/dev.db</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
BIN
prisma/dev.db
Normal file
BIN
prisma/dev.db
Normal file
Binary file not shown.
76
prisma/migrations/20250918123305_blockv2/migration.sql
Normal file
76
prisma/migrations/20250918123305_blockv2/migration.sql
Normal file
@@ -0,0 +1,76 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Guild" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"prefix" TEXT NOT NULL DEFAULT '!'
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PartnershipStats" (
|
||||
"totalPoints" INTEGER NOT NULL DEFAULT 0,
|
||||
"weeklyPoints" INTEGER NOT NULL DEFAULT 0,
|
||||
"monthlyPoints" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastWeeklyReset" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastMonthlyReset" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" TEXT NOT NULL,
|
||||
"guildId" TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY ("userId", "guildId"),
|
||||
CONSTRAINT "PartnershipStats_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "PartnershipStats_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Alliance" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"messageId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
CONSTRAINT "Alliance_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "Alliance_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "EmbedConfig" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT,
|
||||
"title" TEXT,
|
||||
"url" TEXT,
|
||||
"authorName" TEXT,
|
||||
"authorIconURL" TEXT,
|
||||
"authorURL" TEXT,
|
||||
"description" TEXT,
|
||||
"thumbnailURL" TEXT,
|
||||
"imageURL" TEXT,
|
||||
"footerText" TEXT,
|
||||
"footerIconURL" TEXT,
|
||||
"fields" TEXT DEFAULT '[]',
|
||||
"guildId" TEXT NOT NULL,
|
||||
CONSTRAINT "EmbedConfig_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "BlockV2Config" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"config" JSONB NOT NULL,
|
||||
"guildId" TEXT NOT NULL,
|
||||
CONSTRAINT "BlockV2Config_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Alliance_messageId_key" ON "Alliance"("messageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "EmbedConfig_guildId_name_key" ON "EmbedConfig"("guildId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "BlockV2Config_guildId_name_key" ON "BlockV2Config"("guildId", "name");
|
||||
@@ -0,0 +1,31 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "AllianceChannel" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"channelId" TEXT NOT NULL,
|
||||
"blockConfigName" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"guildId" TEXT NOT NULL,
|
||||
CONSTRAINT "AllianceChannel_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PointHistory" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"points" INTEGER NOT NULL DEFAULT 1,
|
||||
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"messageId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"channelId" TEXT NOT NULL,
|
||||
CONSTRAINT "PointHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "PointHistory_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "PointHistory_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "AllianceChannel" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AllianceChannel_channelId_key" ON "AllianceChannel"("channelId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AllianceChannel_guildId_channelId_key" ON "AllianceChannel"("guildId", "channelId");
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
@@ -30,6 +30,9 @@ model Guild {
|
||||
// ✅ CAMBIO: Ahora un Guild puede tener MÚLTIPLES configuraciones de embed.
|
||||
embedConfigs EmbedConfig[]
|
||||
BlockV2Config BlockV2Config[]
|
||||
// ✅ NUEVAS RELACIONES
|
||||
allianceChannels AllianceChannel[]
|
||||
pointsHistory PointHistory[]
|
||||
}
|
||||
/*
|
||||
* -----------------------------------------------------------------------------
|
||||
@@ -43,6 +46,8 @@ model User {
|
||||
// Relaciones
|
||||
partnerStats PartnershipStats[]
|
||||
createdAlliances Alliance[]
|
||||
// ✅ NUEVA RELACIÓN
|
||||
pointsHistory PointHistory[]
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -96,6 +101,62 @@ model Alliance {
|
||||
creatorId String
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* -----------------------------------------------------------------------------
|
||||
* Modelo para Canales de Alianza
|
||||
* -----------------------------------------------------------------------------
|
||||
* Gestiona qué canales están configurados para otorgar puntos y qué bloque enviar
|
||||
*/
|
||||
model AllianceChannel {
|
||||
id String @id @default(cuid())
|
||||
channelId String @unique // ID del canal de Discord
|
||||
|
||||
// Configuración del canal
|
||||
blockConfigName String // Nombre del BlockV2Config a enviar
|
||||
isActive Boolean @default(true)
|
||||
|
||||
// Timestamps
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// --- Relaciones ---
|
||||
guild Guild @relation(fields: [guildId], references: [id])
|
||||
guildId String
|
||||
|
||||
// Historial de puntos otorgados en este canal
|
||||
pointsHistory PointHistory[]
|
||||
|
||||
// Un canal solo puede estar en un servidor
|
||||
@@unique([guildId, channelId])
|
||||
}
|
||||
|
||||
/*
|
||||
* -----------------------------------------------------------------------------
|
||||
* Modelo para Historial de Puntos
|
||||
* -----------------------------------------------------------------------------
|
||||
* Registra cada vez que un usuario gana puntos con fecha y hora
|
||||
*/
|
||||
model PointHistory {
|
||||
id String @id @default(cuid())
|
||||
|
||||
// Información del punto otorgado
|
||||
points Int @default(1)
|
||||
timestamp DateTime @default(now())
|
||||
messageId String // ID del mensaje que generó el punto
|
||||
|
||||
// --- Relaciones ---
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
|
||||
guild Guild @relation(fields: [guildId], references: [id])
|
||||
guildId String
|
||||
|
||||
allianceChannel AllianceChannel @relation(fields: [channelId], references: [id])
|
||||
channelId String
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* -----------------------------------------------------------------------------
|
||||
* Modelo para la Configuración del Embed
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
93
src/commands/messages/alliaces/setupChannel.ts
Normal file
93
src/commands/messages/alliaces/setupChannel.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { CommandMessage } from "../../../core/types/commands";
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: "setchannel-alliance",
|
||||
type: "message",
|
||||
aliases: ["alchannel", "channelally"],
|
||||
cooldown: 10,
|
||||
//@ts-ignore
|
||||
run: async (message, args, client) => {
|
||||
if (!message.member?.permissions.has("Administrator")) {
|
||||
return message.reply("❌ No tienes permisos de Administrador.");
|
||||
}
|
||||
|
||||
// Validar argumentos
|
||||
if (args.length < 2) {
|
||||
return message.reply("❌ Uso correcto: `!setchannel-alliance <#canal|ID> <blockConfigName>`");
|
||||
}
|
||||
|
||||
const channelInput = args[0];
|
||||
const blockConfigName = args[1];
|
||||
|
||||
// Extraer ID del canal
|
||||
let channelId: string;
|
||||
|
||||
// Si es una mención de canal (#canal)
|
||||
if (channelInput.startsWith('<#') && channelInput.endsWith('>')) {
|
||||
channelId = channelInput.slice(2, -1);
|
||||
}
|
||||
// Si es solo un ID
|
||||
else if (/^\d+$/.test(channelInput)) {
|
||||
channelId = channelInput;
|
||||
}
|
||||
else {
|
||||
return message.reply("❌ Formato de canal inválido. Usa `#canal` o el ID del canal.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Verificar que el canal existe en el servidor
|
||||
const channel = await message.guild?.channels.fetch(channelId);
|
||||
if (!channel) {
|
||||
return message.reply("❌ El canal especificado no existe en este servidor.");
|
||||
}
|
||||
|
||||
// Verificar que el canal es un canal de texto
|
||||
if (!channel.isTextBased()) {
|
||||
return message.reply("❌ El canal debe ser un canal de texto.");
|
||||
}
|
||||
|
||||
// Verificar que existe el blockConfig
|
||||
const blockConfig = await client.prisma.blockV2Config.findFirst({
|
||||
where: {
|
||||
guildId: message.guildId,
|
||||
name: blockConfigName
|
||||
}
|
||||
});
|
||||
|
||||
if (!blockConfig) {
|
||||
return message.reply(`❌ No se encontró el bloque de configuración \`${blockConfigName}\`. Asegúrate de que exista.`);
|
||||
}
|
||||
|
||||
// Configurar el canal de alianzas
|
||||
const allianceChannel = await client.prisma.allianceChannel.upsert({
|
||||
where: {
|
||||
guildId_channelId: {
|
||||
guildId: message.guildId,
|
||||
channelId: channelId
|
||||
}
|
||||
},
|
||||
create: {
|
||||
guildId: message.guildId,
|
||||
channelId: channelId,
|
||||
blockConfigName: blockConfigName,
|
||||
isActive: true
|
||||
},
|
||||
update: {
|
||||
blockConfigName: blockConfigName,
|
||||
isActive: true,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return message.reply(`✅ Canal de alianzas configurado correctamente!\n\n` +
|
||||
`**Canal:** <#${channelId}>\n` +
|
||||
`**Configuración:** \`${blockConfigName}\`\n` +
|
||||
`**Estado:** Activo\n\n` +
|
||||
`Los enlaces de Discord válidos en este canal ahora otorgarán puntos de alianza.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error configurando canal de alianzas:', error);
|
||||
return message.reply("❌ Ocurrió un error al configurar el canal de alianzas. Inténtalo de nuevo.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import {Guild, User} from "discord.js";
|
||||
import {Guild, Invite, User} from "discord.js";
|
||||
|
||||
export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any): Promise<string> {
|
||||
//@ts-ignore
|
||||
export async function replaceVars(text: string, user: User | undefined, guild: Guild | undefined, stats?: any, invite: Invite | undefined): Promise<string> {
|
||||
if(!text) return '';
|
||||
|
||||
// Crear inviteObject solo si invite existe y tiene guild
|
||||
const inviteObject = invite?.guild ? {
|
||||
guild: {
|
||||
//@ts-ignore
|
||||
icon: `https://cdn.discordapp.com/icons/${invite.guild.id}/${invite.guild.icon}.webp?size=256`
|
||||
}
|
||||
} : null;
|
||||
|
||||
return text
|
||||
/**
|
||||
* USER INFO
|
||||
@@ -12,9 +21,23 @@ export async function replaceVars(text: string, user: User | undefined, guild: G
|
||||
.replace(/(user\.mention)/g, user ? `<@${user.id}>` : '')
|
||||
.replace(/(user\.avatar)/g, user?.displayAvatarURL({ forceStatic: false }) ?? '')
|
||||
|
||||
/**
|
||||
* USER STATS
|
||||
*/
|
||||
.replace(/(user\.pointsAll)/g, stats?.totalPoints?.toString() ?? '0')
|
||||
.replace(/(user\.pointsWeekly)/g, stats?.weeklyPoints?.toString() ?? '0')
|
||||
.replace(/(user\.pointsMonthly)/g, stats?.monthlyPoints?.toString() ?? '0')
|
||||
|
||||
/**
|
||||
* GUILD INFO
|
||||
*/
|
||||
.replace(/(guild\.name)/g, guild?.name ?? '')
|
||||
.replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? '');
|
||||
.replace(/(guild\.icon)/g, guild?.iconURL({ forceStatic: false }) ?? '')
|
||||
|
||||
/**
|
||||
* INVITE INFO
|
||||
*/
|
||||
.replace(/(invite\.name)/g, invite?.guild?.name ?? "")
|
||||
.replace(/(invite\.icon)/g, inviteObject?.guild.icon ?? '0')
|
||||
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
import {
|
||||
Message
|
||||
} from "discord.js";
|
||||
// Se agrega ts
|
||||
//@ts-ignore
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { replaceVars } from "../../core/lib/vars";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Regex para detectar URLs válidas (corregido)
|
||||
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/gi;
|
||||
|
||||
// Dominios de Discord válidos para invitaciones
|
||||
const DISCORD_DOMAINS = [
|
||||
'discord.gg',
|
||||
'discord.com/invite',
|
||||
'discordapp.com/invite'
|
||||
];
|
||||
|
||||
export async function alliance(message: Message) {
|
||||
try {
|
||||
// Verificar que el mensaje tenga contenido
|
||||
if (!message.content || message.content.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar enlaces en el mensaje
|
||||
const links = extractValidLinks(message.content);
|
||||
|
||||
if (links.length === 0) {
|
||||
return; // No hay enlaces válidos
|
||||
}
|
||||
|
||||
// Verificar si el canal está configurado para alianzas
|
||||
const allianceChannel = await prisma.allianceChannel.findFirst({
|
||||
where: {
|
||||
guildId: message.guild!.id,
|
||||
channelId: message.channel.id,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!allianceChannel) {
|
||||
return; // Canal no configurado para alianzas
|
||||
}
|
||||
|
||||
// Verificar permisos del usuario (corregido para evitar errores con tipos de canal)
|
||||
const member = await message.guild!.members.fetch(message.author.id);
|
||||
|
||||
// Verificar que es un canal de texto antes de verificar permisos
|
||||
if (!message.channel.isTextBased()) {
|
||||
return; // No es un canal de texto
|
||||
}
|
||||
//@ts-ignore
|
||||
const permissions = message.channel.permissionsFor(member);
|
||||
if (!permissions?.has('SendMessages')) {
|
||||
return; // Usuario sin permisos
|
||||
}
|
||||
|
||||
// Validar que los enlaces sean de Discord (invitaciones)
|
||||
const validDiscordLinks = validateDiscordLinks(links);
|
||||
|
||||
if (validDiscordLinks.length === 0) {
|
||||
return; // No hay enlaces válidos de Discord
|
||||
}
|
||||
|
||||
// Procesar cada enlace válido
|
||||
for (const link of validDiscordLinks) {
|
||||
await processValidLink(message, allianceChannel, link);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error en función alliance:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function extractValidLinks(content: string): string[] {
|
||||
const matches = content.match(URL_REGEX);
|
||||
return matches || [];
|
||||
}
|
||||
|
||||
function validateDiscordLinks(links: string[]): string[] {
|
||||
return links.filter(link => {
|
||||
return DISCORD_DOMAINS.some(domain => link.includes(domain));
|
||||
});
|
||||
}
|
||||
|
||||
async function processValidLink(message: Message, allianceChannel: any, link: string) {
|
||||
try {
|
||||
// Verificar si el enlace de Discord es válido (opcional: hacer fetch)
|
||||
const inviteData = await validateDiscordInvite(link);
|
||||
|
||||
if (!inviteData) {
|
||||
return; // Enlace inválido o expirado
|
||||
}
|
||||
|
||||
// Asegurar que el usuario existe en la base de datos
|
||||
await prisma.user.upsert({
|
||||
where: { id: message.author.id },
|
||||
update: {},
|
||||
create: { id: message.author.id }
|
||||
});
|
||||
|
||||
// Asegurar que el guild existe en la base de datos
|
||||
await prisma.guild.upsert({
|
||||
where: { id: message.guild!.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: message.guild!.id,
|
||||
name: message.guild!.name
|
||||
}
|
||||
});
|
||||
|
||||
// Registrar el punto en el historial
|
||||
await prisma.pointHistory.create({
|
||||
data: {
|
||||
userId: message.author.id,
|
||||
guildId: message.guild!.id,
|
||||
channelId: allianceChannel.id,
|
||||
messageId: message.id,
|
||||
points: 1
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar estadísticas del usuario
|
||||
await updateUserStats(message.author.id, message.guild!.id);
|
||||
|
||||
// Obtener estadísticas para reemplazar variables
|
||||
const userStats = await getUserAllianceStats(message.author.id, message.guild!.id);
|
||||
|
||||
// Enviar el bloque configurado usando Display Components
|
||||
await sendBlockConfigV2(message, allianceChannel.blockConfigName, message.guild!.id, link, userStats, inviteData);
|
||||
|
||||
console.log(`✅ Punto otorgado a ${message.author.tag} por enlace válido: ${link}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error procesando enlace válido:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateDiscordInvite(link: string): Promise<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)) {
|
||||
const processedCoverUrl = await replaceVars(config.coverImage, user, guild);
|
||||
if (isValidUrl(processedCoverUrl)) {
|
||||
previewComponents.push({
|
||||
type: 12,
|
||||
items: [{ media: { url: processedCoverUrl } }]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Añadir título después de la portada
|
||||
if (config.title) {
|
||||
previewComponents.push({
|
||||
type: 10,
|
||||
content: await replaceVars(config.title, user, guild)
|
||||
});
|
||||
}
|
||||
|
||||
// Procesar componentes en orden (igual que el editor)
|
||||
if (config.components && Array.isArray(config.components)) {
|
||||
for (const c of config.components) {
|
||||
if (c.type === 10) {
|
||||
// Componente de texto con thumbnail opcional
|
||||
const processedThumbnail = c.thumbnail ? await replaceVars(c.thumbnail, user, guild) : null;
|
||||
|
||||
if (processedThumbnail && isValidUrl(processedThumbnail)) {
|
||||
// Si tiene thumbnail válido, usar contenedor tipo 9 con accessory
|
||||
previewComponents.push({
|
||||
type: 9,
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content: await replaceVars(c.content || " ", user, guild)
|
||||
}
|
||||
],
|
||||
accessory: {
|
||||
type: 11,
|
||||
media: { url: processedThumbnail }
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Sin thumbnail o thumbnail inválido, componente normal
|
||||
previewComponents.push({
|
||||
type: 10,
|
||||
content: await replaceVars(c.content || " ", user, guild)
|
||||
});
|
||||
}
|
||||
} else if (c.type === 14) {
|
||||
// Separador
|
||||
previewComponents.push({
|
||||
type: 14,
|
||||
divider: c.divider ?? true,
|
||||
spacing: c.spacing ?? 1
|
||||
});
|
||||
} else if (c.type === 12) {
|
||||
// Imagen - validar URL también
|
||||
const processedImageUrl = await replaceVars(c.url, user, guild);
|
||||
|
||||
if (isValidUrl(processedImageUrl)) {
|
||||
previewComponents.push({
|
||||
type: 12,
|
||||
items: [{ media: { url: processedImageUrl } }]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retornar la estructura exacta que usa el editor
|
||||
return {
|
||||
type: 17, // Container type
|
||||
accent_color: config.color ?? null,
|
||||
components: previewComponents
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error convirtiendo configuración a Display Component:', error);
|
||||
|
||||
// Fallback: crear un componente básico
|
||||
return {
|
||||
type: 17,
|
||||
accent_color: null,
|
||||
components: [
|
||||
{ type: 10, content: 'Error al procesar la configuración del bloque.' }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Función helper para validar URLs
|
||||
function isValidUrl(url: string): boolean {
|
||||
if (!url || typeof url !== 'string') return false;
|
||||
try {
|
||||
new URL(url);
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function processConfigVariables(config: any, user: any, guild: any, userStats?: any, inviteObject?: any): Promise<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
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2,10 +2,12 @@ import {bot} from "../main";
|
||||
import {Events} from "discord.js";
|
||||
import {redis} from "../core/redis";
|
||||
import {commands} from "../core/loader";
|
||||
import {alliance} from "./extras/alliace";
|
||||
|
||||
|
||||
bot.on(Events.MessageCreate, async (message) => {
|
||||
if (message.author.bot) return;
|
||||
await alliance(message);
|
||||
const server = await bot.prisma.guild.upsert({
|
||||
where: {
|
||||
id: message.guildId
|
||||
|
||||
Reference in New Issue
Block a user