diff --git a/message/interactive.go b/message/interactive.go index 01c42f3..a36e638 100644 --- a/message/interactive.go +++ b/message/interactive.go @@ -15,18 +15,15 @@ func NewInteractiveReplyButtons(to string, opts InteractiveReplyButtonsOpts) Env } } - return Envelope{ - MessagingProduct: "whatsapp", - RecipientType: "individual", - To: to, - Type: "interactive", - Interactive: &Interactive{ - Header: opts.Header, - Body: TextObject{Text: opts.Body}, - Type: "button", - Action: Action{ - Buttons: &buttons, - }, + e := baseEnvelope(to) + e.Type = "interactive" + e.Interactive = &Interactive{ + Header: opts.Header, + Body: TextObject{Text: opts.Body}, + Type: "button", + Action: Action{ + Buttons: &buttons, }, } + return e } diff --git a/message/message.go b/message/message.go index c368cf3..5600186 100644 --- a/message/message.go +++ b/message/message.go @@ -1,9 +1,34 @@ package message +import "regexp" + +// bsuidPattern matches regular BSUIDs (e.g. US.13491208655302741918) and +// parent BSUIDs (e.g. US.ENT.11815799212886844830): +// 2-letter ISO country code + "." + optional "ENT." + 1–128 alphanumeric chars. +var bsuidPattern = regexp.MustCompile(`^[A-Za-z]{2}\.(ENT\.)?[A-Za-z0-9]{1,128}$`) + +func isBSUID(s string) bool { + return bsuidPattern.MatchString(s) +} + +func baseEnvelope(to string) Envelope { + e := Envelope{ + MessagingProduct: "whatsapp", + RecipientType: "individual", + } + if isBSUID(to) { + e.Recipient = to + } else { + e.To = to + } + return e +} + type Envelope struct { MessagingProduct string `json:"messaging_product"` RecipientType string `json:"recipient_type,omitempty"` To string `json:"to,omitempty"` + Recipient string `json:"recipient,omitempty"` Type string `json:"type,omitempty"` Text *Text `json:"text,omitempty"` Image *Image `json:"image,omitempty"` @@ -115,57 +140,48 @@ type NewFlowOpts struct { } func NewText(to string, opts NewTextOpts) Envelope { - return Envelope{ - MessagingProduct: "whatsapp", - RecipientType: "individual", - To: to, - Type: "text", - Text: &Text{ - Body: opts.Text, - PreviewURL: opts.PreviewURL, - }, + e := baseEnvelope(to) + e.Type = "text" + e.Text = &Text{ + Body: opts.Text, + PreviewURL: opts.PreviewURL, } + return e } func NewImageLink(to string, opts NewImageLinkOpts) Envelope { - return Envelope{ - MessagingProduct: "whatsapp", - RecipientType: "individual", - To: to, - Type: "image", - Image: &Image{ - Link: opts.Link, - Caption: opts.Caption, - }, + e := baseEnvelope(to) + e.Type = "image" + e.Image = &Image{ + Link: opts.Link, + Caption: opts.Caption, } + return e } func NewInteractiveFlow(to string, opts NewFlowOpts) Envelope { flow := "flow" - return Envelope{ - MessagingProduct: "whatsapp", - RecipientType: "individual", - To: to, - Type: "interactive", - Interactive: &Interactive{ - Type: "flow", - Header: opts.Header, - Body: opts.Body, - Footer: opts.Footer, - Action: Action{ - Name: &flow, - Parameters: &Parameters{ - Mode: opts.FlowMode, - FlowMessageVersion: "3", - FlowToken: opts.FlowToken, - FlowID: opts.FlowId, - FlowCTA: opts.FlowCTA, - FlowAction: "navigate", - FlowActionPayload: FlowAction{Screen: opts.FirstScreen}, - }, + e := baseEnvelope(to) + e.Type = "interactive" + e.Interactive = &Interactive{ + Type: "flow", + Header: opts.Header, + Body: opts.Body, + Footer: opts.Footer, + Action: Action{ + Name: &flow, + Parameters: &Parameters{ + Mode: opts.FlowMode, + FlowMessageVersion: "3", + FlowToken: opts.FlowToken, + FlowID: opts.FlowId, + FlowCTA: opts.FlowCTA, + FlowAction: "navigate", + FlowActionPayload: FlowAction{Screen: opts.FirstScreen}, }, }, } + return e } func NewMessageRead(messageId string, typingIndicator bool) Envelope { @@ -185,27 +201,19 @@ func NewMessageRead(messageId string, typingIndicator bool) Envelope { } func NewDocument(to string, opts NewDocumentOpts) Envelope { - return Envelope{ - MessagingProduct: "whatsapp", - RecipientType: "individual", - To: to, - Type: "document", - Document: &Document{ - Link: opts.Link, - Caption: opts.Caption, - Filename: opts.Filename, - }, + e := baseEnvelope(to) + e.Type = "document" + e.Document = &Document{ + Link: opts.Link, + Caption: opts.Caption, + Filename: opts.Filename, } + return e } func NewSticker(to, link string) Envelope { - return Envelope{ - MessagingProduct: "whatsapp", - RecipientType: "individual", - To: to, - Type: "sticker", - Sticker: &Sticker{ - Link: link, - }, - } + e := baseEnvelope(to) + e.Type = "sticker" + e.Sticker = &Sticker{Link: link} + return e } diff --git a/message/message_test.go b/message/message_test.go index 5c2584a..ae8cfd0 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -14,6 +14,31 @@ var expectedMessageReadWithTypingJSON string //go:embed testdata/message_read_without_typing.json var expectedMessageReadWithoutTypingJSON string +func TestBaseEnvelopeRouting(t *testing.T) { + tests := []struct { + name string + to string + wantTo string + wantRecipient string + }{ + {name: "phoneNumber", to: "16505551234", wantTo: "16505551234", wantRecipient: ""}, + {name: "bsuid", to: "US.123", wantTo: "", wantRecipient: "US.123"}, + {name: "parentBsuid", to: "US.ENT.123", wantTo: "", wantRecipient: "US.ENT.123"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := baseEnvelope(tt.to) + if e.To != tt.wantTo { + t.Errorf("To: got %q, want %q", e.To, tt.wantTo) + } + if e.Recipient != tt.wantRecipient { + t.Errorf("Recipient: got %q, want %q", e.Recipient, tt.wantRecipient) + } + }) + } +} + func TestNewMessageRead(t *testing.T) { tests := []struct { messageID string diff --git a/webhook/webhook.go b/webhook/webhook.go index 98c64d6..88e0def 100644 --- a/webhook/webhook.go +++ b/webhook/webhook.go @@ -28,22 +28,27 @@ type Metadata struct { } type Contact struct { - WhatsappID string `json:"wa_id"` - Profile Profile `json:"profile"` + WhatsappID string `json:"wa_id"` + UserID string `json:"user_id"` + ParentUserID string `json:"parent_user_id"` + Profile Profile `json:"profile"` } type Profile struct { - Name string `json:"name"` + Name string `json:"name"` + Username string `json:"username"` } type Message struct { - From string `json:"from"` - ID string `json:"id"` - Timestamp string `json:"timestamp"` - Type string `json:"type"` - Context *Context `json:"context"` - Text Text `json:"text"` - Interactive Interactive `json:"interactive"` + From string `json:"from"` + FromUserID string `json:"from_user_id"` + FromParentUserID string `json:"from_parent_user_id"` + ID string `json:"id"` + Timestamp string `json:"timestamp"` + Type string `json:"type"` + Context *Context `json:"context"` + Text Text `json:"text"` + Interactive Interactive `json:"interactive"` } type Text struct {