From dc77b89cca8ace01593327db1d595afd66bf6a84 Mon Sep 17 00:00:00 2001 From: shni Date: Sat, 4 Oct 2025 01:47:02 -0500 Subject: [PATCH] feat: add support for custom emojis in messages and improve metadata handling --- src/commands/messages/AI/chat.ts | 72 +++++++++++++++++++++++++++++--- src/core/services/AIService.ts | 7 +++- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/commands/messages/AI/chat.ts b/src/commands/messages/AI/chat.ts index cf2d49d..cadee39 100644 --- a/src/commands/messages/AI/chat.ts +++ b/src/commands/messages/AI/chat.ts @@ -1,6 +1,6 @@ import logger from "../../../core/lib/logger"; import { CommandMessage } from "../../../core/types/commands"; -import { TextChannel, DMChannel, ThreadChannel, ChannelType } from "discord.js"; +import { TextChannel, DMChannel, ThreadChannel, ChannelType, GuildEmoji } from "discord.js"; import { aiService } from "../../../core/services/AIService"; /** @@ -54,7 +54,57 @@ function smartChunkText(text: string, maxLength: number): string[] { return chunks; } -function buildMessageMeta(message: any): string { +function replaceShortcodesWithEmojisOutsideCode(text: string, emojiMap: Record): string { + if (!text) return text; + // Split by triple backticks to avoid code blocks + const parts = text.split(/```/); + for (let i = 0; i < parts.length; i++) { + // Only replace in non-code blocks (even indices) + if (i % 2 === 0) { + // Also avoid inline code wrapped in single backticks by a simple pass + const inlineParts = parts[i].split(/`/); + for (let j = 0; j < inlineParts.length; j++) { + if (j % 2 === 0) { + inlineParts[j] = inlineParts[j].replace(/:([a-zA-Z0-9_]{2,32}):/g, (match, p1: string) => { + const key = p1; + const found = emojiMap[key]; + return found ? found : match; + }); + } + } + parts[i] = inlineParts.join('`'); + } + } + return parts.join('```'); +} + +async function getGuildCustomEmojis(message: any): Promise<{ names: string[]; map: Record }> { + const result = { names: [] as string[], map: {} as Record }; + try { + const guild = message.guild; + if (!guild) return result; + // Ensure emojis are fetched + const emojis = await guild.emojis.fetch(); + const list = Array.from(emojis.values()) as GuildEmoji[]; + for (const e of list) { + const name = e.name ?? undefined; + const id = e.id; + if (!name || !id) continue; + const tag = e.animated ? `` : `<:${name}:${id}>`; + if (!(name in result.map)) { + result.map[name] = tag; + result.names.push(name); + } + } + // Limit names to 25 for meta context brevity + result.names = result.names.slice(0, 25); + } catch { + // ignore + } + return result; +} + +function buildMessageMeta(message: any, emojiNames?: string[]): string { try { const parts: string[] = []; const inGuild = !!message.guild; @@ -106,6 +156,10 @@ function buildMessageMeta(message: any): string { parts.push(`Adjuntos: ${info}`); } + 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 { @@ -141,8 +195,11 @@ export const command: CommandMessage = { return; } - // Construir metadatos del mensaje para mejor contexto - const meta = buildMessageMeta(message); + // Emojis personalizados del servidor + const { names: emojiNames, map: emojiMap } = await getGuildCustomEmojis(message); + + // Construir metadatos del mensaje para mejor contexto (incluye emojis) + const meta = buildMessageMeta(message, emojiNames); // Indicador de escritura mejorado const typingInterval = setInterval(() => { @@ -153,7 +210,7 @@ export const command: CommandMessage = { // Usar el servicio mejorado con manejo de prioridad const priority = message.member?.permissions.has('Administrator') ? 'high' : 'normal'; - const aiResponse = await aiService.processAIRequest( + let aiResponse = await aiService.processAIRequest( userId, prompt, guildId, @@ -161,6 +218,11 @@ export const command: CommandMessage = { { meta } ); + // Reemplazar :nombre: por el tag real del emoji, evitando bloques de código + if (emojiNames.length > 0) { + aiResponse = replaceShortcodesWithEmojisOutsideCode(aiResponse, emojiMap); + } + // Discord limita el contenido a ~2000 caracteres const MAX_CONTENT = 2000; if (aiResponse.length > MAX_CONTENT) { diff --git a/src/core/services/AIService.ts b/src/core/services/AIService.ts index 2a6f883..66cbffc 100644 --- a/src/core/services/AIService.ts +++ b/src/core/services/AIService.ts @@ -98,7 +98,7 @@ export class AIService { } /** - * Obtener prompt de rol de IA por guild con cache + * Obtener prompt de rol de IA por guild con caché */ public async getGuildAiPrompt(guildId: string): Promise { try { @@ -107,6 +107,7 @@ export class AIService { if (cached && (now - cached.fetchedAt) < this.config.guildConfigTTL) { return cached.prompt; } + // @ts-ignore const guild = await prisma.guild.findUnique({ where: { id: guildId }, select: { aiRolePrompt: true } }); //@ts-ignore const prompt = guild?.aiRolePrompt ?? null; @@ -321,7 +322,7 @@ export class AIService { meta?: string ): string { const recentMessages = context.messages - .slice(-4) // Solo los últimos 4 mensajes + .slice(-4) .map(msg => `${msg.role === 'user' ? 'Usuario' : 'Asistente'}: ${msg.content}`) .join('\n'); @@ -334,6 +335,8 @@ export class AIService { - USA **markdown de Discord**: **negrita**, *cursiva*, \`código\`, \`\`\`bloques\`\`\` - NUNCA uses LaTeX ($$) - Máximo 2-3 emojis por respuesta +- Prefiere emojis Unicode estándar (🙂, 🎯, etc.) cuando no haya más contexto +- Si se te proporciona una lista de "Emojis personalizados disponibles", puedes usarlos escribiendo :nombre: exactamente como aparece; NO inventes nombres - Respuestas concisas y claras ${isImageRequest ? `