feat: añadir ContactsService y usar nombres en menciones de usuarios
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>pull/1/head
parent
7a901c9d95
commit
3ff63f1503
@ -0,0 +1,158 @@
|
||||
import { normalizeWhatsAppId, isUserJid } from '../utils/whatsapp';
|
||||
|
||||
type CacheEntry = {
|
||||
name: string;
|
||||
expiresAt: number;
|
||||
};
|
||||
|
||||
export class ContactsService {
|
||||
// Caché en memoria: userId(normalizado, solo dígitos) -> nombre
|
||||
private static readonly cache = new Map<string, CacheEntry>();
|
||||
private static readonly TTL_MS = 12 * 60 * 60 * 1000; // 12h
|
||||
|
||||
private static getHeaders(): HeadersInit {
|
||||
return {
|
||||
apikey: process.env.EVOLUTION_API_KEY || '',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
private static now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
private static extractName(obj: any): string | null {
|
||||
if (!obj || typeof obj !== 'object') return null;
|
||||
// Intentar múltiples campos posibles que suelen aparecer en catálogos/contactos
|
||||
const candidates = [
|
||||
obj.name,
|
||||
obj.pushname,
|
||||
obj.verifiedName,
|
||||
obj.notify,
|
||||
obj.shortName,
|
||||
obj.displayName,
|
||||
obj.formattedName,
|
||||
obj.subject, // a veces para grupos; pero evitaremos grupos abajo
|
||||
obj.contactName
|
||||
].filter(Boolean) as string[];
|
||||
const name = candidates.find(v => typeof v === 'string' && v.trim().length > 0);
|
||||
return name ? name.trim() : null;
|
||||
}
|
||||
|
||||
static updateFromWebhook(data: any): void {
|
||||
try {
|
||||
// Aceptar varios formatos posibles
|
||||
const tryArrays: any[] = [];
|
||||
if (Array.isArray(data)) {
|
||||
tryArrays.push(data);
|
||||
} else if (data && typeof data === 'object') {
|
||||
for (const key of ['contacts', 'chats', 'data', 'payload', 'updates', 'results']) {
|
||||
if (Array.isArray((data as any)[key])) {
|
||||
tryArrays.push((data as any)[key]);
|
||||
}
|
||||
}
|
||||
// Algunos eventos pueden traer una sola entrada
|
||||
tryArrays.push([data]);
|
||||
}
|
||||
|
||||
for (const arr of tryArrays) {
|
||||
for (const rec of arr) {
|
||||
const idCandidate = rec?.id ?? rec?.jid ?? rec?.user ?? rec?.remoteJid ?? rec?.wid ?? rec?.wid?._serialized;
|
||||
if (!idCandidate) continue;
|
||||
|
||||
// Evitar grupos
|
||||
if (typeof idCandidate === 'string' && idCandidate.endsWith('@g.us')) continue;
|
||||
|
||||
const normalized = normalizeWhatsAppId(String(idCandidate));
|
||||
if (!normalized) continue;
|
||||
|
||||
const name = this.extractName(rec);
|
||||
if (!name) continue;
|
||||
|
||||
this.cache.set(normalized, { name, expiresAt: this.now() + this.TTL_MS });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.warn('ContactsService.updateFromWebhook: parse error', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async maybeFetchFromApi(digitsId: string): Promise<string | null> {
|
||||
const baseUrl = process.env.EVOLUTION_API_URL;
|
||||
const instance = process.env.EVOLUTION_API_INSTANCE;
|
||||
const apikey = process.env.EVOLUTION_API_KEY;
|
||||
|
||||
if (!baseUrl || !instance || !apikey) return null;
|
||||
|
||||
const url = `${baseUrl}/chat/findContacts/${instance}`;
|
||||
const body = {
|
||||
where: { id: `${digitsId}@s.whatsapp.net` }
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const txt = await res.text().catch(() => '');
|
||||
console.warn('ContactsService.findContacts non-OK:', res.status, txt?.slice(0, 200));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => null as any);
|
||||
const arrayCandidates: any[] = Array.isArray(data)
|
||||
? data
|
||||
: Array.isArray((data as any)?.contacts)
|
||||
? (data as any).contacts
|
||||
: Array.isArray((data as any)?.data)
|
||||
? (data as any).data
|
||||
: Array.isArray((data as any)?.result)
|
||||
? (data as any).result
|
||||
: (data && typeof data === 'object')
|
||||
? [data]
|
||||
: [];
|
||||
|
||||
let name: string | null = null;
|
||||
for (const rec of arrayCandidates) {
|
||||
const recId = rec?.id ?? rec?.jid ?? rec?.user ?? rec?.remoteJid ?? rec?.wid ?? rec?.wid?._serialized;
|
||||
if (!recId) continue;
|
||||
const norm = normalizeWhatsAppId(String(recId));
|
||||
if (!norm || norm !== digitsId) continue;
|
||||
name = this.extractName(rec);
|
||||
if (name) break;
|
||||
}
|
||||
|
||||
if (name) {
|
||||
this.cache.set(digitsId, { name, expiresAt: this.now() + this.TTL_MS });
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.warn('ContactsService.findContacts error:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async getDisplayName(idOrJid: string | null | undefined): Promise<string | null> {
|
||||
const normalized = normalizeWhatsAppId(idOrJid || '');
|
||||
if (!normalized) return null;
|
||||
|
||||
const cached = this.cache.get(normalized);
|
||||
if (cached && cached.expiresAt > this.now()) {
|
||||
return cached.name;
|
||||
}
|
||||
|
||||
// Intento de fetch perezoso
|
||||
const fetched = await this.maybeFetchFromApi(normalized);
|
||||
return fetched || null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue