diff --git a/pkg/message/service/message_service.go b/pkg/message/service/message_service.go index b2b21c72..324ea61a 100644 --- a/pkg/message/service/message_service.go +++ b/pkg/message/service/message_service.go @@ -52,6 +52,10 @@ type ChatPresenceStruct struct { Number string `json:"number"` State string `json:"state"` IsAudio bool `json:"isAudio"` + // Delay, in milliseconds, keeps the typing/recording indicator alive by + // re-sending the chat presence every few seconds. Capped at 60s. 0 sends + // the indicator once. + Delay int `json:"delay,omitempty"` } type MarkReadStruct struct { @@ -136,6 +140,9 @@ func (m *messageService) React(data *ReactStruct, instance *instance_model.Insta m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) return nil, errors.New("invalid phone number") } + // Reactions travel as raw protocol nodes, so the target JID must be + // canonical (digits only, no leading "+") or WhatsApp drops them silently. + recipient = utils.CanonicalJID(recipient) if data.Id == "" { m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Missing Id in Payload", instance.Id) @@ -161,7 +168,7 @@ func (m *messageService) React(data *ReactStruct, instance *instance_model.Insta if data.Participant != "" { participantJID, ok := utils.ParseJID(data.Participant) if ok { - messageKey.Participant = proto.String(participantJID.String()) + messageKey.Participant = proto.String(utils.CanonicalJID(participantJID).String()) } } @@ -218,6 +225,9 @@ func (m *messageService) ChatPresence(data *ChatPresenceStruct, instance *instan m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) return "", errors.New("invalid phone number") } + // Chat presence is delivered as a raw protocol node, so the target JID must + // be canonical (digits only, no leading "+") or WhatsApp drops it silently. + recipient = utils.CanonicalJID(recipient) media := "" @@ -225,11 +235,43 @@ func (m *messageService) ChatPresence(data *ChatPresenceStruct, instance *instan media = "audio" } - err = client.SendChatPresence(context.Background(), recipient, types.ChatPresence(data.State), types.ChatPresenceMedia(media)) - if err != nil { + // WhatsApp only forwards chat presence (typing/recording) while the sender + // is marked online, so announce availability before sending the indicator. + if err = client.SendPresence(context.Background(), types.PresenceAvailable); err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] failed to send available presence: %v", instance.Id, err) + } + + presence := types.ChatPresence(data.State) + presenceMedia := types.ChatPresenceMedia(media) + + if err = client.SendChatPresence(context.Background(), recipient, presence, presenceMedia); err != nil { return "", err } + // Optionally sustain the indicator by re-sending it periodically, since a + // single chat presence node expires after a few seconds on the client. + if data.Delay > 0 { + const refreshInterval = 5 * time.Second + const maxDelay = 60 * time.Second + + remaining := time.Duration(data.Delay) * time.Millisecond + if remaining > maxDelay { + remaining = maxDelay + } + + for remaining > refreshInterval { + time.Sleep(refreshInterval) + remaining -= refreshInterval + if err = client.SendChatPresence(context.Background(), recipient, presence, presenceMedia); err != nil { + m.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] failed to refresh chat presence: %v", instance.Id, err) + break + } + } + if remaining > 0 { + time.Sleep(remaining) + } + } + m.loggerWrapper.GetLogger(instance.Id).LogInfo("Message sent to %s", data.Number) return ts.String(), nil @@ -248,6 +290,9 @@ func (m *messageService) MarkRead(data *MarkReadStruct, instance *instance_model m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error validating message fields", instance.Id) return "", errors.New("invalid phone number") } + // Read receipts are sent as raw protocol nodes, so the target JID must be + // canonical (digits only, no leading "+") or WhatsApp drops them silently. + jid = utils.CanonicalJID(jid) err = client.MarkRead(context.Background(), data.Id, time.Now(), jid, jid) if err != nil { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index e7593477..83fa01b3 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -215,6 +215,21 @@ func ParseJID(arg string) (whatsmeow_types.JID, bool) { return recipient, true } +// CanonicalJID returns a copy of the given JID with any leading "+" removed +// from the user part, producing a digits-only WhatsApp JID. +// +// CreateJID/ParseJID intentionally keep the leading "+" (e.g. +// "+5541999999999@s.whatsapp.net") because some flows such as IsOnWhatsApp +// rely on it. However, raw protocol nodes (chat presence, reactions and read +// receipts) are sent without the device resolution / usync normalization that +// happens during normal message sending, so the "+"-prefixed user survives at +// the wire level and WhatsApp silently fails to route the node. Stripping the +// "+" yields the canonical target those raw nodes expect. +func CanonicalJID(jid whatsmeow_types.JID) whatsmeow_types.JID { + jid.User = strings.TrimPrefix(jid.User, "+") + return jid +} + func CreateHTTPProxy(httpHost, httpPort, user, password string) (func(*http.Request) (*url.URL, error), error) { address := fmt.Sprintf("http://%s:%s@%s:%s", user, password, httpHost, httpPort) diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 25a99792..aa5d2f8b 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -2,6 +2,8 @@ package utils import ( "testing" + + whatsmeow_types "go.mau.fi/whatsmeow/types" ) func TestCreateJID(t *testing.T) { @@ -171,6 +173,50 @@ func TestCreateJID(t *testing.T) { } } +func TestCanonicalJID(t *testing.T) { + tests := []struct { + name string + input string + server string + expected string + }{ + { + name: "Strips leading + from user part", + input: "+15551234567", + server: whatsmeow_types.DefaultUserServer, + expected: "15551234567@s.whatsapp.net", + }, + { + name: "Leaves already-canonical user untouched", + input: "15551234567", + server: whatsmeow_types.DefaultUserServer, + expected: "15551234567@s.whatsapp.net", + }, + { + name: "Only strips a single leading +", + input: "+5541999999999", + server: whatsmeow_types.DefaultUserServer, + expected: "5541999999999@s.whatsapp.net", + }, + { + name: "Group JID user is unaffected", + input: "120363123456789012", + server: whatsmeow_types.GroupServer, + expected: "120363123456789012@g.us", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jid := whatsmeow_types.NewJID(tt.input, tt.server) + result := CanonicalJID(jid) + if result.String() != tt.expected { + t.Errorf("For input %q, expected %q, but got %q", tt.input, tt.expected, result.String()) + } + }) + } +} + func TestFormatMXOrARNumber(t *testing.T) { tests := []struct { name string