diff --git a/src/web/inbound.test.ts b/src/web/inbound.test.ts index 32817adfe..68940cc4e 100644 --- a/src/web/inbound.test.ts +++ b/src/web/inbound.test.ts @@ -74,6 +74,15 @@ describe("web inbound helpers", () => { expect(body).toBe(""); }); + it("summarizes empty WhatsApp contact cards with a count", () => { + const body = extractText({ + contactsArrayMessage: { + contacts: [{}, {}], + }, + } as unknown as import("@whiskeysockets/baileys").proto.IMessage); + expect(body).toBe(""); + }); + it("unwraps view-once v2 extension messages", () => { const body = extractText({ viewOnceMessageV2Extension: { diff --git a/src/web/inbound.ts b/src/web/inbound.ts index e44d4065d..9c8a22447 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -40,6 +40,7 @@ import { getStatusCode, waitForWaConnection, } from "./session.js"; +import { parseVcard } from "./vcard.js"; export type WebListenerCloseReason = { status?: number; @@ -789,49 +790,6 @@ function describeContact(input: { return { name, phone }; } -function parseVcard(vcard?: string): { name?: string; phones: string[] } { - if (!vcard) return { phones: [] }; - const lines = vcard.split(/\r?\n/); - let nameFromN: string | undefined; - let nameFromFn: string | undefined; - const phones: string[] = []; - for (const rawLine of lines) { - const line = rawLine.trim(); - if (!line) continue; - const colonIndex = line.indexOf(":"); - if (colonIndex === -1) continue; - const key = line.slice(0, colonIndex).toUpperCase(); - const rawValue = line.slice(colonIndex + 1).trim(); - if (!rawValue) continue; - const value = cleanVcardValue(rawValue); - if (!value) continue; - if (key === "FN" && !nameFromFn) { - nameFromFn = normalizeVcardName(value); - continue; - } - if (key === "N" && !nameFromN) { - nameFromN = normalizeVcardName(value); - continue; - } - if (key.startsWith("TEL") || key.includes(".TEL")) { - phones.push(value); - } - } - return { name: nameFromFn ?? nameFromN, phones }; -} - -function cleanVcardValue(value: string): string { - return value - .replace(/\\n/gi, " ") - .replace(/\\,/g, ",") - .replace(/\\;/g, ";") - .trim(); -} - -function normalizeVcardName(value: string): string { - return value.replace(/;/g, " ").replace(/\s+/g, " ").trim(); -} - function formatContactPlaceholder(name?: string, phone?: string): string { const parts = [name, phone].filter((value): value is string => Boolean(value), @@ -842,7 +800,10 @@ function formatContactPlaceholder(name?: string, phone?: string): string { function formatContactsPlaceholder(labels: string[], total: number): string { const cleaned = labels.map((label) => label.trim()).filter(Boolean); - if (cleaned.length === 0) return ""; + if (cleaned.length === 0) { + const suffix = total === 1 ? "contact" : "contacts"; + return ``; + } const shown = cleaned.slice(0, 3); const remaining = Math.max(total - shown.length, 0); const suffix = remaining > 0 ? ` +${remaining} more` : ""; diff --git a/src/web/vcard.ts b/src/web/vcard.ts new file mode 100644 index 000000000..4d1b48ce2 --- /dev/null +++ b/src/web/vcard.ts @@ -0,0 +1,58 @@ +type ParsedVcard = { + name?: string; + phones: string[]; +}; + +const ALLOWED_VCARD_KEYS = new Set(["FN", "N", "TEL"]); + +export function parseVcard(vcard?: string): ParsedVcard { + if (!vcard) return { phones: [] }; + const lines = vcard.split(/\r?\n/); + let nameFromN: string | undefined; + let nameFromFn: string | undefined; + const phones: string[] = []; + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line) continue; + const colonIndex = line.indexOf(":"); + if (colonIndex === -1) continue; + const key = line.slice(0, colonIndex).toUpperCase(); + const rawValue = line.slice(colonIndex + 1).trim(); + if (!rawValue) continue; + const baseKey = normalizeVcardKey(key); + if (!baseKey || !ALLOWED_VCARD_KEYS.has(baseKey)) continue; + const value = cleanVcardValue(rawValue); + if (!value) continue; + if (baseKey === "FN" && !nameFromFn) { + nameFromFn = normalizeVcardName(value); + continue; + } + if (baseKey === "N" && !nameFromN) { + nameFromN = normalizeVcardName(value); + continue; + } + if (baseKey === "TEL") { + phones.push(value); + } + } + return { name: nameFromFn ?? nameFromN, phones }; +} + +function normalizeVcardKey(key: string): string | undefined { + const [primary] = key.split(";"); + if (!primary) return undefined; + const segments = primary.split("."); + return segments[segments.length - 1] || undefined; +} + +function cleanVcardValue(value: string): string { + return value + .replace(/\\n/gi, " ") + .replace(/\\,/g, ",") + .replace(/\\;/g, ";") + .trim(); +} + +function normalizeVcardName(value: string): string { + return value.replace(/;/g, " ").replace(/\s+/g, " ").trim(); +}