/** * Chat-registry accessors — map between Talon chat ids (string and * numeric) and the WhatsApp JID the socket needs to post back. * * Populated on first inbound message from a chat; consulted by the * action handler (send_message routes by Talon chat id) and by the * Frontend contract's numeric-keyed sendMessage/sendTyping (cron, * pulse, heartbeat outbound). */ import { isJidGroup, jidNormalizedUser } from "baileys"; import { deriveNumericChatId } from "../../util/chat-id.js"; import { bareId } from "./identity.js"; export type WhatsAppChatInfo = { /** Talon chat id: wa_dm_ or wa_group_. */ chatId: string; /** Stable 32-bit hash of chatId (dispatcher/gateway key). */ numericChatId: number; /** The JID the socket sends to: @s.whatsapp.net or @g.us. */ jid: string; isGroup: boolean; title?: string; }; const byNumeric = new Map(); const byString = new Map(); /** * Talon chat id for a WhatsApp JID. * * `canonical` is the identity's preferred id (phone number when known), * so a DM keeps ONE chat id even when WhatsApp switches the conversation * between phone-number and LID addressing mid-thread. */ export function chatIdForJid(jid: string, canonical?: string): string { // `||`, not `??`: an empty canonical id must fall back to the JID // rather than produce the id every DM would share. const bare = canonical || bareId(jidNormalizedUser(jid)) || "unknown"; return isJidGroup(jid) ? `wa_group_${bare}` : `wa_dm_${bare}`; } /** * Record (or refresh) a chat's identity, returning its info. * * The send target (`jid`) is always the address the message arrived on — * that is what WhatsApp routes — while the chat id comes from the stable * canonical identity. A chat already known under one addressing form has * its send target refreshed rather than being duplicated. */ export function registerWhatsAppChat( jid: string, title?: string, canonical?: string, ): WhatsAppChatInfo { const chatId = chatIdForJid(jid, canonical); const sendJid = isJidGroup(jid) ? jid : jidNormalizedUser(jid); const existing = byString.get(chatId); if (existing) { if (title) existing.title = title; existing.jid = sendJid; return existing; } const info: WhatsAppChatInfo = { chatId, numericChatId: deriveNumericChatId(chatId), jid: sendJid, isGroup: Boolean(isJidGroup(jid)), ...(title ? { title } : {}), }; byNumeric.set(info.numericChatId, info); byString.set(info.chatId, info); return info; } export function lookupWhatsAppChat( numericChatId: number, ): WhatsAppChatInfo | undefined { return byNumeric.get(numericChatId); } export function lookupWhatsAppChatByString( chatId: string, ): WhatsAppChatInfo | undefined { return byString.get(chatId); } /** * Resolve an explicit send target — the `target` a cross-frontend * `send_via` names — onto chat info, REGISTERING the chat when it's new * so numeric-id routing and replies work from then on. Accepted forms: * * - a registered Talon id: `wa_dm_*`/`wa_group_*` string, or its * numeric hash (only resolvable while registered — the hash can't * be reversed) * - a raw JID: `@s.whatsapp.net` or `@g.us` * - a phone number with country code: "+353871234567", "353 87 123 4567" * * Digits with punctuation normalize to a DM JID; an all-digit string is * tried as a registered numeric id first, then treated as a phone number. * Returns undefined when nothing sendable can be derived. */ export function resolveWhatsAppTarget( raw: unknown, ): WhatsAppChatInfo | undefined { const target = String(raw ?? "").trim(); if (!target) return undefined; const registered = byString.get(target); if (registered) return registered; if (/^\d+$/.test(target)) { const byHash = byNumeric.get(Number(target)); if (byHash) return byHash; } if (target.includes("@")) return registerWhatsAppChat(target); if (target.startsWith("wa_group_")) { return registerWhatsAppChat(`${target.slice("wa_group_".length)}@g.us`); } if (target.startsWith("wa_dm_")) { return registerWhatsAppChat( `${target.slice("wa_dm_".length)}@s.whatsapp.net`, ); } const digits = target.replace(/[^0-9]/g, ""); return digits ? registerWhatsAppChat(`${digits}@s.whatsapp.net`) : undefined; } /** Test seam: forget every registered chat. */ export function resetWhatsAppRegistry(): void { byNumeric.clear(); byString.clear(); }