From acfb07c3b2661a1cb6436e02a17e463cca0a248f Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 3 Jul 2024 15:40:21 -0500 Subject: [PATCH] Move functionality to generate preview content for template message from send_msg action to TemplateTranslation --- flows/actions/base.go | 12 +--- flows/actions/send_msg.go | 33 +--------- flows/msg.go | 6 ++ flows/template.go | 35 +++++++++++ flows/template_test.go | 127 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 40 deletions(-) diff --git a/flows/actions/base.go b/flows/actions/base.go index b7265d6d5..bce62db41 100644 --- a/flows/actions/base.go +++ b/flows/actions/base.go @@ -22,12 +22,6 @@ import ( // max number of bytes to be saved to extra on a result const resultExtraMaxBytes = 10000 -// max length of a message attachment (type:url) -const maxAttachmentLength = 2048 - -// max length of a quick reply -const maxQuickReplyLength = 64 - // common category names const ( CategorySuccess = "Success" @@ -96,8 +90,8 @@ func (a *baseAction) evaluateMessage(run flows.Run, languages []i18n.Language, a logEvent(events.NewErrorf("attachment text evaluated to empty string, skipping")) continue } - if len(evaluatedAttachment) > maxAttachmentLength { - logEvent(events.NewErrorf("evaluated attachment is longer than %d limit, skipping", maxAttachmentLength)) + if len(evaluatedAttachment) > flows.MaxAttachmentLength { + logEvent(events.NewErrorf("evaluated attachment is longer than %d limit, skipping", flows.MaxAttachmentLength)) continue } evaluatedAttachments = append(evaluatedAttachments, utils.Attachment(evaluatedAttachment)) @@ -112,7 +106,7 @@ func (a *baseAction) evaluateMessage(run flows.Run, languages []i18n.Language, a logEvent(events.NewErrorf("quick reply text evaluated to empty string, skipping")) continue } - evaluatedQuickReplies = append(evaluatedQuickReplies, stringsx.TruncateEllipsis(evaluatedQuickReply, maxQuickReplyLength)) + evaluatedQuickReplies = append(evaluatedQuickReplies, stringsx.TruncateEllipsis(evaluatedQuickReply, flows.MaxQuickReplyLength)) } // although it's possible for the different parts of the message to have different languages, we want to resolve diff --git a/flows/actions/send_msg.go b/flows/actions/send_msg.go index 5555f17c1..dd759ad25 100644 --- a/flows/actions/send_msg.go +++ b/flows/actions/send_msg.go @@ -1,17 +1,12 @@ package actions import ( - "fmt" - "strings" - "github.com/nyaruka/gocommon/i18n" - "github.com/nyaruka/gocommon/stringsx" "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/gocommon/uuids" "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/events" - "github.com/nyaruka/goflow/utils" ) func init() { @@ -153,33 +148,9 @@ func (a *SendMsgAction) getTemplateMsg(run flows.Run, urn urns.URN, channelRef * } // the message we return is an approximate preview of what the channel will send using the template - var previewText []string - var previewAttachments []utils.Attachment - var previewQRs []string - - for _, comp := range translation.Components() { - previewContent := comp.Content() - for key, index := range comp.Variables() { - variable := variables[index] - - if variable.Type == "text" { - previewContent = strings.ReplaceAll(previewContent, fmt.Sprintf("{{%s}}", key), variable.Value) - } else if variable.Type == "image" || variable.Type == "video" || variable.Type == "document" { - previewAttachments = append(previewAttachments, utils.Attachment(variable.Value)) - } - } - - if previewContent != "" { - if comp.Type() == "header/text" || comp.Type() == "body/text" || comp.Type() == "footer/text" { - previewText = append(previewText, previewContent) - } else if strings.HasPrefix(comp.Type(), "button/") { - previewQRs = append(previewQRs, stringsx.TruncateEllipsis(previewContent, maxQuickReplyLength)) - } - } - } - + preview := translation.Preview(variables) locale := translation.Locale() templating := flows.NewMsgTemplating(a.Template, translation.Namespace(), components, variables) - return flows.NewMsgOut(urn, channelRef, strings.Join(previewText, "\n\n"), previewAttachments, previewQRs, templating, flows.NilMsgTopic, locale, unsendableReason) + return flows.NewMsgOut(urn, channelRef, preview.Text, preview.Attachments, preview.QuickReplies, templating, flows.NilMsgTopic, locale, unsendableReason) } diff --git a/flows/msg.go b/flows/msg.go index d356fd78e..f6bbb46d2 100644 --- a/flows/msg.go +++ b/flows/msg.go @@ -22,6 +22,12 @@ func init() { type UnsendableReason string const ( + // max length of a message attachment (type:url) + MaxAttachmentLength = 2048 + + // max length of a quick reply + MaxQuickReplyLength = 64 + NilUnsendableReason UnsendableReason = "" UnsendableReasonNoDestination UnsendableReason = "no_destination" // no sendable channel+URN pair UnsendableReasonContactStatus UnsendableReason = "contact_status" // contact is blocked or stopped or archived diff --git a/flows/template.go b/flows/template.go index 33dad7446..82b0f4c16 100644 --- a/flows/template.go +++ b/flows/template.go @@ -1,8 +1,13 @@ package flows import ( + "fmt" + "strings" + "github.com/nyaruka/gocommon/i18n" + "github.com/nyaruka/gocommon/stringsx" "github.com/nyaruka/goflow/assets" + "github.com/nyaruka/goflow/utils" ) // Template represents messaging templates used by channels types such as WhatsApp @@ -58,6 +63,36 @@ func NewTemplateTranslation(t assets.TemplateTranslation) *TemplateTranslation { // Asset returns the underlying asset func (t *TemplateTranslation) Asset() assets.TemplateTranslation { return t.TemplateTranslation } +// Preview returns message content which will act as a preview of a message sent with this template +func (t *TemplateTranslation) Preview(vars []*TemplatingVariable) *MsgContent { + var text []string + var attachments []utils.Attachment + var quickReplies []string + + for _, comp := range t.Components() { + content := comp.Content() + for key, index := range comp.Variables() { + variable := vars[index] + + if variable.Type == "text" { + content = strings.ReplaceAll(content, fmt.Sprintf("{{%s}}", key), variable.Value) + } else if variable.Type == "image" || variable.Type == "video" || variable.Type == "document" { + attachments = append(attachments, utils.Attachment(variable.Value)) + } + } + + if content != "" { + if comp.Type() == "header/text" || comp.Type() == "body/text" || comp.Type() == "footer/text" { + text = append(text, content) + } else if strings.HasPrefix(comp.Type(), "button/") { + quickReplies = append(quickReplies, stringsx.TruncateEllipsis(content, MaxQuickReplyLength)) + } + } + } + + return &MsgContent{Text: strings.Join(text, "\n\n"), Attachments: attachments, QuickReplies: quickReplies} +} + // TemplateAssets is our type for all the templates in an environment type TemplateAssets struct { templates []*Template diff --git a/flows/template_test.go b/flows/template_test.go index da7c75fb6..d87b33ee4 100644 --- a/flows/template_test.go +++ b/flows/template_test.go @@ -5,10 +5,12 @@ import ( "testing" "github.com/nyaruka/gocommon/i18n" + "github.com/nyaruka/gocommon/jsonx" "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/assets/static" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/test" + "github.com/nyaruka/goflow/utils" "github.com/stretchr/testify/assert" ) @@ -56,3 +58,128 @@ func TestFindTranslation(t *testing.T) { assert.Equal(t, assets.NewTemplateReference("c520cbda-e118-440f-aaf6-c0485088384f", "greeting"), template.Reference()) assert.Equal(t, (*assets.TemplateReference)(nil), (*flows.Template)(nil).Reference()) } + +func TestTemplateTranslationPreview(t *testing.T) { + tcs := []struct { + translation []byte + variables []*flows.TemplatingVariable + expected *flows.MsgContent + }{ + { // 0: empty translation + translation: []byte(`{ + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [], + "variables": [] + }`), + variables: []*flows.TemplatingVariable{}, + expected: &flows.MsgContent{}, + }, + { // 1: body only + translation: []byte(`{ + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "body", + "type": "body/text", + "content": "Hi {{1}}, who's a good {{2}}?", + "variables": {"1": 0, "2": 1} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"} + ] + }`), + variables: []*flows.TemplatingVariable{{Type: "text", Value: "Chef"}, {Type: "text", Value: "boy"}}, + expected: &flows.MsgContent{Text: "Hi Chef, who's a good boy?"}, + }, + { // 2: multiple text component types + translation: []byte(`{ + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "header", + "type": "header/text", + "content": "Header {{1}}", + "variables": {"1": 0} + }, + { + "name": "body", + "type": "body/text", + "content": "Body {{1}}", + "variables": {"1": 1} + }, + { + "name": "footer", + "type": "footer/text", + "content": "Footer {{1}}", + "variables": {"1": 2} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"}, + {"type": "text"} + ] + }`), + variables: []*flows.TemplatingVariable{{Type: "text", Value: "A"}, {Type: "text", Value: "B"}, {Type: "text", Value: "C"}}, + expected: &flows.MsgContent{Text: "Header A\n\nBody B\n\nFooter C"}, + }, + { // 3: buttons become quick replies + translation: []byte(`{ + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "button.1", + "type": "button/quick_reply", + "content": "{{1}}", + "variables": {"1": 0} + }, + { + "name": "button.2", + "type": "button/quick_reply", + "content": "{{1}}", + "variables": {"1": 1} + } + ], + "variables": [ + {"type": "text"}, + {"type": "text"} + ] + }`), + variables: []*flows.TemplatingVariable{{Type: "text", Value: "Yes"}, {Type: "text", Value: "No"}}, + expected: &flows.MsgContent{QuickReplies: []string{"Yes", "No"}}, + }, + { // 4: header image becomes an attachment + translation: []byte(`{ + "channel": {"uuid": "79401ef2-8eb6-48f4-9f9d-0604530b1ac0", "name": "WhatsApp"}, + "locale": "eng", + "components": [ + { + "name": "header", + "type": "header/image", + "content": "{{1}}", + "variables": {"1": 0} + } + ], + "variables": [ + {"type": "image"} + ] + }`), + variables: []*flows.TemplatingVariable{{Type: "image", Value: "image/jpeg:http://example.com/test.jpg"}}, + expected: &flows.MsgContent{Attachments: []utils.Attachment{"image/jpeg:http://example.com/test.jpg"}}, + }, + } + + for i, tc := range tcs { + trans := &static.TemplateTranslation{} + jsonx.MustUnmarshal(tc.translation, trans) + + actual := flows.NewTemplateTranslation(trans).Preview(tc.variables) + assert.Equal(t, tc.expected, actual, "%d: preview mismatch", i) + } +}