diff --git a/src/web/inbound.test.ts b/src/web/inbound.test.ts index 68940cc4e..5850114da 100644 --- a/src/web/inbound.test.ts +++ b/src/web/inbound.test.ts @@ -60,18 +60,72 @@ describe("web inbound helpers", () => { expect(body).toBe(""); }); + it("normalizes tel: prefixes in WhatsApp vcards", () => { + const body = extractText({ + contactMessage: { + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Ada Lovelace", + "TEL;TYPE=CELL:tel:+15555550123", + "END:VCARD", + ].join("\n"), + }, + } as unknown as import("@whiskeysockets/baileys").proto.IMessage); + expect(body).toBe(""); + }); + it("extracts multiple WhatsApp contact cards", () => { const body = extractText({ contactsArrayMessage: { contacts: [ - { displayName: "Alice" }, - { displayName: "Bob" }, - { displayName: "Charlie" }, - { displayName: "Dana" }, + { + displayName: "Alice", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Alice", + "TEL;TYPE=CELL:+15555550101", + "END:VCARD", + ].join("\n"), + }, + { + displayName: "Bob", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Bob", + "TEL;TYPE=CELL:+15555550102", + "END:VCARD", + ].join("\n"), + }, + { + displayName: "Charlie", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Charlie", + "TEL;TYPE=CELL:+15555550103", + "TEL;TYPE=HOME:+15555550104", + "END:VCARD", + ].join("\n"), + }, + { + displayName: "Dana", + vcard: [ + "BEGIN:VCARD", + "VERSION:3.0", + "FN:Dana", + "TEL;TYPE=CELL:+15555550105", + "END:VCARD", + ].join("\n"), + }, ], }, } as unknown as import("@whiskeysockets/baileys").proto.IMessage); - expect(body).toBe(""); + expect(body).toBe( + "", + ); }); it("summarizes empty WhatsApp contact cards with a count", () => { diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 9c8a22447..9a1368367 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -762,11 +762,11 @@ function extractContactPlaceholder( if (!message) return undefined; const contact = message.contactMessage ?? undefined; if (contact) { - const { name, phone } = describeContact({ + const { name, phones } = describeContact({ displayName: contact.displayName, vcard: contact.vcard, }); - return formatContactPlaceholder(name, phone); + return formatContactPlaceholder(name, phones); } const contactsArray = message.contactsArrayMessage?.contacts ?? undefined; if (!contactsArray || contactsArray.length === 0) return undefined; @@ -774,7 +774,7 @@ function extractContactPlaceholder( .map((entry) => describeContact({ displayName: entry.displayName, vcard: entry.vcard }), ) - .map((entry) => entry.name ?? entry.phone) + .map((entry) => formatContactLabel(entry.name, entry.phones)) .filter((value): value is string => Boolean(value)); return formatContactsPlaceholder(labels, contactsArray.length); } @@ -782,20 +782,17 @@ function extractContactPlaceholder( function describeContact(input: { displayName?: string | null; vcard?: string | null; -}): { name?: string; phone?: string } { +}): { name?: string; phones: string[] } { const displayName = (input.displayName ?? "").trim(); const parsed = parseVcard(input.vcard ?? undefined); const name = displayName || parsed.name; - const phone = parsed.phones[0]; - return { name, phone }; + return { name, phones: parsed.phones }; } -function formatContactPlaceholder(name?: string, phone?: string): string { - const parts = [name, phone].filter((value): value is string => - Boolean(value), - ); - if (parts.length === 0) return ""; - return ``; +function formatContactPlaceholder(name?: string, phones?: string[]): string { + const label = formatContactLabel(name, phones); + if (!label) return ""; + return ``; } function formatContactsPlaceholder(labels: string[], total: number): string { @@ -810,6 +807,28 @@ function formatContactsPlaceholder(labels: string[], total: number): string { return ``; } +function formatContactLabel( + name?: string, + phones?: string[], +): string | undefined { + const phoneLabel = formatPhoneList(phones); + const parts = [name, phoneLabel].filter((value): value is string => + Boolean(value), + ); + if (parts.length === 0) return undefined; + return parts.join(", "); +} + +function formatPhoneList(phones?: string[]): string | undefined { + const cleaned = + phones?.map((phone) => phone.trim()).filter(Boolean) ?? []; + if (cleaned.length === 0) return undefined; + const [primary, ...rest] = cleaned; + if (!primary) return undefined; + if (rest.length === 0) return primary; + return `${primary} (+${rest.length} more)`; +} + export function extractLocationData( rawMessage: proto.IMessage | undefined, ): NormalizedLocation | null { diff --git a/src/web/vcard.ts b/src/web/vcard.ts index 4d1b48ce2..fd1da00cb 100644 --- a/src/web/vcard.ts +++ b/src/web/vcard.ts @@ -32,7 +32,8 @@ export function parseVcard(vcard?: string): ParsedVcard { continue; } if (baseKey === "TEL") { - phones.push(value); + const phone = normalizeVcardPhone(value); + if (phone) phones.push(phone); } } return { name: nameFromFn ?? nameFromN, phones }; @@ -56,3 +57,12 @@ function cleanVcardValue(value: string): string { function normalizeVcardName(value: string): string { return value.replace(/;/g, " ").replace(/\s+/g, " ").trim(); } + +function normalizeVcardPhone(value: string): string { + const trimmed = value.trim(); + if (!trimmed) return ""; + if (trimmed.toLowerCase().startsWith("tel:")) { + return trimmed.slice(4).trim(); + } + return trimmed; +}