From 780f2b523dca19e5234566ff409ea2dbb76f8005 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 19 Feb 2025 14:16:40 -0500 Subject: [PATCH] Model quick replies as structs instead of strings --- backends/rapidpro/backend_test.go | 3 +- backends/rapidpro/msg.go | 40 ++++++++++++------------ handlers/dialog360/handler.go | 8 ++--- handlers/dialog360/handler_test.go | 18 +++++------ handlers/discord/handler.go | 2 +- handlers/discord/handler_test.go | 14 ++++----- handlers/external/handler.go | 18 +++++------ handlers/external/handler_test.go | 12 +++---- handlers/facebook_legacy/handler.go | 2 +- handlers/facebook_legacy/handler_test.go | 6 ++-- handlers/firebase/handler.go | 4 +-- handlers/firebase/handler_test.go | 4 +-- handlers/line/handler.go | 4 +-- handlers/line/handler_test.go | 6 ++-- handlers/meta/facebook_test.go | 8 ++--- handlers/meta/handlers.go | 10 +++--- handlers/meta/instagram_test.go | 6 ++-- handlers/meta/whataspp_test.go | 28 ++++++++++------- handlers/telegram/handler_test.go | 4 +-- handlers/telegram/keyboard.go | 10 ++++-- handlers/telegram/keyboard_test.go | 11 ++++--- handlers/test.go | 2 +- handlers/utils.go | 9 ++++++ handlers/viber/handler_test.go | 4 +-- handlers/viber/keyboard.go | 7 +++-- handlers/viber/keyboard_test.go | 19 +++++------ handlers/vk/handler_test.go | 2 +- handlers/vk/keyboard.go | 6 ++-- handlers/vk/keyboard_test.go | 13 ++++---- handlers/whatsapp_legacy/handler.go | 8 ++--- handlers/whatsapp_legacy/handler_test.go | 8 ++--- msg.go | 19 ++++++++++- test/backend.go | 2 +- test/msg.go | 34 ++++++++++---------- 34 files changed, 196 insertions(+), 155 deletions(-) diff --git a/backends/rapidpro/backend_test.go b/backends/rapidpro/backend_test.go index 6380da05d..7446f55e7 100644 --- a/backends/rapidpro/backend_test.go +++ b/backends/rapidpro/backend_test.go @@ -172,7 +172,7 @@ func (ts *BackendTestSuite) TestMsgUnmarshal() { ts.Equal([]string{"https://foo.bar/image.jpg"}, msg.Attachments()) ts.Equal("5ApPVsFDcFt:RZdK9ne7LgfvBYdtCYg7tv99hC9P2", msg.URNAuth_) ts.Equal("", msg.ExternalID()) - ts.Equal([]string{"Yes", "No"}, msg.QuickReplies()) + ts.Equal([]courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, msg.QuickReplies()) ts.Equal("event", msg.Topic()) ts.Equal("external-id", msg.ResponseToExternalID()) ts.True(msg.HighPriority()) @@ -1605,7 +1605,6 @@ SELECT direction, text, attachments, - quick_replies, msg_count, error_count, failed_reason, diff --git a/backends/rapidpro/msg.go b/backends/rapidpro/msg.go index f1ca29b2f..8b5852c5a 100644 --- a/backends/rapidpro/msg.go +++ b/backends/rapidpro/msg.go @@ -45,20 +45,20 @@ const ( // Msg is our base struct to represent msgs both in our JSON and db representations type Msg struct { - OrgID_ OrgID `json:"org_id" db:"org_id"` - ID_ courier.MsgID `json:"id" db:"id"` - UUID_ courier.MsgUUID `json:"uuid" db:"uuid"` - Direction_ MsgDirection ` db:"direction"` - Status_ courier.MsgStatus ` db:"status"` - Visibility_ MsgVisibility ` db:"visibility"` - HighPriority_ bool `json:"high_priority" db:"high_priority"` - Text_ string `json:"text" db:"text"` - Attachments_ pq.StringArray `json:"attachments" db:"attachments"` - QuickReplies_ pq.StringArray `json:"quick_replies" db:"quick_replies"` - Locale_ null.String `json:"locale" db:"locale"` - Templating_ *courier.Templating `json:"templating" db:"templating"` - ExternalID_ null.String ` db:"external_id"` - Metadata_ json.RawMessage `json:"metadata" db:"metadata"` + OrgID_ OrgID `json:"org_id" db:"org_id"` + ID_ courier.MsgID `json:"id" db:"id"` + UUID_ courier.MsgUUID `json:"uuid" db:"uuid"` + Direction_ MsgDirection ` db:"direction"` + Status_ courier.MsgStatus ` db:"status"` + Visibility_ MsgVisibility ` db:"visibility"` + HighPriority_ bool `json:"high_priority" db:"high_priority"` + Text_ string `json:"text" db:"text"` + Attachments_ pq.StringArray `json:"attachments" db:"attachments"` + QuickReplies_ []courier.QuickReply `json:"quick_replies"` + Locale_ null.String `json:"locale" db:"locale"` + Templating_ *courier.Templating `json:"templating" db:"templating"` + ExternalID_ null.String ` db:"external_id"` + Metadata_ json.RawMessage `json:"metadata" db:"metadata"` ChannelID_ courier.ChannelID ` db:"channel_id"` ContactID_ ContactID `json:"contact_id" db:"contact_id"` @@ -135,12 +135,12 @@ func (m *Msg) URN() urns.URN { return m.URN_ } func (m *Msg) Channel() courier.Channel { return m.channel } // outgoing specific -func (m *Msg) QuickReplies() []string { return m.QuickReplies_ } -func (m *Msg) Locale() i18n.Locale { return i18n.Locale(string(m.Locale_)) } -func (m *Msg) Templating() *courier.Templating { return m.Templating_ } -func (m *Msg) URNAuth() string { return m.URNAuth_ } -func (m *Msg) Origin() courier.MsgOrigin { return m.Origin_ } -func (m *Msg) ContactLastSeenOn() *time.Time { return m.ContactLastSeenOn_ } +func (m *Msg) QuickReplies() []courier.QuickReply { return m.QuickReplies_ } +func (m *Msg) Locale() i18n.Locale { return i18n.Locale(string(m.Locale_)) } +func (m *Msg) Templating() *courier.Templating { return m.Templating_ } +func (m *Msg) URNAuth() string { return m.URNAuth_ } +func (m *Msg) Origin() courier.MsgOrigin { return m.Origin_ } +func (m *Msg) ContactLastSeenOn() *time.Time { return m.ContactLastSeenOn_ } func (m *Msg) Topic() string { if m.Metadata_ == nil { return "" diff --git a/handlers/dialog360/handler.go b/handlers/dialog360/handler.go index 9205bb297..17d54608a 100644 --- a/handlers/dialog360/handler.go +++ b/handlers/dialog360/handler.go @@ -357,7 +357,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) - btns[i].Reply.Title = qr + btns[i].Reply.Title = qr.Text } interactive.Action = &struct { Button string "json:\"button,omitempty\"" @@ -376,7 +376,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen for i, qr := range qrs { section.Rows[i] = whatsapp.SectionRow{ ID: fmt.Sprint(i), - Title: qr, + Title: qr.Text, } } @@ -510,7 +510,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) - btns[i].Reply.Title = qr + btns[i].Reply.Title = qr.Text } interactive.Action = &struct { Button string "json:\"button,omitempty\"" @@ -530,7 +530,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen for i, qr := range qrs { section.Rows[i] = whatsapp.SectionRow{ ID: fmt.Sprint(i), - Title: qr, + Title: qr.Text, } } diff --git a/handlers/dialog360/handler_test.go b/handlers/dialog360/handler_test.go index 6e8b0cf86..5e231a78e 100644 --- a/handlers/dialog360/handler_test.go +++ b/handlers/dialog360/handler_test.go @@ -487,7 +487,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive Button Message Send", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -502,7 +502,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive List Message Send", MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -517,7 +517,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive List Message Send more than 10 QRs", MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4", "ROW5", "ROW6", "ROW7", "ROW8", "ROW9", "ROW10", "ROW11", "ROW12"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}, {Text: "ROW5"}, {Text: "ROW6"}, {Text: "ROW7"}, {Text: "ROW8"}, {Text: "ROW9"}, {Text: "ROW10"}, {Text: "ROW11"}, {Text: "ROW12"}}, 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{ MsgText: "Hola", MsgURN: "whatsapp:250788123123", MsgLocale: "spa", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -549,7 +549,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive Button Message Send with image attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { @@ -569,7 +569,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive Button Message Send with video attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MsgAttachments: []string{"video/mp4:https://foo.bar/video.mp4"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { @@ -589,7 +589,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive Button Message Send with document attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MsgAttachments: []string{"document/pdf:https://foo.bar/document.pdf"}, MockResponses: map[string][]*httpx.MockResponse{ "https://waba-v2.360dialog.io/messages": { @@ -609,7 +609,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive Button Message Send with audio attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}}, MsgAttachments: []string{"audio/mp3:https://foo.bar/audio.mp3"}, MockResponses: map[string][]*httpx.MockResponse{ "*/messages": { @@ -627,7 +627,7 @@ var SendTestCasesD3C = []OutgoingTestCase{ Label: "Interactive List Message Send with attachment", MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/messages": { diff --git a/handlers/discord/handler.go b/handlers/discord/handler.go index 270286dcb..ac27d8310 100644 --- a/handlers/discord/handler.go +++ b/handlers/discord/handler.go @@ -171,7 +171,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen To: msg.URN().Path(), Channel: string(msg.Channel().UUID()), Attachments: attachmentURLs, - QuickReplies: msg.QuickReplies(), + QuickReplies: handlers.TextOnlyQuickReplies(msg.QuickReplies()), } var body io.Reader diff --git a/handlers/discord/handler_test.go b/handlers/discord/handler_test.go index 75e5eeab5..a50e628cb 100644 --- a/handlers/discord/handler_test.go +++ b/handlers/discord/handler_test.go @@ -24,7 +24,7 @@ var testChannels = []courier.Channel{ var testCases = []IncomingTestCase{ { - Label: "Recieve Message", + Label: "Receive Message", URL: "/c/ds/bac782c2-7aeb-4389-92f5-97887744f573/receive", Data: `from=694634743521607802&text=hello`, ExpectedRespStatus: 200, @@ -32,7 +32,7 @@ var testCases = []IncomingTestCase{ ExpectedURN: "discord:694634743521607802", }, { - Label: "Recieve Message with attachment", + Label: "Receive Message with attachment", URL: "/c/ds/bac782c2-7aeb-4389-92f5-97887744f573/receive", Data: `from=694634743521607802&text=hello&attachments=https://test.test/foo.png`, ExpectedRespStatus: 200, @@ -90,7 +90,7 @@ var sendTestCases = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/discord/rp/send", - Body: `{"id":"10","text":"Hello World","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":[],"quick_replies":null}`, + Body: `{"id":"10","text":"Hello World","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":[],"quick_replies":[]}`, }, }, }, @@ -107,7 +107,7 @@ var sendTestCases = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/discord/rp/send", - Body: `{"id":"10","text":"Hello World","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":["https://foo.bar/image.jpg"],"quick_replies":null}`, + Body: `{"id":"10","text":"Hello World","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":["https://foo.bar/image.jpg"],"quick_replies":[]}`, }, }, }, @@ -115,7 +115,7 @@ var sendTestCases = []OutgoingTestCase{ Label: "Attachement and quick replies", MsgText: "Hello World", MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, - MsgQuickReplies: []string{"hello", "world"}, + MsgQuickReplies: []courier.QuickReply{{Text: "hello"}, {Text: "world"}}, MsgURN: "discord:694634743521607802", MockResponses: map[string][]*httpx.MockResponse{ "http://example.com/discord/rp/send": { @@ -141,7 +141,7 @@ var sendTestCases = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/discord/rp/send", - Body: `{"id":"10","text":"Error Sending","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":[],"quick_replies":null}`, + Body: `{"id":"10","text":"Error Sending","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":[],"quick_replies":[]}`, }, }, ExpectedError: courier.ErrResponseStatus, @@ -158,7 +158,7 @@ var sendTestCases = []OutgoingTestCase{ ExpectedRequests: []ExpectedRequest{ { Path: "/discord/rp/send", - Body: `{"id":"10","text":"Error","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":[],"quick_replies":null}`, + Body: `{"id":"10","text":"Error","to":"694634743521607802","channel":"bac782c2-7aeb-4389-92f5-97887744f573","attachments":[],"quick_replies":[]}`, }, }, ExpectedError: courier.ErrConnectionFailed, diff --git a/handlers/external/handler.go b/handlers/external/handler.go index bd1d18b39..f296f2028 100644 --- a/handlers/external/handler.go +++ b/handlers/external/handler.go @@ -16,6 +16,7 @@ import ( "github.com/nyaruka/courier" "github.com/nyaruka/courier/handlers" "github.com/nyaruka/gocommon/gsm7" + "github.com/nyaruka/gocommon/jsonx" "github.com/nyaruka/gocommon/urns" ) @@ -328,7 +329,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen if i == len(parts)-1 { formEncoded["quick_replies"] = buildQuickRepliesResponse(msg.QuickReplies(), sendMethod, contentURLEncoded) } else { - formEncoded["quick_replies"] = buildQuickRepliesResponse([]string{}, sendMethod, contentURLEncoded) + formEncoded["quick_replies"] = buildQuickRepliesResponse([]courier.QuickReply{}, sendMethod, contentURLEncoded) } url := replaceVariables(sendURL, formEncoded) @@ -339,7 +340,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen if i == len(parts)-1 { formEncoded["quick_replies"] = buildQuickRepliesResponse(msg.QuickReplies(), sendMethod, contentType) } else { - formEncoded["quick_replies"] = buildQuickRepliesResponse([]string{}, sendMethod, contentType) + formEncoded["quick_replies"] = buildQuickRepliesResponse([]courier.QuickReply{}, sendMethod, contentType) } body = strings.NewReader(replaceVariables(sendBody, formEncoded)) } @@ -381,18 +382,17 @@ type quickReplyXMLItem struct { Value string `xml:",chardata"` } -func buildQuickRepliesResponse(quickReplies []string, sendMethod string, contentType string) string { +func buildQuickRepliesResponse(quickReplies []courier.QuickReply, sendMethod string, contentType string) string { if quickReplies == nil { - quickReplies = []string{} + quickReplies = []courier.QuickReply{} } if (sendMethod == http.MethodPost || sendMethod == http.MethodPut) && contentType == contentJSON { - marshalled, _ := json.Marshal(quickReplies) - return string(marshalled) + return string(jsonx.MustMarshal(handlers.TextOnlyQuickReplies(quickReplies))) } else if (sendMethod == http.MethodPost || sendMethod == http.MethodPut) && contentType == contentXML { items := make([]quickReplyXMLItem, len(quickReplies)) for i, v := range quickReplies { - items[i] = quickReplyXMLItem{Value: v} + items[i] = quickReplyXMLItem{Value: v.Text} } marshalled, _ := xml.Marshal(items) return string(marshalled) @@ -400,8 +400,8 @@ func buildQuickRepliesResponse(quickReplies []string, sendMethod string, content response := bytes.Buffer{} for _, reply := range quickReplies { - reply = url.QueryEscape(reply) - response.WriteString(fmt.Sprintf("&quick_reply=%s", reply)) + reply.Text = url.QueryEscape(reply.Text) + response.WriteString(fmt.Sprintf("&quick_reply=%s", reply.Text)) } return response.String() } diff --git a/handlers/external/handler_test.go b/handlers/external/handler_test.go index cc57982b7..c702a6e82 100644 --- a/handlers/external/handler_test.go +++ b/handlers/external/handler_test.go @@ -313,7 +313,7 @@ var longSendTestCases = []OutgoingTestCase{ { Label: "Long Send", MsgText: "This is a long message that will be longer than 30....... characters", MsgURN: "tel:+250788383383", - MsgQuickReplies: []string{"One"}, + MsgQuickReplies: []courier.QuickReply{{Text: "One"}}, MockResponses: map[string][]*httpx.MockResponse{ "http://example.com/send*": { httpx.NewMockResponse(200, nil, []byte(`0: Accepted for delivery`)), @@ -627,7 +627,7 @@ var jsonSendTestCases = []OutgoingTestCase{ Label: "Send Quick Replies", MsgText: "Some message", MsgURN: "tel:+250788383383", - MsgQuickReplies: []string{"One", "Two", "Three"}, + MsgQuickReplies: []courier.QuickReply{{Text: "One"}, {Text: "Two"}, {Text: "Three"}}, MockResponses: map[string][]*httpx.MockResponse{ "http://example.com/send": { httpx.NewMockResponse(200, nil, []byte(`0: Accepted for delivery`)), @@ -645,7 +645,7 @@ var jsonLongSendTestCases = []OutgoingTestCase{ Label: "Send Long message JSON", MsgText: "This is a long message that will be longer than 30....... characters", MsgURN: "tel:+250788383383", - MsgQuickReplies: []string{"One", "Two", "Three"}, + MsgQuickReplies: []courier.QuickReply{{Text: "One"}, {Text: "Two"}, {Text: "Three"}}, MockResponses: map[string][]*httpx.MockResponse{ "*": { httpx.NewMockResponse(200, nil, []byte(`0: Accepted for delivery`)), @@ -733,7 +733,7 @@ var xmlSendTestCases = []OutgoingTestCase{ Label: "Send Quick Replies", MsgText: "Some message", MsgURN: "tel:+250788383383", - MsgQuickReplies: []string{"One", "Two", "Three"}, + MsgQuickReplies: []courier.QuickReply{{Text: "One"}, {Text: "Two"}, {Text: "Three"}}, MockResponses: map[string][]*httpx.MockResponse{ "http://example.com/send": { httpx.NewMockResponse(200, nil, []byte(`0: Accepted for delivery`)), @@ -751,7 +751,7 @@ var xmlLongSendTestCases = []OutgoingTestCase{ Label: "Send Long message XML", MsgText: "This is a long message that will be longer than 30....... characters", MsgURN: "tel:+250788383383", - MsgQuickReplies: []string{"One", "Two", "Three"}, + MsgQuickReplies: []courier.QuickReply{{Text: "One"}, {Text: "Two"}, {Text: "Three"}}, MockResponses: map[string][]*httpx.MockResponse{ "*": { httpx.NewMockResponse(200, nil, []byte(`0: Accepted for delivery`)), @@ -855,7 +855,7 @@ var xmlSendWithResponseContentTestCases = []OutgoingTestCase{ Label: "Send Quick Replies", MsgText: "Some message", MsgURN: "tel:+250788383383", - MsgQuickReplies: []string{"One", "Two", "Three"}, + MsgQuickReplies: []courier.QuickReply{{Text: "One"}, {Text: "Two"}, {Text: "Three"}}, MockResponses: map[string][]*httpx.MockResponse{ "http://example.com/send": { httpx.NewMockResponse(200, nil, []byte(`0`)), diff --git a/handlers/facebook_legacy/handler.go b/handlers/facebook_legacy/handler.go index f929901e3..d624b02dd 100644 --- a/handlers/facebook_legacy/handler.go +++ b/handlers/facebook_legacy/handler.go @@ -530,7 +530,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen // include any quick replies on the last piece we send if i == (len(msgParts)+len(msg.Attachments()))-1 { for _, qr := range msg.QuickReplies() { - payload.Message.QuickReplies = append(payload.Message.QuickReplies, mtQuickReply{qr, qr, "text"}) + payload.Message.QuickReplies = append(payload.Message.QuickReplies, mtQuickReply{qr.Text, qr.Text, "text"}) } } else { payload.Message.QuickReplies = nil diff --git a/handlers/facebook_legacy/handler_test.go b/handlers/facebook_legacy/handler_test.go index a6fd0b559..8c6633abd 100644 --- a/handlers/facebook_legacy/handler_test.go +++ b/handlers/facebook_legacy/handler_test.go @@ -858,7 +858,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Quick Reply", MsgText: "Are you happy?", MsgURN: "facebook:12345", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v3.3/me/messages*": { httpx.NewMockResponse(200, nil, []byte(`{"message_id": "mid.133"}`)), @@ -876,7 +876,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Long Message", MsgText: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", MsgURN: "facebook:12345", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MsgTopic: "account", MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v3.3/me/messages*": { @@ -922,7 +922,7 @@ var defaultSendTestCases = []OutgoingTestCase{ MsgText: "This is some text.", MsgURN: "facebook:12345", MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MsgTopic: "event", MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v3.3/me/messages*": { diff --git a/handlers/firebase/handler.go b/handlers/firebase/handler.go index e8085a503..3d72ee54f 100644 --- a/handlers/firebase/handler.go +++ b/handlers/firebase/handler.go @@ -205,7 +205,7 @@ func (h *handler) sendWithAPIKey(msg courier.MsgOut, res *courier.SendResult, cl // include any quick replies on the last piece we send if i == len(msgParts)-1 { - payload.Data.QuickReplies = msg.QuickReplies() + payload.Data.QuickReplies = handlers.TextOnlyQuickReplies(msg.QuickReplies()) } payload.To = msg.URNAuth() @@ -289,7 +289,7 @@ func (h *handler) sendWithCredsJSON(msg courier.MsgOut, res *courier.SendResult, if i == len(msgParts)-1 { if msg.QuickReplies() != nil { - payload.Message.Data.QuickReplies = string(jsonx.MustMarshal(msg.QuickReplies())) + payload.Message.Data.QuickReplies = string(jsonx.MustMarshal(handlers.TextOnlyQuickReplies(msg.QuickReplies()))) } } diff --git a/handlers/firebase/handler_test.go b/handlers/firebase/handler_test.go index 7bd373d4a..87ec62559 100644 --- a/handlers/firebase/handler_test.go +++ b/handlers/firebase/handler_test.go @@ -199,7 +199,7 @@ var sendAPIkeyTestCases = []OutgoingTestCase{ MsgText: "Simple Message", MsgURN: "fcm:250788123123", MsgURNAuth: "auth1", - MsgQuickReplies: []string{"yes", "no"}, + MsgQuickReplies: []courier.QuickReply{{Text: "yes"}, {Text: "no"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar"}, MockResponses: map[string][]*httpx.MockResponse{ "https://fcm.googleapis.com/fcm/send": { @@ -326,7 +326,7 @@ var sendTestCases = []OutgoingTestCase{ MsgText: "Simple Message", MsgURN: "fcm:250788123123", MsgURNAuth: "auth1", - MsgQuickReplies: []string{"yes", "no"}, + MsgQuickReplies: []courier.QuickReply{{Text: "yes"}, {Text: "no"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar"}, MockResponses: map[string][]*httpx.MockResponse{ "https://fcm.googleapis.com/v1/projects/foo-project-id/messages:send": { diff --git a/handlers/line/handler.go b/handlers/line/handler.go index a684137aa..ce42ad78c 100644 --- a/handlers/line/handler.go +++ b/handlers/line/handler.go @@ -335,8 +335,8 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, res *courier.Sen for j, qr := range qrs { items[j] = QuickReplyItem{Type: "action"} items[j].Action.Type = "message" - items[j].Action.Label = qr - items[j].Action.Text = qr + items[j].Action.Label = qr.Text + items[j].Action.Text = qr.Text } if len(items) > 0 { mtTextMsg.QuickReply = &mtQuickReply{Items: items} diff --git a/handlers/line/handler_test.go b/handlers/line/handler_test.go index d357cfbba..8d719dc9d 100644 --- a/handlers/line/handler_test.go +++ b/handlers/line/handler_test.go @@ -490,7 +490,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Quick Reply", MsgText: "Are you happy?", MsgURN: "line:uabcdefghij", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://api.line.me/v2/bot/message/push": {httpx.NewMockResponse(200, nil, []byte(`{}`))}, }, @@ -505,7 +505,7 @@ var defaultSendTestCases = []OutgoingTestCase{ MsgText: "Are you happy?", MsgURN: "line:uabcdefghij", MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://api.line.me/v2/bot/message/push": {httpx.NewMockResponse(200, nil, []byte(`{}`))}, }, @@ -521,7 +521,7 @@ var defaultSendTestCases = []OutgoingTestCase{ MsgURN: "line:uabcdefghij", MsgResponseToExternalID: "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA", MsgAttachments: []string{"image/jpeg:http://mock.com/1234/test.jpg"}, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://api.line.me/v2/bot/message/reply": {httpx.NewMockResponse(200, nil, []byte(`{}`))}, }, diff --git a/handlers/meta/facebook_test.go b/handlers/meta/facebook_test.go index f5bbb5ec0..2ef3ca0d8 100644 --- a/handlers/meta/facebook_test.go +++ b/handlers/meta/facebook_test.go @@ -454,7 +454,7 @@ var facebookOutgoingTests = []OutgoingTestCase{ MsgText: "Are you happy?", MsgURN: "facebook:12345", MsgOrigin: courier.MsgOriginBroadcast, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v18.0/me/messages*": { httpx.NewMockResponse(200, nil, []byte(`{"message_id": "mid.133"}`)), @@ -471,7 +471,7 @@ var facebookOutgoingTests = []OutgoingTestCase{ MsgText: "Are you happy?", MsgURN: "facebook:12345", MsgOrigin: courier.MsgOriginBroadcast, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v18.0/me/messages*": { httpx.NewMockResponse(200, nil, []byte(`{"message_id": "mid.133"}`)), @@ -487,7 +487,7 @@ var facebookOutgoingTests = []OutgoingTestCase{ Label: "Message that exceeds max text length", MsgText: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", MsgURN: "facebook:12345", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MsgTopic: "account", MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v18.0/me/messages*": { @@ -527,7 +527,7 @@ var facebookOutgoingTests = []OutgoingTestCase{ MsgText: "This is some text.", MsgURN: "facebook:12345", MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MsgTopic: "event", MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v18.0/me/messages*": { diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 9bc34e46c..11a929a3b 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -706,7 +706,7 @@ func (h *handler) sendFacebookInstagramMsg(ctx context.Context, msg courier.MsgO // include any quick replies on the last piece we send if part.IsLast { for _, qr := range msg.QuickReplies() { - payload.Message.QuickReplies = append(payload.Message.QuickReplies, messenger.QuickReply{Title: qr, Payload: qr, ContentType: "text"}) + payload.Message.QuickReplies = append(payload.Message.QuickReplies, messenger.QuickReply{Title: qr.Text, Payload: qr.Text, ContentType: "text"}) } } else { payload.Message.QuickReplies = nil @@ -855,7 +855,7 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) - btns[i].Reply.Title = qr + btns[i].Reply.Title = qr.Text } interactive.Action = &struct { Button string "json:\"button,omitempty\"" @@ -874,7 +874,7 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * for i, qr := range qrs { section.Rows[i] = whatsapp.SectionRow{ ID: fmt.Sprint(i), - Title: qr, + Title: qr.Text, } } @@ -1011,7 +1011,7 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) - btns[i].Reply.Title = qr + btns[i].Reply.Title = qr.Text } interactive.Action = &struct { Button string "json:\"button,omitempty\"" @@ -1031,7 +1031,7 @@ func (h *handler) sendWhatsAppMsg(ctx context.Context, msg courier.MsgOut, res * for i, qr := range qrs { section.Rows[i] = whatsapp.SectionRow{ ID: fmt.Sprint(i), - Title: qr, + Title: qr.Text, } } diff --git a/handlers/meta/instagram_test.go b/handlers/meta/instagram_test.go index 0f31a6db3..02e989e52 100644 --- a/handlers/meta/instagram_test.go +++ b/handlers/meta/instagram_test.go @@ -238,7 +238,7 @@ var instagramOutgoingTests = []OutgoingTestCase{ MsgText: "Are you happy?", MsgURN: "instagram:12345", MsgOrigin: courier.MsgOriginBroadcast, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v18.0/me/messages*": { httpx.NewMockResponse(200, nil, []byte(`{"message_id": "mid.133"}`)), @@ -254,7 +254,7 @@ var instagramOutgoingTests = []OutgoingTestCase{ Label: "Message that exceeds max text length", MsgText: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", MsgURN: "instagram:12345", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MsgTopic: "account", MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v18.0/me/messages*": { @@ -294,7 +294,7 @@ var instagramOutgoingTests = []OutgoingTestCase{ MsgText: "This is some text.", MsgURN: "instagram:12345", MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MsgTopic: "event", MockResponses: map[string][]*httpx.MockResponse{ "https://graph.facebook.com/v18.0/me/messages*": { diff --git a/handlers/meta/whataspp_test.go b/handlers/meta/whataspp_test.go index 23a9a6646..20cf8231a 100644 --- a/handlers/meta/whataspp_test.go +++ b/handlers/meta/whataspp_test.go @@ -517,7 +517,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Interactive Button Message Send", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -532,7 +532,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Interactive List Message Send", MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -544,10 +544,14 @@ var whatsappOutgoingTests = []OutgoingTestCase{ ExpectedExtIDs: []string{"157b5e14568e8"}, }, { - Label: "Interactive List Message Send, more than 10 QRs", - MsgText: "Interactive List Msg", - MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4", "ROW5", "ROW6", "ROW7", "ROW8", "ROW9", "ROW10", "ROW11", "ROW12"}, + Label: "Interactive List Message Send, more than 10 QRs", + MsgText: "Interactive List Msg", + MsgURN: "whatsapp:250788123123", + MsgQuickReplies: []courier.QuickReply{ + {Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}, + {Text: "ROW5"}, {Text: "ROW6"}, {Text: "ROW7"}, {Text: "ROW8"}, + {Text: "ROW9"}, {Text: "ROW10"}, {Text: "ROW11"}, {Text: "ROW12"}, + }, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -564,7 +568,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ MsgText: "Hola", MsgURN: "whatsapp:250788123123", MsgLocale: "spa", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -579,7 +583,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Interactive Button Message Send with image attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { @@ -598,7 +602,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Interactive Button Message Send with video attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MsgAttachments: []string{"video/mp4:https://foo.bar/video.mp4"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { @@ -617,7 +621,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Interactive Button Message Send with document attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MsgAttachments: []string{"document/pdf:https://foo.bar/document.pdf"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { @@ -636,7 +640,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Interactive Button Message Send with audio attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}}, MsgAttachments: []string{"audio/mp3:https://foo.bar/audio.mp3"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { @@ -654,7 +658,7 @@ var whatsappOutgoingTests = []OutgoingTestCase{ Label: "Interactive List Message Send with attachment", MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/12345_ID/messages": { diff --git a/handlers/telegram/handler_test.go b/handlers/telegram/handler_test.go index df810bff9..e7e047430 100644 --- a/handlers/telegram/handler_test.go +++ b/handlers/telegram/handler_test.go @@ -798,7 +798,7 @@ var outgoingCases = []OutgoingTestCase{ Label: "Quick Reply", MsgText: "Are you happy?", MsgURN: "telegram:12345", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "*/botauth_token/sendMessage": { httpx.NewMockResponse(200, nil, []byte(`{ "ok": true, "result": { "message_id": 133 } }`)), @@ -813,7 +813,7 @@ var outgoingCases = []OutgoingTestCase{ Label: "Quick Reply with multiple attachments", MsgText: "Are you happy?", MsgURN: "telegram:12345", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MsgAttachments: []string{"application/pdf:https://foo.bar/doc1.pdf", "application/pdf:https://foo.bar/document.pdf"}, MockResponses: map[string][]*httpx.MockResponse{ "*/botauth_token/sendMessage": { diff --git a/handlers/telegram/keyboard.go b/handlers/telegram/keyboard.go index 79356c5ea..eb5a7bdf4 100644 --- a/handlers/telegram/keyboard.go +++ b/handlers/telegram/keyboard.go @@ -1,6 +1,10 @@ package telegram -import "github.com/nyaruka/courier/utils" +import ( + "github.com/nyaruka/courier" + "github.com/nyaruka/courier/handlers" + "github.com/nyaruka/courier/utils" +) // KeyboardButton is button on a keyboard, see https://core.telegram.org/bots/api/#keyboardbutton type KeyboardButton struct { @@ -17,8 +21,8 @@ type ReplyKeyboardMarkup struct { } // NewKeyboardFromReplies creates a keyboard from the given quick replies -func NewKeyboardFromReplies(replies []string) *ReplyKeyboardMarkup { - rows := utils.StringsToRows(replies, 5, 30, 2) +func NewKeyboardFromReplies(replies []courier.QuickReply) *ReplyKeyboardMarkup { + rows := utils.StringsToRows(handlers.TextOnlyQuickReplies(replies), 5, 30, 2) keyboard := make([][]KeyboardButton, len(rows)) for i := range rows { diff --git a/handlers/telegram/keyboard_test.go b/handlers/telegram/keyboard_test.go index 5c682abd1..f0a11b57e 100644 --- a/handlers/telegram/keyboard_test.go +++ b/handlers/telegram/keyboard_test.go @@ -3,18 +3,19 @@ package telegram_test import ( "testing" + "github.com/nyaruka/courier" "github.com/nyaruka/courier/handlers/telegram" "github.com/stretchr/testify/assert" ) func TestKeyboardFromReplies(t *testing.T) { tcs := []struct { - replies []string + replies []courier.QuickReply expected *telegram.ReplyKeyboardMarkup }{ { - []string{"OK"}, + []courier.QuickReply{{Text: "OK"}}, &telegram.ReplyKeyboardMarkup{ [][]telegram.KeyboardButton{ {{Text: "OK"}}, @@ -23,7 +24,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"Yes", "No", "Maybe"}, + []courier.QuickReply{{Text: "Yes"}, {Text: "No"}, {Text: "Maybe"}}, &telegram.ReplyKeyboardMarkup{ [][]telegram.KeyboardButton{ {{Text: "Yes"}, {Text: "No"}, {Text: "Maybe"}}, @@ -32,7 +33,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"Vanilla", "Chocolate", "Mint", "Lemon Sorbet", "Papaya", "Strawberry"}, + []courier.QuickReply{{Text: "Vanilla"}, {Text: "Chocolate"}, {Text: "Mint"}, {Text: "Lemon Sorbet"}, {Text: "Papaya"}, {Text: "Strawberry"}}, &telegram.ReplyKeyboardMarkup{ [][]telegram.KeyboardButton{ {{Text: "Vanilla"}, {Text: "Chocolate"}}, @@ -43,7 +44,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"A", "B", "C", "D", "Chicken", "Fish", "Peanut Butter Pickle"}, + []courier.QuickReply{{Text: "A"}, {Text: "B"}, {Text: "C"}, {Text: "D"}, {Text: "Chicken"}, {Text: "Fish"}, {Text: "Peanut Butter Pickle"}}, &telegram.ReplyKeyboardMarkup{ [][]telegram.KeyboardButton{ {{Text: "A"}, {Text: "B"}, {Text: "C"}, {Text: "D"}}, diff --git a/handlers/test.go b/handlers/test.go index 9d3e28469..0008f82bd 100644 --- a/handlers/test.go +++ b/handlers/test.go @@ -299,7 +299,7 @@ type OutgoingTestCase struct { MsgURN string MsgURNAuth string MsgAttachments []string - MsgQuickReplies []string + MsgQuickReplies []courier.QuickReply MsgLocale i18n.Locale MsgTopic string MsgTemplating string diff --git a/handlers/utils.go b/handlers/utils.go index 0155087d1..f16400db3 100644 --- a/handlers/utils.go +++ b/handlers/utils.go @@ -35,6 +35,15 @@ func SplitAttachment(attachment string) (string, string) { return parts[0], parts[1] } +// TextOnlyQuickReplies returns the text of a list of quick replies +func TextOnlyQuickReplies(qrs []courier.QuickReply) []string { + t := make([]string, len(qrs)) + for i, qr := range qrs { + t[i] = qr.Text + } + return t +} + // NameFromFirstLastUsername is a utility function to build a contact's name from the passed // in values, all of which can be empty func NameFromFirstLastUsername(first string, last string, username string) string { diff --git a/handlers/viber/handler_test.go b/handlers/viber/handler_test.go index 0379f4dd4..1dbdcd1e9 100644 --- a/handlers/viber/handler_test.go +++ b/handlers/viber/handler_test.go @@ -97,7 +97,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Quick Reply", MsgText: "Are you happy?", MsgURN: "viber:xy5/5y6O81+/kbWHpLhBoA==", - MsgQuickReplies: []string{"Yes", "No"}, + MsgQuickReplies: []courier.QuickReply{{Text: "Yes"}, {Text: "No"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://chatapi.viber.com/pa/send_message": { httpx.NewMockResponse(200, nil, []byte(`{"status":0,"status_message":"ok","message_token":4987381194038857789}`)), @@ -279,7 +279,7 @@ var buttonLayoutSendTestCases = []OutgoingTestCase{ Label: "Quick Reply With Layout With Column, Row and BgColor definitions", MsgText: "Select a, b, c or d.", MsgURN: "viber:xy5/5y6O81+/kbWHpLhBoA==", - MsgQuickReplies: []string{"a", "b", "c", "d"}, + MsgQuickReplies: []courier.QuickReply{{Text: "a"}, {Text: "b"}, {Text: "c"}, {Text: "d"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://chatapi.viber.com/pa/send_message": { httpx.NewMockResponse(200, nil, []byte(`{"status":0,"status_message":"ok","message_token":4987381194038857789}`)), diff --git a/handlers/viber/keyboard.go b/handlers/viber/keyboard.go index ed031dffc..99231b6ae 100644 --- a/handlers/viber/keyboard.go +++ b/handlers/viber/keyboard.go @@ -5,6 +5,9 @@ import ( "html" "strings" "unicode/utf8" + + "github.com/nyaruka/courier" + "github.com/nyaruka/courier/handlers" ) // KeyboardButton is button on a keyboard, see https://developers.viber.com/docs/tools/keyboards/#buttons-parameters @@ -36,8 +39,8 @@ const ( var textSizes = map[string]bool{"small": true, "regular": true, "large": true} // NewKeyboardFromReplies create a keyboard from the given quick replies -func NewKeyboardFromReplies(replies []string, buttonConfig map[string]any) *Keyboard { - rows := StringsToRows(replies, maxColumns, maxRowRunes, paddingRunes) +func NewKeyboardFromReplies(replies []courier.QuickReply, buttonConfig map[string]any) *Keyboard { + rows := StringsToRows(handlers.TextOnlyQuickReplies(replies), maxColumns, maxRowRunes, paddingRunes) buttons := []KeyboardButton{} for i := range rows { diff --git a/handlers/viber/keyboard_test.go b/handlers/viber/keyboard_test.go index 37d4d30f7..92130f8dc 100644 --- a/handlers/viber/keyboard_test.go +++ b/handlers/viber/keyboard_test.go @@ -3,18 +3,19 @@ package viber_test import ( "testing" + "github.com/nyaruka/courier" "github.com/nyaruka/courier/handlers/viber" "github.com/stretchr/testify/assert" ) func TestKeyboardFromReplies(t *testing.T) { tsc := []struct { - replies []string + replies []courier.QuickReply expected *viber.Keyboard buttonConfig map[string]any }{ { - []string{"OK"}, + []courier.QuickReply{{Text: "OK"}}, &viber.Keyboard{ "keyboard", false, @@ -25,7 +26,7 @@ func TestKeyboardFromReplies(t *testing.T) { map[string]any{}, }, { - []string{"Yes", "No", "Maybe"}, + []courier.QuickReply{{Text: "Yes"}, {Text: "No"}, {Text: "Maybe"}}, &viber.Keyboard{ "keyboard", false, @@ -38,7 +39,7 @@ func TestKeyboardFromReplies(t *testing.T) { map[string]any{}, }, { - []string{"A", "B", "C", "D"}, + []courier.QuickReply{{Text: "A"}, {Text: "B"}, {Text: "C"}, {Text: "D"}}, &viber.Keyboard{ "keyboard", false, @@ -52,7 +53,7 @@ func TestKeyboardFromReplies(t *testing.T) { map[string]any{}, }, { - []string{"\"A\"", ""}, + []courier.QuickReply{{Text: "\"A\""}, {Text: ""}}, &viber.Keyboard{ "keyboard", false, @@ -64,7 +65,7 @@ func TestKeyboardFromReplies(t *testing.T) { map[string]any{}, }, { - []string{"Vanilla", "Chocolate", "Mint", "Lemon Sorbet", "Papaya", "Strawberry"}, + []courier.QuickReply{{Text: "Vanilla"}, {Text: "Chocolate"}, {Text: "Mint"}, {Text: "Lemon Sorbet"}, {Text: "Papaya"}, {Text: "Strawberry"}}, &viber.Keyboard{ "keyboard", false, @@ -80,7 +81,7 @@ func TestKeyboardFromReplies(t *testing.T) { map[string]any{}, }, { - []string{"A", "B", "C", "D", "Chicken", "Fish", "Peanut Butter Pickle"}, + []courier.QuickReply{{Text: "A"}, {Text: "B"}, {Text: "C"}, {Text: "D"}, {Text: "Chicken"}, {Text: "Fish"}, {Text: "Peanut Butter Pickle"}}, &viber.Keyboard{ "keyboard", false, @@ -97,7 +98,7 @@ func TestKeyboardFromReplies(t *testing.T) { map[string]any{}, }, { - []string{"Foo", "Bar", "Baz"}, + []courier.QuickReply{{Text: "Foo"}, {Text: "Bar"}, {Text: "Baz"}}, &viber.Keyboard{ "keyboard", false, @@ -114,7 +115,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"Yes", "No", "Maybe"}, + []courier.QuickReply{{Text: "Yes"}, {Text: "No"}, {Text: "Maybe"}}, &viber.Keyboard{ "keyboard", false, diff --git a/handlers/vk/handler_test.go b/handlers/vk/handler_test.go index 45e180431..a4ebb6efc 100644 --- a/handlers/vk/handler_test.go +++ b/handlers/vk/handler_test.go @@ -473,7 +473,7 @@ var outgoingCases = []OutgoingTestCase{ Label: "Send keyboard", MsgText: "Send keyboard", MsgURN: "vk:123456789", - MsgQuickReplies: []string{"A", "B", "C", "D", "E"}, + MsgQuickReplies: []courier.QuickReply{{Text: "A"}, {Text: "B"}, {Text: "C"}, {Text: "D"}, {Text: "E"}}, MockResponses: map[string][]*httpx.MockResponse{ "https://api.vk.com/method/messages.send.json?*": { httpx.NewMockResponse(200, nil, []byte(`{"response": 1}`)), diff --git a/handlers/vk/keyboard.go b/handlers/vk/keyboard.go index a8b435254..42175b818 100644 --- a/handlers/vk/keyboard.go +++ b/handlers/vk/keyboard.go @@ -1,6 +1,8 @@ package vk import ( + "github.com/nyaruka/courier" + "github.com/nyaruka/courier/handlers" "github.com/nyaruka/courier/utils" "github.com/nyaruka/gocommon/jsonx" ) @@ -23,8 +25,8 @@ type ButtonAction struct { } // NewKeyboardFromReplies creates a keyboard from the given quick replies -func NewKeyboardFromReplies(replies []string) *Keyboard { - rows := utils.StringsToRows(replies, 10, 30, 2) +func NewKeyboardFromReplies(replies []courier.QuickReply) *Keyboard { + rows := utils.StringsToRows(handlers.TextOnlyQuickReplies(replies), 10, 30, 2) buttons := make([][]ButtonPayload, len(rows)) for i := range rows { diff --git a/handlers/vk/keyboard_test.go b/handlers/vk/keyboard_test.go index 6a2491b68..89190a5ba 100644 --- a/handlers/vk/keyboard_test.go +++ b/handlers/vk/keyboard_test.go @@ -3,18 +3,19 @@ package vk_test import ( "testing" + "github.com/nyaruka/courier" "github.com/nyaruka/courier/handlers/vk" "github.com/stretchr/testify/assert" ) func TestKeyboardFromReplies(t *testing.T) { tcs := []struct { - replies []string + replies []courier.QuickReply expected *vk.Keyboard }{ { - []string{"OK"}, + []courier.QuickReply{{Text: "OK"}}, &vk.Keyboard{ true, [][]vk.ButtonPayload{ @@ -26,7 +27,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"Yes", "No", "Maybe"}, + []courier.QuickReply{{Text: "Yes"}, {Text: "No"}, {Text: "Maybe"}}, &vk.Keyboard{ true, [][]vk.ButtonPayload{ @@ -40,7 +41,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"Vanilla", "Chocolate", "Mint", "Lemon Sorbet", "Papaya", "Strawberry"}, + []courier.QuickReply{{Text: "Vanilla"}, {Text: "Chocolate"}, {Text: "Mint"}, {Text: "Lemon Sorbet"}, {Text: "Papaya"}, {Text: "Strawberry"}}, &vk.Keyboard{ true, [][]vk.ButtonPayload{ @@ -56,7 +57,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"A", "B", "C", "D", "Chicken", "Fish", "Peanut Butter Pickle"}, + []courier.QuickReply{{Text: "A"}, {Text: "B"}, {Text: "C"}, {Text: "D"}, {Text: "Chicken"}, {Text: "Fish"}, {Text: "Peanut Butter Pickle"}}, &vk.Keyboard{ true, [][]vk.ButtonPayload{ @@ -73,7 +74,7 @@ func TestKeyboardFromReplies(t *testing.T) { }, }, { - []string{"A", "B", "C", "D", "E"}, + []courier.QuickReply{{Text: "A"}, {Text: "B"}, {Text: "C"}, {Text: "D"}, {Text: "E"}}, &vk.Keyboard{ true, [][]vk.ButtonPayload{ diff --git a/handlers/whatsapp_legacy/handler.go b/handlers/whatsapp_legacy/handler.go index 5d1e734a7..d8de2bc0f 100644 --- a/handlers/whatsapp_legacy/handler.go +++ b/handlers/whatsapp_legacy/handler.go @@ -668,7 +668,7 @@ func buildPayloads(msg courier.MsgOut, h *handler, clog *courier.ChannelLog) ([] Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) - btns[i].Reply.Title = qr + btns[i].Reply.Title = qr.Text } payload.Interactive.Action.Buttons = btns payloads = append(payloads, payload) @@ -682,7 +682,7 @@ func buildPayloads(msg courier.MsgOut, h *handler, clog *courier.ChannelLog) ([] for i, qr := range qrs { section.Rows[i] = mtSectionRow{ ID: fmt.Sprint(i), - Title: qr, + Title: qr.Text, } } payload.Interactive.Action.Sections = []mtSection{ @@ -763,7 +763,7 @@ func buildPayloads(msg courier.MsgOut, h *handler, clog *courier.ChannelLog) ([] Type: "reply", } btns[i].Reply.ID = fmt.Sprint(i) - btns[i].Reply.Title = qr + btns[i].Reply.Title = qr.Text } payload.Interactive.Action.Buttons = btns payloads = append(payloads, payload) @@ -777,7 +777,7 @@ func buildPayloads(msg courier.MsgOut, h *handler, clog *courier.ChannelLog) ([] for i, qr := range qrs { section.Rows[i] = mtSectionRow{ ID: fmt.Sprint(i), - Title: qr, + Title: qr.Text, } } payload.Interactive.Action.Sections = []mtSection{ diff --git a/handlers/whatsapp_legacy/handler_test.go b/handlers/whatsapp_legacy/handler_test.go index 334024572..9ad7d8f91 100644 --- a/handlers/whatsapp_legacy/handler_test.go +++ b/handlers/whatsapp_legacy/handler_test.go @@ -951,7 +951,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Interactive Button Message Send", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MockResponses: map[string][]*httpx.MockResponse{ "*/v1/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -967,7 +967,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Interactive List Message Send", MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MockResponses: map[string][]*httpx.MockResponse{ "*/v1/messages": { httpx.NewMockResponse(201, nil, []byte(`{ "messages": [{"id": "157b5e14568e8"}] }`)), @@ -984,7 +984,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Interactive Button Message Send with attachment", MsgText: "Interactive Button Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"BUTTON1"}, + MsgQuickReplies: []courier.QuickReply{{Text: "BUTTON1"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/v1/messages": { @@ -1002,7 +1002,7 @@ var defaultSendTestCases = []OutgoingTestCase{ Label: "Interactive List Message Send with attachment", MsgText: "Interactive List Msg", MsgURN: "whatsapp:250788123123", - MsgQuickReplies: []string{"ROW1", "ROW2", "ROW3", "ROW4"}, + MsgQuickReplies: []courier.QuickReply{{Text: "ROW1"}, {Text: "ROW2"}, {Text: "ROW3"}, {Text: "ROW4"}}, MsgAttachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, MockResponses: map[string][]*httpx.MockResponse{ "*/v1/messages": { diff --git a/msg.go b/msg.go index 3e751b69f..8ebcd7a35 100644 --- a/msg.go +++ b/msg.go @@ -7,6 +7,7 @@ import ( "time" "github.com/nyaruka/gocommon/i18n" + "github.com/nyaruka/gocommon/jsonx" "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/gocommon/uuids" "github.com/nyaruka/null/v3" @@ -32,6 +33,22 @@ type MsgUUID uuids.UUID // NilMsgUUID is a "zero value" message UUID const NilMsgUUID = MsgUUID("") +type QuickReply struct { + Text string `json:"text"` +} + +func (q *QuickReply) UnmarshalJSON(d []byte) error { + // if we just have a string we unmarshal it into the text field + if len(d) > 2 && d[0] == '"' && d[len(d)-1] == '"' { + return jsonx.Unmarshal(d, &q.Text) + } + + // alias our type so we don't end up here again + type alias QuickReply + + return jsonx.Unmarshal(d, (*alias)(q)) +} + type FlowReference struct { UUID string `json:"uuid" validate:"uuid4"` Name string `json:"name"` @@ -103,7 +120,7 @@ type MsgOut interface { Msg // outgoing specific - QuickReplies() []string + QuickReplies() []QuickReply Locale() i18n.Locale Templating() *Templating URNAuth() string diff --git a/test/backend.go b/test/backend.go index ad055d05a..15c9683bd 100644 --- a/test/backend.go +++ b/test/backend.go @@ -115,7 +115,7 @@ func (mb *MockBackend) NewIncomingMsg(channel courier.Channel, urn urns.URN, tex } // NewOutgoingMsg creates a new outgoing message from the given params -func (mb *MockBackend) NewOutgoingMsg(channel courier.Channel, id courier.MsgID, urn urns.URN, text string, highPriority bool, quickReplies []string, +func (mb *MockBackend) NewOutgoingMsg(channel courier.Channel, id courier.MsgID, urn urns.URN, text string, highPriority bool, quickReplies []courier.QuickReply, topic string, responseToExternalID string, origin courier.MsgOrigin, contactLastSeenOn *time.Time) courier.MsgOut { return &MockMsg{ diff --git a/test/msg.go b/test/msg.go index 556fa6be3..59d75bf24 100644 --- a/test/msg.go +++ b/test/msg.go @@ -23,7 +23,7 @@ type MockMsg struct { externalID string contactName string highPriority bool - quickReplies []string + quickReplies []courier.QuickReply origin courier.MsgOrigin contactLastSeenOn *time.Time topic string @@ -62,22 +62,22 @@ func (m *MockMsg) URN() urns.URN { return m.urn } func (m *MockMsg) Channel() courier.Channel { return m.channel } // outgoing specific -func (m *MockMsg) QuickReplies() []string { return m.quickReplies } -func (m *MockMsg) Locale() i18n.Locale { return m.locale } -func (m *MockMsg) Templating() *courier.Templating { return m.templating } -func (m *MockMsg) URNAuth() string { return m.urnAuth } -func (m *MockMsg) Origin() courier.MsgOrigin { return m.origin } -func (m *MockMsg) ContactLastSeenOn() *time.Time { return m.contactLastSeenOn } -func (m *MockMsg) Topic() string { return m.topic } -func (m *MockMsg) Metadata() json.RawMessage { return m.metadata } -func (m *MockMsg) ResponseToExternalID() string { return m.responseToExternalID } -func (m *MockMsg) SentOn() *time.Time { return m.sentOn } -func (m *MockMsg) IsResend() bool { return m.isResend } -func (m *MockMsg) Flow() *courier.FlowReference { return m.flow } -func (m *MockMsg) OptIn() *courier.OptInReference { return m.optIn } -func (m *MockMsg) UserID() courier.UserID { return m.userID } -func (m *MockMsg) Session() *courier.Session { return m.session } -func (m *MockMsg) HighPriority() bool { return m.highPriority } +func (m *MockMsg) QuickReplies() []courier.QuickReply { return m.quickReplies } +func (m *MockMsg) Locale() i18n.Locale { return m.locale } +func (m *MockMsg) Templating() *courier.Templating { return m.templating } +func (m *MockMsg) URNAuth() string { return m.urnAuth } +func (m *MockMsg) Origin() courier.MsgOrigin { return m.origin } +func (m *MockMsg) ContactLastSeenOn() *time.Time { return m.contactLastSeenOn } +func (m *MockMsg) Topic() string { return m.topic } +func (m *MockMsg) Metadata() json.RawMessage { return m.metadata } +func (m *MockMsg) ResponseToExternalID() string { return m.responseToExternalID } +func (m *MockMsg) SentOn() *time.Time { return m.sentOn } +func (m *MockMsg) IsResend() bool { return m.isResend } +func (m *MockMsg) Flow() *courier.FlowReference { return m.flow } +func (m *MockMsg) OptIn() *courier.OptInReference { return m.optIn } +func (m *MockMsg) UserID() courier.UserID { return m.userID } +func (m *MockMsg) Session() *courier.Session { return m.session } +func (m *MockMsg) HighPriority() bool { return m.highPriority } // incoming specific func (m *MockMsg) ReceivedOn() *time.Time { return m.receivedOn }