From b3c60bc9c32a1b3de66bb2760be7a5d3aafe71dc Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Tue, 5 Sep 2023 13:46:28 +0200 Subject: [PATCH 1/4] Add support to filter media by dimensions and content type precedence for media support config --- handlers/media.go | 20 +++++++++++++++++--- handlers/media_test.go | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/handlers/media.go b/handlers/media.go index 136040584..f1a51bd53 100644 --- a/handlers/media.go +++ b/handlers/media.go @@ -20,8 +20,10 @@ const ( ) type MediaTypeSupport struct { - Types []string - MaxBytes int + Types []string + MaxBytes int + MaxWidth int + MaxHeight int } // Attachment is a resolved attachment @@ -84,7 +86,10 @@ func resolveAttachment(ctx context.Context, b courier.Backend, contentType, medi } mediaType, _ := parseContentType(media.ContentType()) - mediaSupport := support[mediaType] + mediaSupport, ok := support[MediaType(media.ContentType())] + if !ok { + mediaSupport = support[mediaType] + } // our candidates are the uploaded media and any alternates of the same media type candidates := append([]courier.Media{media}, filterMediaByType(media.Alternates(), mediaType)...) @@ -99,6 +104,11 @@ func resolveAttachment(ctx context.Context, b courier.Backend, contentType, medi candidates = filterMediaBySize(candidates, mediaSupport.MaxBytes) } + // narrow down the candidates to the ones that don't exceed our max dimensions + if mediaSupport.MaxWidth > 0 && mediaSupport.MaxHeight > 0 { + candidates = filterMediaByDimensions(candidates, mediaSupport.MaxWidth, mediaSupport.MaxHeight) + } + // if we have no candidates, we can't use this media if len(candidates) == 0 { return nil, nil @@ -144,6 +154,10 @@ func filterMediaBySize(in []courier.Media, maxBytes int) []courier.Media { return filterMedia(in, func(m courier.Media) bool { return m.Size() <= maxBytes }) } +func filterMediaByDimensions(in []courier.Media, maxWidth int, MaxHeight int) []courier.Media { + return filterMedia(in, func(m courier.Media) bool { return m.Width() <= maxWidth && m.Height() <= MaxHeight }) +} + func filterMedia(in []courier.Media, f func(courier.Media) bool) []courier.Media { filtered := make([]courier.Media, 0, len(in)) for _, m := range in { diff --git a/handlers/media_test.go b/handlers/media_test.go index 5fd132db6..84d8f3d13 100644 --- a/handlers/media_test.go +++ b/handlers/media_test.go @@ -139,6 +139,29 @@ func TestResolveAttachments(t *testing.T) { mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{}, err: "invalid attachment format: http://mock.com/1234/test.jpg", }, + { // 14: resolveable uploaded image URL with matching dimensions + attachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, + mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 1000, MaxHeight: 1000}}, + allowURLOnly: true, + resolved: []*handlers.Attachment{ + {Type: handlers.MediaTypeImage, Name: "test.jpg", ContentType: "image/jpeg", URL: "http://mock.com/1234/test.jpg", Media: imageJPG, Thumbnail: nil}, + }, + }, + { // 15: resolveable uploaded image URL without matching dimensions + attachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, + mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 100, MaxHeight: 100}}, + allowURLOnly: true, + resolved: []*handlers.Attachment{}, + errors: []*courier.ChannelError{courier.ErrorMediaUnresolveable("image/jpeg")}, + }, + { // 16: resolveable uploaded image URL without matching dimensions by specific content type precendence + attachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, + mediaSupport: map[handlers.MediaType]handlers.MediaTypeSupport{handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 100, MaxHeight: 100}, handlers.MediaType("image/jpeg"): {Types: []string{"image/jpeg", "image/png"}, MaxWidth: 1000, MaxHeight: 1000}}, + allowURLOnly: true, + resolved: []*handlers.Attachment{ + {Type: handlers.MediaTypeImage, Name: "test.jpg", ContentType: "image/jpeg", URL: "http://mock.com/1234/test.jpg", Media: imageJPG, Thumbnail: nil}, + }, + }, } for i, tc := range tcs { From 1c58a29060528fb8205cad396ffe89bda59cbd1d Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Tue, 23 Apr 2024 15:46:38 +0200 Subject: [PATCH 2/4] Support receiving stickers --- handlers/dialog360/handler.go | 2 + handlers/dialog360/handler_test.go | 17 +++++++ handlers/meta/handlers.go | 2 + handlers/meta/testdata/wac/audio.json | 2 +- handlers/meta/testdata/wac/sticker.json | 42 ++++++++++++++++ handlers/meta/whataspp_test.go | 66 ++++++++++++++++++++++++- handlers/meta/whatsapp/api.go | 1 + 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 handlers/meta/testdata/wac/sticker.json diff --git a/handlers/dialog360/handler.go b/handlers/dialog360/handler.go index 4001e917b..dc78e264e 100644 --- a/handlers/dialog360/handler.go +++ b/handlers/dialog360/handler.go @@ -160,6 +160,8 @@ func (h *handler) processWhatsAppPayload(ctx context.Context, channel courier.Ch } else if msg.Type == "image" && msg.Image != nil { text = msg.Image.Caption mediaURL, err = h.resolveMediaURL(channel, msg.Image.ID, clog) + } else if msg.Type == "sticker" && msg.Sticker != nil { + mediaURL, err = h.resolveMediaURL(channel, msg.Sticker.ID, clog) } else if msg.Type == "video" && msg.Video != nil { text = msg.Video.Caption mediaURL, err = h.resolveMediaURL(channel, msg.Video.ID, clog) diff --git a/handlers/dialog360/handler_test.go b/handlers/dialog360/handler_test.go index 4eca75dea..b8167468e 100644 --- a/handlers/dialog360/handler_test.go +++ b/handlers/dialog360/handler_test.go @@ -154,6 +154,20 @@ var testCasesD3C = []IncomingTestCase{ ExpectedExternalID: "external_id", ExpectedDate: time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC), }, + { + Label: "Receive Valid Sticker Message", + URL: d3CReceiveURL, + Data: string(test.ReadFile("../meta/testdata/wac/sticker.json")), + ExpectedRespStatus: 200, + ExpectedBodyContains: "Handled", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + ExpectedMsgText: Sp(""), + ExpectedURN: "whatsapp:5678", + ExpectedExternalID: "external_id", + ExpectedAttachments: []string{"https://waba-v2.360dialog.io/whatsapp_business/attachments/?mid=id_sticker"}, + ExpectedDate: time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC), + }, { Label: "Receive Invalid JSON", URL: d3CReceiveURL, @@ -271,6 +285,9 @@ func buildMockD3MediaService(testChannels []courier.Channel, testCases []Incomin if strings.HasSuffix(r.URL.Path, "id_audio") { fileURL = "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=id_audio" } + if strings.HasSuffix(r.URL.Path, "id_sticker") { + fileURL = "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=id_sticker" + } w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf(`{ "url": "%s" }`, fileURL))) diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 4005b6010..27b157dfb 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -309,6 +309,8 @@ func (h *handler) processWhatsAppPayload(ctx context.Context, channel courier.Ch } else if msg.Type == "image" && msg.Image != nil { text = msg.Image.Caption mediaURL, err = h.resolveMediaURL(msg.Image.ID, token, clog) + } else if msg.Type == "sticker" && msg.Sticker != nil { + mediaURL, err = h.resolveMediaURL(msg.Sticker.ID, token, clog) } else if msg.Type == "video" && msg.Video != nil { text = msg.Video.Caption mediaURL, err = h.resolveMediaURL(msg.Video.ID, token, clog) diff --git a/handlers/meta/testdata/wac/audio.json b/handlers/meta/testdata/wac/audio.json index f578e5fc9..40e5233b0 100644 --- a/handlers/meta/testdata/wac/audio.json +++ b/handlers/meta/testdata/wac/audio.json @@ -26,7 +26,7 @@ "audio": { "file": "/usr/local/wamedia/shared/b1cf38-8734-4ad3-b4a1-ef0c10d0d683", "id": "id_audio", - "mime_type": "image/jpeg", + "mime_type": "audio/mpeg", "sha256": "29ed500fa64eb55fc19dc4124acb300e5dcc54a0f822a301ae99944db", "caption": "Check out my new phone!" }, diff --git a/handlers/meta/testdata/wac/sticker.json b/handlers/meta/testdata/wac/sticker.json new file mode 100644 index 000000000..bec57fece --- /dev/null +++ b/handlers/meta/testdata/wac/sticker.json @@ -0,0 +1,42 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "+250 788 123 200", + "phone_number_id": "12345" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "5678" + } + ], + "messages": [ + { + "from": "5678", + "id": "external_id", + "sticker": { + "file": "/usr/local/wamedia/shared/b1cf38-8734-4ad3-b4a1-ef0c10d0d683", + "id": "id_sticker", + "mime_type": "image/webp", + "sha256": "29ed500fa64eb55fc19dc4124acb300e5dcc54a0f822a301ae99944db" + }, + "timestamp": "1454119029", + "type": "sticker" + } + ] + }, + "field": "messages" + } + ] + } + ] + } \ No newline at end of file diff --git a/handlers/meta/whataspp_test.go b/handlers/meta/whataspp_test.go index cef934d16..cd79253b7 100644 --- a/handlers/meta/whataspp_test.go +++ b/handlers/meta/whataspp_test.go @@ -2,6 +2,10 @@ package meta import ( "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" "testing" "time" @@ -150,6 +154,21 @@ var whatsappIncomingTests = []IncomingTestCase{ ExpectedDate: time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC), PrepRequest: addValidSignature, }, + { + Label: "Receive Valid Sticker Message", + URL: whatappReceiveURL, + Data: string(test.ReadFile("./testdata/wac/sticker.json")), + ExpectedRespStatus: 200, + ExpectedBodyContains: "Handled", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + ExpectedMsgText: Sp(""), + ExpectedURN: "whatsapp:5678", + ExpectedExternalID: "external_id", + ExpectedAttachments: []string{"https://foo.bar/attachmentURL_Sticker"}, + ExpectedDate: time.Date(2016, 1, 30, 1, 57, 9, 0, time.UTC), + PrepRequest: addValidSignature, + }, { Label: "Receive Invalid JSON", URL: whatappReceiveURL, @@ -277,7 +296,52 @@ var whatsappIncomingTests = []IncomingTestCase{ } func TestWhatsAppIncoming(t *testing.T) { - graphURL = createMockGraphAPI().URL + 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" && accessToken != "Bearer wac_admin_system_user_token" { + fmt.Printf("Access token: %s\n", accessToken) + http.Error(w, "invalid auth token", http.StatusForbidden) + 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 + } + + if strings.HasSuffix(r.URL.Path, "sticker") { + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL_Sticker"}`)) + return + } + + // valid token + w.Write([]byte(`{"url": "https://foo.bar/attachmentURL"}`)) + + })) + graphURL = server.URL RunIncomingTestCases(t, whatsappTestChannels, newHandler("WAC", "Cloud API WhatsApp"), whatsappIncomingTests) } diff --git a/handlers/meta/whatsapp/api.go b/handlers/meta/whatsapp/api.go index 5a36d008b..2891f0fc6 100644 --- a/handlers/meta/whatsapp/api.go +++ b/handlers/meta/whatsapp/api.go @@ -56,6 +56,7 @@ type Change struct { Video *MOMedia `json:"video"` Document *MOMedia `json:"document"` Voice *MOMedia `json:"voice"` + Sticker *MOMedia `json:"sticker"` Location *struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` From 4459d2b595d3e045cbee712789b8a8c80fee8c1f Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Tue, 23 Apr 2024 16:37:24 +0200 Subject: [PATCH 3/4] Support sending stickers --- handlers/dialog360/handler.go | 65 ++++++++++++++++++++---------- handlers/dialog360/handler_test.go | 62 +++++++++++++++++++--------- handlers/meta/handlers.go | 63 ++++++++++++++++++++--------- handlers/meta/whataspp_test.go | 62 +++++++++++++++++++--------- handlers/meta/whatsapp/api.go | 1 + 5 files changed, 174 insertions(+), 79 deletions(-) diff --git a/handlers/dialog360/handler.go b/handlers/dialog360/handler.go index dc78e264e..73eb23750 100644 --- a/handlers/dialog360/handler.go +++ b/handlers/dialog360/handler.go @@ -18,6 +18,7 @@ import ( "github.com/nyaruka/courier/utils" "github.com/nyaruka/gocommon/jsonx" "github.com/nyaruka/gocommon/urns" + "github.com/pkg/errors" ) const ( @@ -29,6 +30,15 @@ var ( maxMsgLength = 4096 ) +// see https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#supported-media-types +var mediaSupport = map[handlers.MediaType]handlers.MediaTypeSupport{ + handlers.MediaType("image/webp"): {Types: []string{"image/webp"}, MaxBytes: 100 * 1024, MaxWidth: 512, MaxHeight: 512}, + handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxBytes: 5 * 1024 * 1024}, + handlers.MediaTypeAudio: {Types: []string{"audio/aac", "audio/mp4", "audio/mpeg", "audio/amr", "audio/ogg"}, MaxBytes: 16 * 1024 * 1024}, + handlers.MediaTypeVideo: {Types: []string{"video/mp4", "video/3gp"}, MaxBytes: 16 * 1024 * 1024}, + handlers.MediaTypeApplication: {MaxBytes: 100 * 1024 * 1024}, +} + func init() { courier.RegisterHandler(newWAHandler(courier.ChannelType("D3C"), "360Dialog")) } @@ -313,23 +323,28 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen var payloadAudio whatsapp.SendRequest - for i := 0; i < len(msgParts)+len(msg.Attachments()); i++ { + attachments, err := handlers.ResolveAttachments(ctx, h.Backend(), msg.Attachments(), mediaSupport, false, clog) + if err != nil { + return errors.Wrap(err, "error resolving attachments") + } + + for i := 0; i < len(msgParts)+len(attachments); i++ { payload := whatsapp.SendRequest{MessagingProduct: "whatsapp", RecipientType: "individual", To: msg.URN().Path()} - if len(msg.Attachments()) == 0 { + if len(attachments) == 0 { // do we have a template? if msg.Templating() != nil { payload.Type = "template" payload.Template = whatsapp.GetTemplatePayload(msg.Templating()) } else { - if i < (len(msgParts) + len(msg.Attachments()) - 1) { + if i < (len(msgParts) + len(attachments) - 1) { // this is still a msg part text := &whatsapp.Text{PreviewURL: false} payload.Type = "text" - if strings.Contains(msgParts[i-len(msg.Attachments())], "https://") || strings.Contains(msgParts[i-len(msg.Attachments())], "http://") { + if strings.Contains(msgParts[i-len(attachments)], "https://") || strings.Contains(msgParts[i-len(attachments)], "http://") { text.PreviewURL = true } - text.Body = msgParts[i-len(msg.Attachments())] + text.Body = msgParts[i-len(attachments)] payload.Text = text } else { if len(qrs) > 0 { @@ -345,7 +360,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen if len(qrs) <= 3 { interactive := whatsapp.Interactive{Type: "button", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} btns := make([]whatsapp.Button, len(qrs)) for i, qr := range qrs { @@ -364,7 +379,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen } else { interactive := whatsapp.Interactive{Type: "list", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} section := whatsapp.Section{ Rows: make([]whatsapp.SectionRow, len(qrs)), @@ -390,31 +405,38 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen // this is still a msg part text := &whatsapp.Text{PreviewURL: false} payload.Type = "text" - if strings.Contains(msgParts[i-len(msg.Attachments())], "https://") || strings.Contains(msgParts[i-len(msg.Attachments())], "http://") { + if strings.Contains(msgParts[i-len(attachments)], "https://") || strings.Contains(msgParts[i-len(attachments)], "http://") { text.PreviewURL = true } - text.Body = msgParts[i-len(msg.Attachments())] + text.Body = msgParts[i-len(attachments)] payload.Text = text } } } - } else if i < len(msg.Attachments()) && (len(qrs) == 0 || len(qrs) > 3) { - attType, attURL := handlers.SplitAttachment(msg.Attachments()[i]) - attType = strings.Split(attType, "/")[0] + } else if i < len(attachments) && (len(qrs) == 0 || len(qrs) > 3) { + attURL := attachments[i].Media.URL() + attType := attachments[i].Type + attContentType := attachments[i].Media.ContentType() + if attType == "application" { attType = "document" } - payload.Type = attType + payload.Type = string(attType) media := whatsapp.Media{Link: attURL} - if len(msgParts) == 1 && attType != "audio" && len(msg.Attachments()) == 1 && len(msg.QuickReplies()) == 0 { + if len(msgParts) == 1 && attType != "audio" && len(attachments) == 1 && len(msg.QuickReplies()) == 0 { media.Caption = msgParts[i] hasCaption = true } if attType == "image" { - payload.Image = &media + if attContentType == "image/webp" { + payload.Type = "sticker" + payload.Sticker = &media + } else { + payload.Image = &media + } } else if attType == "audio" { payload.Audio = &media } else if attType == "video" { @@ -444,10 +466,11 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen Text string "json:\"text\"" }{Text: msgParts[i]}} - if len(msg.Attachments()) > 0 { + if len(attachments) > 0 { hasCaption = true - attType, attURL := handlers.SplitAttachment(msg.Attachments()[i]) - attType = strings.Split(attType, "/")[0] + attURL := attachments[i].Media.URL() + attType := attachments[i].Type + if attType == "application" { attType = "document" } @@ -519,7 +542,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen } else { interactive := whatsapp.Interactive{Type: "list", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} section := whatsapp.Section{ Rows: make([]whatsapp.SectionRow, len(qrs)), @@ -545,10 +568,10 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen // this is still a msg part text := &whatsapp.Text{PreviewURL: false} payload.Type = "text" - if strings.Contains(msgParts[i-len(msg.Attachments())], "https://") || strings.Contains(msgParts[i-len(msg.Attachments())], "http://") { + if strings.Contains(msgParts[i-len(attachments)], "https://") || strings.Contains(msgParts[i-len(attachments)], "http://") { text.PreviewURL = true } - text.Body = msgParts[i-len(msg.Attachments())] + text.Body = msgParts[i-len(attachments)] payload.Text = text } } diff --git a/handlers/dialog360/handler_test.go b/handlers/dialog360/handler_test.go index b8167468e..afe5d4407 100644 --- a/handlers/dialog360/handler_test.go +++ b/handlers/dialog360/handler_test.go @@ -369,7 +369,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Audio Send", MsgText: "audio caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"audio/mpeg:https://foo.bar/audio.mp3"}, + MsgAttachments: []string{"audio/mpeg:http://mock.com/3456/test.mp3"}, MockResponses: map[string][]*httpx.MockResponse{ "*/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -377,7 +377,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ }, }, ExpectedRequests: []ExpectedRequest{ - {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"https://foo.bar/audio.mp3"}}`}, + {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"http://mock.com/3456/test.mp3"}}`}, {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"audio caption","preview_url":false}}`}, }, ExpectedExtIDs: []string{"157b5e14568e8", "157b5e14568e8"}, @@ -386,7 +386,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Document Send", MsgText: "document caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"application/pdf:https://foo.bar/document.pdf"}, + MsgAttachments: []string{"application/pdf:http://mock.com/7890/test.pdf"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -395,7 +395,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"document","document":{"link":"https://foo.bar/document.pdf","caption":"document caption","filename":"document.pdf"}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"document","document":{"link":"http://mock.com/7890/test.pdf","caption":"document caption","filename":"test.pdf"}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -404,7 +404,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Image Send", MsgText: "image caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -413,7 +413,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg","caption":"image caption"}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"http://mock.com/1234/test.jpg","caption":"image caption"}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -422,7 +422,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Video Send", MsgText: "video caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"video/mp4:https://foo.bar/video.mp4"}, + MsgAttachments: []string{"video/mp4:http://mock.com/5678/test.mp4"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -431,7 +431,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"video","video":{"link":"https://foo.bar/video.mp4","caption":"video caption"}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"video","video":{"link":"http://mock.com/5678/test.mp4","caption":"video caption"}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -524,7 +524,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"BUTTON1"}, - MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -534,7 +534,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"image","image":{"link":"https://foo.bar/image.jpg"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"image","image":{"link":"http://mock.com/1234/test.jpg"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -544,7 +544,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"BUTTON1"}, - MsgAttachments: []string{"video/mp4:https://foo.bar/video.mp4"}, + MsgAttachments: []string{"video/mp4:http://mock.com/5678/test.mp4"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -554,7 +554,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"video","video":{"link":"https://foo.bar/video.mp4"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"video","video":{"link":"http://mock.com/5678/test.mp4"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -564,7 +564,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"BUTTON1"}, - MsgAttachments: []string{"document/pdf:https://foo.bar/document.pdf"}, + MsgAttachments: []string{"document/pdf:http://mock.com/7890/test.pdf"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -574,7 +574,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"document","document":{"link":"https://foo.bar/document.pdf","filename":"document.pdf"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"document","document":{"link":"http://mock.com/7890/test.pdf","filename":"test.pdf"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -584,7 +584,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3"}, - MsgAttachments: []string{"audio/mp3:https://foo.bar/audio.mp3"}, + MsgAttachments: []string{"audio/mp3:http://mock.com/3456/test.mp3"}, MockResponses: map[string][]*httpx.MockResponse{ "*/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -592,7 +592,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ }, }, ExpectedRequests: []ExpectedRequest{ - {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"https://foo.bar/audio.mp3"}}`}, + {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"http://mock.com/3456/test.mp3"}}`}, {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"ROW1"}},{"type":"reply","reply":{"id":"1","title":"ROW2"}},{"type":"reply","reply":{"id":"2","title":"ROW3"}}]}}}`}, }, ExpectedExtIDs: []string{"157b5e14568e8", "157b5e14568e8"}, @@ -602,7 +602,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, - MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -610,7 +610,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ }, }, ExpectedRequests: []ExpectedRequest{ - {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg"}}`}, + {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"http://mock.com/1234/test.jpg"}}`}, {Body: `{"messaging_product":"whatsapp","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"}]}]}}}`}, }, ExpectedExtIDs: []string{"157b5e14568e8", "157b5e14568e8"}, @@ -686,6 +686,30 @@ var SendTestCasesD3C = []OutgoingTestCase{ }, } +// setupMedia takes care of having the media files needed to our test server host +func setupMedia(mb *test.MockBackend) { + imageJPG := test.NewMockMedia("test.jpg", "image/jpeg", "http://mock.com/1234/test.jpg", 1024*1024, 640, 480, 0, nil) + + audioM4A := test.NewMockMedia("test.m4a", "audio/mp4", "http://mock.com/2345/test.m4a", 1024*1024, 0, 0, 200, nil) + audioMP3 := test.NewMockMedia("test.mp3", "audio/mpeg", "http://mock.com/3456/test.mp3", 1024*1024, 0, 0, 200, []courier.Media{audioM4A}) + + thumbJPG := test.NewMockMedia("test.jpg", "image/jpeg", "http://mock.com/4567/test.jpg", 1024*1024, 640, 480, 0, nil) + videoMP4 := test.NewMockMedia("test.mp4", "video/mp4", "http://mock.com/5678/test.mp4", 1024*1024, 0, 0, 1000, []courier.Media{thumbJPG}) + + videoMOV := test.NewMockMedia("test.mov", "video/quicktime", "http://mock.com/6789/test.mov", 100*1024*1024, 0, 0, 2000, nil) + + filePDF := test.NewMockMedia("test.pdf", "application/pdf", "http://mock.com/7890/test.pdf", 100*1024*1024, 0, 0, 0, nil) + + stickerWEBP := test.NewMockMedia("test.webp", "image/webp", "http://mock.com/8901/test.webp", 50*1024, 480, 480, 0, nil) + + mb.MockMedia(imageJPG) + mb.MockMedia(audioMP3) + mb.MockMedia(videoMP4) + mb.MockMedia(videoMOV) + mb.MockMedia(filePDF) + mb.MockMedia(stickerWEBP) +} + func TestOutgoing(t *testing.T) { // shorter max msg length for testing maxMsgLength = 100 @@ -696,5 +720,5 @@ func TestOutgoing(t *testing.T) { }) checkRedacted := []string{"the-auth-token"} - RunOutgoingTestCases(t, ChannelWAC, newWAHandler(courier.ChannelType("D3C"), "360Dialog"), SendTestCasesD3C, checkRedacted, nil) + RunOutgoingTestCases(t, ChannelWAC, newWAHandler(courier.ChannelType("D3C"), "360Dialog"), SendTestCasesD3C, checkRedacted, setupMedia) } diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 27b157dfb..4d7ccf27c 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -62,6 +62,15 @@ const ( payloadKey = "payload" ) +// see https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#supported-media-types +var wacMediaSupport = map[handlers.MediaType]handlers.MediaTypeSupport{ + handlers.MediaType("image/webp"): {Types: []string{"image/webp"}, MaxBytes: 100 * 1024, MaxWidth: 512, MaxHeight: 512}, + handlers.MediaTypeImage: {Types: []string{"image/jpeg", "image/png"}, MaxBytes: 5 * 1024 * 1024}, + handlers.MediaTypeAudio: {Types: []string{"audio/aac", "audio/mp4", "audio/mpeg", "audio/amr", "audio/ogg"}, MaxBytes: 16 * 1024 * 1024}, + handlers.MediaTypeVideo: {Types: []string{"video/mp4", "video/3gp"}, MaxBytes: 16 * 1024 * 1024}, + handlers.MediaTypeApplication: {MaxBytes: 100 * 1024 * 1024}, +} + func newHandler(channelType courier.ChannelType, name string) courier.ChannelHandler { return &handler{handlers.NewBaseHandler(channelType, name, handlers.DisableUUIDRouting(), handlers.WithRedactConfigKeys(courier.ConfigAuthToken))} } @@ -802,24 +811,29 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * var payloadAudio whatsapp.SendRequest - for i := 0; i < len(msgParts)+len(msg.Attachments()); i++ { + attachments, err := handlers.ResolveAttachments(ctx, h.Backend(), msg.Attachments(), wacMediaSupport, false, clog) + if err != nil { + return errors.Wrap(err, "error resolving attachments") + } + + for i := 0; i < len(msgParts)+len(attachments); i++ { payload := whatsapp.SendRequest{MessagingProduct: "whatsapp", RecipientType: "individual", To: msg.URN().Path()} - if len(msg.Attachments()) == 0 { + if len(attachments) == 0 { // do we have a template? if msg.Templating() != nil { payload.Type = "template" payload.Template = whatsapp.GetTemplatePayload(msg.Templating()) } else { - if i < (len(msgParts) + len(msg.Attachments()) - 1) { + if i < (len(msgParts) + len(attachments) - 1) { // this is still a msg part text := &whatsapp.Text{PreviewURL: false} payload.Type = "text" - if strings.Contains(msgParts[i-len(msg.Attachments())], "https://") || strings.Contains(msgParts[i-len(msg.Attachments())], "http://") { + if strings.Contains(msgParts[i-len(attachments)], "https://") || strings.Contains(msgParts[i-len(attachments)], "http://") { text.PreviewURL = true } - text.Body = msgParts[i-len(msg.Attachments())] + text.Body = msgParts[i-len(attachments)] payload.Text = text } else { if len(qrs) > 0 { @@ -835,7 +849,7 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * if len(qrs) <= 3 { interactive := whatsapp.Interactive{Type: "button", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} btns := make([]whatsapp.Button, len(qrs)) for i, qr := range qrs { @@ -854,7 +868,7 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * } else { interactive := whatsapp.Interactive{Type: "list", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} section := whatsapp.Section{ Rows: make([]whatsapp.SectionRow, len(qrs)), @@ -880,31 +894,39 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * // this is still a msg part text := &whatsapp.Text{PreviewURL: false} payload.Type = "text" - if strings.Contains(msgParts[i-len(msg.Attachments())], "https://") || strings.Contains(msgParts[i-len(msg.Attachments())], "http://") { + if strings.Contains(msgParts[i-len(attachments)], "https://") || strings.Contains(msgParts[i-len(attachments)], "http://") { text.PreviewURL = true } - text.Body = msgParts[i-len(msg.Attachments())] + text.Body = msgParts[i-len(attachments)] payload.Text = text } } } - } else if i < len(msg.Attachments()) && (len(qrs) == 0 || len(qrs) > 3) { - attType, attURL := handlers.SplitAttachment(msg.Attachments()[i]) - attType = strings.Split(attType, "/")[0] + } else if i < len(attachments) && (len(qrs) == 0 || len(qrs) > 3) { + + attURL := attachments[i].Media.URL() + attType := string(attachments[i].Type) + attContentType := attachments[i].Media.ContentType() + if attType == "application" { attType = "document" } payload.Type = attType media := whatsapp.Media{Link: attURL} - if len(msgParts) == 1 && attType != "audio" && len(msg.Attachments()) == 1 && len(msg.QuickReplies()) == 0 { + if len(msgParts) == 1 && attType != "audio" && len(attachments) == 1 && len(msg.QuickReplies()) == 0 { media.Caption = msgParts[i] hasCaption = true } if attType == "image" { - payload.Image = &media + if attContentType == "image/webp" { + payload.Type = "sticker" + payload.Sticker = &media + } else { + payload.Image = &media + } } else if attType == "audio" { payload.Audio = &media } else if attType == "video" { @@ -935,10 +957,11 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * Text string "json:\"text\"" }{Text: msgParts[i]}} - if len(msg.Attachments()) > 0 { + if len(attachments) > 0 { hasCaption = true - attType, attURL := handlers.SplitAttachment(msg.Attachments()[i]) - attType = strings.Split(attType, "/")[0] + attURL := attachments[i].Media.URL() + attType := string(attachments[i].Type) + if attType == "application" { attType = "document" } @@ -1011,7 +1034,7 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * } else { interactive := whatsapp.Interactive{Type: "list", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} section := whatsapp.Section{ Rows: make([]whatsapp.SectionRow, len(qrs)), @@ -1037,10 +1060,10 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * // this is still a msg part text := &whatsapp.Text{PreviewURL: false} payload.Type = "text" - if strings.Contains(msgParts[i-len(msg.Attachments())], "https://") || strings.Contains(msgParts[i-len(msg.Attachments())], "http://") { + if strings.Contains(msgParts[i-len(attachments)], "https://") || strings.Contains(msgParts[i-len(attachments)], "http://") { text.PreviewURL = true } - text.Body = msgParts[i-len(msg.Attachments())] + text.Body = msgParts[i-len(attachments)] payload.Text = text } } diff --git a/handlers/meta/whataspp_test.go b/handlers/meta/whataspp_test.go index cd79253b7..5181b6de8 100644 --- a/handlers/meta/whataspp_test.go +++ b/handlers/meta/whataspp_test.go @@ -385,7 +385,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Audio Send", MsgText: "audio caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"audio/mpeg:https://foo.bar/audio.mp3"}, + MsgAttachments: []string{"audio/mpeg:http://mock.com/3456/test.mp3"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -393,7 +393,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ }, }, ExpectedRequests: []ExpectedRequest{ - {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"https://foo.bar/audio.mp3"}}`}, + {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"http://mock.com/3456/test.mp3"}}`}, {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"text","text":{"body":"audio caption","preview_url":false}}`}, }, ExpectedExtIDs: []string{"157b5e14568e8", "157b5e14568e8"}, @@ -402,7 +402,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Document Send", MsgText: "document caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"application/pdf:https://foo.bar/document.pdf"}, + MsgAttachments: []string{"application/pdf:http://mock.com/7890/test.pdf"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -411,7 +411,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/12345_ID/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"document","document":{"link":"https://foo.bar/document.pdf","caption":"document caption","filename":"document.pdf"}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"document","document":{"link":"http://mock.com/7890/test.pdf","caption":"document caption","filename":"test.pdf"}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -420,7 +420,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Image Send", MsgText: "image caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -429,7 +429,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/12345_ID/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg","caption":"image caption"}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"http://mock.com/1234/test.jpg","caption":"image caption"}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -438,7 +438,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Video Send", MsgText: "video caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"video/mp4:https://foo.bar/video.mp4"}, + MsgAttachments: []string{"video/mp4:http://mock.com/5678/test.mp4"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -447,7 +447,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/12345_ID/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"video","video":{"link":"https://foo.bar/video.mp4","caption":"video caption"}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"video","video":{"link":"http://mock.com/5678/test.mp4","caption":"video caption"}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -594,7 +594,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"BUTTON1"}, - MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -603,7 +603,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/12345_ID/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"image","image":{"link":"https://foo.bar/image.jpg"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"image","image":{"link":"http://mock.com/1234/test.jpg"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -613,7 +613,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"BUTTON1"}, - MsgAttachments: []string{"video/mp4:https://foo.bar/video.mp4"}, + MsgAttachments: []string{"video/mp4:http://mock.com/5678/test.mp4"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -622,7 +622,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/12345_ID/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"video","video":{"link":"https://foo.bar/video.mp4"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"video","video":{"link":"http://mock.com/5678/test.mp4"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -632,7 +632,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"BUTTON1"}, - MsgAttachments: []string{"document/pdf:https://foo.bar/document.pdf"}, + MsgAttachments: []string{"document/pdf:http://mock.com/7890/test.pdf"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -641,7 +641,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/12345_ID/messages", - Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"document","document":{"link":"https://foo.bar/document.pdf","filename":"document.pdf"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","header":{"type":"document","document":{"link":"http://mock.com/7890/test.pdf","filename":"test.pdf"}},"body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"BUTTON1"}}]}}}`, }, }, ExpectedExtIDs: []string{"157b5e14568e8"}, @@ -651,7 +651,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3"}, - MsgAttachments: []string{"audio/mp3:https://foo.bar/audio.mp3"}, + MsgAttachments: []string{"audio/mp3:http://mock.com/3456/test.mp3"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -659,7 +659,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ }, }, ExpectedRequests: []ExpectedRequest{ - {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"https://foo.bar/audio.mp3"}}`}, + {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"audio","audio":{"link":"http://mock.com/3456/test.mp3"}}`}, {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"interactive","interactive":{"type":"button","body":{"text":"Interactive Button Msg"},"action":{"buttons":[{"type":"reply","reply":{"id":"0","title":"ROW1"}},{"type":"reply","reply":{"id":"1","title":"ROW2"}},{"type":"reply","reply":{"id":"2","title":"ROW3"}}]}}}`}, }, ExpectedExtIDs: []string{"157b5e14568e8", "157b5e14568e8"}, @@ -669,7 +669,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, - MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -677,7 +677,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ }, }, ExpectedRequests: []ExpectedRequest{ - {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg"}}`}, + {Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"http://mock.com/1234/test.jpg"}}`}, {Body: `{"messaging_product":"whatsapp","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"}]}]}}}`}, }, ExpectedExtIDs: []string{"157b5e14568e8", "157b5e14568e8"}, @@ -734,6 +734,30 @@ var whatsappOutgoingTests = []OutgoingTestCase{ }, } +// setupMedia takes care of having the media files needed to our test server host +func setupMedia(mb *test.MockBackend) { + imageJPG := test.NewMockMedia("test.jpg", "image/jpeg", "http://mock.com/1234/test.jpg", 1024*1024, 640, 480, 0, nil) + + audioM4A := test.NewMockMedia("test.m4a", "audio/mp4", "http://mock.com/2345/test.m4a", 1024*1024, 0, 0, 200, nil) + audioMP3 := test.NewMockMedia("test.mp3", "audio/mpeg", "http://mock.com/3456/test.mp3", 1024*1024, 0, 0, 200, []courier.Media{audioM4A}) + + thumbJPG := test.NewMockMedia("test.jpg", "image/jpeg", "http://mock.com/4567/test.jpg", 1024*1024, 640, 480, 0, nil) + videoMP4 := test.NewMockMedia("test.mp4", "video/mp4", "http://mock.com/5678/test.mp4", 1024*1024, 0, 0, 1000, []courier.Media{thumbJPG}) + + videoMOV := test.NewMockMedia("test.mov", "video/quicktime", "http://mock.com/6789/test.mov", 100*1024*1024, 0, 0, 2000, nil) + + filePDF := test.NewMockMedia("test.pdf", "application/pdf", "http://mock.com/7890/test.pdf", 100*1024*1024, 0, 0, 0, nil) + + stickerWEBP := test.NewMockMedia("test.webp", "image/webp", "http://mock.com/8901/test.webp", 50*1024, 480, 480, 0, nil) + + mb.MockMedia(imageJPG) + mb.MockMedia(audioMP3) + mb.MockMedia(videoMP4) + mb.MockMedia(videoMOV) + mb.MockMedia(filePDF) + mb.MockMedia(stickerWEBP) +} + func TestWhatsAppOutgoing(t *testing.T) { // shorter max msg length for testing maxMsgLength = 100 @@ -742,7 +766,7 @@ func TestWhatsAppOutgoing(t *testing.T) { checkRedacted := []string{"wac_admin_system_user_token", "missing_facebook_app_secret", "missing_facebook_webhook_secret", "a123"} - RunOutgoingTestCases(t, channel, newHandler("WAC", "Cloud API WhatsApp"), whatsappOutgoingTests, checkRedacted, nil) + RunOutgoingTestCases(t, channel, newHandler("WAC", "Cloud API WhatsApp"), whatsappOutgoingTests, checkRedacted, setupMedia) } func TestWhatsAppDescribeURN(t *testing.T) { diff --git a/handlers/meta/whatsapp/api.go b/handlers/meta/whatsapp/api.go index 2891f0fc6..0055ee66d 100644 --- a/handlers/meta/whatsapp/api.go +++ b/handlers/meta/whatsapp/api.go @@ -217,6 +217,7 @@ type SendRequest struct { Image *Media `json:"image,omitempty"` Audio *Media `json:"audio,omitempty"` Video *Media `json:"video,omitempty"` + Sticker *Media `json:"sticker,omitempty"` Interactive *Interactive `json:"interactive,omitempty"` From a393565c9bbb0ba3a8b8300396e796738c17fa06 Mon Sep 17 00:00:00 2001 From: Norbert Kwizera Date: Tue, 23 Apr 2024 17:27:33 +0200 Subject: [PATCH 4/4] coverage --- handlers/dialog360/handler_test.go | 18 ++++++++++++++++++ handlers/meta/whataspp_test.go | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/handlers/dialog360/handler_test.go b/handlers/dialog360/handler_test.go index afe5d4407..7575830aa 100644 --- a/handlers/dialog360/handler_test.go +++ b/handlers/dialog360/handler_test.go @@ -436,6 +436,24 @@ var SendTestCasesD3C = []OutgoingTestCase{ }, ExpectedExtIDs: []string{"157b5e14568e8"}, }, + { + Label: "Sticker Send", + MsgText: "Hello there", + MsgURN: "whatsapp:250788123123", + MsgAttachments: []string{"image/webp:http://mock.com/8901/test.webp"}, + MockResponses: map[string][]*httpx.MockResponse{ + "https://waba-v2.360dialog.io/messages": { + httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), + }, + }, + ExpectedRequests: []ExpectedRequest{ + { + Path: "/messages", + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"sticker","sticker":{"link":"http://mock.com/8901/test.webp","caption":"Hello there"}}`, + }, + }, + ExpectedExtIDs: []string{"157b5e14568e8"}, + }, { Label: "Template Send", MsgText: "templated message", diff --git a/handlers/meta/whataspp_test.go b/handlers/meta/whataspp_test.go index 5181b6de8..ba8476788 100644 --- a/handlers/meta/whataspp_test.go +++ b/handlers/meta/whataspp_test.go @@ -452,6 +452,24 @@ var whatsappOutgoingTests = []OutgoingTestCase{ }, ExpectedExtIDs: []string{"157b5e14568e8"}, }, + { + Label: "Sticker Send", + MsgText: "Hello there", + MsgURN: "whatsapp:250788123123", + MsgAttachments: []string{"image/webp:http://mock.com/8901/test.webp"}, + MockResponses: map[string][]*httpx.MockResponse{ + "*/12345_ID/messages": { + httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), + }, + }, + ExpectedRequests: []ExpectedRequest{ + { + Path: "/12345_ID/messages", + Body: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"sticker","sticker":{"link":"http://mock.com/8901/test.webp","caption":"Hello there"}}`, + }, + }, + ExpectedExtIDs: []string{"157b5e14568e8"}, + }, { Label: "Template Send", MsgText: "templated message",