Skip to content

Commit

Permalink
Merge pull request #142 from nyaruka/quick_replies
Browse files Browse the repository at this point in the history
Add support for quick replies on reply actions
  • Loading branch information
rowanseymour authored Jan 25, 2018
2 parents b4a438c + b73af61 commit 8d8fdcb
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 55 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ _testmain.go

debug
.DS_Store
dist/
5 changes: 3 additions & 2 deletions cmd/flowrunner/testdata/flows/all_actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@
{
"uuid": "62a30ab4-d73c-447d-a989-39c49115153e",
"type": "reply",
"text": "This is a reply with attachments",
"attachments": ["image/jpeg:http://s3.amazon.com/bucket/test_en.jpg?a=@contact.fields.state"]
"text": "This is a reply with attachments and quick replies",
"attachments": ["image/jpeg:http://s3.amazon.com/bucket/test_en.jpg?a=@contact.fields.state"],
"quick_replies": ["Yes", "No"]
},
{
"uuid": "5508e6a7-26ce-4b3b-b32e-bb4e2e614f5d",
Expand Down
12 changes: 10 additions & 2 deletions cmd/flowrunner/testdata/flows/all_actions_test.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,11 @@
}
],
"created_on": "2000-01-01T00:00:00.000000000-00:00",
"text": "This is a reply with attachments",
"quick_replies": [
"Yes",
"No"
],
"text": "This is a reply with attachments and quick replies",
"type": "send_msg"
},
"step_uuid": ""
Expand Down Expand Up @@ -582,7 +586,11 @@
}
],
"created_on": "2000-01-01T00:00:00.000000000-00:00",
"text": "This is a reply with attachments",
"quick_replies": [
"Yes",
"No"
],
"text": "This is a reply with attachments and quick replies",
"type": "send_msg"
},
{
Expand Down
18 changes: 16 additions & 2 deletions flows/actions/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func (a *BaseAction) resolveLabels(run flows.FlowRun, step flows.Step, reference
}

// helper function for actions that send a message (text + attachments) that must be localized and evalulated
func (a *BaseAction) evaluateMessage(run flows.FlowRun, step flows.Step, actionText string, actionAttachments []string, log flows.EventLog) (string, []string) {
func (a *BaseAction) evaluateMessage(run flows.FlowRun, step flows.Step, actionText string, actionAttachments []string, actionQuickReplies []string, log flows.EventLog) (string, []string, []string) {
// localize and evaluate the message text
localizedText := run.GetText(flows.UUID(a.UUID()), "text", actionText)
evaluatedText, err := excellent.EvaluateTemplateAsString(run.Environment(), run.Context(), localizedText)
Expand All @@ -165,7 +165,21 @@ func (a *BaseAction) evaluateMessage(run flows.FlowRun, step flows.Step, actionT
evaluatedAttachments = append(evaluatedAttachments, evaluatedAttachment)
}

return evaluatedText, evaluatedAttachments
// localize and evaluate the quick replies
translatedQuickReplies := run.GetTextArray(flows.UUID(a.UUID()), "quick_replies", actionQuickReplies)
evaluatedQuickReplies := make([]string, 0, len(translatedQuickReplies))
for n := range translatedQuickReplies {
evaluatedQuickReply, err := excellent.EvaluateTemplateAsString(run.Environment(), run.Context(), translatedQuickReplies[n])
if err != nil {
log.Add(events.NewErrorEvent(err))
} else if evaluatedQuickReply == "" {
log.Add(events.NewErrorEvent(fmt.Errorf("quick reply text evaluated to empty string, skipping")))
continue
}
evaluatedQuickReplies = append(evaluatedQuickReplies, evaluatedQuickReply)
}

return evaluatedText, evaluatedAttachments, evaluatedQuickReplies
}

func (a *BaseAction) resolveContactsAndGroups(run flows.FlowRun, step flows.Step, actionURNs []urns.URN, actionContacts []*flows.ContactReference, actionGroups []*flows.GroupReference, actionLegacyVars []string, log flows.EventLog) ([]urns.URN, []*flows.ContactReference, []*flows.GroupReference, error) {
Expand Down
13 changes: 7 additions & 6 deletions flows/actions/reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ const TypeReply string = "reply"
// @action reply
type ReplyAction struct {
BaseAction
Text string `json:"text"`
Attachments []string `json:"attachments"`
AllURNs bool `json:"all_urns,omitempty"`
Text string `json:"text"`
Attachments []string `json:"attachments"`
QuickReplies []string `json:"quick_replies,omitempty"`
AllURNs bool `json:"all_urns,omitempty"`
}

// Type returns the type of this action
Expand All @@ -40,14 +41,14 @@ func (a *ReplyAction) Validate(assets flows.SessionAssets) error {

// Execute runs this action
func (a *ReplyAction) Execute(run flows.FlowRun, step flows.Step, log flows.EventLog) error {
evaluatedText, evaluatedAttachments := a.evaluateMessage(run, step, a.Text, a.Attachments, log)
evaluatedText, evaluatedAttachments, evaluatedQuickReplies := a.evaluateMessage(run, step, a.Text, a.Attachments, a.QuickReplies, log)

urns := run.Contact().URNs()

if a.AllURNs && len(urns) > 0 {
log.Add(events.NewSendMsgEvent(evaluatedText, evaluatedAttachments, urns, nil, nil))
log.Add(events.NewSendMsgEvent(evaluatedText, evaluatedAttachments, evaluatedQuickReplies, urns, nil, nil))
} else {
log.Add(events.NewSendMsgToContactEvent(evaluatedText, evaluatedAttachments, run.Contact().Reference()))
log.Add(events.NewSendMsgToContactEvent(evaluatedText, evaluatedAttachments, evaluatedQuickReplies, run.Contact().Reference()))
}

return nil
Expand Down
17 changes: 9 additions & 8 deletions flows/actions/send_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ const TypeSendMsg string = "send_msg"
// @action send_msg
type SendMsgAction struct {
BaseAction
Text string `json:"text"`
Attachments []string `json:"attachments"`
URNs []urns.URN `json:"urns,omitempty"`
Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"`
Groups []*flows.GroupReference `json:"groups,omitempty" validate:"dive"`
LegacyVars []string `json:"legacy_vars,omitempty"`
Text string `json:"text"`
Attachments []string `json:"attachments"`
QuickReplies []string `json:"quick_replies,omitempty"`
URNs []urns.URN `json:"urns,omitempty"`
Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"`
Groups []*flows.GroupReference `json:"groups,omitempty" validate:"dive"`
LegacyVars []string `json:"legacy_vars,omitempty"`
}

// Type returns the type of this action
Expand All @@ -45,14 +46,14 @@ func (a *SendMsgAction) Validate(assets flows.SessionAssets) error {

// Execute runs this action
func (a *SendMsgAction) Execute(run flows.FlowRun, step flows.Step, log flows.EventLog) error {
evaluatedText, evaluatedAttachments := a.evaluateMessage(run, step, a.Text, a.Attachments, log)
evaluatedText, evaluatedAttachments, evaluatedQuickReplies := a.evaluateMessage(run, step, a.Text, a.Attachments, a.QuickReplies, log)

urnList, contactRefs, groupRefs, err := a.resolveContactsAndGroups(run, step, a.URNs, a.Contacts, a.Groups, a.LegacyVars, log)
if err != nil {
return err
}

log.Add(events.NewSendMsgEvent(evaluatedText, evaluatedAttachments, urnList, contactRefs, groupRefs))
log.Add(events.NewSendMsgEvent(evaluatedText, evaluatedAttachments, evaluatedQuickReplies, urnList, contactRefs, groupRefs))

return nil
}
86 changes: 69 additions & 17 deletions flows/definition/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,10 @@ type legacyAction struct {
Name string `json:"name"`

// message and email
Msg json.RawMessage `json:"msg"`
Media json.RawMessage `json:"media"`
SendAll bool `json:"send_all"`
Msg json.RawMessage `json:"msg"`
Media json.RawMessage `json:"media"`
QuickReplies json.RawMessage `json:"quick_replies"`
SendAll bool `json:"send_all"`

// variable contact actions
Contacts []legacyContactReference `json:"contacts"`
Expand Down Expand Up @@ -272,15 +273,35 @@ type wardTest struct {

type localizations map[utils.Language]flows.Action

type translationMap map[utils.Language]string

func addTranslationMap(baseLanguage utils.Language, translations *flowTranslations, mapped translationMap, uuid flows.UUID, key string) {
for language, translation := range mapped {
expression, _ := excellent.MigrateTemplate(translation)
func addTranslationMap(baseLanguage utils.Language, translations *flowTranslations, mapped map[utils.Language]string, uuid flows.UUID, key string) string {
var inBaseLanguage string
for language, item := range mapped {
expression, _ := excellent.MigrateTemplate(item)
if language != baseLanguage {
addTranslation(translations, language, uuid, key, []string{expression})
} else {
inBaseLanguage = expression
}
}

return inBaseLanguage
}

func addTranslationMultiMap(baseLanguage utils.Language, translations *flowTranslations, mapped map[utils.Language][]string, uuid flows.UUID, key string) []string {
var inBaseLanguage []string
for language, items := range mapped {
expressions := make([]string, len(items))
for i := range items {
expression, _ := excellent.MigrateTemplate(items[i])
expressions[i] = expression
}
if language != baseLanguage {
addTranslation(translations, language, uuid, key, expressions)
} else {
inBaseLanguage = expressions
}
}
return inBaseLanguage
}

func addTranslation(translations *flowTranslations, lang utils.Language, itemUUID flows.UUID, propKey string, translation []string) {
Expand All @@ -301,6 +322,27 @@ func addTranslation(translations *flowTranslations, lang utils.Language, itemUUI
itemTrans[propKey] = translation
}

// Transforms a list of single item translations into a map of multi-item translations, e.g.
//
// [{"eng": "yes", "fra": "oui"}, {"eng": "no", "fra": "non"}] becomes {"eng": ["yes", "no"], "fra": ["oui", "non"]}
//
func transformTranslations(items []map[utils.Language]string) map[utils.Language][]string {
// re-organize into a map of arrays
transformed := make(map[utils.Language][]string)

for i := range items {
for language, translation := range items[i] {
perLanguage, found := transformed[language]
if !found {
perLanguage = make([]string, len(items))
transformed[language] = perLanguage
}
perLanguage[i] = translation
}
}
return transformed
}

var testTypeMappings = map[string]string{
"between": "has_number_between",
"contains": "has_all_words",
Expand Down Expand Up @@ -412,6 +454,7 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *fl
case "reply", "send":
msg := make(map[utils.Language]string)
media := make(map[utils.Language]string)
var quickReplies map[utils.Language][]string

err := json.Unmarshal(a.Msg, &msg)
if err != nil {
Expand All @@ -424,12 +467,20 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *fl
return nil, err
}
}
if a.QuickReplies != nil {
legacyQuickReplies := make([]map[utils.Language]string, 0)

addTranslationMap(baseLanguage, translations, msg, flows.UUID(a.UUID), "text")
addTranslationMap(baseLanguage, translations, media, flows.UUID(a.UUID), "attachments")
err := json.Unmarshal(a.QuickReplies, &legacyQuickReplies)
if err != nil {
return nil, err
}

quickReplies = transformTranslations(legacyQuickReplies)
}

migratedText, _ := excellent.MigrateTemplate(msg[baseLanguage])
migratedMedia, _ := excellent.MigrateTemplate(media[baseLanguage])
migratedText := addTranslationMap(baseLanguage, translations, msg, flows.UUID(a.UUID), "text")
migratedMedia := addTranslationMap(baseLanguage, translations, media, flows.UUID(a.UUID), "attachments")
migratedQuickReplies := addTranslationMultiMap(baseLanguage, translations, quickReplies, flows.UUID(a.UUID), "quick_replies")

attachments := []string{}
if migratedMedia != "" {
Expand All @@ -438,10 +489,11 @@ func migrateAction(baseLanguage utils.Language, a legacyAction, translations *fl

if a.Type == "reply" {
return &actions.ReplyAction{
BaseAction: actions.NewBaseAction(a.UUID),
Text: migratedText,
Attachments: attachments,
AllURNs: a.SendAll,
BaseAction: actions.NewBaseAction(a.UUID),
Text: migratedText,
Attachments: attachments,
QuickReplies: migratedQuickReplies,
AllURNs: a.SendAll,
}, nil
}

Expand Down Expand Up @@ -650,7 +702,7 @@ func migrateRule(baseLanguage utils.Language, exitMap map[string]flows.Exit, r l
type categoryName struct {
uuid flows.ExitUUID
destination flows.NodeUUID
translations translationMap
translations map[utils.Language]string
order int
}

Expand Down
12 changes: 12 additions & 0 deletions flows/definition/legacy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/nyaruka/goflow/flows"
"github.com/nyaruka/goflow/flows/routers"
"github.com/nyaruka/goflow/utils"
"github.com/stretchr/testify/assert"
)

var legacyActionHolderDef = `
Expand Down Expand Up @@ -298,3 +299,14 @@ func wildcardEquals(actual string, expected string) bool {
}
return string(actualRunes) == string(substituted)
}

func TestTranslations(t *testing.T) {
translations := []map[utils.Language]string{
{"eng": "Yes", "fra": "Oui"},
{"eng": "No", "fra": "Non"},
}
assert.Equal(t, map[utils.Language][]string{
"eng": {"Yes", "No"},
"fra": {"Oui", "Non"},
}, transformTranslations(translations))
}
8 changes: 7 additions & 1 deletion flows/definition/testdata/migrations/actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,26 @@
"eng": "image/jpeg:http://s3.amazon.com/bucket/test_en.jpg?a=@contact.age",
"fra": "image/jpeg:http://s3.amazon.com/bucket/test_fr.jpg?a=@contact.age"
},
"quick_replies": [
{"eng": "Yes", "fra": "Oui"},
{"eng": "No", "fra": "Non"}
],
"send_all": true
},
"expected_action": {
"type": "reply",
"uuid": "5a4d00aa-807e-44af-9693-64b9fdedd352",
"text": "Do you still live in @contact.fields.city?",
"attachments": ["image/jpeg:http://s3.amazon.com/bucket/test_en.jpg?a=@contact.fields.age"],
"quick_replies": ["Yes", "No"],
"all_urns": true
},
"expected_localization": {
"fra": {
"5a4d00aa-807e-44af-9693-64b9fdedd352": {
"attachments": ["image/jpeg:http://s3.amazon.com/bucket/test_fr.jpg?a=@contact.fields.age"],
"text": ["Vous habitez toujours à @contact.fields.city"]
"text": ["Vous habitez toujours à @contact.fields.city"],
"quick_replies": ["Oui", "Non"]
}
}
}
Expand Down
38 changes: 21 additions & 17 deletions flows/events/send_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TypeSendMsg string = "send_msg"
// "created_on": "2006-01-02T15:04:05Z",
// "text": "hi, what's up",
// "attachments": [],
// "quick_replies": ["Doing Fine", "Got 99 problems"],
// "urns": ["tel:+12065551212"],
// "contacts": [{"uuid": "0e06f977-cbb7-475f-9d0b-a0c4aaec7f6a", "name": "Bob"}]
// }
Expand All @@ -24,33 +25,36 @@ const TypeSendMsg string = "send_msg"
// @event send_msg
type SendMsgEvent struct {
BaseEvent
Text string `json:"text"`
Attachments []string `json:"attachments,omitempty"`
URNs []urns.URN `json:"urns,omitempty" validate:"dive,urn"`
Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"`
Groups []*flows.GroupReference `json:"groups,omitempty" validate:"dive"`
Text string `json:"text"`
Attachments []string `json:"attachments,omitempty"`
QuickReplies []string `json:"quick_replies,omitempty"`
URNs []urns.URN `json:"urns,omitempty" validate:"dive,urn"`
Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"`
Groups []*flows.GroupReference `json:"groups,omitempty" validate:"dive"`
}

// NewSendMsgToContactEvent creates a new outgoing msg event to a single contact
func NewSendMsgToContactEvent(text string, attachments []string, contact *flows.ContactReference) *SendMsgEvent {
func NewSendMsgToContactEvent(text string, attachments []string, quickReples []string, contact *flows.ContactReference) *SendMsgEvent {
event := SendMsgEvent{
BaseEvent: NewBaseEvent(),
Text: text,
Attachments: attachments,
Contacts: []*flows.ContactReference{contact},
BaseEvent: NewBaseEvent(),
Text: text,
Attachments: attachments,
QuickReplies: quickReples,
Contacts: []*flows.ContactReference{contact},
}
return &event
}

// NewSendMsgEvent creates a new outgoing msg event for the given recipients
func NewSendMsgEvent(text string, attachments []string, urns []urns.URN, contacts []*flows.ContactReference, groups []*flows.GroupReference) *SendMsgEvent {
func NewSendMsgEvent(text string, attachments []string, quickReples []string, urns []urns.URN, contacts []*flows.ContactReference, groups []*flows.GroupReference) *SendMsgEvent {
event := SendMsgEvent{
BaseEvent: NewBaseEvent(),
Text: text,
Attachments: attachments,
URNs: urns,
Contacts: contacts,
Groups: groups,
BaseEvent: NewBaseEvent(),
Text: text,
Attachments: attachments,
QuickReplies: quickReples,
URNs: urns,
Contacts: contacts,
Groups: groups,
}
return &event
}
Expand Down

0 comments on commit 8d8fdcb

Please sign in to comment.