diff --git a/handlers/whatsapp/whatsapp.go b/handlers/whatsapp/whatsapp.go index e1fd4116d..afb574be5 100644 --- a/handlers/whatsapp/whatsapp.go +++ b/handlers/whatsapp/whatsapp.go @@ -172,6 +172,7 @@ type eventPayload struct { // receiveMessage is our HTTP handler function for incoming messages func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) { payload := &eventPayload{} + err := handlers.DecodeAndValidateJSON(payload, r) if err != nil { return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) @@ -419,9 +420,24 @@ type LocalizableParam struct { Default string `json:"default"` } +type mmtImage struct { + Link string `json:"link,omitempty"` +} + +type mmtDocument struct { + Link string `json:"link,omitempty"` +} + +type mmtVideo struct { + Link string `json:"link,omitempty"` +} + type Param struct { - Type string `json:"type"` - Text string `json:"text"` + Type string `json:"type"` + Text string `json:"text,omitempty"` + Image *mmtImage `json:"image,omitempty"` + Document *mmtDocument `json:"document,omitempty"` + Video *mmtVideo `json:"video,omitempty"` } type Component struct { @@ -495,6 +511,7 @@ const maxMsgLength = 4096 // SendMsg sends the passed in message, returning any error func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { start := time.Now() + // get our token token := msg.Channel().StringConfigForKey(courier.ConfigAuthToken, "") if token == "" { @@ -525,6 +542,7 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat for i, payload := range payloads { externalID := "" + wppID, externalID, logs, err = sendWhatsAppMsg(msg, sendPath, payload) // add logs to our status for _, log := range logs { @@ -564,76 +582,10 @@ func buildPayloads(msg courier.Msg, h *handler) ([]interface{}, []*courier.Chann var logs []*courier.ChannelLog var err error - if len(msg.Attachments()) > 0 { - for attachmentCount, attachment := range msg.Attachments() { + // do we have a template? + templating, err := h.getTemplate(msg) + if templating != nil || len(msg.Attachments()) == 0 { - mimeType, mediaURL := handlers.SplitAttachment(attachment) - mediaID, mediaLogs, err := h.fetchMediaID(msg, mimeType, mediaURL) - if len(mediaLogs) > 0 { - logs = append(logs, mediaLogs...) - } - if err != nil { - logrus.WithField("channel_uuid", msg.Channel().UUID().String()).WithError(err).Error("error while uploading media to whatsapp") - } - if err == nil && mediaID != "" { - mediaURL = "" - } - mediaPayload := &mediaObject{ID: mediaID, Link: mediaURL} - if strings.HasPrefix(mimeType, "audio") { - payload := mtAudioPayload{ - To: msg.URN().Path(), - Type: "audio", - } - payload.Audio = mediaPayload - payloads = append(payloads, payload) - } else if strings.HasPrefix(mimeType, "application") { - payload := mtDocumentPayload{ - To: msg.URN().Path(), - Type: "document", - } - if attachmentCount == 0 { - mediaPayload.Caption = msg.Text() - } - mediaPayload.Filename, err = utils.BasePathForURL(mediaURL) - - // Logging error - if err != nil { - logrus.WithField("channel_uuid", msg.Channel().UUID().String()).WithError(err).Error("Error while parsing the media URL") - } - payload.Document = mediaPayload - payloads = append(payloads, payload) - } else if strings.HasPrefix(mimeType, "image") { - payload := mtImagePayload{ - To: msg.URN().Path(), - Type: "image", - } - if attachmentCount == 0 { - mediaPayload.Caption = msg.Text() - } - payload.Image = mediaPayload - payloads = append(payloads, payload) - } else if strings.HasPrefix(mimeType, "video") { - payload := mtVideoPayload{ - To: msg.URN().Path(), - Type: "video", - } - if attachmentCount == 0 { - mediaPayload.Caption = msg.Text() - } - payload.Video = mediaPayload - payloads = append(payloads, payload) - } else { - duration := time.Since(start) - err = fmt.Errorf("unknown attachment mime type: %s", mimeType) - attachmentLogs := []*courier.ChannelLog{courier.NewChannelLogFromError("Error sending message", msg.Channel(), msg.ID(), duration, err)} - logs = append(logs, attachmentLogs...) - break - } - } - } else { - // do we have a template? - var templating *MsgTemplating - templating, err := h.getTemplate(msg) if err != nil { return nil, nil, errors.Wrapf(err, "unable to decode template: %s for channel: %s", string(msg.Metadata()), msg.Channel().UUID()) } @@ -660,7 +612,6 @@ func buildPayloads(msg courier.Msg, h *handler) ([]interface{}, []*courier.Chann } payloads = append(payloads, payload) } else { - payload := templatePayload{ To: msg.URN().Path(), Type: "template", @@ -677,6 +628,50 @@ func buildPayloads(msg courier.Msg, h *handler) ([]interface{}, []*courier.Chann } payload.Template.Components = append(payload.Template.Components, *component) + if len(msg.Attachments()) > 0 { + + header := &Component{Type: "header"} + + for _, attachment := range msg.Attachments() { + + mimeType, mediaURL := handlers.SplitAttachment(attachment) + mediaID, mediaLogs, err := h.fetchMediaID(msg, mimeType, mediaURL) + if len(mediaLogs) > 0 { + logs = append(logs, mediaLogs...) + } + if err != nil { + logrus.WithField("channel_uuid", msg.Channel().UUID().String()).WithError(err).Error("error while uploading media to whatsapp") + } + if err != nil && mediaID != "" { + mediaURL = "" + } + if strings.HasPrefix(mimeType, "image") { + image := &mmtImage{ + Link: mediaURL, + } + header.Parameters = append(header.Parameters, Param{Type: "image", Image: image}) + payload.Template.Components = append(payload.Template.Components, *header) + } else if strings.HasPrefix(mimeType, "application") { + document := &mmtDocument{ + Link: mediaURL, + } + header.Parameters = append(header.Parameters, Param{Type: "document", Document: document}) + payload.Template.Components = append(payload.Template.Components, *header) + } else if strings.HasPrefix(mimeType, "video") { + video := &mmtVideo{ + Link: mediaURL, + } + header.Parameters = append(header.Parameters, Param{Type: "video", Video: video}) + payload.Template.Components = append(payload.Template.Components, *header) + } else { + duration := time.Since(start) + err = fmt.Errorf("unknown attachment mime type: %s", mimeType) + attachmentLogs := []*courier.ChannelLog{courier.NewChannelLogFromError("Error sending message", msg.Channel(), msg.ID(), duration, err)} + logs = append(logs, attachmentLogs...) + break + } + } + } payloads = append(payloads, payload) } } else { @@ -748,6 +743,75 @@ func buildPayloads(msg courier.Msg, h *handler) ([]interface{}, []*courier.Chann } } } + } else { + + if len(msg.Attachments()) > 0 { + for attachmentCount, attachment := range msg.Attachments() { + + mimeType, mediaURL := handlers.SplitAttachment(attachment) + mediaID, mediaLogs, err := h.fetchMediaID(msg, mimeType, mediaURL) + if len(mediaLogs) > 0 { + logs = append(logs, mediaLogs...) + } + if err != nil { + logrus.WithField("channel_uuid", msg.Channel().UUID().String()).WithError(err).Error("error while uploading media to whatsapp") + } + if err == nil && mediaID != "" { + mediaURL = "" + } + mediaPayload := &mediaObject{ID: mediaID, Link: mediaURL} + if strings.HasPrefix(mimeType, "audio") { + payload := mtAudioPayload{ + To: msg.URN().Path(), + Type: "audio", + } + payload.Audio = mediaPayload + payloads = append(payloads, payload) + } else if strings.HasPrefix(mimeType, "application") { + payload := mtDocumentPayload{ + To: msg.URN().Path(), + Type: "document", + } + if attachmentCount == 0 { + mediaPayload.Caption = msg.Text() + } + mediaPayload.Filename, err = utils.BasePathForURL(mediaURL) + + // Logging error + if err != nil { + logrus.WithField("channel_uuid", msg.Channel().UUID().String()).WithError(err).Error("Error while parsing the media URL") + } + payload.Document = mediaPayload + payloads = append(payloads, payload) + } else if strings.HasPrefix(mimeType, "image") { + payload := mtImagePayload{ + To: msg.URN().Path(), + Type: "image", + } + if attachmentCount == 0 { + mediaPayload.Caption = msg.Text() + } + payload.Image = mediaPayload + payloads = append(payloads, payload) + } else if strings.HasPrefix(mimeType, "video") { + payload := mtVideoPayload{ + To: msg.URN().Path(), + Type: "video", + } + if attachmentCount == 0 { + mediaPayload.Caption = msg.Text() + } + payload.Video = mediaPayload + payloads = append(payloads, payload) + } else { + duration := time.Since(start) + err = fmt.Errorf("unknown attachment mime type: %s", mimeType) + attachmentLogs := []*courier.ChannelLog{courier.NewChannelLogFromError("Error sending message", msg.Channel(), msg.ID(), duration, err)} + logs = append(logs, attachmentLogs...) + break + } + } + } } return payloads, logs, err } @@ -836,6 +900,7 @@ func sendWhatsAppMsg(msg courier.Msg, sendPath *url.URL, payload interface{}) (s log := courier.NewChannelLogFromError("unable to build JSON body", msg.Channel(), msg.ID(), elapsed, err) return "", "", []*courier.ChannelLog{log}, err } + req, _ := http.NewRequest(http.MethodPost, sendPath.String(), bytes.NewReader(jsonBody)) req.Header = buildWhatsAppHeaders(msg.Channel()) rr, err := utils.MakeHTTPRequest(req) diff --git a/handlers/whatsapp/whatsapp_test.go b/handlers/whatsapp/whatsapp_test.go index 40d940445..6cde2514b 100644 --- a/handlers/whatsapp/whatsapp_test.go +++ b/handlers/whatsapp/whatsapp_test.go @@ -599,6 +599,30 @@ var defaultSendTestCases = []ChannelSendTestCase{ ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, RequestBody: `{"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: "Media Message Template Send - Image", + Text: "Media Message Msg", URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Metadata: json.RawMessage(`{ "templating": { "template": { "name": "revive_issue", "uuid": "171f8a4d-f725-46d7-85a6-11aceff0bfe3" }, "namespace": "wa_template_namespace", "language": "eng", "country": "US", "variables": ["Chef", "tomorrow"]}}`), + Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, + RequestBody: `{"to":"250788123123","type":"template","template":{"namespace":"wa_template_namespace","name":"revive_issue","language":{"policy":"deterministic","code":"en_US"},"components":[{"type":"body","parameters":[{"type":"text","text":"Chef"},{"type":"text","text":"tomorrow"}]},{"type":"header","parameters":[{"type":"image","image":{"link":"https://foo.bar/image.jpg"}}]}]}}`, + SendPrep: setSendURL}, + {Label: "Media Message Template Send - Video", + Text: "Media Message Msg", URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Metadata: json.RawMessage(`{ "templating": { "template": { "name": "revive_issue", "uuid": "171f8a4d-f725-46d7-85a6-11aceff0bfe3" }, "namespace": "wa_template_namespace", "language": "eng", "country": "US", "variables": ["Chef", "tomorrow"]}}`), + Attachments: []string{"video/mp4:https://foo.bar/video.mp4"}, + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, + RequestBody: `{"to":"250788123123","type":"template","template":{"namespace":"wa_template_namespace","name":"revive_issue","language":{"policy":"deterministic","code":"en_US"},"components":[{"type":"body","parameters":[{"type":"text","text":"Chef"},{"type":"text","text":"tomorrow"}]},{"type":"header","parameters":[{"type":"video","video":{"link":"https://foo.bar/video.mp4"}}]}]}}`, + SendPrep: setSendURL}, + {Label: "Media Message Template Send - Document", + Text: "Media Message Msg", URN: "whatsapp:250788123123", + Status: "W", ExternalID: "157b5e14568e8", + Metadata: json.RawMessage(`{ "templating": { "template": { "name": "revive_issue", "uuid": "171f8a4d-f725-46d7-85a6-11aceff0bfe3" }, "namespace": "wa_template_namespace", "language": "eng", "country": "US", "variables": ["Chef", "tomorrow"]}}`), + Attachments: []string{"application/pdf:https://foo.bar/document.pdf"}, + ResponseBody: `{ "messages": [{"id": "157b5e14568e8"}] }`, ResponseStatus: 201, + RequestBody: `{"to":"250788123123","type":"template","template":{"namespace":"wa_template_namespace","name":"revive_issue","language":{"policy":"deterministic","code":"en_US"},"components":[{"type":"body","parameters":[{"type":"text","text":"Chef"},{"type":"text","text":"tomorrow"}]},{"type":"header","parameters":[{"type":"document","document":{"link":"https://foo.bar/document.pdf"}}]}]}}`, + SendPrep: setSendURL}, } var mediaCacheSendTestCases = []ChannelSendTestCase{