Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 74 additions & 9 deletions whatsapp-bridge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,30 @@ func extractTextContent(msg *waProto.Message) string {
return doc.GetCaption()
}

// Shared contact cards (vCards) carry no URL/MediaKey — the vCard text is
// embedded directly in the message rather than fetched from the CDN — so
// without this branch extractMediaInfo also returns "" for them and the
// message is silently dropped at the "no content and no media" gate in
// handleMessage: shared contacts were vanishing entirely.
if contact := msg.GetContactMessage(); contact != nil {
if body := formatContactContent(contact.GetDisplayName(), contact.GetVcard()); body != "" {
return "📇 " + body
}
}
if contacts := msg.GetContactsArrayMessage(); contacts != nil {
if list := contacts.GetContacts(); len(list) > 0 {
names := make([]string, 0, len(list))
for _, c := range list {
if body := formatContactContent(c.GetDisplayName(), c.GetVcard()); body != "" {
names = append(names, body)
}
}
if len(names) > 0 {
return fmt.Sprintf("📇 %d contacts shared: %s", len(names), strings.Join(names, "; "))
}
}
}

// WhatsApp Business templates arrive hydrated — body lives in
// HydratedTemplate.HydratedContentText. Without this branch every
// template-sent message (e.g. WABA Connect Hrms_* notifications)
Expand Down Expand Up @@ -1061,6 +1085,49 @@ func extractTextContent(msg *waProto.Message) string {
return ""
}

// formatContactContent renders a shared contact card as searchable text:
// the display name plus every phone number in the vCard body. All numbers are
// kept (not just the first) because the vCard is the only copy we ever get —
// there is no CDN payload to re-download later. Returns "" when there is
// nothing usable (no display name and no TEL line).
func formatContactContent(displayName, vcard string) string {
phones := extractVCardPhones(vcard)
if displayName == "" && len(phones) == 0 {
return ""
}
if len(phones) > 0 {
return fmt.Sprintf("%s (%s)", displayName, strings.Join(phones, ", "))
}
return displayName
}

// extractVCardPhones returns the values of all TEL lines in a vCard blob,
// e.g. "TEL;type=CELL;waid=6281234567890:+62 812-3456-7890" -> "+62 812-3456-7890".
// iPhone-exported vCards wrap properties in groups ("item1.TEL;...:+62 ..."),
// so the property name is compared after stripping any group prefix.
func extractVCardPhones(vcard string) []string {
var phones []string
for _, line := range strings.Split(vcard, "\n") {
line = strings.TrimSpace(line)
prop := line
if i := strings.IndexAny(prop, ";:"); i != -1 {
prop = prop[:i]
}
if dot := strings.LastIndex(prop, "."); dot != -1 {
prop = prop[dot+1:]
}
if !strings.EqualFold(prop, "TEL") {
continue
}
if idx := strings.LastIndex(line, ":"); idx != -1 {
if phone := strings.TrimSpace(line[idx+1:]); phone != "" {
phones = append(phones, phone)
}
}
}
return phones
}

// SendMessageResponse represents the response for the send message API
type SendMessageResponse struct {
Success bool `json:"success"`
Expand Down Expand Up @@ -3399,15 +3466,13 @@ func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, his
continue
}

// Extract text content
var content string
if msg.Message.Message != nil {
if conv := msg.Message.Message.GetConversation(); conv != "" {
content = conv
} else if ext := msg.Message.Message.GetExtendedTextMessage(); ext != nil {
content = ext.GetText()
}
}
// Extract text content via the shared extractor — the same
// one the live path uses — so contact cards, media captions
// and hydrated templates are surfaced on the history-sync
// path too. This inline block previously handled only
// Conversation/ExtendedText and silently dropped everything
// else arriving through history sync.
content := extractTextContent(msg.Message.Message)

// Extract media info - pass message timestamp + ID for unique filenames
var mediaType, filename, url string
Expand Down
80 changes: 80 additions & 0 deletions whatsapp-bridge/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,86 @@ func TestExtractTextContent_SurfacesMediaCaptions(t *testing.T) {
msg: &waProto.Message{ImageMessage: &waProto.ImageMessage{}},
want: "",
},
{
name: "ContactMessage with phone number in vCard",
msg: &waProto.Message{
ContactMessage: &waProto.ContactMessage{
DisplayName: proto.String("John Doe"),
Vcard: proto.String("BEGIN:VCARD\nVERSION:3.0\nN:;John Doe;;;\nFN:John Doe\n" +
"TEL;type=CELL;waid=6281234567890:+62 812-3456-7890\nEND:VCARD"),
},
},
want: "📇 John Doe (+62 812-3456-7890)",
},
{
name: "ContactMessage with iPhone-style grouped TEL property",
msg: &waProto.Message{
ContactMessage: &waProto.ContactMessage{
DisplayName: proto.String("Adie Taxi"),
Vcard: proto.String("BEGIN:VCARD\nVERSION:3.0\nFN:Adie Taxi\n" +
"item1.TEL;waid=6281338417222:+62 813-3841-7222\nitem1.X-ABLabel:Mobil\nEND:VCARD"),
},
},
want: "📇 Adie Taxi (+62 813-3841-7222)",
},
{
name: "ContactMessage with multiple TEL lines keeps every number",
msg: &waProto.Message{
ContactMessage: &waProto.ContactMessage{
DisplayName: proto.String("John Doe"),
Vcard: proto.String("BEGIN:VCARD\r\nVERSION:3.0\r\nFN:John Doe\r\n" +
"TEL;type=CELL:+62 812-3456-7890\r\nTEL;type=WORK:+47 22 33 44 55\r\nEND:VCARD"),
},
},
want: "📇 John Doe (+62 812-3456-7890, +47 22 33 44 55)",
},
{
name: "ContactMessage without a TEL line falls back to name only",
msg: &waProto.Message{
ContactMessage: &waProto.ContactMessage{
DisplayName: proto.String("Jane Doe"),
Vcard: proto.String("BEGIN:VCARD\nVERSION:3.0\nFN:Jane Doe\nEND:VCARD"),
},
},
want: "📇 Jane Doe",
},
{
name: "ContactMessage with neither name nor TEL returns empty (no placeholder row)",
msg: &waProto.Message{
ContactMessage: &waProto.ContactMessage{
Vcard: proto.String("BEGIN:VCARD\nVERSION:3.0\nEND:VCARD"),
},
},
want: "",
},
{
name: "ContactsArrayMessage with multiple shared contacts",
msg: &waProto.Message{
ContactsArrayMessage: &waProto.ContactsArrayMessage{
DisplayName: proto.String("2 contacts"),
Contacts: []*waProto.ContactMessage{
{
DisplayName: proto.String("John Doe"),
Vcard: proto.String("BEGIN:VCARD\nTEL;waid=1:+1 111\nEND:VCARD"),
},
{
DisplayName: proto.String("Jane Doe"),
Vcard: proto.String("BEGIN:VCARD\nFN:Jane Doe\nEND:VCARD"),
},
},
},
},
want: "📇 2 contacts shared: John Doe (+1 111); Jane Doe",
},
{
name: "ContactsArrayMessage with no contacts returns empty (no placeholder row)",
msg: &waProto.Message{
ContactsArrayMessage: &waProto.ContactsArrayMessage{
DisplayName: proto.String("0 contacts"),
},
},
want: "",
},
{
name: "Nil message returns empty",
msg: nil,
Expand Down