From e223a2eb93487fb32b9bb8ad7fb13dc1670e8a42 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Fri, 8 Apr 2022 19:24:46 +0200 Subject: [PATCH 1/5] Add support for WA Cloud API --- handlers/facebookapp/facebookapp.go | 781 +++++++++++++++++- handlers/facebookapp/facebookapp_test.go | 310 ++++++- .../facebookapp/testdata/cwa/audioCWA.json | 43 + .../facebookapp/testdata/cwa/buttonCWA.json | 44 + .../facebookapp/testdata/cwa/documentCWA.json | 43 + .../testdata/cwa/duplicateCWA.json | 48 ++ .../facebookapp/testdata/cwa/helloCWA.json | 39 + .../testdata/cwa/ignoreStatusCWA.json | 49 ++ .../facebookapp/testdata/cwa/imageCWA.json | 43 + .../facebookapp/testdata/cwa/invalidFrom.json | 39 + .../testdata/cwa/invalidStatusCWA.json | 49 ++ .../testdata/cwa/invalidTimestamp.json | 39 + .../facebookapp/testdata/cwa/locationCWA.json | 43 + .../testdata/cwa/validStatusCWA.json | 49 ++ .../facebookapp/testdata/cwa/videoCWA.json | 43 + .../facebookapp/testdata/cwa/voiceCWA.json | 42 + 16 files changed, 1684 insertions(+), 20 deletions(-) create mode 100644 handlers/facebookapp/testdata/cwa/audioCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/buttonCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/documentCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/duplicateCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/helloCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/ignoreStatusCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/imageCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/invalidFrom.json create mode 100644 handlers/facebookapp/testdata/cwa/invalidStatusCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/invalidTimestamp.json create mode 100644 handlers/facebookapp/testdata/cwa/locationCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/validStatusCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/videoCWA.json create mode 100644 handlers/facebookapp/testdata/cwa/voiceCWA.json diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index b4c879a0b..b81b4aba8 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -28,6 +29,8 @@ var ( signatureHeader = "X-Hub-Signature" + configCWAPhoneNumberID = "cwa_phone_number_id" + // max for the body maxMsgLength = 1000 @@ -56,6 +59,17 @@ const ( payloadKey = "payload" ) +var waStatusMapping = map[string]courier.MsgStatusValue{ + "sent": courier.MsgSent, + "delivered": courier.MsgDelivered, + "read": courier.MsgDelivered, + "failed": courier.MsgFailed, +} + +var waIgnoreStatuses = map[string]bool{ + "deleted": true, +} + func newHandler(channelType courier.ChannelType, name string, useUUIDRoutes bool) courier.ChannelHandler { return &handler{handlers.NewBaseHandlerWithParams(channelType, name, useUUIDRoutes)} } @@ -63,6 +77,7 @@ func newHandler(channelType courier.ChannelType, name string, useUUIDRoutes bool func init() { courier.RegisterHandler(newHandler("IG", "Instagram", false)) courier.RegisterHandler(newHandler("FBA", "Facebook", false)) + courier.RegisterHandler(newHandler("CWA", "Cloud API WhatsApp", false)) } @@ -104,11 +119,88 @@ type User struct { // }] // }] // } + +type cwaMedia struct { + Caption string `json:"caption"` + Filename string `json:"filename"` + ID string `json:"id"` + Mimetype string `json:"mime_type"` + SHA256 string `json:"sha256"` +} type moPayload struct { Object string `json:"object"` Entry []struct { - ID string `json:"id"` - Time int64 `json:"time"` + ID string `json:"id"` + Time int64 `json:"time"` + Changes []struct { + Field string `json:"field"` + Value struct { + MessagingProduct string `json:"messaging_product"` + Metadata *struct { + DisplayPhoneNumber string `json:"display_phone_number"` + PhoneNumberID string `json:"phone_number_id"` + } `json:"metadata"` + Contacts []struct { + Profile struct { + Name string `json:"name"` + } `json:"profile"` + WaID string `json:"wa_id"` + } `json:"contacts"` + Messages []struct { + ID string `json:"id"` + From string `json:"from"` + Timestamp string `json:"timestamp"` + Type string `json:"type"` + Context *struct { + Forwarded bool `json:"forwarded"` + FrequentlyForwarded bool `json:"frequently_forwarded"` + From string `json:"from"` + ID string `json:"id"` + } `json:"context"` + Text struct { + Body string `json:"body"` + } `json:"text"` + Image *cwaMedia `json:"image"` + Audio *cwaMedia `json:"audio"` + Video *cwaMedia `json:"video"` + Document *cwaMedia `json:"document"` + Voice *cwaMedia `json:"voice"` + Location *struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Name string `json:"name"` + Address string `json:"address"` + } `json:"location"` + Button *struct { + Text string `json:"text"` + Payload string `json:"payload"` + } `json:"button"` + } `json:"messages"` + Statuses []struct { + ID string `json:"id"` + RecipientID string `json:"recipient_id"` + Status string `json:"status"` + Timestamp string `json:"timestamp"` + Type string `json:"type"` + Conversation *struct { + ID string `json:"id"` + Origin *struct { + Type string `json:"type"` + } `json:"origin"` + ExpirationTimestamp int64 `json:"expiration_timestamp"` + } `json:"conversation"` + Pricing *struct { + PricingModel string `json:"pricing_model"` + Billable bool `json:"billable"` + Category string `json:"category"` + } `json:"pricing"` + } `json:"statuses"` + Errors []struct { + Code int `json:"code"` + Title string `json:"title"` + } `json:"errors"` + } `json:"value"` + } `json:"changes"` Messaging []struct { Sender Sender `json:"sender"` Recipient User `json:"recipient"` @@ -177,8 +269,8 @@ func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Chan } // is not a 'page' and 'instagram' object? ignore it - if payload.Object != "page" && payload.Object != "instagram" { - return nil, fmt.Errorf("object expected 'page' or 'instagram', found %s", payload.Object) + if payload.Object != "page" && payload.Object != "instagram" && payload.Object != "whatsapp_business_account" { + return nil, fmt.Errorf("object expected 'page', 'instagram' or 'whatsapp_business_account', found %s", payload.Object) } // no entries? ignore this request @@ -186,13 +278,25 @@ func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Chan return nil, fmt.Errorf("no entries found") } - entryID := payload.Entry[0].ID + var channelAddress string //if object is 'page' returns type FBA, if object is 'instagram' returns type IG if payload.Object == "page" { - return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("FBA"), courier.ChannelAddress(entryID)) + channelAddress = payload.Entry[0].ID + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("FBA"), courier.ChannelAddress(channelAddress)) + } else if payload.Object == "instagram" { + channelAddress = payload.Entry[0].ID + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("IG"), courier.ChannelAddress(channelAddress)) } else { - return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("IG"), courier.ChannelAddress(entryID)) + if len(payload.Entry[0].Changes) == 0 { + return nil, fmt.Errorf("no changes found") + } + + channelAddress = payload.Entry[0].Changes[0].Value.Metadata.DisplayPhoneNumber + if channelAddress == "" { + return nil, fmt.Errorf("no channel adress found") + } + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("CWA"), courier.ChannelAddress(channelAddress)) } } @@ -215,6 +319,30 @@ func (h *handler) receiveVerify(ctx context.Context, channel courier.Channel, w return nil, err } +func resolveMediaURL(channel courier.Channel, mediaID string) (string, error) { + token := channel.StringConfigForKey(courier.ConfigAuthToken, "") + if token == "" { + return "", fmt.Errorf("missing token for WA channel") + } + + base, _ := url.Parse(graphURL) + path, _ := url.Parse(fmt.Sprintf("/%s", mediaID)) + retreiveURL := base.ResolveReference(path) + + // set the access token as the authorization header + req, _ := http.NewRequest(http.MethodGet, retreiveURL.String(), nil) + //req.Header.Set("User-Agent", utils.HTTPUserAgent) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + resp, err := utils.MakeHTTPRequest(req) + if err != nil { + return "", err + } + + mediaURL, err := jsonparser.GetString(resp.Body, "url") + return mediaURL, err +} + // receiveEvent is our HTTP handler function for incoming messages and status updates func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) { err := h.validateSignature(r) @@ -229,7 +357,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h } // is not a 'page' and 'instagram' object? ignore it - if payload.Object != "page" && payload.Object != "instagram" { + if payload.Object != "page" && payload.Object != "instagram" && payload.Object != "whatsapp_business_account" { return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request") } @@ -238,6 +366,150 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request, no entries") } + var events []courier.Event + var data []interface{} + + if channel.ChannelType() == "FBA" || channel.ChannelType() == "IG" { + events, data, err = h.processFacebookInstagramPayload(ctx, channel, payload, w, r) + } else { + events, data, err = h.processCloudWhatsAppPayload(ctx, channel, payload, w, r) + + } + + if err != nil { + return nil, err + } + + return events, courier.WriteDataResponse(ctx, w, http.StatusOK, "Events Handled", data) +} + +func (h *handler) processCloudWhatsAppPayload(ctx context.Context, channel courier.Channel, payload *moPayload, w http.ResponseWriter, r *http.Request) ([]courier.Event, []interface{}, error) { + // the list of events we deal with + events := make([]courier.Event, 0, 2) + + // the list of data we will return in our response + data := make([]interface{}, 0, 2) + + var contactNames = make(map[string]string) + + // for each entry + for _, entry := range payload.Entry { + if len(entry.Changes) == 0 { + continue + } + + for _, change := range entry.Changes { + + for _, contact := range change.Value.Contacts { + contactNames[contact.WaID] = contact.Profile.Name + } + + for _, msg := range change.Value.Messages { + // create our date from the timestamp + ts, err := strconv.ParseInt(msg.Timestamp, 10, 64) + if err != nil { + return nil, nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("invalid timestamp: %s", msg.Timestamp)) + } + date := time.Unix(ts, 0).UTC() + + urn, err := urns.NewWhatsAppURN(msg.From) + if err != nil { + return nil, nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } + + text := "" + mediaURL := "" + + if msg.Type == "text" { + text = msg.Text.Body + } else if msg.Type == "audio" && msg.Audio != nil { + text = msg.Audio.Caption + mediaURL, err = resolveMediaURL(channel, msg.Audio.ID) + } else if msg.Type == "voice" && msg.Voice != nil { + text = msg.Voice.Caption + mediaURL, err = resolveMediaURL(channel, msg.Voice.ID) + } else if msg.Type == "button" && msg.Button != nil { + text = msg.Button.Text + } else if msg.Type == "document" && msg.Document != nil { + text = msg.Document.Caption + mediaURL, err = resolveMediaURL(channel, msg.Document.ID) + } else if msg.Type == "image" && msg.Image != nil { + text = msg.Image.Caption + mediaURL, err = resolveMediaURL(channel, msg.Image.ID) + } else if msg.Type == "video" && msg.Video != nil { + text = msg.Video.Caption + mediaURL, err = resolveMediaURL(channel, msg.Video.ID) + } else if msg.Type == "location" && msg.Location != nil { + mediaURL = fmt.Sprintf("geo:%f,%f", msg.Location.Latitude, msg.Location.Longitude) + } else { + // we received a message type we do not support. + courier.LogRequestError(r, channel, fmt.Errorf("unsupported message type %s", msg.Type)) + } + + // create our message + ev := h.Backend().NewIncomingMsg(channel, urn, text).WithReceivedOn(date).WithExternalID(msg.ID).WithContactName(contactNames[msg.From]) + event := h.Backend().CheckExternalIDSeen(ev) + + // we had an error downloading media + if err != nil { + courier.LogRequestError(r, channel, err) + } + + if mediaURL != "" { + event.WithAttachment(mediaURL) + } + + err = h.Backend().WriteMsg(ctx, event) + if err != nil { + return nil, nil, err + } + + h.Backend().WriteExternalIDSeen(event) + + events = append(events, event) + data = append(data, courier.NewMsgReceiveData(event)) + + } + + for _, status := range change.Value.Statuses { + + msgStatus, found := waStatusMapping[status.Status] + if !found { + if waIgnoreStatuses[status.Status] { + data = append(data, courier.NewInfoData(fmt.Sprintf("ignoring status: %s", status.Status))) + } else { + handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("unknown status: %s", status.Status)) + } + continue + } + + event := h.Backend().NewMsgStatusForExternalID(channel, status.ID, msgStatus) + err := h.Backend().WriteMsgStatus(ctx, event) + + // we don't know about this message, just tell them we ignored it + if err == courier.ErrMsgNotFound { + data = append(data, courier.NewInfoData(fmt.Sprintf("message id: %s not found, ignored", status.ID))) + continue + } + + if err != nil { + return nil, nil, err + } + + events = append(events, event) + data = append(data, courier.NewStatusData(event)) + + } + + } + + } + return events, data, nil +} + +func (h *handler) processFacebookInstagramPayload(ctx context.Context, channel courier.Channel, payload *moPayload, w http.ResponseWriter, r *http.Request) ([]courier.Event, []interface{}, error) { + var err error + // the list of events we deal with events := make([]courier.Event, 0, 2) @@ -273,12 +545,12 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h if payload.Object == "instagram" { urn, err = urns.NewInstagramURN(sender) if err != nil { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + return nil, nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) } } else { urn, err = urns.NewFacebookURN(sender) if err != nil { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + return nil, nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) } } @@ -292,7 +564,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h if msg.OptIn.UserRef != "" { urn, err = urns.NewFacebookURN(urns.FacebookRefPrefix + msg.OptIn.UserRef) if err != nil { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + return nil, nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) } } @@ -306,7 +578,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h err := h.Backend().WriteChannelEvent(ctx, event) if err != nil { - return nil, err + return nil, nil, err } events = append(events, event) @@ -341,7 +613,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h err := h.Backend().WriteChannelEvent(ctx, event) if err != nil { - return nil, err + return nil, nil, err } events = append(events, event) @@ -370,7 +642,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h err := h.Backend().WriteChannelEvent(ctx, event) if err != nil { - return nil, err + return nil, nil, err } events = append(events, event) @@ -435,7 +707,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h err := h.Backend().WriteMsg(ctx, event) if err != nil { - return nil, err + return nil, nil, err } h.Backend().WriteExternalIDSeen(event) @@ -456,7 +728,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h } if err != nil { - return nil, err + return nil, nil, err } events = append(events, event) @@ -468,7 +740,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h } } - return events, courier.WriteDataResponse(ctx, w, http.StatusOK, "Events Handled", data) + return events, data, nil } // { @@ -516,6 +788,16 @@ type mtQuickReply struct { } func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { + if msg.Channel().ChannelType() == "FBA" || msg.Channel().ChannelType() == "IG" { + return h.sendFacebookInstagramMsg(ctx, msg) + } else if msg.Channel().ChannelType() == "CWA" { + return h.sendCloudAPIWhatsappMsg(ctx, msg) + } + + return nil, fmt.Errorf("unssuported channel type") +} + +func (h *handler) sendFacebookInstagramMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { // can't do anything without an access token accessToken := msg.Channel().StringConfigForKey(courier.ConfigAuthToken, "") if accessToken == "" { @@ -661,8 +943,346 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat return status, nil } +type cwaMTMedia struct { + ID string `json:"id,omitempty"` + Link string `json:"link,omitempty"` + Caption string `json:"caption,omitempty"` + Filename string `json:"filename,omitempty"` +} + +type cwaMTSection struct { + Title string `json:"title,omitempty"` + Rows []cwaMTSectionRow `json:"rows" validate:"required"` +} + +type cwaMTSectionRow struct { + ID string `json:"id" validate:"required"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` +} + +type cwaMTButton struct { + Type string `json:"type" validate:"required"` + Reply struct { + ID string `json:"id" validate:"required"` + Title string `json:"title" validate:"required"` + } `json:"reply" validate:"required"` +} + +type cwaParam struct { + Type string `json:"type"` + Text string `json:"text"` +} + +type cwaComponent struct { + Type string `json:"type"` + SubType string `json:"sub_type"` + Index string `json:"index"` + Params []*cwaParam `json:"parameters"` +} + +type cwaText struct { + Body string `json:"body"` +} + +type cwaLanguage struct { + Policy string `json:"policy"` + Code string `json:"code"` +} + +type cwaTemplate struct { + Name string `json:"name"` + Language *cwaLanguage `json:"language"` + Components []*cwaComponent `json:"components"` +} + +type cwaInteractive struct { + Type string `json:"type"` + Header *struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + Video string `json:"video,omitempty"` + Image string `json:"image,omitempty"` + Document string `json:"document,omitempty"` + } `json:"header,omitempty"` + Body struct { + Text string `json:"text"` + } `json:"body" validate:"required"` + Footer *struct { + Text string `json:"text"` + } `json:"footer,omitempty"` + Action *struct { + Button string `json:"button,omitempty"` + Sections []cwaMTSection `json:"sections,omitempty"` + Buttons []cwaMTButton `json:"buttons,omitempty"` + } `json:"action,omitempty"` +} + +type cwaMTPayload struct { + MessagingProduct string `json:"messaging_product"` + PreviewURL bool `json:"preview_url"` + RecipientType string `json:"recipient_type"` + To string `json:"to"` + Type string `json:"type"` + + Text *cwaText `json:"text,omitempty"` + + Document *cwaMTMedia `json:"document,omitempty"` + Image *cwaMTMedia `json:"image,omitempty"` + Audio *cwaMTMedia `json:"audio,omitempty"` + Video *cwaMTMedia `json:"video,omitempty"` + + Interactive *cwaInteractive `json:"interactive,omitempty"` + + Template *cwaTemplate `json:"template,omitempty"` +} + +type cwaMTResponse struct { + Messages []*struct { + ID string `json:"id"` + } `json:"messages"` +} + +func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { + // can't do anything without an access token + accessToken := msg.Channel().StringConfigForKey(courier.ConfigAuthToken, "") + if accessToken == "" { + return nil, fmt.Errorf("missing access token") + } + + phoneNumberId := msg.Channel().StringConfigForKey(configCWAPhoneNumberID, "") + if phoneNumberId == "" { + return nil, fmt.Errorf("missing CWA phone number ID") + } + + base, _ := url.Parse(graphURL) + path, _ := url.Parse(fmt.Sprintf("/%s/messages", phoneNumberId)) + cwaPhoneURL := base.ResolveReference(path) + + status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored) + + msgParts := make([]string, 0) + if msg.Text() != "" { + msgParts = handlers.SplitMsgByChannel(msg.Channel(), msg.Text(), maxMsgLength) + } + qrs := msg.QuickReplies() + + for i := 0; i < len(msgParts)+len(msg.Attachments()); i++ { + payload := cwaMTPayload{MessagingProduct: "whatsapp", RecipientType: "individual", To: msg.URN().Path()} + + if len(msg.Attachments()) == 0 { + // do we have a template? + var templating *MsgTemplating + templating, err := h.getTemplate(msg) + if err != nil { + return nil, errors.Wrapf(err, "unable to decode template: %s for channel: %s", string(msg.Metadata()), msg.Channel().UUID()) + } + if templating != nil { + + payload.Type = "template" + + template := cwaTemplate{Name: templating.Template.Name, Language: &cwaLanguage{Policy: "deterministic", Code: templating.Language}} + payload.Template = &template + + component := &cwaComponent{Type: "body"} + + for _, v := range templating.Variables { + component.Params = append(component.Params, &cwaParam{Type: "text", Text: v}) + } + template.Components = append(payload.Template.Components, component) + + } else { + if i < (len(msgParts) + len(msg.Attachments()) - 1) { + // this is still a msg part + payload.Type = "text" + payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + } else { + if len(qrs) > 0 { + payload.Type = "interactive" + // We can use buttons + if len(qrs) <= 3 { + interactive := cwaInteractive{Type: "button", Body: struct { + Text string "json:\"text\"" + }{Text: msgParts[i-len(msg.Attachments())]}} + + btns := make([]cwaMTButton, len(qrs)) + for i, qr := range qrs { + btns[i] = cwaMTButton{ + Type: "reply", + } + btns[i].Reply.ID = fmt.Sprint(i) + btns[i].Reply.Title = qr + } + interactive.Action = &struct { + Button string "json:\"button,omitempty\"" + Sections []cwaMTSection "json:\"sections,omitempty\"" + Buttons []cwaMTButton "json:\"buttons,omitempty\"" + }{Buttons: btns} + payload.Interactive = &interactive + } else if len(qrs) <= 10 { + interactive := cwaInteractive{Type: "list", Body: struct { + Text string "json:\"text\"" + }{Text: msgParts[i-len(msg.Attachments())]}} + + section := cwaMTSection{ + Rows: make([]cwaMTSectionRow, len(qrs)), + } + for i, qr := range qrs { + section.Rows[i] = cwaMTSectionRow{ + ID: fmt.Sprint(i), + Title: qr, + } + } + + interactive.Action = &struct { + Button string "json:\"button,omitempty\"" + Sections []cwaMTSection "json:\"sections,omitempty\"" + Buttons []cwaMTButton "json:\"buttons,omitempty\"" + }{Button: "Menu", Sections: []cwaMTSection{ + section, + }} + + payload.Interactive = &interactive + } else { + return nil, fmt.Errorf("too many quick replies CWA supports only up to 10 quick replies") + } + } else { + // this is still a msg part + payload.Type = "text" + payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + } + } + } + + } else if i < len(msg.Attachments()) { + attType, attURL := handlers.SplitAttachment(msg.Attachments()[i]) + attType = strings.Split(attType, "/")[0] + if attType == "application" { + attType = "document" + } + payload.Type = attType + media := cwaMTMedia{Link: attURL} + + if attType == "image" { + payload.Image = &media + } else if attType == "audio" { + payload.Audio = &media + } else if attType == "video" { + payload.Video = &media + } else if attType == "document" { + payload.Document = &media + } + } else { + if i < (len(msgParts) + len(msg.Attachments()) - 1) { + // this is still a msg part + payload.Type = "text" + payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + } else { + if len(qrs) > 0 { + payload.Type = "interactive" + // We can use buttons + if len(qrs) <= 3 { + interactive := cwaInteractive{Type: "button", Body: struct { + Text string "json:\"text\"" + }{Text: msgParts[i-len(msg.Attachments())]}} + + btns := make([]cwaMTButton, len(qrs)) + for i, qr := range qrs { + btns[i] = cwaMTButton{ + Type: "reply", + } + btns[i].Reply.ID = fmt.Sprint(i) + btns[i].Reply.Title = qr + } + interactive.Action = &struct { + Button string "json:\"button,omitempty\"" + Sections []cwaMTSection "json:\"sections,omitempty\"" + Buttons []cwaMTButton "json:\"buttons,omitempty\"" + }{Buttons: btns} + payload.Interactive = &interactive + + } else if len(qrs) <= 10 { + interactive := cwaInteractive{Type: "list", Body: struct { + Text string "json:\"text\"" + }{Text: msgParts[i-len(msg.Attachments())]}} + + section := cwaMTSection{ + Rows: make([]cwaMTSectionRow, len(qrs)), + } + for i, qr := range qrs { + section.Rows[i] = cwaMTSectionRow{ + ID: fmt.Sprint(i), + Title: qr, + } + } + + interactive.Action = &struct { + Button string "json:\"button,omitempty\"" + Sections []cwaMTSection "json:\"sections,omitempty\"" + Buttons []cwaMTButton "json:\"buttons,omitempty\"" + }{Button: "Menu", Sections: []cwaMTSection{ + section, + }} + + payload.Interactive = &interactive + } else { + return nil, fmt.Errorf("too many quick replies CWA supports only up to 10 quick replies") + } + } else { + // this is still a msg part + payload.Type = "text" + payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + } + } + + } + + jsonBody, err := json.Marshal(payload) + if err != nil { + return status, err + } + + req, err := http.NewRequest(http.MethodPost, cwaPhoneURL.String(), bytes.NewReader(jsonBody)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + rr, err := utils.MakeHTTPRequest(req) + + // record our status and log + log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), rr).WithError("Message Send Error", err) + status.AddLog(log) + if err != nil { + return status, nil + } + + respPayload := &cwaMTResponse{} + err = json.Unmarshal(rr.Body, respPayload) + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to unmarshal response body")) + return status, nil + } + externalID := respPayload.Messages[0].ID + if i == 0 && externalID != "" { + status.SetExternalID(externalID) + } + // this was wired successfully + status.SetStatus(courier.MsgWired) + + } + return status, nil +} + // DescribeURN looks up URN metadata for new contacts func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn urns.URN) (map[string]string, error) { + if channel.ChannelType() == "CWA" { + return map[string]string{}, nil + + } + // can't do anything with facebook refs, ignore them if urn.IsFacebookRef() { return map[string]string{}, nil @@ -746,3 +1366,130 @@ func fbCalculateSignature(appSecret string, body []byte) (string, error) { return hex.EncodeToString(mac.Sum(nil)), nil } + +func (h *handler) getTemplate(msg courier.Msg) (*MsgTemplating, error) { + mdJSON := msg.Metadata() + if len(mdJSON) == 0 { + return nil, nil + } + metadata := &TemplateMetadata{} + err := json.Unmarshal(mdJSON, metadata) + if err != nil { + return nil, err + } + templating := metadata.Templating + if templating == nil { + return nil, nil + } + + // check our template is valid + err = handlers.Validate(templating) + if err != nil { + return nil, errors.Wrapf(err, "invalid templating definition") + } + // check country + if templating.Country != "" { + templating.Language = fmt.Sprintf("%s_%s", templating.Language, templating.Country) + } + + // map our language from iso639-3_iso3166-2 to the WA country / iso638-2 pair + language, found := languageMap[templating.Language] + if !found { + return nil, fmt.Errorf("unable to find mapping for language: %s", templating.Language) + } + templating.Language = language + + return templating, err +} + +type TemplateMetadata struct { + Templating *MsgTemplating `json:"templating"` +} + +type MsgTemplating struct { + Template struct { + Name string `json:"name" validate:"required"` + UUID string `json:"uuid" validate:"required"` + } `json:"template" validate:"required,dive"` + Language string `json:"language" validate:"required"` + Country string `json:"country"` + Namespace string `json:"namespace"` + Variables []string `json:"variables"` +} + +// mapping from iso639-3_iso3166-2 to WA language code +var languageMap = map[string]string{ + "afr": "af", // Afrikaans + "sqi": "sq", // Albanian + "ara": "ar", // Arabic + "aze": "az", // Azerbaijani + "ben": "bn", // Bengali + "bul": "bg", // Bulgarian + "cat": "ca", // Catalan + "zho": "zh_CN", // Chinese + "zho_CN": "zh_CN", // Chinese (CHN) + "zho_HK": "zh_HK", // Chinese (HKG) + "zho_TW": "zh_TW", // Chinese (TAI) + "hrv": "hr", // Croatian + "ces": "cs", // Czech + "dah": "da", // Danish + "nld": "nl", // Dutch + "eng": "en", // English + "eng_GB": "en_GB", // English (UK) + "eng_US": "en_US", // English (US) + "est": "et", // Estonian + "fil": "fil", // Filipino + "fin": "fi", // Finnish + "fra": "fr", // French + "kat": "ka", // Georgian + "deu": "de", // German + "ell": "el", // Greek + "guj": "gu", // Gujarati + "hau": "ha", // Hausa + "enb": "he", // Hebrew + "hin": "hi", // Hindi + "hun": "hu", // Hungarian + "ind": "id", // Indonesian + "gle": "ga", // Irish + "ita": "it", // Italian + "jpn": "ja", // Japanese + "kan": "kn", // Kannada + "kaz": "kk", // Kazakh + "kin": "rw_RW", // Kinyarwanda + "kor": "ko", // Korean + "kir": "ky_KG", // Kyrgyzstan + "lao": "lo", // Lao + "lav": "lv", // Latvian + "lit": "lt", // Lithuanian + "mal": "ml", // Malayalam + "mkd": "mk", // Macedonian + "msa": "ms", // Malay + "mar": "mr", // Marathi + "nob": "nb", // Norwegian + "fas": "fa", // Persian + "pol": "pl", // Polish + "por": "pt_PT", // Portuguese + "por_BR": "pt_BR", // Portuguese (BR) + "por_PT": "pt_PT", // Portuguese (POR) + "pan": "pa", // Punjabi + "ron": "ro", // Romanian + "rus": "ru", // Russian + "srp": "sr", // Serbian + "slk": "sk", // Slovak + "slv": "sl", // Slovenian + "spa": "es", // Spanish + "spa_AR": "es_AR", // Spanish (ARG) + "spa_ES": "es_ES", // Spanish (SPA) + "spa_MX": "es_MX", // Spanish (MEX) + "swa": "sw", // Swahili + "swe": "sv", // Swedish + "tam": "ta", // Tamil + "tel": "te", // Telugu + "tha": "th", // Thai + "tur": "tr", // Turkish + "ukr": "uk", // Ukrainian + "urd": "ur", // Urdu + "uzb": "uz", // Uzbek + "vie": "vi", // Vietnamese + "zul": "zu", // Zulu +} diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 9ec8576c0..c5a526ebe 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -2,6 +2,7 @@ package facebookapp import ( "context" + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -24,6 +25,10 @@ var testChannelsIG = []courier.Channel{ courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), } +var testChannelsCWA = []courier.Channel{ + courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "CWA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), +} + var testCasesFBA = []ChannelHandleTestCase{ {Label: "Receive Message FBA", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/helloMsgFBA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("Hello World"), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), @@ -77,7 +82,7 @@ var testCasesFBA = []ChannelHandleTestCase{ {Label: "Different Page", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/differentPageFBA.json")), Status: 200, Response: `"data":[]`, PrepRequest: addValidSignature}, {Label: "Echo", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/echoFBA.json")), Status: 200, Response: `ignoring echo`, PrepRequest: addValidSignature}, - {Label: "Not Page", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/notPage.json")), Status: 400, Response: "object expected 'page' or 'instagram', found notpage", PrepRequest: addValidSignature}, + {Label: "Not Page", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/notPage.json")), Status: 400, Response: "object expected 'page', 'instagram' or 'whatsapp_business_account', found notpage", PrepRequest: addValidSignature}, {Label: "No Entries", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/noEntriesFBA.json")), Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, {Label: "No Messaging Entries", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/noMessagingEntriesFBA.json")), Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Unknown Messaging Entry", URL: "/c/fba/receive", Data: string(courier.ReadFile("./testdata/fba/unknownMessagingEntryFBA.json")), Status: 200, Response: "Handled", PrepRequest: addValidSignature}, @@ -110,7 +115,7 @@ var testCasesIG = []ChannelHandleTestCase{ {Label: "Different Page", URL: "/c/ig/receive", Data: string(courier.ReadFile("./testdata/ig/differentPageIG.json")), Status: 200, Response: `"data":[]`, PrepRequest: addValidSignature}, {Label: "Echo", URL: "/c/ig/receive", Data: string(courier.ReadFile("./testdata/ig/echoIG.json")), Status: 200, Response: `ignoring echo`, PrepRequest: addValidSignature}, {Label: "No Entries", URL: "/c/ig/receive", Data: string(courier.ReadFile("./testdata/ig/noEntriesIG.json")), Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, - {Label: "Not Instagram", URL: "/c/ig/receive", Data: string(courier.ReadFile("./testdata/ig/notInstagram.json")), Status: 400, Response: "object expected 'page' or 'instagram', found notinstagram", PrepRequest: addValidSignature}, + {Label: "Not Instagram", URL: "/c/ig/receive", Data: string(courier.ReadFile("./testdata/ig/notInstagram.json")), Status: 400, Response: "object expected 'page', 'instagram' or 'whatsapp_business_account', found notinstagram", PrepRequest: addValidSignature}, {Label: "No Messaging Entries", URL: "/c/ig/receive", Data: string(courier.ReadFile("./testdata/ig/noMessagingEntriesIG.json")), Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: string(courier.ReadFile("./testdata/ig/unknownMessagingEntryIG.json")), Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Not JSON", URL: "/c/ig/receive", Data: "not JSON", Status: 400, Response: "Error", PrepRequest: addValidSignature}, @@ -213,10 +218,111 @@ func TestDescribeIG(t *testing.T) { } } +func TestDescribeCWA(t *testing.T) { + handler := newHandler("CWA", "Cloud API WhatsApp", false).(courier.URNDescriber) + + tcs := []struct { + urn urns.URN + metadata map[string]string + }{{"whatsapp:1337", map[string]string{}}, + {"whatsapp:4567", map[string]string{}}} + + for _, tc := range tcs { + metadata, _ := handler.DescribeURN(context.Background(), testChannelsCWA[0], tc.urn) + assert.Equal(t, metadata, tc.metadata) + } +} + +var cwaReceiveURL = "/c/cwa/receive" + +var testCasesCWA = []ChannelHandleTestCase{ + {Label: "Receive Message CWA", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/helloCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("Hello World"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + {Label: "Receive Duplicate Valid Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/duplicateCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("Hello World"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Valid Voice Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/voiceCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp(""), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Voice"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Valid Button Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/buttonCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("No"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Valid Document Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/documentCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("80skaraokesonglistartist"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Document"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + {Label: "Receive Valid Image Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/imageCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("Check out my new phone!"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Image"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + {Label: "Receive Valid Video Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/videoCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("Check out my new phone!"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Video"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + {Label: "Receive Valid Audio Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/audioCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("Check out my new phone!"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Audio"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + {Label: "Receive Valid Location Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/locationCWA.json")), Status: 200, Response: `"type":"msg"`, + Text: Sp(""), Attachment: Sp("geo:0.000000,1.000000"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Invalid JSON", URL: cwaReceiveURL, Data: "not json", Status: 400, Response: "unable to parse", PrepRequest: addValidSignature}, + {Label: "Receive Invalid JSON", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/invalidFrom.json")), Status: 400, Response: "invalid whatsapp id", PrepRequest: addValidSignature}, + {Label: "Receive Invalid JSON", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/invalidTimestamp.json")), Status: 400, Response: "invalid timestamp", PrepRequest: addValidSignature}, + + {Label: "Receive Valid Status", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/validStatusCWA.json")), Status: 200, Response: `"type":"status"`, + MsgStatus: Sp("S"), ExternalID: Sp("external_id"), PrepRequest: addValidSignature}, + {Label: "Receive Invalid Status", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/invalidStatusCWA.json")), Status: 400, Response: `"unknown status: in_orbit"`, PrepRequest: addValidSignature}, + {Label: "Receive Ignore Status", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/ignoreStatusCWA.json")), Status: 200, Response: `"ignoring status: deleted"`, PrepRequest: addValidSignature}, +} + func TestHandler(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + accessToken := r.Header.Get("Authorization") + defer r.Body.Close() + + // invalid auth token + if accessToken != "Bearer a123" { + fmt.Printf("Access token: %s\n", accessToken) + http.Error(w, "invalid auth token", 403) + return + } + + if strings.HasSuffix(r.URL.Path, "image") { + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL_Image"}`)) + return + } + + if strings.HasSuffix(r.URL.Path, "audio") { + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL_Audio"}`)) + return + } + + if strings.HasSuffix(r.URL.Path, "voice") { + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL_Voice"}`)) + return + } + + if strings.HasSuffix(r.URL.Path, "video") { + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL_Video"}`)) + return + } + + if strings.HasSuffix(r.URL.Path, "document") { + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL_Document"}`)) + return + } + + // valid token + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL"}`)) + + })) + graphURL = server.URL + + RunChannelTestCases(t, testChannelsCWA, newHandler("CWA", "Cloud API WhatsApp", false), testCasesCWA) RunChannelTestCases(t, testChannelsFBA, newHandler("FBA", "Facebook", false), testCasesFBA) RunChannelTestCases(t, testChannelsIG, newHandler("IG", "Instagram", false), testCasesIG) - } func BenchmarkHandler(b *testing.B) { @@ -256,6 +362,7 @@ func TestVerify(t *testing.T) { // setSendURL takes care of setting the send_url to our test server host func setSendURL(s *httptest.Server, h courier.ChannelHandler, c courier.Channel, m courier.Msg) { sendURL = s.URL + graphURL = s.URL } var SendTestCasesFBA = []ChannelSendTestCase{ @@ -387,13 +494,210 @@ var SendTestCasesIG = []ChannelSendTestCase{ SendPrep: setSendURL}, } +var SendTestCasesCWA = []ChannelSendTestCase{ + {Label: "Plain Send", + Text: "Simple Message", URN: "whatsapp:250788123123", Path: "/12345_ID/messages", + Status: "W", ExternalID: "157b5e14568e8", + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, + RequestBody: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"Simple Message"}}`, + SendPrep: setSendURL}, + {Label: "Unicode Send", + Text: "☺", URN: "whatsapp:250788123123", Path: "/12345_ID/messages", + Status: "W", ExternalID: "157b5e14568e8", + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, + RequestBody: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"☺"}}`, + SendPrep: setSendURL}, + {Label: "Audio Send", + Text: "audio caption", + URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Attachments: []string{"audio/mpeg:https://foo.bar/audio.mp3"}, + Responses: map[MockedRequest]MockedResponse{ + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"https://foo.bar/audio.mp3"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"audio caption"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + }, + SendPrep: setSendURL}, + {Label: "Document Send", + Text: "document caption", + URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Attachments: []string{"application/pdf:https://foo.bar/document.pdf"}, + Responses: map[MockedRequest]MockedResponse{ + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"document","document":{"link":"https://foo.bar/document.pdf"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"document caption"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + }, + SendPrep: setSendURL}, + + {Label: "Image Send", + Text: "document caption", + URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + Responses: map[MockedRequest]MockedResponse{ + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"document caption"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + }, + SendPrep: setSendURL}, + {Label: "Video Send", + Text: "video caption", + URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Attachments: []string{"video/mp4:https://foo.bar/video.mp4"}, + Responses: map[MockedRequest]MockedResponse{ + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"video","video":{"link":"https://foo.bar/video.mp4"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"video caption"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + }, + SendPrep: setSendURL}, + + {Label: "Template Send", + Text: "templated message", + URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Metadata: json.RawMessage(`{ "templating": { "template": { "name": "revive_issue", "uuid": "171f8a4d-f725-46d7-85a6-11aceff0bfe3" }, "language": "eng", "variables": ["Chef", "tomorrow"]}}`), + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 200, + RequestBody: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"template","template":{"name":"revive_issue","language":{"policy":"deterministic","code":"en"},"components":[{"type":"body","sub_type":"","index":"","parameters":[{"type":"text","text":"Chef"},{"type":"text","text":"tomorrow"}]}]}}`, + SendPrep: setSendURL, + }, + + {Label: "Template Country Language", + Text: "templated message", + URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Metadata: json.RawMessage(`{ "templating": { "template": { "name": "revive_issue", "uuid": "171f8a4d-f725-46d7-85a6-11aceff0bfe3" }, "language": "eng", "country": "US", "variables": ["Chef", "tomorrow"]}}`), + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 200, + RequestBody: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"template","template":{"name":"revive_issue","language":{"policy":"deterministic","code":"en_US"},"components":[{"type":"body","sub_type":"","index":"","parameters":[{"type":"text","text":"Chef"},{"type":"text","text":"tomorrow"}]}]}}`, + SendPrep: setSendURL, + }, + {Label: "Template Invalid Language", + Text: "templated message", URN: "whatsapp:250788123123", + Error: `unable to decode template: {"templating": { "template": { "name": "revive_issue", "uuid": "8ca114b4-bee2-4d3b-aaf1-9aa6b48d41e8" }, "language": "bnt", "variables": ["Chef", "tomorrow"]}} for channel: 8eb23e93-5ecb-45ba-b726-3b064e0c56ab: unable to find mapping for language: bnt`, + Metadata: json.RawMessage(`{"templating": { "template": { "name": "revive_issue", "uuid": "8ca114b4-bee2-4d3b-aaf1-9aa6b48d41e8" }, "language": "bnt", "variables": ["Chef", "tomorrow"]}}`), + }, + {Label: "Interactive Button Message Send", + Text: "Interactive Button Msg", URN: "whatsapp:250788123123", QuickReplies: []string{"BUTTON1"}, + Status: "W", ExternalID: "157b5e14568e8", + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, + RequestBody: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + SendPrep: setSendURL}, + {Label: "Interactive List Message Send", + Text: "Interactive List Msg", URN: "whatsapp:250788123123", QuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + Status: "W", ExternalID: "157b5e14568e8", + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, + RequestBody: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"list","body":{"text":"Interactive List Msg"},"action":{"button":"Menu","sections":[{"rows":[{"id":"0","title":"ROW1"},{"id":"1","title":"ROW2"},{"id":"2","title":"ROW3"},{"id":"3","title":"ROW4"}]}]}}}`, + SendPrep: setSendURL}, + {Label: "Interactive Button Message Send with attachment", + Text: "Interactive Button Msg", URN: "whatsapp:250788123123", QuickReplies: []string{"BUTTON1"}, + Status: "W", ExternalID: "157b5e14568e8", + Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + Responses: map[MockedRequest]MockedResponse{ + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + }, + SendPrep: setSendURL}, + {Label: "Interactive List Message Send with attachment", + Text: "Interactive List Msg", URN: "whatsapp:250788123123", QuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + Status: "W", ExternalID: "157b5e14568e8", + Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + Responses: map[MockedRequest]MockedResponse{ + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg"}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + MockedRequest{ + Method: "POST", + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","preview_url":false,"recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"list","body":{"text":"Interactive List Msg"},"action":{"button":"Menu","sections":[{"rows":[{"id":"0","title":"ROW1"},{"id":"1","title":"ROW2"},{"id":"2","title":"ROW3"},{"id":"3","title":"ROW4"}]}]}}}`, + }: MockedResponse{ + Status: 201, + Body: `{ "messages": [{"id": "157b5e14568e8"}] }`, + }, + }, + SendPrep: setSendURL}, +} + func TestSending(t *testing.T) { // shorter max msg length for testing maxMsgLength = 100 var ChannelFBA = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "FBA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) var ChannelIG = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) + var ChannelCWA = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "CWA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123", configCWAPhoneNumberID: "12345_ID"}) RunChannelSendTestCases(t, ChannelFBA, newHandler("FBA", "Facebook", false), SendTestCasesFBA, nil) RunChannelSendTestCases(t, ChannelIG, newHandler("IG", "Instagram", false), SendTestCasesIG, nil) + RunChannelSendTestCases(t, ChannelCWA, newHandler("CWA", "Cloud API WhatsApp", false), SendTestCasesCWA, nil) } func TestSigning(t *testing.T) { diff --git a/handlers/facebookapp/testdata/cwa/audioCWA.json b/handlers/facebookapp/testdata/cwa/audioCWA.json new file mode 100644 index 000000000..47ce575db --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/audioCWA.json @@ -0,0 +1,43 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "audio": { + "file": "/usr/local/wamedia/shared/b1cf38-8734-4ad3-b4a1-ef0c10d0d683", + "id": "id_audio", + "mime_type": "image/jpeg", + "sha256": "29ed500fa64eb55fc19dc4124acb300e5dcc54a0f822a301ae99944db", + "caption": "Check out my new phone!" + }, + "timestamp": "1454119029", + "type": "audio" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/buttonCWA.json b/handlers/facebookapp/testdata/cwa/buttonCWA.json new file mode 100644 index 000000000..36efcca6e --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/buttonCWA.json @@ -0,0 +1,44 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "button": { + "payload": "No-Button-Payload", + "text": "No" + }, + "context": { + "from": "5678", + "id": "gBGGFmkiWVVPAgkgQkwi7IORac0" + }, + "from": "5678", + "id": "external_id", + "timestamp": "1454119029", + "type": "button" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/documentCWA.json b/handlers/facebookapp/testdata/cwa/documentCWA.json new file mode 100644 index 000000000..d65921ef4 --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/documentCWA.json @@ -0,0 +1,43 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "timestamp": "1454119029", + "type": "document", + "document": { + "caption": "80skaraokesonglistartist", + "file": "/usr/local/wamedia/shared/fc233119-733f-49c-bcbd-b2f68f798e33", + "id": "id_document", + "mime_type": "application/pdf", + "sha256": "3b11fa6ef2bde1dd14726e09d3edaf782120919d06f6484f32d5d5caa4b8e" + } + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/duplicateCWA.json b/handlers/facebookapp/testdata/cwa/duplicateCWA.json new file mode 100644 index 000000000..f857c63f7 --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/duplicateCWA.json @@ -0,0 +1,48 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "timestamp": "1454119029", + "text": { + "body": "Hello World" + }, + "type": "text" + }, + { + "from": "5678", + "id": "external_id", + "timestamp": "1454119029", + "text": { + "body": "Hello World" + }, + "type": "text" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/helloCWA.json b/handlers/facebookapp/testdata/cwa/helloCWA.json new file mode 100644 index 000000000..f49303d2f --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/helloCWA.json @@ -0,0 +1,39 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "timestamp": "1454119029", + "text": { + "body": "Hello World" + }, + "type": "text" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/ignoreStatusCWA.json b/handlers/facebookapp/testdata/cwa/ignoreStatusCWA.json new file mode 100644 index 000000000..bf9f69714 --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/ignoreStatusCWA.json @@ -0,0 +1,49 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "statuses": [ + { + "id": "external_id", + "recipient_id": "5678", + "status": "deleted", + "timestamp": "1454119029", + "type": "message", + "conversation": { + "id": "CONVERSATION_ID", + "expiration_timestamp": 1454119029, + "origin": { + "type": "referral_conversion" + } + }, + "pricing": { + "pricing_model": "CBP", + "billable": false, + "category": "referral_conversion" + } + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/imageCWA.json b/handlers/facebookapp/testdata/cwa/imageCWA.json new file mode 100644 index 000000000..f06b631fc --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/imageCWA.json @@ -0,0 +1,43 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "image": { + "file": "/usr/local/wamedia/shared/b1cf38-8734-4ad3-b4a1-ef0c10d0d683", + "id": "id_image", + "mime_type": "image/jpeg", + "sha256": "29ed500fa64eb55fc19dc4124acb300e5dcc54a0f822a301ae99944db", + "caption": "Check out my new phone!" + }, + "timestamp": "1454119029", + "type": "image" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/invalidFrom.json b/handlers/facebookapp/testdata/cwa/invalidFrom.json new file mode 100644 index 000000000..052db4a38 --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/invalidFrom.json @@ -0,0 +1,39 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "bla" + } + ], + "messages": [ + { + "from": "bla", + "id": "external_id", + "timestamp": "1454119029", + "text": { + "body": "Hello World" + }, + "type": "text" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/invalidStatusCWA.json b/handlers/facebookapp/testdata/cwa/invalidStatusCWA.json new file mode 100644 index 000000000..60676257f --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/invalidStatusCWA.json @@ -0,0 +1,49 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "statuses": [ + { + "id": "external_id", + "recipient_id": "5678", + "status": "in_orbit", + "timestamp": "1454119029", + "type": "message", + "conversation": { + "id": "CONVERSATION_ID", + "expiration_timestamp": 1454119029, + "origin": { + "type": "referral_conversion" + } + }, + "pricing": { + "pricing_model": "CBP", + "billable": false, + "category": "referral_conversion" + } + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/invalidTimestamp.json b/handlers/facebookapp/testdata/cwa/invalidTimestamp.json new file mode 100644 index 000000000..e9f301ce9 --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/invalidTimestamp.json @@ -0,0 +1,39 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "bla" + } + ], + "messages": [ + { + "from": "bla", + "id": "external_id", + "timestamp": "asdf", + "text": { + "body": "Hello World" + }, + "type": "text" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/locationCWA.json b/handlers/facebookapp/testdata/cwa/locationCWA.json new file mode 100644 index 000000000..15cedaa1f --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/locationCWA.json @@ -0,0 +1,43 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "location": { + "address": "Main Street Beach, Santa Cruz, CA", + "latitude": 0.000000, + "longitude": 1.000000, + "name": "Main Street Beach", + "url": "https://foursquare.com/v/4d7031d35b5df7744" + }, + "timestamp": "1454119029", + "type": "location" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/validStatusCWA.json b/handlers/facebookapp/testdata/cwa/validStatusCWA.json new file mode 100644 index 000000000..0aaf2edb7 --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/validStatusCWA.json @@ -0,0 +1,49 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "statuses": [ + { + "id": "external_id", + "recipient_id": "5678", + "status": "sent", + "timestamp": "1454119029", + "type": "message", + "conversation": { + "id": "CONVERSATION_ID", + "expiration_timestamp": 1454119029, + "origin": { + "type": "referral_conversion" + } + }, + "pricing": { + "pricing_model": "CBP", + "billable": false, + "category": "referral_conversion" + } + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/videoCWA.json b/handlers/facebookapp/testdata/cwa/videoCWA.json new file mode 100644 index 000000000..210dd9a81 --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/videoCWA.json @@ -0,0 +1,43 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "video": { + "file": "/usr/local/wamedia/shared/b1cf38-8734-4ad3-b4a1-ef0c10d0d683", + "id": "id_video", + "mime_type": "image/jpeg", + "sha256": "29ed500fa64eb55fc19dc4124acb300e5dcc54a0f822a301ae99944db", + "caption": "Check out my new phone!" + }, + "timestamp": "1454119029", + "type": "video" + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file diff --git a/handlers/facebookapp/testdata/cwa/voiceCWA.json b/handlers/facebookapp/testdata/cwa/voiceCWA.json new file mode 100644 index 000000000..3e2022eff --- /dev/null +++ b/handlers/facebookapp/testdata/cwa/voiceCWA.json @@ -0,0 +1,42 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "12345", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "timestamp": "1454119029", + "type": "voice", + "voice": { + "file": "/usr/local/wamedia/shared/463e/b7ec/ff4e4d9bb1101879cbd411b2", + "id": "id_voice", + "mime_type": "audio/ogg; codecs=opus", + "sha256": "fa9e1807d936b7cebe63654ea3a7912b1fa9479220258d823590521ef53b0710" + } + } + ] + }, + "field": "messages" + } + ] + } + ] +} \ No newline at end of file From 65a09d27e1ab8b1b5833ccba9d828c8975cd00ba Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Wed, 13 Apr 2022 19:31:19 +0200 Subject: [PATCH 2/5] Rename to use WAC and WhatsApp Cloud --- handlers/facebookapp/facebookapp.go | 154 +++++++++--------- handlers/facebookapp/facebookapp_test.go | 52 +++--- .../{cwa/audioCWA.json => wac/audioWAC.json} | 0 .../buttonCWA.json => wac/buttonWAC.json} | 0 .../documentCWA.json => wac/documentWAC.json} | 0 .../duplicateWAC.json} | 0 .../{cwa/helloCWA.json => wac/helloWAC.json} | 0 .../ignoreStatusWAC.json} | 0 .../{cwa/imageCWA.json => wac/imageWAC.json} | 0 .../testdata/{cwa => wac}/invalidFrom.json | 0 .../invalidStatusWAC.json} | 0 .../{cwa => wac}/invalidTimestamp.json | 0 .../locationCWA.json => wac/locationWAC.json} | 0 .../validStatusWAC.json} | 0 .../{cwa/videoCWA.json => wac/videoWAC.json} | 0 .../{cwa/voiceCWA.json => wac/voiceWAC.json} | 0 16 files changed, 103 insertions(+), 103 deletions(-) rename handlers/facebookapp/testdata/{cwa/audioCWA.json => wac/audioWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/buttonCWA.json => wac/buttonWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/documentCWA.json => wac/documentWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/duplicateCWA.json => wac/duplicateWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/helloCWA.json => wac/helloWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/ignoreStatusCWA.json => wac/ignoreStatusWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/imageCWA.json => wac/imageWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa => wac}/invalidFrom.json (100%) rename handlers/facebookapp/testdata/{cwa/invalidStatusCWA.json => wac/invalidStatusWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa => wac}/invalidTimestamp.json (100%) rename handlers/facebookapp/testdata/{cwa/locationCWA.json => wac/locationWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/validStatusCWA.json => wac/validStatusWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/videoCWA.json => wac/videoWAC.json} (100%) rename handlers/facebookapp/testdata/{cwa/voiceCWA.json => wac/voiceWAC.json} (100%) diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index b81b4aba8..c5e0bf11b 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -29,7 +29,7 @@ var ( signatureHeader = "X-Hub-Signature" - configCWAPhoneNumberID = "cwa_phone_number_id" + configWACPhoneNumberID = "wac_phone_number_id" // max for the body maxMsgLength = 1000 @@ -77,7 +77,7 @@ func newHandler(channelType courier.ChannelType, name string, useUUIDRoutes bool func init() { courier.RegisterHandler(newHandler("IG", "Instagram", false)) courier.RegisterHandler(newHandler("FBA", "Facebook", false)) - courier.RegisterHandler(newHandler("CWA", "Cloud API WhatsApp", false)) + courier.RegisterHandler(newHandler("WAC", "WhatsApp Cloud", false)) } @@ -120,7 +120,7 @@ type User struct { // }] // } -type cwaMedia struct { +type wacMedia struct { Caption string `json:"caption"` Filename string `json:"filename"` ID string `json:"id"` @@ -160,11 +160,11 @@ type moPayload struct { Text struct { Body string `json:"body"` } `json:"text"` - Image *cwaMedia `json:"image"` - Audio *cwaMedia `json:"audio"` - Video *cwaMedia `json:"video"` - Document *cwaMedia `json:"document"` - Voice *cwaMedia `json:"voice"` + Image *wacMedia `json:"image"` + Audio *wacMedia `json:"audio"` + Video *wacMedia `json:"video"` + Document *wacMedia `json:"document"` + Voice *wacMedia `json:"voice"` Location *struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` @@ -294,9 +294,9 @@ func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Chan channelAddress = payload.Entry[0].Changes[0].Value.Metadata.DisplayPhoneNumber if channelAddress == "" { - return nil, fmt.Errorf("no channel adress found") + return nil, fmt.Errorf("no channel address found") } - return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("CWA"), courier.ChannelAddress(channelAddress)) + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("WAC"), courier.ChannelAddress(channelAddress)) } } @@ -790,7 +790,7 @@ type mtQuickReply struct { func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { if msg.Channel().ChannelType() == "FBA" || msg.Channel().ChannelType() == "IG" { return h.sendFacebookInstagramMsg(ctx, msg) - } else if msg.Channel().ChannelType() == "CWA" { + } else if msg.Channel().ChannelType() == "WAC" { return h.sendCloudAPIWhatsappMsg(ctx, msg) } @@ -943,25 +943,25 @@ func (h *handler) sendFacebookInstagramMsg(ctx context.Context, msg courier.Msg) return status, nil } -type cwaMTMedia struct { +type wacMTMedia struct { ID string `json:"id,omitempty"` Link string `json:"link,omitempty"` Caption string `json:"caption,omitempty"` Filename string `json:"filename,omitempty"` } -type cwaMTSection struct { +type wacMTSection struct { Title string `json:"title,omitempty"` - Rows []cwaMTSectionRow `json:"rows" validate:"required"` + Rows []wacMTSectionRow `json:"rows" validate:"required"` } -type cwaMTSectionRow struct { +type wacMTSectionRow struct { ID string `json:"id" validate:"required"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` } -type cwaMTButton struct { +type wacMTButton struct { Type string `json:"type" validate:"required"` Reply struct { ID string `json:"id" validate:"required"` @@ -969,34 +969,34 @@ type cwaMTButton struct { } `json:"reply" validate:"required"` } -type cwaParam struct { +type wacParam struct { Type string `json:"type"` Text string `json:"text"` } -type cwaComponent struct { +type wacComponent struct { Type string `json:"type"` SubType string `json:"sub_type"` Index string `json:"index"` - Params []*cwaParam `json:"parameters"` + Params []*wacParam `json:"parameters"` } -type cwaText struct { +type wacText struct { Body string `json:"body"` } -type cwaLanguage struct { +type wacLanguage struct { Policy string `json:"policy"` Code string `json:"code"` } -type cwaTemplate struct { +type wacTemplate struct { Name string `json:"name"` - Language *cwaLanguage `json:"language"` - Components []*cwaComponent `json:"components"` + Language *wacLanguage `json:"language"` + Components []*wacComponent `json:"components"` } -type cwaInteractive struct { +type wacInteractive struct { Type string `json:"type"` Header *struct { Type string `json:"type"` @@ -1013,31 +1013,31 @@ type cwaInteractive struct { } `json:"footer,omitempty"` Action *struct { Button string `json:"button,omitempty"` - Sections []cwaMTSection `json:"sections,omitempty"` - Buttons []cwaMTButton `json:"buttons,omitempty"` + Sections []wacMTSection `json:"sections,omitempty"` + Buttons []wacMTButton `json:"buttons,omitempty"` } `json:"action,omitempty"` } -type cwaMTPayload struct { +type wacMTPayload struct { MessagingProduct string `json:"messaging_product"` PreviewURL bool `json:"preview_url"` RecipientType string `json:"recipient_type"` To string `json:"to"` Type string `json:"type"` - Text *cwaText `json:"text,omitempty"` + Text *wacText `json:"text,omitempty"` - Document *cwaMTMedia `json:"document,omitempty"` - Image *cwaMTMedia `json:"image,omitempty"` - Audio *cwaMTMedia `json:"audio,omitempty"` - Video *cwaMTMedia `json:"video,omitempty"` + Document *wacMTMedia `json:"document,omitempty"` + Image *wacMTMedia `json:"image,omitempty"` + Audio *wacMTMedia `json:"audio,omitempty"` + Video *wacMTMedia `json:"video,omitempty"` - Interactive *cwaInteractive `json:"interactive,omitempty"` + Interactive *wacInteractive `json:"interactive,omitempty"` - Template *cwaTemplate `json:"template,omitempty"` + Template *wacTemplate `json:"template,omitempty"` } -type cwaMTResponse struct { +type wacMTResponse struct { Messages []*struct { ID string `json:"id"` } `json:"messages"` @@ -1050,14 +1050,14 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) return nil, fmt.Errorf("missing access token") } - phoneNumberId := msg.Channel().StringConfigForKey(configCWAPhoneNumberID, "") + phoneNumberId := msg.Channel().StringConfigForKey(configWACPhoneNumberID, "") if phoneNumberId == "" { - return nil, fmt.Errorf("missing CWA phone number ID") + return nil, fmt.Errorf("missing WAC phone number ID") } base, _ := url.Parse(graphURL) path, _ := url.Parse(fmt.Sprintf("/%s/messages", phoneNumberId)) - cwaPhoneURL := base.ResolveReference(path) + wacPhoneURL := base.ResolveReference(path) status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored) @@ -1068,7 +1068,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) qrs := msg.QuickReplies() for i := 0; i < len(msgParts)+len(msg.Attachments()); i++ { - payload := cwaMTPayload{MessagingProduct: "whatsapp", RecipientType: "individual", To: msg.URN().Path()} + payload := wacMTPayload{MessagingProduct: "whatsapp", RecipientType: "individual", To: msg.URN().Path()} if len(msg.Attachments()) == 0 { // do we have a template? @@ -1081,13 +1081,13 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) payload.Type = "template" - template := cwaTemplate{Name: templating.Template.Name, Language: &cwaLanguage{Policy: "deterministic", Code: templating.Language}} + template := wacTemplate{Name: templating.Template.Name, Language: &wacLanguage{Policy: "deterministic", Code: templating.Language}} payload.Template = &template - component := &cwaComponent{Type: "body"} + component := &wacComponent{Type: "body"} for _, v := range templating.Variables { - component.Params = append(component.Params, &cwaParam{Type: "text", Text: v}) + component.Params = append(component.Params, &wacParam{Type: "text", Text: v}) } template.Components = append(payload.Template.Components, component) @@ -1095,19 +1095,19 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) if i < (len(msgParts) + len(msg.Attachments()) - 1) { // this is still a msg part payload.Type = "text" - payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + payload.Text = &wacText{Body: msgParts[i-len(msg.Attachments())]} } else { if len(qrs) > 0 { payload.Type = "interactive" // We can use buttons if len(qrs) <= 3 { - interactive := cwaInteractive{Type: "button", Body: struct { + interactive := wacInteractive{Type: "button", Body: struct { Text string "json:\"text\"" }{Text: msgParts[i-len(msg.Attachments())]}} - btns := make([]cwaMTButton, len(qrs)) + btns := make([]wacMTButton, len(qrs)) for i, qr := range qrs { - btns[i] = cwaMTButton{ + btns[i] = wacMTButton{ Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) @@ -1115,20 +1115,20 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) } interactive.Action = &struct { Button string "json:\"button,omitempty\"" - Sections []cwaMTSection "json:\"sections,omitempty\"" - Buttons []cwaMTButton "json:\"buttons,omitempty\"" + Sections []wacMTSection "json:\"sections,omitempty\"" + Buttons []wacMTButton "json:\"buttons,omitempty\"" }{Buttons: btns} payload.Interactive = &interactive } else if len(qrs) <= 10 { - interactive := cwaInteractive{Type: "list", Body: struct { + interactive := wacInteractive{Type: "list", Body: struct { Text string "json:\"text\"" }{Text: msgParts[i-len(msg.Attachments())]}} - section := cwaMTSection{ - Rows: make([]cwaMTSectionRow, len(qrs)), + section := wacMTSection{ + Rows: make([]wacMTSectionRow, len(qrs)), } for i, qr := range qrs { - section.Rows[i] = cwaMTSectionRow{ + section.Rows[i] = wacMTSectionRow{ ID: fmt.Sprint(i), Title: qr, } @@ -1136,20 +1136,20 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) interactive.Action = &struct { Button string "json:\"button,omitempty\"" - Sections []cwaMTSection "json:\"sections,omitempty\"" - Buttons []cwaMTButton "json:\"buttons,omitempty\"" - }{Button: "Menu", Sections: []cwaMTSection{ + Sections []wacMTSection "json:\"sections,omitempty\"" + Buttons []wacMTButton "json:\"buttons,omitempty\"" + }{Button: "Menu", Sections: []wacMTSection{ section, }} payload.Interactive = &interactive } else { - return nil, fmt.Errorf("too many quick replies CWA supports only up to 10 quick replies") + return nil, fmt.Errorf("too many quick replies WAC supports only up to 10 quick replies") } } else { // this is still a msg part payload.Type = "text" - payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + payload.Text = &wacText{Body: msgParts[i-len(msg.Attachments())]} } } } @@ -1161,7 +1161,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) attType = "document" } payload.Type = attType - media := cwaMTMedia{Link: attURL} + media := wacMTMedia{Link: attURL} if attType == "image" { payload.Image = &media @@ -1176,19 +1176,19 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) if i < (len(msgParts) + len(msg.Attachments()) - 1) { // this is still a msg part payload.Type = "text" - payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + payload.Text = &wacText{Body: msgParts[i-len(msg.Attachments())]} } else { if len(qrs) > 0 { payload.Type = "interactive" // We can use buttons if len(qrs) <= 3 { - interactive := cwaInteractive{Type: "button", Body: struct { + interactive := wacInteractive{Type: "button", Body: struct { Text string "json:\"text\"" }{Text: msgParts[i-len(msg.Attachments())]}} - btns := make([]cwaMTButton, len(qrs)) + btns := make([]wacMTButton, len(qrs)) for i, qr := range qrs { - btns[i] = cwaMTButton{ + btns[i] = wacMTButton{ Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) @@ -1196,21 +1196,21 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) } interactive.Action = &struct { Button string "json:\"button,omitempty\"" - Sections []cwaMTSection "json:\"sections,omitempty\"" - Buttons []cwaMTButton "json:\"buttons,omitempty\"" + Sections []wacMTSection "json:\"sections,omitempty\"" + Buttons []wacMTButton "json:\"buttons,omitempty\"" }{Buttons: btns} payload.Interactive = &interactive } else if len(qrs) <= 10 { - interactive := cwaInteractive{Type: "list", Body: struct { + interactive := wacInteractive{Type: "list", Body: struct { Text string "json:\"text\"" }{Text: msgParts[i-len(msg.Attachments())]}} - section := cwaMTSection{ - Rows: make([]cwaMTSectionRow, len(qrs)), + section := wacMTSection{ + Rows: make([]wacMTSectionRow, len(qrs)), } for i, qr := range qrs { - section.Rows[i] = cwaMTSectionRow{ + section.Rows[i] = wacMTSectionRow{ ID: fmt.Sprint(i), Title: qr, } @@ -1218,20 +1218,20 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) interactive.Action = &struct { Button string "json:\"button,omitempty\"" - Sections []cwaMTSection "json:\"sections,omitempty\"" - Buttons []cwaMTButton "json:\"buttons,omitempty\"" - }{Button: "Menu", Sections: []cwaMTSection{ + Sections []wacMTSection "json:\"sections,omitempty\"" + Buttons []wacMTButton "json:\"buttons,omitempty\"" + }{Button: "Menu", Sections: []wacMTSection{ section, }} payload.Interactive = &interactive } else { - return nil, fmt.Errorf("too many quick replies CWA supports only up to 10 quick replies") + return nil, fmt.Errorf("too many quick replies WAC supports only up to 10 quick replies") } } else { // this is still a msg part payload.Type = "text" - payload.Text = &cwaText{Body: msgParts[i-len(msg.Attachments())]} + payload.Text = &wacText{Body: msgParts[i-len(msg.Attachments())]} } } @@ -1242,7 +1242,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) return status, err } - req, err := http.NewRequest(http.MethodPost, cwaPhoneURL.String(), bytes.NewReader(jsonBody)) + req, err := http.NewRequest(http.MethodPost, wacPhoneURL.String(), bytes.NewReader(jsonBody)) if err != nil { return nil, err } @@ -1259,7 +1259,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) return status, nil } - respPayload := &cwaMTResponse{} + respPayload := &wacMTResponse{} err = json.Unmarshal(rr.Body, respPayload) if err != nil { log.WithError("Message Send Error", errors.Errorf("unable to unmarshal response body")) @@ -1278,7 +1278,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) // DescribeURN looks up URN metadata for new contacts func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn urns.URN) (map[string]string, error) { - if channel.ChannelType() == "CWA" { + if channel.ChannelType() == "WAC" { return map[string]string{}, nil } diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index c5a526ebe..506d0dfd8 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -25,8 +25,8 @@ var testChannelsIG = []courier.Channel{ courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), } -var testChannelsCWA = []courier.Channel{ - courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "CWA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), +var testChannelsWAC = []courier.Channel{ + courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "WAC", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), } var testCasesFBA = []ChannelHandleTestCase{ @@ -218,8 +218,8 @@ func TestDescribeIG(t *testing.T) { } } -func TestDescribeCWA(t *testing.T) { - handler := newHandler("CWA", "Cloud API WhatsApp", false).(courier.URNDescriber) +func TestDescribeWAC(t *testing.T) { + handler := newHandler("WAC", "Cloud API WhatsApp", false).(courier.URNDescriber) tcs := []struct { urn urns.URN @@ -228,53 +228,53 @@ func TestDescribeCWA(t *testing.T) { {"whatsapp:4567", map[string]string{}}} for _, tc := range tcs { - metadata, _ := handler.DescribeURN(context.Background(), testChannelsCWA[0], tc.urn) + metadata, _ := handler.DescribeURN(context.Background(), testChannelsWAC[0], tc.urn) assert.Equal(t, metadata, tc.metadata) } } -var cwaReceiveURL = "/c/cwa/receive" +var wacReceiveURL = "/c/wac/receive" -var testCasesCWA = []ChannelHandleTestCase{ - {Label: "Receive Message CWA", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/helloCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, +var testCasesWAC = []ChannelHandleTestCase{ + {Label: "Receive Message WAC", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/helloWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("Hello World"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Duplicate Valid Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/duplicateCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + {Label: "Receive Duplicate Valid Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/duplicateWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("Hello World"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Valid Voice Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/voiceCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + {Label: "Receive Valid Voice Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/voiceWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp(""), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Voice"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Valid Button Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/buttonCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + {Label: "Receive Valid Button Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/buttonWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("No"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Valid Document Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/documentCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + {Label: "Receive Valid Document Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/documentWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("80skaraokesonglistartist"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Document"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Valid Image Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/imageCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + {Label: "Receive Valid Image Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/imageWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("Check out my new phone!"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Image"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Valid Video Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/videoCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + {Label: "Receive Valid Video Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/videoWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("Check out my new phone!"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Video"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Valid Audio Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/audioCWA.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + {Label: "Receive Valid Audio Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/audioWAC.json")), Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("Check out my new phone!"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Attachment: Sp("https://foo.bar/attachmentURL_Audio"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Valid Location Message", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/locationCWA.json")), Status: 200, Response: `"type":"msg"`, + {Label: "Receive Valid Location Message", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/locationWAC.json")), Status: 200, Response: `"type":"msg"`, Text: Sp(""), Attachment: Sp("geo:0.000000,1.000000"), URN: Sp("whatsapp:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Invalid JSON", URL: cwaReceiveURL, Data: "not json", Status: 400, Response: "unable to parse", PrepRequest: addValidSignature}, - {Label: "Receive Invalid JSON", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/invalidFrom.json")), Status: 400, Response: "invalid whatsapp id", PrepRequest: addValidSignature}, - {Label: "Receive Invalid JSON", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/invalidTimestamp.json")), Status: 400, Response: "invalid timestamp", PrepRequest: addValidSignature}, + {Label: "Receive Invalid JSON", URL: wacReceiveURL, Data: "not json", Status: 400, Response: "unable to parse", PrepRequest: addValidSignature}, + {Label: "Receive Invalid JSON", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/invalidFrom.json")), Status: 400, Response: "invalid whatsapp id", PrepRequest: addValidSignature}, + {Label: "Receive Invalid JSON", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/invalidTimestamp.json")), Status: 400, Response: "invalid timestamp", PrepRequest: addValidSignature}, - {Label: "Receive Valid Status", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/validStatusCWA.json")), Status: 200, Response: `"type":"status"`, + {Label: "Receive Valid Status", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/validStatusWAC.json")), Status: 200, Response: `"type":"status"`, MsgStatus: Sp("S"), ExternalID: Sp("external_id"), PrepRequest: addValidSignature}, - {Label: "Receive Invalid Status", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/invalidStatusCWA.json")), Status: 400, Response: `"unknown status: in_orbit"`, PrepRequest: addValidSignature}, - {Label: "Receive Ignore Status", URL: cwaReceiveURL, Data: string(courier.ReadFile("./testdata/cwa/ignoreStatusCWA.json")), Status: 200, Response: `"ignoring status: deleted"`, PrepRequest: addValidSignature}, + {Label: "Receive Invalid Status", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/invalidStatusWAC.json")), Status: 400, Response: `"unknown status: in_orbit"`, PrepRequest: addValidSignature}, + {Label: "Receive Ignore Status", URL: wacReceiveURL, Data: string(courier.ReadFile("./testdata/wac/ignoreStatusWAC.json")), Status: 200, Response: `"ignoring status: deleted"`, PrepRequest: addValidSignature}, } func TestHandler(t *testing.T) { @@ -320,7 +320,7 @@ func TestHandler(t *testing.T) { })) graphURL = server.URL - RunChannelTestCases(t, testChannelsCWA, newHandler("CWA", "Cloud API WhatsApp", false), testCasesCWA) + RunChannelTestCases(t, testChannelsWAC, newHandler("WAC", "Cloud API WhatsApp", false), testCasesWAC) RunChannelTestCases(t, testChannelsFBA, newHandler("FBA", "Facebook", false), testCasesFBA) RunChannelTestCases(t, testChannelsIG, newHandler("IG", "Instagram", false), testCasesIG) } @@ -494,7 +494,7 @@ var SendTestCasesIG = []ChannelSendTestCase{ SendPrep: setSendURL}, } -var SendTestCasesCWA = []ChannelSendTestCase{ +var SendTestCasesWAC = []ChannelSendTestCase{ {Label: "Plain Send", Text: "Simple Message", URN: "whatsapp:250788123123", Path: "/12345_ID/messages", Status: "W", ExternalID: "157b5e14568e8", @@ -694,10 +694,10 @@ func TestSending(t *testing.T) { maxMsgLength = 100 var ChannelFBA = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "FBA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) var ChannelIG = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) - var ChannelCWA = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "CWA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123", configCWAPhoneNumberID: "12345_ID"}) + var ChannelWAC = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "WAC", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123", configWACPhoneNumberID: "12345_ID"}) RunChannelSendTestCases(t, ChannelFBA, newHandler("FBA", "Facebook", false), SendTestCasesFBA, nil) RunChannelSendTestCases(t, ChannelIG, newHandler("IG", "Instagram", false), SendTestCasesIG, nil) - RunChannelSendTestCases(t, ChannelCWA, newHandler("CWA", "Cloud API WhatsApp", false), SendTestCasesCWA, nil) + RunChannelSendTestCases(t, ChannelWAC, newHandler("WAC", "Cloud API WhatsApp", false), SendTestCasesWAC, nil) } func TestSigning(t *testing.T) { diff --git a/handlers/facebookapp/testdata/cwa/audioCWA.json b/handlers/facebookapp/testdata/wac/audioWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/audioCWA.json rename to handlers/facebookapp/testdata/wac/audioWAC.json diff --git a/handlers/facebookapp/testdata/cwa/buttonCWA.json b/handlers/facebookapp/testdata/wac/buttonWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/buttonCWA.json rename to handlers/facebookapp/testdata/wac/buttonWAC.json diff --git a/handlers/facebookapp/testdata/cwa/documentCWA.json b/handlers/facebookapp/testdata/wac/documentWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/documentCWA.json rename to handlers/facebookapp/testdata/wac/documentWAC.json diff --git a/handlers/facebookapp/testdata/cwa/duplicateCWA.json b/handlers/facebookapp/testdata/wac/duplicateWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/duplicateCWA.json rename to handlers/facebookapp/testdata/wac/duplicateWAC.json diff --git a/handlers/facebookapp/testdata/cwa/helloCWA.json b/handlers/facebookapp/testdata/wac/helloWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/helloCWA.json rename to handlers/facebookapp/testdata/wac/helloWAC.json diff --git a/handlers/facebookapp/testdata/cwa/ignoreStatusCWA.json b/handlers/facebookapp/testdata/wac/ignoreStatusWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/ignoreStatusCWA.json rename to handlers/facebookapp/testdata/wac/ignoreStatusWAC.json diff --git a/handlers/facebookapp/testdata/cwa/imageCWA.json b/handlers/facebookapp/testdata/wac/imageWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/imageCWA.json rename to handlers/facebookapp/testdata/wac/imageWAC.json diff --git a/handlers/facebookapp/testdata/cwa/invalidFrom.json b/handlers/facebookapp/testdata/wac/invalidFrom.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/invalidFrom.json rename to handlers/facebookapp/testdata/wac/invalidFrom.json diff --git a/handlers/facebookapp/testdata/cwa/invalidStatusCWA.json b/handlers/facebookapp/testdata/wac/invalidStatusWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/invalidStatusCWA.json rename to handlers/facebookapp/testdata/wac/invalidStatusWAC.json diff --git a/handlers/facebookapp/testdata/cwa/invalidTimestamp.json b/handlers/facebookapp/testdata/wac/invalidTimestamp.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/invalidTimestamp.json rename to handlers/facebookapp/testdata/wac/invalidTimestamp.json diff --git a/handlers/facebookapp/testdata/cwa/locationCWA.json b/handlers/facebookapp/testdata/wac/locationWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/locationCWA.json rename to handlers/facebookapp/testdata/wac/locationWAC.json diff --git a/handlers/facebookapp/testdata/cwa/validStatusCWA.json b/handlers/facebookapp/testdata/wac/validStatusWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/validStatusCWA.json rename to handlers/facebookapp/testdata/wac/validStatusWAC.json diff --git a/handlers/facebookapp/testdata/cwa/videoCWA.json b/handlers/facebookapp/testdata/wac/videoWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/videoCWA.json rename to handlers/facebookapp/testdata/wac/videoWAC.json diff --git a/handlers/facebookapp/testdata/cwa/voiceCWA.json b/handlers/facebookapp/testdata/wac/voiceWAC.json similarity index 100% rename from handlers/facebookapp/testdata/cwa/voiceCWA.json rename to handlers/facebookapp/testdata/wac/voiceWAC.json From 097b939444b1c9927dd39ba67c5c25c11bb52ff4 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Thu, 28 Apr 2022 18:40:33 +0200 Subject: [PATCH 3/5] Adjust to use phone ID stored as channel address --- handlers/facebookapp/facebookapp.go | 9 ++------- handlers/facebookapp/facebookapp_test.go | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index c5e0bf11b..4c3c45908 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -292,7 +292,7 @@ func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Chan return nil, fmt.Errorf("no changes found") } - channelAddress = payload.Entry[0].Changes[0].Value.Metadata.DisplayPhoneNumber + channelAddress = payload.Entry[0].Changes[0].Value.Metadata.PhoneNumberID if channelAddress == "" { return nil, fmt.Errorf("no channel address found") } @@ -1050,13 +1050,8 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) return nil, fmt.Errorf("missing access token") } - phoneNumberId := msg.Channel().StringConfigForKey(configWACPhoneNumberID, "") - if phoneNumberId == "" { - return nil, fmt.Errorf("missing WAC phone number ID") - } - base, _ := url.Parse(graphURL) - path, _ := url.Parse(fmt.Sprintf("/%s/messages", phoneNumberId)) + path, _ := url.Parse(fmt.Sprintf("/%s/messages", msg.Channel().Address())) wacPhoneURL := base.ResolveReference(path) status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored) diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 506d0dfd8..aaba35fd3 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -694,7 +694,7 @@ func TestSending(t *testing.T) { maxMsgLength = 100 var ChannelFBA = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "FBA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) var ChannelIG = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) - var ChannelWAC = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "WAC", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123", configWACPhoneNumberID: "12345_ID"}) + var ChannelWAC = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "WAC", "12345_ID", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) RunChannelSendTestCases(t, ChannelFBA, newHandler("FBA", "Facebook", false), SendTestCasesFBA, nil) RunChannelSendTestCases(t, ChannelIG, newHandler("IG", "Instagram", false), SendTestCasesIG, nil) RunChannelSendTestCases(t, ChannelWAC, newHandler("WAC", "Cloud API WhatsApp", false), SendTestCasesWAC, nil) From bd84397b84a44342ea201e091b2bd7a07b614306 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Thu, 28 Apr 2022 20:05:36 +0200 Subject: [PATCH 4/5] Fix tests --- handlers/facebookapp/testdata/wac/audioWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/buttonWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/documentWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/duplicateWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/helloWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/ignoreStatusWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/imageWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/invalidFrom.json | 4 ++-- handlers/facebookapp/testdata/wac/invalidStatusWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/invalidTimestamp.json | 4 ++-- handlers/facebookapp/testdata/wac/locationWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/validStatusWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/videoWAC.json | 4 ++-- handlers/facebookapp/testdata/wac/voiceWAC.json | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/handlers/facebookapp/testdata/wac/audioWAC.json b/handlers/facebookapp/testdata/wac/audioWAC.json index 47ce575db..f578e5fc9 100644 --- a/handlers/facebookapp/testdata/wac/audioWAC.json +++ b/handlers/facebookapp/testdata/wac/audioWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/buttonWAC.json b/handlers/facebookapp/testdata/wac/buttonWAC.json index 36efcca6e..10f592773 100644 --- a/handlers/facebookapp/testdata/wac/buttonWAC.json +++ b/handlers/facebookapp/testdata/wac/buttonWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/documentWAC.json b/handlers/facebookapp/testdata/wac/documentWAC.json index d65921ef4..1c5f08eab 100644 --- a/handlers/facebookapp/testdata/wac/documentWAC.json +++ b/handlers/facebookapp/testdata/wac/documentWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/duplicateWAC.json b/handlers/facebookapp/testdata/wac/duplicateWAC.json index f857c63f7..69463fb0f 100644 --- a/handlers/facebookapp/testdata/wac/duplicateWAC.json +++ b/handlers/facebookapp/testdata/wac/duplicateWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/helloWAC.json b/handlers/facebookapp/testdata/wac/helloWAC.json index f49303d2f..d7cf38ee8 100644 --- a/handlers/facebookapp/testdata/wac/helloWAC.json +++ b/handlers/facebookapp/testdata/wac/helloWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/ignoreStatusWAC.json b/handlers/facebookapp/testdata/wac/ignoreStatusWAC.json index bf9f69714..2b2e583a1 100644 --- a/handlers/facebookapp/testdata/wac/ignoreStatusWAC.json +++ b/handlers/facebookapp/testdata/wac/ignoreStatusWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/imageWAC.json b/handlers/facebookapp/testdata/wac/imageWAC.json index f06b631fc..7d3728e5b 100644 --- a/handlers/facebookapp/testdata/wac/imageWAC.json +++ b/handlers/facebookapp/testdata/wac/imageWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/invalidFrom.json b/handlers/facebookapp/testdata/wac/invalidFrom.json index 052db4a38..12a28cc54 100644 --- a/handlers/facebookapp/testdata/wac/invalidFrom.json +++ b/handlers/facebookapp/testdata/wac/invalidFrom.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/invalidStatusWAC.json b/handlers/facebookapp/testdata/wac/invalidStatusWAC.json index 60676257f..6a3a4fbcc 100644 --- a/handlers/facebookapp/testdata/wac/invalidStatusWAC.json +++ b/handlers/facebookapp/testdata/wac/invalidStatusWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/invalidTimestamp.json b/handlers/facebookapp/testdata/wac/invalidTimestamp.json index e9f301ce9..dc0dd66d5 100644 --- a/handlers/facebookapp/testdata/wac/invalidTimestamp.json +++ b/handlers/facebookapp/testdata/wac/invalidTimestamp.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/locationWAC.json b/handlers/facebookapp/testdata/wac/locationWAC.json index 15cedaa1f..09a721c8d 100644 --- a/handlers/facebookapp/testdata/wac/locationWAC.json +++ b/handlers/facebookapp/testdata/wac/locationWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/validStatusWAC.json b/handlers/facebookapp/testdata/wac/validStatusWAC.json index 0aaf2edb7..8a3360787 100644 --- a/handlers/facebookapp/testdata/wac/validStatusWAC.json +++ b/handlers/facebookapp/testdata/wac/validStatusWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/videoWAC.json b/handlers/facebookapp/testdata/wac/videoWAC.json index 210dd9a81..234422efe 100644 --- a/handlers/facebookapp/testdata/wac/videoWAC.json +++ b/handlers/facebookapp/testdata/wac/videoWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { diff --git a/handlers/facebookapp/testdata/wac/voiceWAC.json b/handlers/facebookapp/testdata/wac/voiceWAC.json index 3e2022eff..03e03375f 100644 --- a/handlers/facebookapp/testdata/wac/voiceWAC.json +++ b/handlers/facebookapp/testdata/wac/voiceWAC.json @@ -8,8 +8,8 @@ "value": { "messaging_product": "whatsapp", "metadata": { - "display_phone_number": "12345", - "phone_number_id": "27681414235104944" + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" }, "contacts": [ { From aab4afa908148ac615681698840cad60bede5d10 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Fri, 29 Apr 2022 12:35:00 +0200 Subject: [PATCH 5/5] Fix token to send WA messages --- config.go | 43 +++++++++++++++-------------- handlers/facebookapp/facebookapp.go | 5 +--- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/config.go b/config.go index 5e6447690..fb9d59a5b 100644 --- a/config.go +++ b/config.go @@ -30,6 +30,8 @@ type Config struct { LogLevel string `help:"the logging level courier should use"` Version string `help:"the version that will be used in request and response headers"` + WhatsappAdminSystemUserToken string `help:"the token of the admin system user for WhatsApp"` + // IncludeChannels is the list of channels to enable, empty means include all IncludeChannels []string @@ -40,26 +42,27 @@ type Config struct { // NewConfig returns a new default configuration object func NewConfig() *Config { return &Config{ - Backend: "rapidpro", - Domain: "localhost", - Address: "", - Port: 8080, - DB: "postgres://temba:temba@localhost/temba?sslmode=disable", - Redis: "redis://localhost:6379/15", - SpoolDir: "/var/spool/courier", - S3Endpoint: "https://s3.amazonaws.com", - S3Region: "us-east-1", - S3MediaBucket: "courier-media", - S3MediaPrefix: "/media/", - S3DisableSSL: false, - S3ForcePathStyle: false, - AWSAccessKeyID: "", - AWSSecretAccessKey: "", - FacebookApplicationSecret: "missing_facebook_app_secret", - FacebookWebhookSecret: "missing_facebook_webhook_secret", - MaxWorkers: 32, - LogLevel: "error", - Version: "Dev", + Backend: "rapidpro", + Domain: "localhost", + Address: "", + Port: 8080, + DB: "postgres://temba:temba@localhost/temba?sslmode=disable", + Redis: "redis://localhost:6379/15", + SpoolDir: "/var/spool/courier", + S3Endpoint: "https://s3.amazonaws.com", + S3Region: "us-east-1", + S3MediaBucket: "courier-media", + S3MediaPrefix: "/media/", + S3DisableSSL: false, + S3ForcePathStyle: false, + AWSAccessKeyID: "", + AWSSecretAccessKey: "", + FacebookApplicationSecret: "missing_facebook_app_secret", + FacebookWebhookSecret: "missing_facebook_webhook_secret", + WhatsappAdminSystemUserToken: "missing_whatsapp_admin_system_user_token", + MaxWorkers: 32, + LogLevel: "error", + Version: "Dev", } } diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index 4c3c45908..e3e2e29af 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -1045,10 +1045,7 @@ type wacMTResponse struct { func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { // can't do anything without an access token - accessToken := msg.Channel().StringConfigForKey(courier.ConfigAuthToken, "") - if accessToken == "" { - return nil, fmt.Errorf("missing access token") - } + accessToken := h.Server().Config().WhatsappAdminSystemUserToken base, _ := url.Parse(graphURL) path, _ := url.Parse(fmt.Sprintf("/%s/messages", msg.Channel().Address()))