feat: introduce Appwrite guild cache support and refactor guild config caching
This commit is contained in:
@@ -21,10 +21,14 @@ ENABLE_MEMORY_OPTIMIZER=false
|
|||||||
REDIS_URL=
|
REDIS_URL=
|
||||||
REDIS_PASS=
|
REDIS_PASS=
|
||||||
|
|
||||||
# Appwrite (for reminders)
|
# Appwrite (for reminders, AI conversations, and guild cache)
|
||||||
APPWRITE_ENDPOINT=
|
APPWRITE_ENDPOINT=
|
||||||
APPWRITE_PROJECT_ID=
|
APPWRITE_PROJECT_ID=
|
||||||
APPWRITE_API_KEY=
|
APPWRITE_API_KEY=
|
||||||
|
APPWRITE_DATABASE_ID=
|
||||||
|
APPWRITE_COLLECTION_REMINDERS_ID=
|
||||||
|
APPWRITE_COLLECTION_AI_CONVERSATIONS_ID=
|
||||||
|
APPWRITE_COLLECTION_GUILD_CACHE_ID=
|
||||||
|
|
||||||
# Reminders
|
# Reminders
|
||||||
REMINDERS_POLL_INTERVAL_SECONDS=30
|
REMINDERS_POLL_INTERVAL_SECONDS=30
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npx tsx watch src/main.ts",
|
"start": "npx tsx watch src/main.ts",
|
||||||
|
"script:guild": "node scripts/setupGuildCacheCollection.js",
|
||||||
"dev": "npx tsx watch src/main.ts",
|
"dev": "npx tsx watch src/main.ts",
|
||||||
"dev:light": "CACHE_MESSAGES_LIMIT=25 CACHE_MEMBERS_LIMIT=50 SWEEP_MESSAGES_LIFETIME_SECONDS=600 SWEEP_MESSAGES_INTERVAL_SECONDS=240 npx tsx watch --clear-screen=false src/main.ts",
|
"dev:light": "CACHE_MESSAGES_LIMIT=25 CACHE_MEMBERS_LIMIT=50 SWEEP_MESSAGES_LIFETIME_SECONDS=600 SWEEP_MESSAGES_INTERVAL_SECONDS=240 npx tsx watch --clear-screen=false src/main.ts",
|
||||||
"dev:mem": "MEMORY_LOG_INTERVAL_SECONDS=120 npx tsx watch src/main.ts",
|
"dev:mem": "MEMORY_LOG_INTERVAL_SECONDS=120 npx tsx watch src/main.ts",
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
// Simple Appwrite client wrapper
|
// Simple Appwrite client wrapper
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Client, Databases } from 'node-appwrite';
|
import { Client, Databases } from "node-appwrite";
|
||||||
|
|
||||||
const endpoint = process.env.APPWRITE_ENDPOINT || '';
|
const endpoint = process.env.APPWRITE_ENDPOINT || "";
|
||||||
const projectId = process.env.APPWRITE_PROJECT_ID || '';
|
const projectId = process.env.APPWRITE_PROJECT_ID || "";
|
||||||
const apiKey = process.env.APPWRITE_API_KEY || '';
|
const apiKey = process.env.APPWRITE_API_KEY || "";
|
||||||
|
|
||||||
export const APPWRITE_DATABASE_ID = process.env.APPWRITE_DATABASE_ID || '';
|
export const APPWRITE_DATABASE_ID = process.env.APPWRITE_DATABASE_ID || "";
|
||||||
export const APPWRITE_COLLECTION_REMINDERS_ID = process.env.APPWRITE_COLLECTION_REMINDERS_ID || '';
|
export const APPWRITE_COLLECTION_REMINDERS_ID =
|
||||||
export const APPWRITE_COLLECTION_AI_CONVERSATIONS_ID = process.env.APPWRITE_COLLECTION_AI_CONVERSATIONS_ID || '';
|
process.env.APPWRITE_COLLECTION_REMINDERS_ID || "";
|
||||||
|
export const APPWRITE_COLLECTION_AI_CONVERSATIONS_ID =
|
||||||
|
process.env.APPWRITE_COLLECTION_AI_CONVERSATIONS_ID || "";
|
||||||
|
export const APPWRITE_COLLECTION_GUILD_CACHE_ID =
|
||||||
|
process.env.APPWRITE_COLLECTION_GUILD_CACHE_ID || "";
|
||||||
|
|
||||||
let client: Client | null = null;
|
let client: Client | null = null;
|
||||||
let databases: Databases | null = null;
|
let databases: Databases | null = null;
|
||||||
@@ -16,7 +20,10 @@ let databases: Databases | null = null;
|
|||||||
function ensureClient() {
|
function ensureClient() {
|
||||||
if (!endpoint || !projectId || !apiKey) return null;
|
if (!endpoint || !projectId || !apiKey) return null;
|
||||||
if (client) return client;
|
if (client) return client;
|
||||||
client = new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey);
|
client = new Client()
|
||||||
|
.setEndpoint(endpoint)
|
||||||
|
.setProject(projectId)
|
||||||
|
.setKey(apiKey);
|
||||||
databases = new Databases(client);
|
databases = new Databases(client);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
@@ -26,9 +33,31 @@ export function getDatabases(): Databases | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isAppwriteConfigured(): boolean {
|
export function isAppwriteConfigured(): boolean {
|
||||||
return Boolean(endpoint && projectId && apiKey && APPWRITE_DATABASE_ID && APPWRITE_COLLECTION_REMINDERS_ID);
|
return Boolean(
|
||||||
|
endpoint &&
|
||||||
|
projectId &&
|
||||||
|
apiKey &&
|
||||||
|
APPWRITE_DATABASE_ID &&
|
||||||
|
APPWRITE_COLLECTION_REMINDERS_ID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAIConversationsConfigured(): boolean {
|
export function isAIConversationsConfigured(): boolean {
|
||||||
return Boolean(endpoint && projectId && apiKey && APPWRITE_DATABASE_ID && APPWRITE_COLLECTION_AI_CONVERSATIONS_ID);
|
return Boolean(
|
||||||
|
endpoint &&
|
||||||
|
projectId &&
|
||||||
|
apiKey &&
|
||||||
|
APPWRITE_DATABASE_ID &&
|
||||||
|
APPWRITE_COLLECTION_AI_CONVERSATIONS_ID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGuildCacheConfigured(): boolean {
|
||||||
|
return Boolean(
|
||||||
|
endpoint &&
|
||||||
|
projectId &&
|
||||||
|
apiKey &&
|
||||||
|
APPWRITE_DATABASE_ID &&
|
||||||
|
APPWRITE_COLLECTION_GUILD_CACHE_ID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,238 +1,280 @@
|
|||||||
import {bot} from "../main";
|
import { bot } from "../main";
|
||||||
import {Events} from "discord.js";
|
import { Events } from "discord.js";
|
||||||
import {redis} from "../core/database/redis";
|
import { redis } from "../core/database/redis";
|
||||||
import {commands} from "../core/loaders/loader";
|
import { commands } from "../core/loaders/loader";
|
||||||
import {alliance} from "./extras/alliace";
|
import { alliance } from "./extras/alliace";
|
||||||
import logger from "../core/lib/logger";
|
import logger from "../core/lib/logger";
|
||||||
import { aiService } from "../core/services/AIService";
|
import { aiService } from "../core/services/AIService";
|
||||||
|
import { getGuildConfig } from "../core/database/guildCache";
|
||||||
|
|
||||||
// Función para manejar respuestas automáticas a la AI
|
// Función para manejar respuestas automáticas a la AI
|
||||||
async function handleAIReply(message: any) {
|
async function handleAIReply(message: any) {
|
||||||
// Verificar si es una respuesta a un mensaje del bot
|
// Verificar si es una respuesta a un mensaje del bot
|
||||||
if (!message.reference?.messageId || message.author.bot) return;
|
if (!message.reference?.messageId || message.author.bot) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const referencedMessage = await message.channel.messages.fetch(
|
||||||
|
message.reference.messageId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verificar si el mensaje referenciado es del bot
|
||||||
|
if (referencedMessage.author.id !== message.client.user?.id) return;
|
||||||
|
|
||||||
|
// Verificar que el contenido no sea un comando (para evitar loops)
|
||||||
|
const guildConfig = await getGuildConfig(
|
||||||
|
message.guildId || message.guild!.id,
|
||||||
|
message.guild!.name,
|
||||||
|
bot.prisma
|
||||||
|
);
|
||||||
|
const PREFIX = guildConfig.prefix || "!";
|
||||||
|
|
||||||
|
if (message.content.startsWith(PREFIX)) return;
|
||||||
|
|
||||||
|
// Verificar que el mensaje tenga contenido válido
|
||||||
|
if (!message.content || message.content.trim().length === 0) return;
|
||||||
|
|
||||||
|
// Limitar longitud del mensaje
|
||||||
|
if (message.content.length > 4000) {
|
||||||
|
await message.reply(
|
||||||
|
"❌ **Error:** Tu mensaje es demasiado largo (máximo 4000 caracteres)."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Respuesta automática a AI detectada - Usuario: ${message.author.id}, Guild: ${message.guildId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Indicador de que está escribiendo
|
||||||
|
const typingInterval = setInterval(() => {
|
||||||
|
message.channel.sendTyping().catch(() => {});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const referencedMessage = await message.channel.messages.fetch(message.reference.messageId);
|
// Obtener emojis personalizados del servidor
|
||||||
|
const emojiResult = {
|
||||||
// Verificar si el mensaje referenciado es del bot
|
names: [] as string[],
|
||||||
if (referencedMessage.author.id !== message.client.user?.id) return;
|
map: {} as Record<string, string>,
|
||||||
|
};
|
||||||
// Verificar que el contenido no sea un comando (para evitar loops)
|
try {
|
||||||
const server = await bot.prisma.guild.findUnique({
|
const guild = message.guild;
|
||||||
where: { id: message.guildId || undefined }
|
if (guild) {
|
||||||
});
|
const emojis = await guild.emojis.fetch();
|
||||||
const PREFIX = server?.prefix || "!";
|
const list = Array.from(emojis.values());
|
||||||
|
for (const e of list) {
|
||||||
if (message.content.startsWith(PREFIX)) return;
|
// @ts-ignore
|
||||||
|
const name = e.name;
|
||||||
// Verificar que el mensaje tenga contenido válido
|
// @ts-ignore
|
||||||
if (!message.content || message.content.trim().length === 0) return;
|
const id = e.id;
|
||||||
|
if (!name || !id) continue;
|
||||||
// Limitar longitud del mensaje
|
// @ts-ignore
|
||||||
if (message.content.length > 4000) {
|
const tag = e.animated ? `<a:${name}:${id}>` : `<:${name}:${id}>`;
|
||||||
await message.reply('❌ **Error:** Tu mensaje es demasiado largo (máximo 4000 caracteres).');
|
if (!(name in emojiResult.map)) {
|
||||||
return;
|
emojiResult.map[name] = tag;
|
||||||
|
emojiResult.names.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emojiResult.names = emojiResult.names.slice(0, 25);
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignorar errores de emojis
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`Respuesta automática a AI detectada - Usuario: ${message.author.id}, Guild: ${message.guildId}`);
|
// Construir metadatos del mensaje
|
||||||
|
const buildMessageMeta = (msg: any, emojiNames?: string[]): string => {
|
||||||
// Indicador de que está escribiendo
|
|
||||||
const typingInterval = setInterval(() => {
|
|
||||||
message.channel.sendTyping().catch(() => {});
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Obtener emojis personalizados del servidor
|
const parts: string[] = [];
|
||||||
const emojiResult = { names: [] as string[], map: {} as Record<string, string> };
|
|
||||||
try {
|
|
||||||
const guild = message.guild;
|
|
||||||
if (guild) {
|
|
||||||
const emojis = await guild.emojis.fetch();
|
|
||||||
const list = Array.from(emojis.values());
|
|
||||||
for (const e of list) {
|
|
||||||
// @ts-ignore
|
|
||||||
const name = e.name;
|
|
||||||
// @ts-ignore
|
|
||||||
const id = e.id;
|
|
||||||
if (!name || !id) continue;
|
|
||||||
// @ts-ignore
|
|
||||||
const tag = e.animated ? `<a:${name}:${id}>` : `<:${name}:${id}>`;
|
|
||||||
if (!(name in emojiResult.map)) {
|
|
||||||
emojiResult.map[name] = tag;
|
|
||||||
emojiResult.names.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emojiResult.names = emojiResult.names.slice(0, 25);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignorar errores de emojis
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construir metadatos del mensaje
|
if (msg.channel?.name) {
|
||||||
const buildMessageMeta = (msg: any, emojiNames?: string[]): string => {
|
parts.push(`Canal: #${msg.channel.name}`);
|
||||||
try {
|
}
|
||||||
const parts: string[] = [];
|
|
||||||
|
|
||||||
if (msg.channel?.name) {
|
const userMentions = msg.mentions?.users
|
||||||
parts.push(`Canal: #${msg.channel.name}`);
|
? Array.from(msg.mentions.users.values())
|
||||||
}
|
: [];
|
||||||
|
const roleMentions = msg.mentions?.roles
|
||||||
|
? Array.from(msg.mentions.roles.values())
|
||||||
|
: [];
|
||||||
|
|
||||||
const userMentions = msg.mentions?.users ? Array.from(msg.mentions.users.values()) : [];
|
if (userMentions.length) {
|
||||||
const roleMentions = msg.mentions?.roles ? Array.from(msg.mentions.roles.values()) : [];
|
parts.push(
|
||||||
|
`Menciones usuario: ${userMentions
|
||||||
if (userMentions.length) {
|
.slice(0, 5)
|
||||||
parts.push(`Menciones usuario: ${userMentions.slice(0, 5).map((u: any) => u.username ?? u.tag ?? u.id).join(', ')}`);
|
.map((u: any) => u.username ?? u.tag ?? u.id)
|
||||||
}
|
.join(", ")}`
|
||||||
if (roleMentions.length) {
|
|
||||||
parts.push(`Menciones rol: ${roleMentions.slice(0, 5).map((r: any) => r.name ?? r.id).join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.reference?.messageId) {
|
|
||||||
parts.push('Es una respuesta a mensaje de AI');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emojiNames && emojiNames.length) {
|
|
||||||
parts.push(`Emojis personalizados disponibles (usa :nombre:): ${emojiNames.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metaRaw = parts.join(' | ');
|
|
||||||
return metaRaw.length > 800 ? metaRaw.slice(0, 800) : metaRaw;
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const messageMeta = buildMessageMeta(message, emojiResult.names);
|
|
||||||
|
|
||||||
// Verificar si hay imágenes adjuntas
|
|
||||||
const attachments = Array.from(message.attachments.values());
|
|
||||||
const hasImages = attachments.length > 0 && aiService.hasImageAttachments(attachments);
|
|
||||||
|
|
||||||
// Procesar con el servicio de AI usando memoria persistente y soporte para imágenes
|
|
||||||
const aiResponse = await aiService.processAIRequestWithMemory(
|
|
||||||
message.author.id,
|
|
||||||
message.content,
|
|
||||||
message.guildId,
|
|
||||||
message.channel.id,
|
|
||||||
message.id,
|
|
||||||
message.reference.messageId,
|
|
||||||
message.client,
|
|
||||||
'normal',
|
|
||||||
{
|
|
||||||
meta: messageMeta + (hasImages ? ` | Tiene ${attachments.length} imagen(es) adjunta(s)` : ''),
|
|
||||||
attachments: hasImages ? attachments : undefined
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
if (roleMentions.length) {
|
||||||
|
parts.push(
|
||||||
|
`Menciones rol: ${roleMentions
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((r: any) => r.name ?? r.id)
|
||||||
|
.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Reemplazar emojis personalizados
|
if (msg.reference?.messageId) {
|
||||||
let finalResponse = aiResponse;
|
parts.push("Es una respuesta a mensaje de AI");
|
||||||
if (emojiResult.names.length > 0) {
|
}
|
||||||
finalResponse = finalResponse.replace(/:([a-zA-Z0-9_]{2,32}):/g, (match, p1: string) => {
|
|
||||||
const found = emojiResult.map[p1];
|
if (emojiNames && emojiNames.length) {
|
||||||
return found ? found : match;
|
parts.push(
|
||||||
});
|
`Emojis personalizados disponibles (usa :nombre:): ${emojiNames.join(
|
||||||
|
", "
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaRaw = parts.join(" | ");
|
||||||
|
return metaRaw.length > 800 ? metaRaw.slice(0, 800) : metaRaw;
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageMeta = buildMessageMeta(message, emojiResult.names);
|
||||||
|
|
||||||
|
// Verificar si hay imágenes adjuntas
|
||||||
|
const attachments = Array.from(message.attachments.values());
|
||||||
|
const hasImages =
|
||||||
|
attachments.length > 0 && aiService.hasImageAttachments(attachments);
|
||||||
|
|
||||||
|
// Procesar con el servicio de AI usando memoria persistente y soporte para imágenes
|
||||||
|
const aiResponse = await aiService.processAIRequestWithMemory(
|
||||||
|
message.author.id,
|
||||||
|
message.content,
|
||||||
|
message.guildId,
|
||||||
|
message.channel.id,
|
||||||
|
message.id,
|
||||||
|
message.reference.messageId,
|
||||||
|
message.client,
|
||||||
|
"normal",
|
||||||
|
{
|
||||||
|
meta:
|
||||||
|
messageMeta +
|
||||||
|
(hasImages
|
||||||
|
? ` | Tiene ${attachments.length} imagen(es) adjunta(s)`
|
||||||
|
: ""),
|
||||||
|
attachments: hasImages ? attachments : undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reemplazar emojis personalizados
|
||||||
|
let finalResponse = aiResponse;
|
||||||
|
if (emojiResult.names.length > 0) {
|
||||||
|
finalResponse = finalResponse.replace(
|
||||||
|
/:([a-zA-Z0-9_]{2,32}):/g,
|
||||||
|
(match, p1: string) => {
|
||||||
|
const found = emojiResult.map[p1];
|
||||||
|
return found ? found : match;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar respuesta (dividir si es muy larga)
|
||||||
|
const MAX_CONTENT = 2000;
|
||||||
|
if (finalResponse.length > MAX_CONTENT) {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
let currentChunk = "";
|
||||||
|
const lines = finalResponse.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (currentChunk.length + line.length + 1 > MAX_CONTENT) {
|
||||||
|
if (currentChunk) {
|
||||||
|
chunks.push(currentChunk.trim());
|
||||||
|
currentChunk = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Enviar respuesta (dividir si es muy larga)
|
currentChunk += (currentChunk ? "\n" : "") + line;
|
||||||
const MAX_CONTENT = 2000;
|
|
||||||
if (finalResponse.length > MAX_CONTENT) {
|
|
||||||
const chunks: string[] = [];
|
|
||||||
let currentChunk = '';
|
|
||||||
const lines = finalResponse.split('\n');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (currentChunk.length + line.length + 1 > MAX_CONTENT) {
|
|
||||||
if (currentChunk) {
|
|
||||||
chunks.push(currentChunk.trim());
|
|
||||||
currentChunk = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentChunk += (currentChunk ? '\n' : '') + line;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentChunk) {
|
|
||||||
chunks.push(currentChunk.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < chunks.length && i < 3; i++) {
|
|
||||||
if (i === 0) {
|
|
||||||
await message.reply({ content: chunks[i] });
|
|
||||||
} else {
|
|
||||||
if ('send' in message.channel) {
|
|
||||||
await message.channel.send({ content: chunks[i] });
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunks.length > 3) {
|
|
||||||
if ('send' in message.channel) {
|
|
||||||
await message.channel.send({ content: "⚠️ Respuesta truncada por longitud." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await message.reply({ content: finalResponse });
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
logger.error(`Error en respuesta automática AI:`, error);
|
|
||||||
await message.reply({
|
|
||||||
content: `❌ **Error:** ${error.message || 'No pude procesar tu respuesta. Intenta de nuevo.'}`
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
clearInterval(typingInterval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
if (currentChunk) {
|
||||||
// Mensaje referenciado no encontrado o error, ignorar silenciosamente
|
chunks.push(currentChunk.trim());
|
||||||
logger.debug(`Error obteniendo mensaje referenciado: ${error}`);
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks.length && i < 3; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
await message.reply({ content: chunks[i] });
|
||||||
|
} else {
|
||||||
|
if ("send" in message.channel) {
|
||||||
|
await message.channel.send({ content: chunks[i] });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunks.length > 3) {
|
||||||
|
if ("send" in message.channel) {
|
||||||
|
await message.channel.send({
|
||||||
|
content: "⚠️ Respuesta truncada por longitud.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await message.reply({ content: finalResponse });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Error en respuesta automática AI:`, error);
|
||||||
|
await message.reply({
|
||||||
|
content: `❌ **Error:** ${
|
||||||
|
error.message || "No pude procesar tu respuesta. Intenta de nuevo."
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearInterval(typingInterval);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Mensaje referenciado no encontrado o error, ignorar silenciosamente
|
||||||
|
logger.debug(`Error obteniendo mensaje referenciado: ${error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.on(Events.MessageCreate, async (message) => {
|
bot.on(Events.MessageCreate, async (message) => {
|
||||||
if (message.author.bot) return;
|
if (message.author.bot) return;
|
||||||
|
|
||||||
// Manejar respuestas automáticas a la AI
|
// Manejar respuestas automáticas a la AI
|
||||||
await handleAIReply(message);
|
await handleAIReply(message);
|
||||||
|
|
||||||
await alliance(message);
|
await alliance(message);
|
||||||
const server = await bot.prisma.guild.upsert({
|
|
||||||
where: {
|
|
||||||
id: message.guildId || undefined
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: message!.guildId || message.guild!.id,
|
|
||||||
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+/);
|
// Usar caché para obtener la configuración del guild
|
||||||
const command = commands.get(cmdName);
|
const guildConfig = await getGuildConfig(
|
||||||
if (!command) return;
|
message.guildId || message.guild!.id,
|
||||||
|
message.guild!.name,
|
||||||
|
bot.prisma
|
||||||
|
);
|
||||||
|
|
||||||
const cooldown = Math.floor(Number(command.cooldown) || 0);
|
const PREFIX = guildConfig.prefix || "!";
|
||||||
|
if (!message.content.startsWith(PREFIX)) return;
|
||||||
|
|
||||||
if (cooldown > 0) {
|
const [cmdName, ...args] = message.content
|
||||||
const key = `cooldown:${command.name}:${message.author.id}`;
|
.slice(PREFIX.length)
|
||||||
const ttl = await redis.ttl(key);
|
.trim()
|
||||||
logger.debug(`Key: ${key}, TTL: ${ttl}`);
|
.split(/\s+/);
|
||||||
|
const command = commands.get(cmdName);
|
||||||
|
if (!command) return;
|
||||||
|
|
||||||
if (ttl > 0) {
|
const cooldown = Math.floor(Number(command.cooldown) || 0);
|
||||||
return message.reply(`⏳ Espera ${ttl}s antes de volver a usar **${command.name}**.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SET con expiración correcta para redis v4+
|
if (cooldown > 0) {
|
||||||
await redis.set(key, "1", { EX: cooldown });
|
const key = `cooldown:${command.name}:${message.author.id}`;
|
||||||
|
const ttl = await redis.ttl(key);
|
||||||
|
logger.debug(`Key: ${key}, TTL: ${ttl}`);
|
||||||
|
|
||||||
|
if (ttl > 0) {
|
||||||
|
return message.reply(
|
||||||
|
`⏳ Espera ${ttl}s antes de volver a usar **${command.name}**.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SET con expiración correcta para redis v4+
|
||||||
|
await redis.set(key, "1", { EX: cooldown });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await command.run(message, args, message.client);
|
await command.run(message, args, message.client);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "Error ejecutando comando");
|
logger.error({ err: error }, "Error ejecutando comando");
|
||||||
await message.reply("❌ Hubo un error ejecutando el comando.");
|
await message.reply("❌ Hubo un error ejecutando el comando.");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|||||||
341
src/main.ts
341
src/main.ts
@@ -1,216 +1,273 @@
|
|||||||
|
|
||||||
import Amayo from "./core/client";
|
import Amayo from "./core/client";
|
||||||
import { loadCommands } from "./core/loaders/loader";
|
import { loadCommands } from "./core/loaders/loader";
|
||||||
import { loadEvents } from "./core/loaders/loaderEvents";
|
import { loadEvents } from "./core/loaders/loaderEvents";
|
||||||
import { redis, redisConnect } from "./core/database/redis";
|
import { redis, redisConnect } from "./core/database/redis";
|
||||||
import { registeringCommands } from "./core/api/discordAPI";
|
import { registeringCommands } from "./core/api/discordAPI";
|
||||||
import {loadComponents} from "./core/lib/components";
|
import { loadComponents } from "./core/lib/components";
|
||||||
import { startMemoryMonitor } from "./core/memory/memoryMonitor";
|
import { startMemoryMonitor } from "./core/memory/memoryMonitor";
|
||||||
import {memoryOptimizer} from "./core/memory/memoryOptimizer";
|
import { memoryOptimizer } from "./core/memory/memoryOptimizer";
|
||||||
import { startReminderPoller } from "./core/api/reminders";
|
import { startReminderPoller } from "./core/api/reminders";
|
||||||
import { ensureRemindersSchema } from "./core/api/remindersSchema";
|
import { ensureRemindersSchema } from "./core/api/remindersSchema";
|
||||||
|
import { cleanExpiredGuildCache } from "./core/database/guildCache";
|
||||||
import logger from "./core/lib/logger";
|
import logger from "./core/lib/logger";
|
||||||
import { applyModalSubmitInteractionPatch } from "./core/patches/discordModalPatch";
|
import { applyModalSubmitInteractionPatch } from "./core/patches/discordModalPatch";
|
||||||
import { server } from "./server/server";
|
import { server } from "./server/server";
|
||||||
|
|
||||||
// Activar monitor de memoria si se define la variable
|
// Activar monitor de memoria si se define la variable
|
||||||
const __memInt = parseInt(process.env.MEMORY_LOG_INTERVAL_SECONDS || '0', 10);
|
const __memInt = parseInt(process.env.MEMORY_LOG_INTERVAL_SECONDS || "0", 10);
|
||||||
if (__memInt > 0) {
|
if (__memInt > 0) {
|
||||||
startMemoryMonitor({ intervalSeconds: __memInt });
|
startMemoryMonitor({ intervalSeconds: __memInt });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activar optimizador de memoria adicional
|
// Activar optimizador de memoria adicional
|
||||||
if (process.env.ENABLE_MEMORY_OPTIMIZER === 'true') {
|
if (process.env.ENABLE_MEMORY_OPTIMIZER === "true") {
|
||||||
memoryOptimizer.start();
|
memoryOptimizer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply safety patch for ModalSubmitInteraction members resolution before anything else
|
// Apply safety patch for ModalSubmitInteraction members resolution before anything else
|
||||||
try {
|
try {
|
||||||
applyModalSubmitInteractionPatch();
|
applyModalSubmitInteractionPatch();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn({ err: e }, 'No se pudo aplicar el patch de ModalSubmitInteraction');
|
logger.warn(
|
||||||
|
{ err: e },
|
||||||
|
"No se pudo aplicar el patch de ModalSubmitInteraction"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bot = new Amayo();
|
export const bot = new Amayo();
|
||||||
|
|
||||||
// Listeners de robustez del cliente Discord
|
// Listeners de robustez del cliente Discord
|
||||||
bot.on('error', (e) => logger.error({ err: e }, '🐞 Discord client error'));
|
bot.on("error", (e) => logger.error({ err: e }, "🐞 Discord client error"));
|
||||||
bot.on('warn', (m) => logger.warn('⚠️ Discord warn: %s', m));
|
bot.on("warn", (m) => logger.warn("⚠️ Discord warn: %s", m));
|
||||||
|
|
||||||
// Evitar reintentos de re-login simultáneos
|
// Evitar reintentos de re-login simultáneos
|
||||||
let relogging = false;
|
let relogging = false;
|
||||||
// Cuando la sesión es invalidada, intentamos reconectar/login
|
// Cuando la sesión es invalidada, intentamos reconectar/login
|
||||||
bot.on('invalidated', () => {
|
bot.on("invalidated", () => {
|
||||||
if (relogging) return;
|
if (relogging) return;
|
||||||
relogging = true;
|
relogging = true;
|
||||||
logger.error('🔄 Sesión de Discord invalidada. Reintentando login...');
|
logger.error("🔄 Sesión de Discord invalidada. Reintentando login...");
|
||||||
withRetry('Re-login tras invalidated', () => bot.play(), { minDelayMs: 2000, maxDelayMs: 60_000 })
|
withRetry("Re-login tras invalidated", () => bot.play(), {
|
||||||
.catch(() => {
|
minDelayMs: 2000,
|
||||||
logger.error('No se pudo reloguear tras invalidated, se seguirá intentando en el bucle general.');
|
maxDelayMs: 60_000,
|
||||||
})
|
})
|
||||||
.finally(() => { relogging = false; });
|
.catch(() => {
|
||||||
|
logger.error(
|
||||||
|
"No se pudo reloguear tras invalidated, se seguirá intentando en el bucle general."
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
relogging = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Utilidad: reintentos con backoff exponencial + jitter
|
// Utilidad: reintentos con backoff exponencial + jitter
|
||||||
async function withRetry<T>(name: string, fn: () => Promise<T>, opts?: {
|
async function withRetry<T>(
|
||||||
|
name: string,
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
opts?: {
|
||||||
retries?: number;
|
retries?: number;
|
||||||
minDelayMs?: number;
|
minDelayMs?: number;
|
||||||
maxDelayMs?: number;
|
maxDelayMs?: number;
|
||||||
factor?: number;
|
factor?: number;
|
||||||
jitter?: boolean;
|
jitter?: boolean;
|
||||||
isRetryable?: (err: unknown, attempt: number) => boolean;
|
isRetryable?: (err: unknown, attempt: number) => boolean;
|
||||||
}): Promise<T> {
|
}
|
||||||
const {
|
): Promise<T> {
|
||||||
retries = Infinity,
|
const {
|
||||||
minDelayMs = 1000,
|
retries = Infinity,
|
||||||
maxDelayMs = 30_000,
|
minDelayMs = 1000,
|
||||||
factor = 1.8,
|
maxDelayMs = 30_000,
|
||||||
jitter = true,
|
factor = 1.8,
|
||||||
isRetryable = () => true,
|
jitter = true,
|
||||||
} = opts ?? {};
|
isRetryable = () => true,
|
||||||
|
} = opts ?? {};
|
||||||
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
let delay = minDelayMs;
|
let delay = minDelayMs;
|
||||||
|
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
return await fn();
|
return await fn();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
attempt++;
|
attempt++;
|
||||||
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
const errMsg =
|
||||||
logger.error(`❌ ${name} falló (intento ${attempt}) => %s`, errMsg);
|
err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
||||||
|
logger.error(`❌ ${name} falló (intento ${attempt}) => %s`, errMsg);
|
||||||
|
|
||||||
if (!isRetryable(err, attempt)) {
|
if (!isRetryable(err, attempt)) {
|
||||||
logger.error(`⛔ ${name}: error no recuperable, deteniendo reintentos.`);
|
logger.error(
|
||||||
throw err;
|
`⛔ ${name}: error no recuperable, deteniendo reintentos.`
|
||||||
}
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt >= retries) throw err;
|
if (attempt >= retries) throw err;
|
||||||
|
|
||||||
// calcular backoff
|
// calcular backoff
|
||||||
let wait = delay;
|
let wait = delay;
|
||||||
if (jitter) {
|
if (jitter) {
|
||||||
const rand = Math.random() + 0.5; // 0.5x a 1.5x
|
const rand = Math.random() + 0.5; // 0.5x a 1.5x
|
||||||
wait = Math.min(maxDelayMs, Math.floor(delay * rand));
|
wait = Math.min(maxDelayMs, Math.floor(delay * rand));
|
||||||
} else {
|
} else {
|
||||||
wait = Math.min(maxDelayMs, delay);
|
wait = Math.min(maxDelayMs, delay);
|
||||||
}
|
}
|
||||||
logger.warn(`⏳ Reintentando ${name} en ${wait}ms...`);
|
logger.warn(`⏳ Reintentando ${name} en ${wait}ms...`);
|
||||||
await new Promise((r) => setTimeout(r, wait));
|
await new Promise((r) => setTimeout(r, wait));
|
||||||
delay = Math.min(maxDelayMs, Math.floor(delay * factor));
|
delay = Math.min(maxDelayMs, Math.floor(delay * factor));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handlers globales para robustez
|
// Handlers globales para robustez
|
||||||
process.on('unhandledRejection', (reason: any, p) => {
|
process.on("unhandledRejection", (reason: any, p) => {
|
||||||
logger.error({ promise: p, reason }, '🚨 UnhandledRejection en Promise');
|
logger.error({ promise: p, reason }, "🚨 UnhandledRejection en Promise");
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on("uncaughtException", (err) => {
|
||||||
logger.error({ err }, '🚨 UncaughtException');
|
logger.error({ err }, "🚨 UncaughtException");
|
||||||
// No salimos; dejamos que el bot continúe vivo
|
// No salimos; dejamos que el bot continúe vivo
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('multipleResolves', (type, promise, reason: any) => {
|
process.on("multipleResolves", (type, promise, reason: any) => {
|
||||||
// Ignorar resoluciones sin razón (ruido)
|
// Ignorar resoluciones sin razón (ruido)
|
||||||
if (type === 'resolve' && (reason === undefined || reason === null)) {
|
if (type === "resolve" && (reason === undefined || reason === null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const msg = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
|
const msg =
|
||||||
const stack = (reason && (reason as any).stack) ? String((reason as any).stack) : '';
|
reason instanceof Error
|
||||||
const isAbortErr = (reason && ((reason as any).code === 'ABORT_ERR' || /AbortError|operation was aborted/i.test(msg)));
|
? `${reason.name}: ${reason.message}`
|
||||||
const isDiscordWs = /@discordjs\/ws|WebSocketShard/.test(stack);
|
: String(reason);
|
||||||
if (isAbortErr && isDiscordWs) {
|
const stack =
|
||||||
// Ruido benigno de reconexiones del WS de Discord: ignorar
|
reason && (reason as any).stack ? String((reason as any).stack) : "";
|
||||||
return;
|
const isAbortErr =
|
||||||
}
|
reason &&
|
||||||
logger.warn('⚠️ multipleResolves: %s %s', type, msg);
|
((reason as any).code === "ABORT_ERR" ||
|
||||||
|
/AbortError|operation was aborted/i.test(msg));
|
||||||
|
const isDiscordWs = /@discordjs\/ws|WebSocketShard/.test(stack);
|
||||||
|
if (isAbortErr && isDiscordWs) {
|
||||||
|
// Ruido benigno de reconexiones del WS de Discord: ignorar
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.warn("⚠️ multipleResolves: %s %s", type, msg);
|
||||||
});
|
});
|
||||||
|
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
async function gracefulShutdown() {
|
async function gracefulShutdown() {
|
||||||
if (shuttingDown) return;
|
if (shuttingDown) return;
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
logger.info('🛑 Apagado controlado iniciado...');
|
logger.info("🛑 Apagado controlado iniciado...");
|
||||||
try {
|
try {
|
||||||
// Detener optimizador de memoria
|
// Detener optimizador de memoria
|
||||||
memoryOptimizer.stop();
|
memoryOptimizer.stop();
|
||||||
|
|
||||||
// Cerrar Redis si procede
|
// Cerrar Redis si procede
|
||||||
try {
|
try {
|
||||||
if (redis?.isOpen) {
|
if (redis?.isOpen) {
|
||||||
await redis.quit();
|
await redis.quit();
|
||||||
logger.info('🔌 Redis cerrado');
|
logger.info("🔌 Redis cerrado");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn({ err: e }, 'No se pudo cerrar Redis limpiamente');
|
logger.warn({ err: e }, "No se pudo cerrar Redis limpiamente");
|
||||||
}
|
|
||||||
// Cerrar Prisma y Discord
|
|
||||||
try {
|
|
||||||
await bot.prisma.$disconnect();
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
await bot.destroy();
|
|
||||||
} catch {}
|
|
||||||
} finally {
|
|
||||||
logger.info('✅ Apagado controlado completo');
|
|
||||||
}
|
}
|
||||||
|
// Cerrar Prisma y Discord
|
||||||
|
try {
|
||||||
|
await bot.prisma.$disconnect();
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
await bot.destroy();
|
||||||
|
} catch {}
|
||||||
|
} finally {
|
||||||
|
logger.info("✅ Apagado controlado completo");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('SIGINT', gracefulShutdown);
|
process.on("SIGINT", gracefulShutdown);
|
||||||
process.on('SIGTERM', gracefulShutdown);
|
process.on("SIGTERM", gracefulShutdown);
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
logger.info("🚀 Iniciando bot...");
|
logger.info("🚀 Iniciando bot...");
|
||||||
await server.listen(process.env.PORT || 3000, () => {
|
await server.listen(process.env.PORT || 3000, () => {
|
||||||
logger.info(`📘 Amayo Docs disponible en http://localhost:${process.env.PORT || 3000}`);
|
logger.info(
|
||||||
});
|
`📘 Amayo Docs disponible en http://localhost:${process.env.PORT || 3000}`
|
||||||
// Cargar recursos locales (no deberían tirar el proceso si fallan)
|
);
|
||||||
try { loadCommands(); } catch (e) { logger.error({ err: e }, 'Error cargando comandos'); }
|
});
|
||||||
try { loadComponents(); } catch (e) { logger.error({ err: e }, 'Error cargando componentes'); }
|
// Cargar recursos locales (no deberían tirar el proceso si fallan)
|
||||||
try { loadEvents(); } catch (e) { logger.error({ err: e }, 'Error cargando eventos'); }
|
try {
|
||||||
|
loadCommands();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error({ err: e }, "Error cargando comandos");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loadComponents();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error({ err: e }, "Error cargando componentes");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loadEvents();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error({ err: e }, "Error cargando eventos");
|
||||||
|
}
|
||||||
|
|
||||||
// Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot
|
// Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot
|
||||||
withRetry('Registrar slash commands', async () => {
|
withRetry("Registrar slash commands", async () => {
|
||||||
await registeringCommands();
|
await registeringCommands();
|
||||||
}).catch((e) => logger.error({ err: e }, 'Registro de comandos agotó reintentos'));
|
}).catch((e) =>
|
||||||
|
logger.error({ err: e }, "Registro de comandos agotó reintentos")
|
||||||
|
);
|
||||||
|
|
||||||
// Conectar Redis con reintentos
|
// Conectar Redis con reintentos
|
||||||
await withRetry('Conectar a Redis', async () => {
|
await withRetry("Conectar a Redis", async () => {
|
||||||
await redisConnect();
|
await redisConnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Login Discord + DB con reintentos (gestionado en Amayo.play -> conecta Prisma + login)
|
// Login Discord + DB con reintentos (gestionado en Amayo.play -> conecta Prisma + login)
|
||||||
await withRetry('Login de Discord', async () => {
|
await withRetry(
|
||||||
await bot.play();
|
"Login de Discord",
|
||||||
}, {
|
async () => {
|
||||||
isRetryable: (err) => {
|
await bot.play();
|
||||||
const msg = err instanceof Error ? `${err.message}` : String(err);
|
},
|
||||||
// Si falta el TOKEN o token inválido, no tiene sentido reintentar sin cambiar config
|
{
|
||||||
return !/missing discord token|invalid token/i.test(msg);
|
isRetryable: (err) => {
|
||||||
}
|
const msg = err instanceof Error ? `${err.message}` : String(err);
|
||||||
});
|
// Si falta el TOKEN o token inválido, no tiene sentido reintentar sin cambiar config
|
||||||
|
return !/missing discord token|invalid token/i.test(msg);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Asegurar esquema de Appwrite para recordatorios (colección + atributos + índice)
|
// Asegurar esquema de Appwrite para recordatorios (colección + atributos + índice)
|
||||||
try { await ensureRemindersSchema(); } catch (e) { logger.warn({ err: e }, 'No se pudo asegurar el esquema de recordatorios'); }
|
try {
|
||||||
|
await ensureRemindersSchema();
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn({ err: e }, "No se pudo asegurar el esquema de recordatorios");
|
||||||
|
}
|
||||||
|
|
||||||
// Iniciar poller de recordatorios si Appwrite está configurado
|
// Iniciar poller de recordatorios si Appwrite está configurado
|
||||||
startReminderPoller(bot);
|
startReminderPoller(bot);
|
||||||
|
|
||||||
logger.info("✅ Bot conectado a Discord");
|
// Iniciar limpieza periódica de caché de guilds (cada 10 minutos)
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await cleanExpiredGuildCache();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "❌ Error en limpieza periódica de caché");
|
||||||
|
}
|
||||||
|
}, 10 * 60 * 1000); // 10 minutos
|
||||||
|
|
||||||
|
logger.info("✅ Bot conectado a Discord");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bucle de arranque resiliente: si bootstrap completo falla, reintenta sin matar el proceso
|
// Bucle de arranque resiliente: si bootstrap completo falla, reintenta sin matar el proceso
|
||||||
(async function startLoop() {
|
(async function startLoop() {
|
||||||
await withRetry('Arranque', bootstrap, {
|
await withRetry("Arranque", bootstrap, {
|
||||||
minDelayMs: 1000,
|
minDelayMs: 1000,
|
||||||
maxDelayMs: 60_000,
|
maxDelayMs: 60_000,
|
||||||
isRetryable: (err) => {
|
isRetryable: (err) => {
|
||||||
const msg = err instanceof Error ? `${err.message}` : String(err);
|
const msg = err instanceof Error ? `${err.message}` : String(err);
|
||||||
// No reintentar en bucle si el problema es falta/invalid token
|
// No reintentar en bucle si el problema es falta/invalid token
|
||||||
return !/missing discord token|invalid token/i.test(msg);
|
return !/missing discord token|invalid token/i.test(msg);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user