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
51 changes: 48 additions & 3 deletions pkg/message/service/message_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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())
}
}

Expand Down Expand Up @@ -218,18 +225,53 @@ 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 := ""

if data.IsAudio {
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
Expand All @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
46 changes: 46 additions & 0 deletions pkg/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package utils

import (
"testing"

whatsmeow_types "go.mau.fi/whatsmeow/types"
)

func TestCreateJID(t *testing.T) {
Expand Down Expand Up @@ -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
Expand Down