Merge pull request #625 from mahmoudashraf93/fix/whatsapp-contact-cards-multi

fix: include numbers for WhatsApp contact arrays
This commit is contained in:
Peter Steinberger
2026-01-10 00:04:00 +00:00
committed by GitHub
4 changed files with 123 additions and 18 deletions

View File

@@ -3,6 +3,7 @@
## Unreleased ## Unreleased
- WhatsApp: refactor vCard parsing helper and improve empty contact card summaries. (#624) — thanks @steipete - WhatsApp: refactor vCard parsing helper and improve empty contact card summaries. (#624) — thanks @steipete
- WhatsApp: include phone numbers when multiple contacts are shared. (#625) — thanks @mahmoudashraf93
- Pairing: cap pending DM pairing requests at 3 per provider and avoid pairing replies for outbound DMs. — thanks @steipete - Pairing: cap pending DM pairing requests at 3 per provider and avoid pairing replies for outbound DMs. — thanks @steipete
- macOS: replace relay smoke test with version check in packaging script. (#615) — thanks @YuriNachos - macOS: replace relay smoke test with version check in packaging script. (#615) — thanks @YuriNachos
- macOS: avoid clearing Launch at Login during app initialization. (#607) — thanks @wes-davis - macOS: avoid clearing Launch at Login during app initialization. (#607) — thanks @wes-davis

View File

@@ -60,18 +60,94 @@ describe("web inbound helpers", () => {
expect(body).toBe("<contact: Ada Lovelace, +15555550123>"); expect(body).toBe("<contact: Ada Lovelace, +15555550123>");
}); });
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("<contact: Ada Lovelace, +15555550123>");
});
it("extracts multiple WhatsApp contact cards", () => { it("extracts multiple WhatsApp contact cards", () => {
const body = extractText({ const body = extractText({
contactsArrayMessage: { contactsArrayMessage: {
contacts: [ contacts: [
{ displayName: "Alice" }, {
{ displayName: "Bob" }, displayName: "Alice",
{ displayName: "Charlie" }, vcard: [
{ displayName: "Dana" }, "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); } as unknown as import("@whiskeysockets/baileys").proto.IMessage);
expect(body).toBe("<contacts: Alice, Bob, Charlie +1 more>"); expect(body).toBe(
"<contacts: Alice, +15555550101, Bob, +15555550102, Charlie, +15555550103 (+1 more) +1 more>",
);
});
it("counts empty WhatsApp contact cards in array summaries", () => {
const body = extractText({
contactsArrayMessage: {
contacts: [
{
displayName: "Alice",
vcard: [
"BEGIN:VCARD",
"VERSION:3.0",
"FN:Alice",
"TEL;TYPE=CELL:+15555550101",
"END:VCARD",
].join("\n"),
},
{},
{},
],
},
} as unknown as import("@whiskeysockets/baileys").proto.IMessage);
expect(body).toBe("<contacts: Alice, +15555550101 +2 more>");
}); });
it("summarizes empty WhatsApp contact cards with a count", () => { it("summarizes empty WhatsApp contact cards with a count", () => {

View File

@@ -762,11 +762,11 @@ function extractContactPlaceholder(
if (!message) return undefined; if (!message) return undefined;
const contact = message.contactMessage ?? undefined; const contact = message.contactMessage ?? undefined;
if (contact) { if (contact) {
const { name, phone } = describeContact({ const { name, phones } = describeContact({
displayName: contact.displayName, displayName: contact.displayName,
vcard: contact.vcard, vcard: contact.vcard,
}); });
return formatContactPlaceholder(name, phone); return formatContactPlaceholder(name, phones);
} }
const contactsArray = message.contactsArrayMessage?.contacts ?? undefined; const contactsArray = message.contactsArrayMessage?.contacts ?? undefined;
if (!contactsArray || contactsArray.length === 0) return undefined; if (!contactsArray || contactsArray.length === 0) return undefined;
@@ -774,7 +774,7 @@ function extractContactPlaceholder(
.map((entry) => .map((entry) =>
describeContact({ displayName: entry.displayName, vcard: entry.vcard }), 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)); .filter((value): value is string => Boolean(value));
return formatContactsPlaceholder(labels, contactsArray.length); return formatContactsPlaceholder(labels, contactsArray.length);
} }
@@ -782,20 +782,17 @@ function extractContactPlaceholder(
function describeContact(input: { function describeContact(input: {
displayName?: string | null; displayName?: string | null;
vcard?: string | null; vcard?: string | null;
}): { name?: string; phone?: string } { }): { name?: string; phones: string[] } {
const displayName = (input.displayName ?? "").trim(); const displayName = (input.displayName ?? "").trim();
const parsed = parseVcard(input.vcard ?? undefined); const parsed = parseVcard(input.vcard ?? undefined);
const name = displayName || parsed.name; const name = displayName || parsed.name;
const phone = parsed.phones[0]; return { name, phones: parsed.phones };
return { name, phone };
} }
function formatContactPlaceholder(name?: string, phone?: string): string { function formatContactPlaceholder(name?: string, phones?: string[]): string {
const parts = [name, phone].filter((value): value is string => const label = formatContactLabel(name, phones);
Boolean(value), if (!label) return "<contact>";
); return `<contact: ${label}>`;
if (parts.length === 0) return "<contact>";
return `<contact: ${parts.join(", ")}>`;
} }
function formatContactsPlaceholder(labels: string[], total: number): string { function formatContactsPlaceholder(labels: string[], total: number): string {
@@ -810,6 +807,27 @@ function formatContactsPlaceholder(labels: string[], total: number): string {
return `<contacts: ${shown.join(", ")}${suffix}>`; return `<contacts: ${shown.join(", ")}${suffix}>`;
} }
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( export function extractLocationData(
rawMessage: proto.IMessage | undefined, rawMessage: proto.IMessage | undefined,
): NormalizedLocation | null { ): NormalizedLocation | null {

View File

@@ -32,7 +32,8 @@ export function parseVcard(vcard?: string): ParsedVcard {
continue; continue;
} }
if (baseKey === "TEL") { if (baseKey === "TEL") {
phones.push(value); const phone = normalizeVcardPhone(value);
if (phone) phones.push(phone);
} }
} }
return { name: nameFromFn ?? nameFromN, phones }; return { name: nameFromFn ?? nameFromN, phones };
@@ -56,3 +57,12 @@ function cleanVcardValue(value: string): string {
function normalizeVcardName(value: string): string { function normalizeVcardName(value: string): string {
return value.replace(/;/g, " ").replace(/\s+/g, " ").trim(); 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;
}