Skip to content

Commit

Permalink
Merge pull request #1272 from nyaruka/templates_rework_1
Browse files Browse the repository at this point in the history
Make template message code usable outside of actions part 1
  • Loading branch information
rowanseymour authored Jul 3, 2024
2 parents 4348b2e + acfb07c commit 2459f40
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 40 deletions.
12 changes: 3 additions & 9 deletions flows/actions/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
33 changes: 2 additions & 31 deletions flows/actions/send_msg.go
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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)
}
6 changes: 6 additions & 0 deletions flows/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions flows/template.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
127 changes: 127 additions & 0 deletions flows/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
}

0 comments on commit 2459f40

Please sign in to comment.