diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index d50fbd358..1db3b06b9 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -70,6 +70,15 @@ var waIgnoreStatuses = map[string]bool{ "deleted": true, } +// 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, useUUIDRoutes bool) courier.ChannelHandler { return &handler{handlers.NewBaseHandlerWithParams(channelType, name, useUUIDRoutes, []string{courier.ConfigAuthToken})} } @@ -1129,12 +1138,17 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, qrs := msg.QuickReplies() lang := getSupportedLanguage(msg.Locale()) + attachments, err := handlers.ResolveAttachments(ctx, h.Backend(), msg.Attachments(), wacMediaSupport, false) + if err != nil { + return nil, errors.Wrap(err, "error resolving attachments") + } + var payloadAudio wacMTPayload - for i := 0; i < len(msgParts)+len(msg.Attachments()); i++ { + for i := 0; i < len(msgParts)+len(attachments); i++ { payload := wacMTPayload{MessagingProduct: "whatsapp", RecipientType: "individual", To: msg.URN().Path()} - if len(msg.Attachments()) == 0 { + if len(attachments) == 0 { // do we have a template? templating, err := h.getTemplating(msg) if err != nil { @@ -1155,14 +1169,14 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, template.Components = append(payload.Template.Components, component) } else { - if i < (len(msgParts) + len(msg.Attachments()) - 1) { + if i < (len(msgParts) + len(attachments) - 1) { // this is still a msg part text := &wacText{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 { @@ -1171,7 +1185,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, if len(qrs) <= 3 { interactive := wacInteractive{Type: "button", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} btns := make([]wacMTButton, len(qrs)) for i, qr := range qrs { @@ -1190,7 +1204,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, } else if len(qrs) <= 10 { interactive := wacInteractive{Type: "list", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} section := wacMTSection{ Rows: make([]wacMTSectionRow, len(qrs)), @@ -1218,20 +1232,19 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, // this is still a msg part text := &wacText{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]) - splitedAttType := strings.Split(attType, "/") - attType = splitedAttType[0] - attFormat := splitedAttType[1] + } 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" @@ -1239,13 +1252,13 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, payload.Type = attType media := wacMTMedia{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" { - if attFormat == "webp" { + if attContentType == "image/webp" { payload.Type = "sticker" payload.Sticker = &media } else { @@ -1276,8 +1289,8 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, if len(msg.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" } @@ -1353,7 +1366,7 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, } else if len(qrs) <= 10 { interactive := wacInteractive{Type: "list", Body: struct { Text string "json:\"text\"" - }{Text: msgParts[i-len(msg.Attachments())]}} + }{Text: msgParts[i-len(attachments)]}} section := wacMTSection{ Rows: make([]wacMTSectionRow, len(qrs)), @@ -1381,10 +1394,10 @@ func (h *handler) sendCloudAPIWhatsappMsg(ctx context.Context, msg courier.Msg, // this is still a msg part text := &wacText{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/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 56a3140bd..00cab1e16 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -1288,12 +1288,12 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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[MockedRequest]*httpx.MockResponse{ { Method: "POST", Path: "/12345_ID/messages", - 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"}}`, }: httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), { Method: "POST", @@ -1309,10 +1309,10 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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"}, MockResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, MockResponseStatus: 201, - ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"document","document":{"link":"https://foo.bar/document.pdf","caption":"document caption","filename":"document.pdf"}}`, + ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"document","document":{"link":"http://mock.com/7890/test.pdf","caption":"document caption","filename":"test.pdf"}}`, ExpectedRequestPath: "/12345_ID/messages", ExpectedMsgStatus: "W", ExpectedExternalID: "157b5e14568e8", @@ -1322,10 +1322,10 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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"}, MockResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, MockResponseStatus: 201, - ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"https://foo.bar/image.jpg","caption":"image caption"}}`, + ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"image","image":{"link":"http://mock.com/1234/test.jpg","caption":"image caption"}}`, ExpectedRequestPath: "/12345_ID/messages", ExpectedMsgStatus: "W", ExpectedExternalID: "157b5e14568e8", @@ -1335,10 +1335,10 @@ var SendTestCasesWAC = []ChannelSendTestCase{ Label: "Sticker Send", MsgText: "sticker caption", MsgURN: "whatsapp:250788123123", - MsgAttachments: []string{"image/webp:https://foo.bar/sticker.webp"}, + MsgAttachments: []string{"image/webp:http://mock.com/8901/test.webp"}, MockResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, MockResponseStatus: 201, - ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"sticker","sticker":{"link":"https://foo.bar/sticker.webp","caption":"sticker caption"}}`, + ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"sticker","sticker":{"link":"http://mock.com/8901/test.webp","caption":"sticker caption"}}`, ExpectedRequestPath: "/12345_ID/messages", ExpectedMsgStatus: "W", ExpectedExternalID: "157b5e14568e8", @@ -1348,10 +1348,10 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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"}, MockResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, MockResponseStatus: 201, - ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"video","video":{"link":"https://foo.bar/video.mp4","caption":"video caption"}}`, + ExpectedRequestBody: `{"messaging_product":"whatsapp","recipient_type":"individual","to":"250788123123","type":"video","video":{"link":"http://mock.com/5678/test.mp4","caption":"video caption"}}`, ExpectedRequestPath: "/12345_ID/messages", ExpectedMsgStatus: "W", ExpectedExternalID: "157b5e14568e8", @@ -1438,10 +1438,10 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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"}, MockResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, MockResponseStatus: 201, - ExpectedRequestBody: `{"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"}}]}}}`, + ExpectedRequestBody: `{"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"}}]}}}`, ExpectedRequestPath: "/12345_ID/messages", ExpectedMsgStatus: "W", ExpectedExternalID: "157b5e14568e8", @@ -1452,10 +1452,10 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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"}, MockResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, MockResponseStatus: 201, - ExpectedRequestBody: `{"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"}}]}}}`, + ExpectedRequestBody: `{"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"}}]}}}`, ExpectedRequestPath: "/12345_ID/messages", ExpectedMsgStatus: "W", ExpectedExternalID: "157b5e14568e8", @@ -1466,10 +1466,10 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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"}, MockResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, MockResponseStatus: 201, - ExpectedRequestBody: `{"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"}}]}}}`, + ExpectedRequestBody: `{"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"}}]}}}`, ExpectedRequestPath: "/12345_ID/messages", ExpectedMsgStatus: "W", ExpectedExternalID: "157b5e14568e8", @@ -1480,12 +1480,12 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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[MockedRequest]*httpx.MockResponse{ { Method: "POST", Path: "/12345_ID/messages", - 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"}}`, }: httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), { Method: "POST", @@ -1502,12 +1502,12 @@ var SendTestCasesWAC = []ChannelSendTestCase{ 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[MockedRequest]*httpx.MockResponse{ { Method: "POST", Path: "/12345_ID/messages", - 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"}}`, }: httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), { Method: "POST", @@ -1553,6 +1553,30 @@ var SendTestCasesWAC = []ChannelSendTestCase{ }, } +// 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 TestSending(t *testing.T) { // shorter max msg length for testing maxMsgLength = 100 @@ -1565,7 +1589,7 @@ func TestSending(t *testing.T) { RunChannelSendTestCases(t, ChannelFBA, newHandler("FBA", "Facebook", false), SendTestCasesFBA, checkRedacted, nil) RunChannelSendTestCases(t, ChannelIG, newHandler("IG", "Instagram", false), SendTestCasesIG, checkRedacted, nil) - RunChannelSendTestCases(t, ChannelWAC, newHandler("WAC", "Cloud API WhatsApp", false), SendTestCasesWAC, checkRedacted, nil) + RunChannelSendTestCases(t, ChannelWAC, newHandler("WAC", "Cloud API WhatsApp", false), SendTestCasesWAC, checkRedacted, setupMedia) } func TestSigning(t *testing.T) {