Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make template message code usable outside of actions part 1 #1272

Merged
merged 1 commit into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
Loading