Skip to content

Commit c97bb25

Browse files
committed
feat(whatsapp): implement recipient JID parsing and enhance message sending logging
1 parent 998f97a commit c97bb25

2 files changed

Lines changed: 138 additions & 33 deletions

File tree

internal/messaging/whatsapp_service.go

Lines changed: 101 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type WhatsAppService struct {
4242
done chan struct{}
4343
mu sync.RWMutex
4444
stopped bool
45+
lidMu sync.RWMutex
46+
lidByPhone map[string]string // canonical phone -> JID string for LID addressing
4547
}
4648

4749
// NewWhatsAppService creates a new WhatsAppService wrapping the given WhatsAppSender.
@@ -51,6 +53,7 @@ func NewWhatsAppService(client whatsapp.WhatsAppSender) *WhatsAppService {
5153
receipts: make(chan models.Receipt, DefaultChannelBufferSize),
5254
responses: make(chan models.Response, DefaultChannelBufferSize),
5355
done: make(chan struct{}),
56+
lidByPhone: make(map[string]string),
5457
}
5558

5659
// If the client is a full Client (not just an interface), store it for event handling
@@ -64,6 +67,33 @@ func NewWhatsAppService(client whatsapp.WhatsAppSender) *WhatsAppService {
6467
return service
6568
}
6669

70+
func (s *WhatsAppService) storeRecipientLID(canonicalPhone string, lidJID string) {
71+
if canonicalPhone == "" || lidJID == "" {
72+
return
73+
}
74+
75+
s.lidMu.Lock()
76+
defer s.lidMu.Unlock()
77+
78+
if existing, ok := s.lidByPhone[canonicalPhone]; ok && existing == lidJID {
79+
return
80+
}
81+
82+
s.lidByPhone[canonicalPhone] = lidJID
83+
slog.Debug("WhatsAppService stored LID mapping", "phone", canonicalPhone, "lid", lidJID)
84+
}
85+
86+
func (s *WhatsAppService) resolveRecipientAddress(canonicalPhone string) string {
87+
s.lidMu.RLock()
88+
defer s.lidMu.RUnlock()
89+
90+
if lidJID, ok := s.lidByPhone[canonicalPhone]; ok && lidJID != "" {
91+
return lidJID
92+
}
93+
94+
return canonicalPhone
95+
}
96+
6797
// ValidateAndCanonicalizeRecipient validates and canonicalizes a WhatsApp phone number.
6898
// It removes all non-numeric characters and validates the result has at least 6 digits.
6999
func (s *WhatsAppService) ValidateAndCanonicalizeRecipient(recipient string) (string, error) {
@@ -148,10 +178,15 @@ func (s *WhatsAppService) SendMessage(ctx context.Context, to string, body strin
148178
return err
149179
}
150180

151-
slog.Debug("WhatsAppService SendMessage invoked", "to", canonicalTo, "body_length", len(body))
152-
err = s.client.SendMessage(ctx, canonicalTo, body)
181+
sendTo := s.resolveRecipientAddress(canonicalTo)
182+
if sendTo != canonicalTo {
183+
slog.Debug("WhatsAppService SendMessage using LID recipient", "to", sendTo, "phone", canonicalTo)
184+
} else {
185+
slog.Debug("WhatsAppService SendMessage invoked", "to", canonicalTo, "body_length", len(body))
186+
}
187+
err = s.client.SendMessage(ctx, sendTo, body)
153188
if err != nil {
154-
slog.Error("WhatsAppService SendMessage error", "error", err, "to", canonicalTo)
189+
slog.Error("WhatsAppService SendMessage error", "error", err, "to", sendTo, "phone", canonicalTo)
155190
return err
156191
}
157192

@@ -183,21 +218,23 @@ func (s *WhatsAppService) SendPromptWithButtons(ctx context.Context, to string,
183218
return err
184219
}
185220

221+
sendTo := s.resolveRecipientAddress(canonicalTo)
222+
186223
var sendErr error
187224
if sender, ok := s.client.(promptButtonsClient); ok {
188-
slog.Debug("WhatsAppService SendPromptWithButtons using poll message", "to", canonicalTo)
189-
sendErr = sender.SendPromptButtons(ctx, canonicalTo, body)
225+
slog.Debug("WhatsAppService SendPromptWithButtons using poll message", "to", sendTo, "phone", canonicalTo)
226+
sendErr = sender.SendPromptButtons(ctx, sendTo, body)
190227
} else {
191-
slog.Debug("WhatsAppService SendPromptWithButtons falling back to text message", "to", canonicalTo)
192-
sendErr = s.client.SendMessage(ctx, canonicalTo, body)
228+
slog.Debug("WhatsAppService SendPromptWithButtons falling back to text message", "to", sendTo, "phone", canonicalTo)
229+
sendErr = s.client.SendMessage(ctx, sendTo, body)
193230
}
194231

195232
if sendErr != nil {
196-
slog.Error("WhatsAppService SendPromptWithButtons error", "error", sendErr, "to", canonicalTo)
233+
slog.Error("WhatsAppService SendPromptWithButtons error", "error", sendErr, "to", sendTo, "phone", canonicalTo)
197234
return sendErr
198235
}
199236
s.safeEmitReceipt(models.Receipt{To: canonicalTo, Status: models.MessageStatusSent, Time: time.Now().Unix()})
200-
slog.Info("WhatsAppService prompt with poll sent and receipt emitted", "to", canonicalTo)
237+
slog.Info("WhatsAppService prompt with poll sent and receipt emitted", "to", sendTo, "phone", canonicalTo)
201238
return nil
202239
}
203240

@@ -217,23 +254,25 @@ func (s *WhatsAppService) SendIntensityAdjustmentPoll(ctx context.Context, to st
217254
return err
218255
}
219256

257+
sendTo := s.resolveRecipientAddress(canonicalTo)
258+
220259
var sendErr error
221260
if sender, ok := s.client.(promptButtonsClient); ok {
222-
slog.Debug("WhatsAppService SendIntensityAdjustmentPoll using poll message", "to", canonicalTo, "currentIntensity", currentIntensity)
223-
sendErr = sender.SendIntensityAdjustmentPoll(ctx, canonicalTo, currentIntensity)
261+
slog.Debug("WhatsAppService SendIntensityAdjustmentPoll using poll message", "to", sendTo, "phone", canonicalTo, "currentIntensity", currentIntensity)
262+
sendErr = sender.SendIntensityAdjustmentPoll(ctx, sendTo, currentIntensity)
224263
} else {
225264
// Fallback: send text message asking about intensity
226-
slog.Debug("WhatsAppService SendIntensityAdjustmentPoll falling back to text message", "to", canonicalTo)
227-
sendErr = s.client.SendMessage(ctx, canonicalTo, "How's the intensity? Reply with 'low', 'normal', or 'high'.")
265+
slog.Debug("WhatsAppService SendIntensityAdjustmentPoll falling back to text message", "to", sendTo, "phone", canonicalTo)
266+
sendErr = s.client.SendMessage(ctx, sendTo, "How's the intensity? Reply with 'low', 'normal', or 'high'.")
228267
}
229268

230269
if sendErr != nil {
231-
slog.Error("WhatsAppService SendIntensityAdjustmentPoll error", "error", sendErr, "to", canonicalTo)
270+
slog.Error("WhatsAppService SendIntensityAdjustmentPoll error", "error", sendErr, "to", sendTo, "phone", canonicalTo)
232271
return sendErr
233272
}
234273

235274
s.safeEmitReceipt(models.Receipt{To: canonicalTo, Status: models.MessageStatusSent, Time: time.Now().Unix()})
236-
slog.Info("WhatsAppService intensity adjustment poll sent and receipt emitted", "to", canonicalTo, "currentIntensity", currentIntensity)
275+
slog.Info("WhatsAppService intensity adjustment poll sent and receipt emitted", "to", sendTo, "phone", canonicalTo, "currentIntensity", currentIntensity)
237276
return nil
238277
}
239278

@@ -253,9 +292,11 @@ func (s *WhatsAppService) SendTypingIndicator(ctx context.Context, to string, ty
253292
return err
254293
}
255294

256-
slog.Debug("WhatsAppService SendTypingIndicator invoked", "to", canonicalTo, "typing", typing)
257-
if err := s.client.SendTypingIndicator(ctx, canonicalTo, typing); err != nil {
258-
slog.Warn("WhatsAppService SendTypingIndicator error", "error", err, "to", canonicalTo, "typing", typing)
295+
sendTo := s.resolveRecipientAddress(canonicalTo)
296+
297+
slog.Debug("WhatsAppService SendTypingIndicator invoked", "to", sendTo, "phone", canonicalTo, "typing", typing)
298+
if err := s.client.SendTypingIndicator(ctx, sendTo, typing); err != nil {
299+
slog.Warn("WhatsAppService SendTypingIndicator error", "error", err, "to", sendTo, "phone", canonicalTo, "typing", typing)
259300
return err
260301
}
261302

@@ -364,10 +405,47 @@ func (s *WhatsAppService) handleIncomingMessage(evt *events.Message) {
364405
return
365406
}
366407

367-
// Convert JID to E.164 format (remove @s.whatsapp.net suffix)
368-
fromNumber := strings.TrimSuffix(evt.Info.Sender.User, "")
369-
if !strings.HasPrefix(fromNumber, "+") {
370-
fromNumber = "+" + fromNumber
408+
// Convert JID to E.164 format and store LID mapping if available
409+
fromNumber := ""
410+
canonicalPhone := ""
411+
412+
sender := evt.Info.Sender.ToNonAD()
413+
senderAlt := evt.Info.SenderAlt.ToNonAD()
414+
415+
var phoneJID types.JID
416+
var lidJID types.JID
417+
418+
if evt.Info.AddressingMode == types.AddressingModeLID {
419+
lidJID = sender
420+
phoneJID = senderAlt
421+
} else {
422+
phoneJID = sender
423+
lidJID = senderAlt
424+
}
425+
426+
if phoneJID.User == "" || phoneJID.Server != types.DefaultUserServer {
427+
phoneJID = sender
428+
}
429+
430+
if phoneJID.User != "" {
431+
fromNumber = phoneJID.User
432+
if !strings.HasPrefix(fromNumber, "+") {
433+
fromNumber = "+" + fromNumber
434+
}
435+
}
436+
437+
if fromNumber == "" {
438+
fromNumber = sender.User
439+
if !strings.HasPrefix(fromNumber, "+") {
440+
fromNumber = "+" + fromNumber
441+
}
442+
}
443+
444+
if canonical, err := s.ValidateAndCanonicalizeRecipient(fromNumber); err == nil {
445+
canonicalPhone = canonical
446+
if lidJID.User != "" && lidJID.Server == types.HiddenUserServer {
447+
s.storeRecipientLID(canonicalPhone, lidJID.String())
448+
}
371449
}
372450

373451
response := models.Response{
@@ -376,7 +454,7 @@ func (s *WhatsAppService) handleIncomingMessage(evt *events.Message) {
376454
Time: evt.Info.Timestamp.Unix(),
377455
}
378456

379-
slog.Debug("WhatsAppService processing incoming message", "from", response.From, "body_length", len(response.Body))
457+
slog.Debug("WhatsAppService processing incoming message", "from", response.From, "body_length", len(response.Body), "addressingMode", evt.Info.AddressingMode, "canonicalPhone", canonicalPhone)
380458

381459
// Send to responses channel (non-blocking)
382460
s.safeEmitResponse(response)

internal/whatsapp/whatsapp.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,15 @@ func (c *Client) SendMessage(ctx context.Context, to string, body string) error
262262
// Note: Phone number validation and canonicalization should be handled by the service layer.
263263
// The recipient should already be validated and canonicalized when this method is called.
264264

265-
slog.Debug("Sending WhatsApp message", "to", to, "body_length", len(body))
266-
jid := types.NewJID(to, JIDSuffix)
265+
jid, err := parseRecipientJID(to)
266+
if err != nil {
267+
return err
268+
}
269+
270+
slog.Debug("Sending WhatsApp message", "to", jid.String(), "body_length", len(body))
267271
msg := &waE2E.Message{Conversation: &body}
268272

269-
_, err := c.waClient.SendMessage(ctx, jid, msg)
273+
_, err = c.waClient.SendMessage(ctx, jid, msg)
270274
if err != nil {
271275
slog.Error("Failed to send WhatsApp message", "error", err, "to", to)
272276
return fmt.Errorf("failed to send message to %s: %w", to, err)
@@ -292,12 +296,16 @@ func (c *Client) SendPromptButtons(ctx context.Context, to string, body string)
292296
return fmt.Errorf("message body cannot be empty")
293297
}
294298

295-
slog.Debug("Sending WhatsApp prompt with poll", "to", to, "body_length", len(body))
296-
jid := types.NewJID(to, JIDSuffix)
299+
jid, err := parseRecipientJID(to)
300+
if err != nil {
301+
return err
302+
}
303+
304+
slog.Debug("Sending WhatsApp prompt with poll", "to", jid.String(), "body_length", len(body))
297305

298306
// First send the main prompt message
299307
mainMsg := &waE2E.Message{Conversation: &body}
300-
_, err := c.waClient.SendMessage(ctx, jid, mainMsg)
308+
_, err = c.waClient.SendMessage(ctx, jid, mainMsg)
301309
if err != nil {
302310
slog.Error("Failed to send WhatsApp prompt message", "error", err, "to", to)
303311
return fmt.Errorf("failed to send prompt message to %s: %w", to, err)
@@ -340,8 +348,12 @@ func (c *Client) SendIntensityAdjustmentPoll(ctx context.Context, to string, cur
340348
return fmt.Errorf("invalid intensity level: %s", currentIntensity)
341349
}
342350

343-
slog.Debug("Sending intensity adjustment poll", "to", to, "currentIntensity", currentIntensity, "options", options)
344-
jid := types.NewJID(to, JIDSuffix)
351+
jid, err := parseRecipientJID(to)
352+
if err != nil {
353+
return err
354+
}
355+
356+
slog.Debug("Sending intensity adjustment poll", "to", jid.String(), "currentIntensity", currentIntensity, "options", options)
345357

346358
// Send poll using Whatsmeow's built-in helper
347359
pollMsg := c.waClient.BuildPollCreation(
@@ -350,7 +362,7 @@ func (c *Client) SendIntensityAdjustmentPoll(ctx context.Context, to string, cur
350362
1, // single-select
351363
)
352364

353-
_, err := c.waClient.SendMessage(ctx, jid, pollMsg)
365+
_, err = c.waClient.SendMessage(ctx, jid, pollMsg)
354366
if err != nil {
355367
slog.Error("Failed to send intensity adjustment poll", "error", err, "to", to)
356368
return fmt.Errorf("failed to send intensity poll to %s: %w", to, err)
@@ -375,7 +387,10 @@ func (c *Client) SendTypingIndicator(ctx context.Context, to string, typing bool
375387
default:
376388
}
377389

378-
jid := types.NewJID(to, JIDSuffix)
390+
jid, err := parseRecipientJID(to)
391+
if err != nil {
392+
return err
393+
}
379394
presence := types.ChatPresencePaused
380395
if typing {
381396
presence = types.ChatPresenceComposing
@@ -390,6 +405,18 @@ func (c *Client) SendTypingIndicator(ctx context.Context, to string, typing bool
390405
return nil
391406
}
392407

408+
func parseRecipientJID(to string) (types.JID, error) {
409+
if strings.Contains(to, "@") {
410+
jid, err := types.ParseJID(to)
411+
if err != nil {
412+
return types.JID{}, fmt.Errorf("invalid recipient JID %q: %w", to, err)
413+
}
414+
return jid.ToNonAD(), nil
415+
}
416+
417+
return types.NewJID(to, JIDSuffix), nil
418+
}
419+
393420
// GetClient returns the underlying whatsmeow client for event handling
394421
func (c *Client) GetClient() *whatsmeow.Client {
395422
return c.waClient

0 commit comments

Comments
 (0)