2025-10-11 20:21:10 -05:00
|
|
|
import "dotenv/config";
|
2025-10-11 20:01:01 -05:00
|
|
|
|
2025-09-17 13:33:10 -05:00
|
|
|
import Amayo from "./core/client";
|
2025-09-27 16:00:12 -05:00
|
|
|
import { loadCommands } from "./core/loaders/loader";
|
|
|
|
|
import { loadEvents } from "./core/loaders/loaderEvents";
|
|
|
|
|
import { redis, redisConnect } from "./core/database/redis";
|
2025-09-17 13:33:10 -05:00
|
|
|
import { registeringCommands } from "./core/api/discordAPI";
|
2025-10-07 10:52:47 -05:00
|
|
|
import { loadComponents } from "./core/lib/components";
|
2025-09-27 16:00:12 -05:00
|
|
|
import { startMemoryMonitor } from "./core/memory/memoryMonitor";
|
2025-10-07 10:52:47 -05:00
|
|
|
import { memoryOptimizer } from "./core/memory/memoryOptimizer";
|
2025-09-28 01:00:43 -05:00
|
|
|
import { startReminderPoller } from "./core/api/reminders";
|
2025-09-28 01:09:26 -05:00
|
|
|
import { ensureRemindersSchema } from "./core/api/remindersSchema";
|
2025-10-07 10:52:47 -05:00
|
|
|
import { cleanExpiredGuildCache } from "./core/database/guildCache";
|
2025-10-02 20:11:33 -05:00
|
|
|
import logger from "./core/lib/logger";
|
2025-10-03 23:52:15 -05:00
|
|
|
import { applyModalSubmitInteractionPatch } from "./core/patches/discordModalPatch";
|
2025-10-05 22:58:56 -05:00
|
|
|
import { server } from "./server/server";
|
2025-09-23 22:38:06 -05:00
|
|
|
|
|
|
|
|
// Activar monitor de memoria si se define la variable
|
2025-10-07 10:52:47 -05:00
|
|
|
const __memInt = parseInt(process.env.MEMORY_LOG_INTERVAL_SECONDS || "0", 10);
|
2025-09-23 22:38:06 -05:00
|
|
|
if (__memInt > 0) {
|
2025-10-07 10:52:47 -05:00
|
|
|
startMemoryMonitor({ intervalSeconds: __memInt });
|
2025-09-23 22:38:06 -05:00
|
|
|
}
|
2025-09-17 13:33:10 -05:00
|
|
|
|
2025-09-25 22:57:24 -05:00
|
|
|
// Activar optimizador de memoria adicional
|
2025-10-07 10:52:47 -05:00
|
|
|
if (process.env.ENABLE_MEMORY_OPTIMIZER === "true") {
|
|
|
|
|
memoryOptimizer.start();
|
2025-09-25 22:57:24 -05:00
|
|
|
}
|
|
|
|
|
|
2025-10-03 23:52:15 -05:00
|
|
|
// Apply safety patch for ModalSubmitInteraction members resolution before anything else
|
|
|
|
|
try {
|
2025-10-07 10:52:47 -05:00
|
|
|
applyModalSubmitInteractionPatch();
|
2025-10-03 23:52:15 -05:00
|
|
|
} catch (e) {
|
2025-10-07 10:52:47 -05:00
|
|
|
logger.warn(
|
|
|
|
|
{ err: e },
|
|
|
|
|
"No se pudo aplicar el patch de ModalSubmitInteraction"
|
|
|
|
|
);
|
2025-10-03 23:52:15 -05:00
|
|
|
}
|
|
|
|
|
|
2025-09-17 13:33:10 -05:00
|
|
|
export const bot = new Amayo();
|
|
|
|
|
|
2025-09-22 22:00:11 -05:00
|
|
|
// Listeners de robustez del cliente Discord
|
2025-10-07 10:52:47 -05:00
|
|
|
bot.on("error", (e) => logger.error({ err: e }, "🐞 Discord client error"));
|
|
|
|
|
bot.on("warn", (m) => logger.warn("⚠️ Discord warn: %s", m));
|
2025-09-25 22:57:24 -05:00
|
|
|
|
2025-09-22 22:09:06 -05:00
|
|
|
// Evitar reintentos de re-login simultáneos
|
|
|
|
|
let relogging = false;
|
2025-09-22 22:00:11 -05:00
|
|
|
// Cuando la sesión es invalidada, intentamos reconectar/login
|
2025-10-07 10:52:47 -05:00
|
|
|
bot.on("invalidated", () => {
|
|
|
|
|
if (relogging) return;
|
|
|
|
|
relogging = true;
|
|
|
|
|
logger.error("🔄 Sesión de Discord invalidada. Reintentando login...");
|
|
|
|
|
withRetry("Re-login tras invalidated", () => bot.play(), {
|
|
|
|
|
minDelayMs: 2000,
|
|
|
|
|
maxDelayMs: 60_000,
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
logger.error(
|
|
|
|
|
"No se pudo reloguear tras invalidated, se seguirá intentando en el bucle general."
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
relogging = false;
|
|
|
|
|
});
|
2025-09-22 22:00:11 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Utilidad: reintentos con backoff exponencial + jitter
|
2025-10-07 10:52:47 -05:00
|
|
|
async function withRetry<T>(
|
|
|
|
|
name: string,
|
|
|
|
|
fn: () => Promise<T>,
|
|
|
|
|
opts?: {
|
2025-09-22 22:00:11 -05:00
|
|
|
retries?: number;
|
|
|
|
|
minDelayMs?: number;
|
|
|
|
|
maxDelayMs?: number;
|
|
|
|
|
factor?: number;
|
|
|
|
|
jitter?: boolean;
|
|
|
|
|
isRetryable?: (err: unknown, attempt: number) => boolean;
|
2025-10-07 10:52:47 -05:00
|
|
|
}
|
|
|
|
|
): Promise<T> {
|
|
|
|
|
const {
|
|
|
|
|
retries = Infinity,
|
|
|
|
|
minDelayMs = 1000,
|
|
|
|
|
maxDelayMs = 30_000,
|
|
|
|
|
factor = 1.8,
|
|
|
|
|
jitter = true,
|
|
|
|
|
isRetryable = () => true,
|
|
|
|
|
} = opts ?? {};
|
|
|
|
|
|
|
|
|
|
let attempt = 0;
|
|
|
|
|
let delay = minDelayMs;
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
|
while (true) {
|
|
|
|
|
try {
|
|
|
|
|
return await fn();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
attempt++;
|
|
|
|
|
const errMsg =
|
|
|
|
|
err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
|
|
|
logger.error(`❌ ${name} falló (intento ${attempt}) => %s`, errMsg);
|
|
|
|
|
|
|
|
|
|
if (!isRetryable(err, attempt)) {
|
|
|
|
|
logger.error(
|
|
|
|
|
`⛔ ${name}: error no recuperable, deteniendo reintentos.`
|
|
|
|
|
);
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (attempt >= retries) throw err;
|
|
|
|
|
|
|
|
|
|
// calcular backoff
|
|
|
|
|
let wait = delay;
|
|
|
|
|
if (jitter) {
|
|
|
|
|
const rand = Math.random() + 0.5; // 0.5x a 1.5x
|
|
|
|
|
wait = Math.min(maxDelayMs, Math.floor(delay * rand));
|
|
|
|
|
} else {
|
|
|
|
|
wait = Math.min(maxDelayMs, delay);
|
|
|
|
|
}
|
|
|
|
|
logger.warn(`⏳ Reintentando ${name} en ${wait}ms...`);
|
|
|
|
|
await new Promise((r) => setTimeout(r, wait));
|
|
|
|
|
delay = Math.min(maxDelayMs, Math.floor(delay * factor));
|
2025-09-22 22:00:11 -05:00
|
|
|
}
|
2025-10-07 10:52:47 -05:00
|
|
|
}
|
2025-09-22 22:00:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handlers globales para robustez
|
2025-10-07 10:52:47 -05:00
|
|
|
process.on("unhandledRejection", (reason: any, p) => {
|
|
|
|
|
logger.error({ promise: p, reason }, "🚨 UnhandledRejection en Promise");
|
2025-09-22 22:00:11 -05:00
|
|
|
});
|
|
|
|
|
|
2025-10-07 10:52:47 -05:00
|
|
|
process.on("uncaughtException", (err) => {
|
|
|
|
|
logger.error({ err }, "🚨 UncaughtException");
|
|
|
|
|
// No salimos; dejamos que el bot continúe vivo
|
2025-09-22 22:00:11 -05:00
|
|
|
});
|
|
|
|
|
|
2025-10-07 10:52:47 -05:00
|
|
|
process.on("multipleResolves", (type, promise, reason: any) => {
|
|
|
|
|
// Ignorar resoluciones sin razón (ruido)
|
|
|
|
|
if (type === "resolve" && (reason === undefined || reason === null)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const msg =
|
|
|
|
|
reason instanceof Error
|
|
|
|
|
? `${reason.name}: ${reason.message}`
|
|
|
|
|
: String(reason);
|
|
|
|
|
const stack =
|
|
|
|
|
reason && (reason as any).stack ? String((reason as any).stack) : "";
|
|
|
|
|
const isAbortErr =
|
|
|
|
|
reason &&
|
|
|
|
|
((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);
|
2025-09-22 22:00:11 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let shuttingDown = false;
|
|
|
|
|
async function gracefulShutdown() {
|
2025-10-07 10:52:47 -05:00
|
|
|
if (shuttingDown) return;
|
|
|
|
|
shuttingDown = true;
|
|
|
|
|
logger.info("🛑 Apagado controlado iniciado...");
|
|
|
|
|
try {
|
|
|
|
|
// Detener optimizador de memoria
|
|
|
|
|
memoryOptimizer.stop();
|
|
|
|
|
|
|
|
|
|
// Cerrar Redis si procede
|
2025-09-22 22:00:11 -05:00
|
|
|
try {
|
2025-10-07 10:52:47 -05:00
|
|
|
if (redis?.isOpen) {
|
|
|
|
|
await redis.quit();
|
|
|
|
|
logger.info("🔌 Redis cerrado");
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.warn({ err: e }, "No se pudo cerrar Redis limpiamente");
|
2025-09-22 22:00:11 -05:00
|
|
|
}
|
2025-10-07 10:52:47 -05:00
|
|
|
// Cerrar Prisma y Discord
|
|
|
|
|
try {
|
|
|
|
|
await bot.prisma.$disconnect();
|
|
|
|
|
} catch {}
|
|
|
|
|
try {
|
|
|
|
|
await bot.destroy();
|
|
|
|
|
} catch {}
|
|
|
|
|
} finally {
|
|
|
|
|
logger.info("✅ Apagado controlado completo");
|
|
|
|
|
}
|
2025-09-22 22:00:11 -05:00
|
|
|
}
|
|
|
|
|
|
2025-10-07 10:52:47 -05:00
|
|
|
process.on("SIGINT", gracefulShutdown);
|
|
|
|
|
process.on("SIGTERM", gracefulShutdown);
|
2025-09-22 22:00:11 -05:00
|
|
|
|
2025-09-17 13:33:10 -05:00
|
|
|
async function bootstrap() {
|
2025-10-07 10:52:47 -05:00
|
|
|
logger.info("🚀 Iniciando bot...");
|
|
|
|
|
await server.listen(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");
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
loadEvents();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
logger.error({ err: e }, "Error cargando eventos");
|
|
|
|
|
}
|
2025-09-22 22:00:11 -05:00
|
|
|
|
2025-10-07 10:52:47 -05:00
|
|
|
// Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot
|
|
|
|
|
withRetry("Registrar slash commands", async () => {
|
|
|
|
|
await registeringCommands();
|
|
|
|
|
}).catch((e) =>
|
|
|
|
|
logger.error({ err: e }, "Registro de comandos agotó reintentos")
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Conectar Redis con reintentos
|
|
|
|
|
await withRetry("Conectar a Redis", async () => {
|
|
|
|
|
await redisConnect();
|
|
|
|
|
});
|
2025-09-17 13:33:10 -05:00
|
|
|
|
2025-10-07 10:52:47 -05:00
|
|
|
// Login Discord + DB con reintentos (gestionado en Amayo.play -> conecta Prisma + login)
|
|
|
|
|
await withRetry(
|
|
|
|
|
"Login de Discord",
|
|
|
|
|
async () => {
|
|
|
|
|
await bot.play();
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
2025-09-28 01:09:26 -05:00
|
|
|
|
2025-10-07 10:52:47 -05:00
|
|
|
// 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");
|
|
|
|
|
}
|
2025-09-28 01:00:43 -05:00
|
|
|
|
2025-10-07 10:52:47 -05:00
|
|
|
// Iniciar poller de recordatorios si Appwrite está configurado
|
|
|
|
|
startReminderPoller(bot);
|
|
|
|
|
|
|
|
|
|
// 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");
|
2025-09-17 13:33:10 -05:00
|
|
|
}
|
|
|
|
|
|
2025-09-22 22:00:11 -05:00
|
|
|
// Bucle de arranque resiliente: si bootstrap completo falla, reintenta sin matar el proceso
|
|
|
|
|
(async function startLoop() {
|
2025-10-07 10:52:47 -05:00
|
|
|
await withRetry("Arranque", bootstrap, {
|
|
|
|
|
minDelayMs: 1000,
|
|
|
|
|
maxDelayMs: 60_000,
|
|
|
|
|
isRetryable: (err) => {
|
|
|
|
|
const msg = err instanceof Error ? `${err.message}` : String(err);
|
|
|
|
|
// No reintentar en bucle si el problema es falta/invalid token
|
|
|
|
|
return !/missing discord token|invalid token/i.test(msg);
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-09-22 22:00:11 -05:00
|
|
|
})();
|