feat: introduce Appwrite guild cache support and refactor guild config caching
This commit is contained in:
@@ -1,238 +1,280 @@
|
||||
import {bot} from "../main";
|
||||
import {Events} from "discord.js";
|
||||
import {redis} from "../core/database/redis";
|
||||
import {commands} from "../core/loaders/loader";
|
||||
import {alliance} from "./extras/alliace";
|
||||
import { bot } from "../main";
|
||||
import { Events } from "discord.js";
|
||||
import { redis } from "../core/database/redis";
|
||||
import { commands } from "../core/loaders/loader";
|
||||
import { alliance } from "./extras/alliace";
|
||||
import logger from "../core/lib/logger";
|
||||
import { aiService } from "../core/services/AIService";
|
||||
import { getGuildConfig } from "../core/database/guildCache";
|
||||
|
||||
// Función para manejar respuestas automáticas a la AI
|
||||
async function handleAIReply(message: any) {
|
||||
// Verificar si es una respuesta a un mensaje del bot
|
||||
if (!message.reference?.messageId || message.author.bot) return;
|
||||
// Verificar si es una respuesta a un mensaje del bot
|
||||
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 {
|
||||
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 server = await bot.prisma.guild.findUnique({
|
||||
where: { id: message.guildId || undefined }
|
||||
});
|
||||
const PREFIX = server?.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;
|
||||
// Obtener emojis personalizados del servidor
|
||||
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
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Construir metadatos del mensaje
|
||||
const buildMessageMeta = (msg: any, emojiNames?: string[]): string => {
|
||||
try {
|
||||
// Obtener emojis personalizados del servidor
|
||||
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
|
||||
}
|
||||
const parts: string[] = [];
|
||||
|
||||
// Construir metadatos del mensaje
|
||||
const buildMessageMeta = (msg: any, emojiNames?: string[]): string => {
|
||||
try {
|
||||
const parts: string[] = [];
|
||||
if (msg.channel?.name) {
|
||||
parts.push(`Canal: #${msg.channel.name}`);
|
||||
}
|
||||
|
||||
if (msg.channel?.name) {
|
||||
parts.push(`Canal: #${msg.channel.name}`);
|
||||
}
|
||||
const userMentions = msg.mentions?.users
|
||||
? 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()) : [];
|
||||
const roleMentions = msg.mentions?.roles ? Array.from(msg.mentions.roles.values()) : [];
|
||||
|
||||
if (userMentions.length) {
|
||||
parts.push(`Menciones usuario: ${userMentions.slice(0, 5).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 (userMentions.length) {
|
||||
parts.push(
|
||||
`Menciones usuario: ${userMentions
|
||||
.slice(0, 5)
|
||||
.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(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
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,
|
||||
}
|
||||
);
|
||||
|
||||
// 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)
|
||||
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);
|
||||
}
|
||||
currentChunk += (currentChunk ? "\n" : "") + line;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Mensaje referenciado no encontrado o error, ignorar silenciosamente
|
||||
logger.debug(`Error obteniendo mensaje referenciado: ${error}`);
|
||||
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) {
|
||||
// Mensaje referenciado no encontrado o error, ignorar silenciosamente
|
||||
logger.debug(`Error obteniendo mensaje referenciado: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
bot.on(Events.MessageCreate, async (message) => {
|
||||
if (message.author.bot) return;
|
||||
if (message.author.bot) return;
|
||||
|
||||
// Manejar respuestas automáticas a la AI
|
||||
await handleAIReply(message);
|
||||
// Manejar respuestas automáticas a la AI
|
||||
await handleAIReply(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;
|
||||
await alliance(message);
|
||||
|
||||
const [cmdName, ...args] = message.content.slice(PREFIX.length).trim().split(/\s+/);
|
||||
const command = commands.get(cmdName);
|
||||
if (!command) return;
|
||||
// Usar caché para obtener la configuración del guild
|
||||
const guildConfig = await getGuildConfig(
|
||||
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 key = `cooldown:${command.name}:${message.author.id}`;
|
||||
const ttl = await redis.ttl(key);
|
||||
logger.debug(`Key: ${key}, TTL: ${ttl}`);
|
||||
const [cmdName, ...args] = message.content
|
||||
.slice(PREFIX.length)
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
const command = commands.get(cmdName);
|
||||
if (!command) return;
|
||||
|
||||
if (ttl > 0) {
|
||||
return message.reply(`⏳ Espera ${ttl}s antes de volver a usar **${command.name}**.`);
|
||||
}
|
||||
const cooldown = Math.floor(Number(command.cooldown) || 0);
|
||||
|
||||
// SET con expiración correcta para redis v4+
|
||||
await redis.set(key, "1", { EX: cooldown });
|
||||
if (cooldown > 0) {
|
||||
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 {
|
||||
await command.run(message, args, message.client);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Error ejecutando comando");
|
||||
await message.reply("❌ Hubo un error ejecutando el comando.");
|
||||
}
|
||||
})
|
||||
try {
|
||||
await command.run(message, args, message.client);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Error ejecutando comando");
|
||||
await message.reply("❌ Hubo un error ejecutando el comando.");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user