From d454e4b6d0aa196babe678b33e033c65d39b33c1 Mon Sep 17 00:00:00 2001 From: cesar carlos Date: Tue, 28 Jul 2026 15:03:54 +0000 Subject: [PATCH] fix(send): handle document-with-caption and @lid JIDs in mentionAll Document+caption moves DocumentMessage under DocumentWithCaptionMessage but mentionAll still wrote ContextInfo on the nil pointer. Prefer PhoneNumber over LID when building mention JIDs. Closes #114 Co-authored-by: Cursor --- pkg/sendMessage/service/mention.go | 113 ++++++++++++ pkg/sendMessage/service/send_service.go | 110 +----------- .../service/send_service_mention_test.go | 164 ++++++++++++++++++ 3 files changed, 280 insertions(+), 107 deletions(-) create mode 100644 pkg/sendMessage/service/mention.go create mode 100644 pkg/sendMessage/service/send_service_mention_test.go diff --git a/pkg/sendMessage/service/mention.go b/pkg/sendMessage/service/mention.go new file mode 100644 index 00000000..7497cbb8 --- /dev/null +++ b/pkg/sendMessage/service/mention.go @@ -0,0 +1,113 @@ +package send_service + +import ( + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/types" +) + +// participantMentionJID prefers the phone-number JID when available. +// Mentions require @s.whatsapp.net; participant.JID may be @lid. +func participantMentionJID(p types.GroupParticipant) string { + if !p.PhoneNumber.IsEmpty() { + return p.PhoneNumber.String() + } + return p.JID.String() +} + +// setMessageMentionedJIDs writes ContextInfo.MentionedJID for the given message type. +// DocumentMessage may live under DocumentWithCaptionMessage when a caption is set. +func setMessageMentionedJIDs(msg *waE2E.Message, messageType string, mentionedJIDs []string) { + if msg == nil || len(mentionedJIDs) == 0 { + return + } + + switch messageType { + case "ExtendedTextMessage": + if msg.ExtendedTextMessage == nil { + return + } + if msg.ExtendedTextMessage.ContextInfo == nil { + msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ExtendedTextMessage.ContextInfo.MentionedJID = mentionedJIDs + case "ImageMessage": + if msg.ImageMessage == nil { + return + } + if msg.ImageMessage.ContextInfo == nil { + msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ImageMessage.ContextInfo.MentionedJID = mentionedJIDs + case "VideoMessage": + if msg.VideoMessage == nil { + return + } + if msg.VideoMessage.ContextInfo == nil { + msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.VideoMessage.ContextInfo.MentionedJID = mentionedJIDs + case "PtvMessage": + if msg.PtvMessage == nil { + return + } + if msg.PtvMessage.ContextInfo == nil { + msg.PtvMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.PtvMessage.ContextInfo.MentionedJID = mentionedJIDs + case "AudioMessage": + if msg.AudioMessage == nil { + return + } + if msg.AudioMessage.ContextInfo == nil { + msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.AudioMessage.ContextInfo.MentionedJID = mentionedJIDs + case "DocumentMessage": + if msg.DocumentMessage != nil { + if msg.DocumentMessage.ContextInfo == nil { + msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.DocumentMessage.ContextInfo.MentionedJID = mentionedJIDs + } else if msg.DocumentWithCaptionMessage != nil && + msg.DocumentWithCaptionMessage.Message != nil && + msg.DocumentWithCaptionMessage.Message.DocumentMessage != nil { + doc := msg.DocumentWithCaptionMessage.Message.DocumentMessage + if doc.ContextInfo == nil { + doc.ContextInfo = &waE2E.ContextInfo{} + } + doc.ContextInfo.MentionedJID = mentionedJIDs + } + case "PollCreationMessage": + if msg.PollCreationMessage == nil { + return + } + if msg.PollCreationMessage.ContextInfo == nil { + msg.PollCreationMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.PollCreationMessage.ContextInfo.MentionedJID = mentionedJIDs + case "StickerMessage": + if msg.StickerMessage == nil { + return + } + if msg.StickerMessage.ContextInfo == nil { + msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.StickerMessage.ContextInfo.MentionedJID = mentionedJIDs + case "LocationMessage": + if msg.LocationMessage == nil { + return + } + if msg.LocationMessage.ContextInfo == nil { + msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.LocationMessage.ContextInfo.MentionedJID = mentionedJIDs + case "ContactMessage": + if msg.ContactMessage == nil { + return + } + if msg.ContactMessage.ContextInfo == nil { + msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} + } + msg.ContactMessage.ContextInfo.MentionedJID = mentionedJIDs + } +} diff --git a/pkg/sendMessage/service/send_service.go b/pkg/sendMessage/service/send_service.go index b8850089..421aa784 100644 --- a/pkg/sendMessage/service/send_service.go +++ b/pkg/sendMessage/service/send_service.go @@ -2638,117 +2638,13 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. var mentionedJIDs []string for _, participant := range groupInfo.Participants { - mentionedJIDs = append(mentionedJIDs, participant.JID.String()) + mentionedJIDs = append(mentionedJIDs, participantMentionJID(participant)) } - - switch messageType { - case "ExtendedTextMessage": - if msg.ExtendedTextMessage.ContextInfo == nil { - msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.ExtendedTextMessage.ContextInfo.MentionedJID = mentionedJIDs - case "ImageMessage": - if msg.ImageMessage.ContextInfo == nil { - msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.ImageMessage.ContextInfo.MentionedJID = mentionedJIDs - case "VideoMessage": - if msg.VideoMessage.ContextInfo == nil { - msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.VideoMessage.ContextInfo.MentionedJID = mentionedJIDs - case "PtvMessage": - if msg.PtvMessage.ContextInfo == nil { - msg.PtvMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.PtvMessage.ContextInfo.MentionedJID = mentionedJIDs - case "AudioMessage": - if msg.AudioMessage.ContextInfo == nil { - msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.AudioMessage.ContextInfo.MentionedJID = mentionedJIDs - case "DocumentMessage": - if msg.DocumentMessage.ContextInfo == nil { - msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.DocumentMessage.ContextInfo.MentionedJID = mentionedJIDs - case "PollCreationMessage": - if msg.PollCreationMessage.ContextInfo == nil { - msg.PollCreationMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.PollCreationMessage.ContextInfo.MentionedJID = mentionedJIDs - case "StickerMessage": - if msg.StickerMessage.ContextInfo == nil { - msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.StickerMessage.ContextInfo.MentionedJID = mentionedJIDs - case "LocationMessage": - if msg.LocationMessage.ContextInfo == nil { - msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.LocationMessage.ContextInfo.MentionedJID = mentionedJIDs - case "ContactMessage": - if msg.ContactMessage.ContextInfo == nil { - msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.ContactMessage.ContextInfo.MentionedJID = mentionedJIDs - } - + setMessageMentionedJIDs(msg, messageType, mentionedJIDs) } if len(data.MentionedJID) > 0 { - switch messageType { - case "ExtendedTextMessage": - if msg.ExtendedTextMessage.ContextInfo == nil { - msg.ExtendedTextMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.ExtendedTextMessage.ContextInfo.MentionedJID = data.MentionedJID - case "ImageMessage": - if msg.ImageMessage.ContextInfo == nil { - msg.ImageMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.ImageMessage.ContextInfo.MentionedJID = data.MentionedJID - case "VideoMessage": - if msg.VideoMessage.ContextInfo == nil { - msg.VideoMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.VideoMessage.ContextInfo.MentionedJID = data.MentionedJID - case "PtvMessage": - if msg.PtvMessage.ContextInfo == nil { - msg.PtvMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.PtvMessage.ContextInfo.MentionedJID = data.MentionedJID - case "AudioMessage": - if msg.AudioMessage.ContextInfo == nil { - msg.AudioMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.AudioMessage.ContextInfo.MentionedJID = data.MentionedJID - case "DocumentMessage": - if msg.DocumentMessage.ContextInfo == nil { - msg.DocumentMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.DocumentMessage.ContextInfo.MentionedJID = data.MentionedJID - case "PollCreationMessage": - if msg.PollCreationMessage.ContextInfo == nil { - msg.PollCreationMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.PollCreationMessage.ContextInfo.MentionedJID = data.MentionedJID - case "StickerMessage": - if msg.StickerMessage.ContextInfo == nil { - msg.StickerMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.StickerMessage.ContextInfo.MentionedJID = data.MentionedJID - case "LocationMessage": - if msg.LocationMessage.ContextInfo == nil { - msg.LocationMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.LocationMessage.ContextInfo.MentionedJID = data.MentionedJID - case "ContactMessage": - if msg.ContactMessage.ContextInfo == nil { - msg.ContactMessage.ContextInfo = &waE2E.ContextInfo{} - } - msg.ContactMessage.ContextInfo.MentionedJID = data.MentionedJID - } + setMessageMentionedJIDs(msg, messageType, data.MentionedJID) } } diff --git a/pkg/sendMessage/service/send_service_mention_test.go b/pkg/sendMessage/service/send_service_mention_test.go new file mode 100644 index 00000000..41ac3240 --- /dev/null +++ b/pkg/sendMessage/service/send_service_mention_test.go @@ -0,0 +1,164 @@ +package send_service + +import ( + "testing" + + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/types" + "google.golang.org/protobuf/proto" +) + +func TestParticipantMentionJID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + p types.GroupParticipant + want string + }{ + { + name: "prefers_phone_number_over_lid", + p: types.GroupParticipant{ + JID: types.NewJID("123456789012345", types.HiddenUserServer), + PhoneNumber: types.NewJID("5511999999999", types.DefaultUserServer), + }, + want: "5511999999999@s.whatsapp.net", + }, + { + name: "falls_back_to_jid_when_phone_empty", + p: types.GroupParticipant{ + JID: types.NewJID("5511888888888", types.DefaultUserServer), + }, + want: "5511888888888@s.whatsapp.net", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := participantMentionJID(tt.p) + if got != tt.want { + t.Fatalf("participantMentionJID() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestSetMessageMentionedJIDs(t *testing.T) { + t.Parallel() + + mentioned := []string{"5511999999999@s.whatsapp.net", "5511888888888@s.whatsapp.net"} + + tests := []struct { + name string + messageType string + msg *waE2E.Message + wantFrom func(msg *waE2E.Message) []string + }{ + { + name: "doc_without_caption", + messageType: "DocumentMessage", + msg: &waE2E.Message{ + DocumentMessage: &waE2E.DocumentMessage{ + Caption: proto.String(""), + }, + }, + wantFrom: func(msg *waE2E.Message) []string { + return msg.DocumentMessage.ContextInfo.MentionedJID + }, + }, + { + name: "doc_with_caption", + messageType: "DocumentMessage", + msg: &waE2E.Message{ + DocumentWithCaptionMessage: &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + DocumentMessage: &waE2E.DocumentMessage{ + Caption: proto.String("hello @all"), + }, + }, + }, + }, + wantFrom: func(msg *waE2E.Message) []string { + return msg.DocumentWithCaptionMessage.Message.DocumentMessage.ContextInfo.MentionedJID + }, + }, + { + name: "image", + messageType: "ImageMessage", + msg: &waE2E.Message{ + ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String("caption"), + }, + }, + wantFrom: func(msg *waE2E.Message) []string { + return msg.ImageMessage.ContextInfo.MentionedJID + }, + }, + { + name: "video", + messageType: "VideoMessage", + msg: &waE2E.Message{ + VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String("caption"), + }, + }, + wantFrom: func(msg *waE2E.Message) []string { + return msg.VideoMessage.ContextInfo.MentionedJID + }, + }, + { + name: "extended_text", + messageType: "ExtendedTextMessage", + msg: &waE2E.Message{ + ExtendedTextMessage: &waE2E.ExtendedTextMessage{ + Text: proto.String("@todos"), + }, + }, + wantFrom: func(msg *waE2E.Message) []string { + return msg.ExtendedTextMessage.ContextInfo.MentionedJID + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + setMessageMentionedJIDs(tt.msg, tt.messageType, mentioned) + got := tt.wantFrom(tt.msg) + if len(got) != len(mentioned) { + t.Fatalf("MentionedJID len = %d, want %d (got %#v)", len(got), len(mentioned), got) + } + for i := range mentioned { + if got[i] != mentioned[i] { + t.Fatalf("MentionedJID[%d] = %q, want %q", i, got[i], mentioned[i]) + } + } + }) + } +} + +func TestSetMessageMentionedJIDs_DocWithCaptionDoesNotPanic(t *testing.T) { + t.Parallel() + + // Reproduces the #114 failure mode: DocumentMessage is nil after wrap. + msg := &waE2E.Message{ + DocumentWithCaptionMessage: &waE2E.FutureProofMessage{ + Message: &waE2E.Message{ + DocumentMessage: &waE2E.DocumentMessage{ + Caption: proto.String("doc caption"), + }, + }, + }, + } + + setMessageMentionedJIDs(msg, "DocumentMessage", []string{"5511999999999@s.whatsapp.net"}) + + got := msg.DocumentWithCaptionMessage.Message.DocumentMessage.ContextInfo.MentionedJID + if len(got) != 1 || got[0] != "5511999999999@s.whatsapp.net" { + t.Fatalf("unexpected MentionedJID: %#v", got) + } + if msg.DocumentMessage != nil { + t.Fatal("DocumentMessage should remain nil for captioned documents") + } +}