From f3b6705f825781eba522c89d62055ac297925c48 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 8 Feb 2023 15:52:49 -0500 Subject: [PATCH] Add support for resolving broadcast translations for different contacts --- cmd/flowrunner/main_test.go | 2 +- flows/actions/send_broadcast.go | 4 +-- flows/events/base_test.go | 2 +- flows/events/broadcast_created.go | 22 +++++----------- flows/msg.go | 44 +++++++++++++++++++++++++++++++ flows/msg_test.go | 38 +++++++++++++++++++++++++- flows/runs/environment.go | 12 ++------- 7 files changed, 94 insertions(+), 30 deletions(-) diff --git a/cmd/flowrunner/main_test.go b/cmd/flowrunner/main_test.go index 425323751..fe380a026 100644 --- a/cmd/flowrunner/main_test.go +++ b/cmd/flowrunner/main_test.go @@ -67,7 +67,7 @@ func TestPrintEvent(t *testing.T) { event flows.Event expected string }{ - {events.NewBroadcastCreated(map[envs.Language]*events.BroadcastTranslation{"eng": {Text: "hello"}}, "eng", nil, nil, "", nil), `🔉 broadcasted 'hello' to ...`}, + {events.NewBroadcastCreated(flows.BroadcastTranslations{"eng": {Text: "hello"}}, "eng", nil, nil, "", nil), `🔉 broadcasted 'hello' to ...`}, {events.NewContactFieldChanged(sa.Fields().Get("gender"), flows.NewValue(types.NewXText("M"), nil, nil, "", "", "")), `✏️ field 'gender' changed to 'M'`}, {events.NewContactFieldChanged(sa.Fields().Get("gender"), nil), `✏️ field 'gender' cleared`}, {events.NewContactGroupsChanged([]*flows.Group{sa.Groups().Get("b7cf0d83-f1c9-411c-96fd-c511a4cfa86d")}, nil), `👪 added to 'Testers'`}, diff --git a/flows/actions/send_broadcast.go b/flows/actions/send_broadcast.go index fdb129940..db6047a28 100644 --- a/flows/actions/send_broadcast.go +++ b/flows/actions/send_broadcast.go @@ -67,7 +67,7 @@ func (a *SendBroadcastAction) Execute(run flows.Run, step flows.Step, logModifie return nil } - translations := make(map[envs.Language]*events.BroadcastTranslation) + translations := make(flows.BroadcastTranslations) languages := append([]envs.Language{run.Flow().Language()}, run.Flow().Localization().Languages()...) // evaluate the broadcast in each language we have translations for @@ -75,7 +75,7 @@ func (a *SendBroadcastAction) Execute(run flows.Run, step flows.Step, logModifie languages := []envs.Language{language, run.Flow().Language()} evaluatedText, evaluatedAttachments, evaluatedQuickReplies, _ := a.evaluateMessage(run, languages, a.Text, a.Attachments, a.QuickReplies, logEvent) - translations[language] = &events.BroadcastTranslation{ + translations[language] = &flows.BroadcastTranslation{ Text: evaluatedText, Attachments: evaluatedAttachments, QuickReplies: evaluatedQuickReplies, diff --git a/flows/events/base_test.go b/flows/events/base_test.go index 5c4c9399b..f0dc5806d 100644 --- a/flows/events/base_test.go +++ b/flows/events/base_test.go @@ -102,7 +102,7 @@ func TestEventMarshaling(t *testing.T) { }, { events.NewBroadcastCreated( - map[envs.Language]*events.BroadcastTranslation{ + flows.BroadcastTranslations{ "eng": {Text: "Hello", Attachments: nil, QuickReplies: nil}, "spa": {Text: "Hola", Attachments: nil, QuickReplies: nil}, }, diff --git a/flows/events/broadcast_created.go b/flows/events/broadcast_created.go index 5b6b82090..ee6ef680b 100644 --- a/flows/events/broadcast_created.go +++ b/flows/events/broadcast_created.go @@ -5,7 +5,6 @@ import ( "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/goflow/utils" ) func init() { @@ -15,13 +14,6 @@ func init() { // TypeBroadcastCreated is a constant for outgoing message events const TypeBroadcastCreated string = "broadcast_created" -// BroadcastTranslation is the broadcast content in a particular language -type BroadcastTranslation struct { - Text string `json:"text"` - Attachments []utils.Attachment `json:"attachments,omitempty"` - QuickReplies []string `json:"quick_replies,omitempty"` -} - // BroadcastCreatedEvent events are created when an action wants to send a message to other contacts. // // { @@ -48,16 +40,16 @@ type BroadcastTranslation struct { type BroadcastCreatedEvent struct { BaseEvent - Translations map[envs.Language]*BroadcastTranslation `json:"translations" validate:"min=1,dive"` - BaseLanguage envs.Language `json:"base_language" validate:"required"` - Groups []*assets.GroupReference `json:"groups,omitempty" validate:"dive"` - Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"` - ContactQuery string `json:"contact_query,omitempty"` - URNs []urns.URN `json:"urns,omitempty" validate:"dive,urn"` + Translations flows.BroadcastTranslations `json:"translations" validate:"min=1,dive"` + BaseLanguage envs.Language `json:"base_language" validate:"required"` + Groups []*assets.GroupReference `json:"groups,omitempty" validate:"dive"` + Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"` + ContactQuery string `json:"contact_query,omitempty"` + URNs []urns.URN `json:"urns,omitempty" validate:"dive,urn"` } // NewBroadcastCreated creates a new outgoing msg event for the given recipients -func NewBroadcastCreated(translations map[envs.Language]*BroadcastTranslation, baseLanguage envs.Language, groups []*assets.GroupReference, contacts []*flows.ContactReference, contactQuery string, urns []urns.URN) *BroadcastCreatedEvent { +func NewBroadcastCreated(translations flows.BroadcastTranslations, baseLanguage envs.Language, groups []*assets.GroupReference, contacts []*flows.ContactReference, contactQuery string, urns []urns.URN) *BroadcastCreatedEvent { return &BroadcastCreatedEvent{ BaseEvent: NewBaseEvent(TypeBroadcastCreated), Translations: translations, diff --git a/flows/msg.go b/flows/msg.go index 41388b915..55aaa33ca 100644 --- a/flows/msg.go +++ b/flows/msg.go @@ -1,6 +1,9 @@ package flows import ( + "database/sql/driver" + "encoding/json" + "errors" "fmt" "github.com/go-playground/validator/v10" @@ -9,6 +12,7 @@ import ( "github.com/nyaruka/goflow/assets" "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/utils" + "golang.org/x/exp/slices" ) func init() { @@ -187,3 +191,43 @@ func NewMsgTemplating(template *assets.TemplateReference, variables []string, na Namespace_: namespace, } } + +// BroadcastTranslation is the broadcast content in a particular language +type BroadcastTranslation struct { + Text string `json:"text"` + Attachments []utils.Attachment `json:"attachments,omitempty"` + QuickReplies []string `json:"quick_replies,omitempty"` +} + +type BroadcastTranslations map[envs.Language]*BroadcastTranslation + +// ForContact is a utility to help callers select the translation for a contact +func (b BroadcastTranslations) ForContact(e envs.Environment, c *Contact, baseLanguage envs.Language) *BroadcastTranslation { + // first try the contact language if it is valid + if c.Language() != envs.NilLanguage && slices.Contains(e.AllowedLanguages(), c.Language()) { + t := b[c.Language()] + if t != nil { + return t + } + } + + // second try the default flow language + t := b[e.DefaultLanguage()] + if t != nil { + return t + } + + // finally return the base language + return b[baseLanguage] +} + +// Scan supports reading translation values from JSON in database +func (t *BroadcastTranslations) Scan(value any) error { + b, ok := value.([]byte) + if !ok { + return errors.New("failed type assertion to []byte") + } + return json.Unmarshal(b, &t) +} + +func (t BroadcastTranslations) Value() (driver.Value, error) { return json.Marshal(t) } diff --git a/flows/msg_test.go b/flows/msg_test.go index 77dff3433..0bd982709 100644 --- a/flows/msg_test.go +++ b/flows/msg_test.go @@ -7,10 +7,12 @@ import ( "github.com/nyaruka/gocommon/urns" "github.com/nyaruka/gocommon/uuids" "github.com/nyaruka/goflow/assets" + "github.com/nyaruka/goflow/assets/static" + "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/flows" + "github.com/nyaruka/goflow/flows/engine" "github.com/nyaruka/goflow/test" "github.com/nyaruka/goflow/utils" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -117,3 +119,37 @@ func TestIVRMsgOut(t *testing.T) { "locale": "eng-US" }`), marshaled, "JSON mismatch") } + +func TestBroadcastTranslations(t *testing.T) { + bcastTrans := flows.BroadcastTranslations{ + "eng": &flows.BroadcastTranslation{Text: "Hello"}, + "fra": &flows.BroadcastTranslation{Text: "Bonjour"}, + "spa": &flows.BroadcastTranslation{Text: "Hola"}, + } + baseLanguage := envs.Language("eng") + + assertTranslation := func(contactLanguage envs.Language, allowedLanguages []envs.Language, expected string) { + env := envs.NewBuilder().WithAllowedLanguages(allowedLanguages).Build() + sa, err := engine.NewSessionAssets(env, static.NewEmptySource(), nil) + require.NoError(t, err) + + contact := flows.NewEmptyContact(sa, "Bob", contactLanguage, nil) + + assert.Equal(t, expected, bcastTrans.ForContact(env, contact, baseLanguage).Text) + } + + assertTranslation("eng", []envs.Language{"eng"}, "Hello") // uses contact language + assertTranslation("fra", []envs.Language{"eng", "fra"}, "Bonjour") // uses contact language + assertTranslation("kin", []envs.Language{"eng", "spa"}, "Hello") // uses default flow language + assertTranslation("kin", []envs.Language{"spa", "eng"}, "Hola") // uses default flow language + assertTranslation("kin", []envs.Language{"kin"}, "Hello") // uses base language + + val, err := bcastTrans.Value() + assert.NoError(t, err) + assert.JSONEq(t, `{"eng": {"text": "Hello"}, "fra": {"text": "Bonjour"}, "spa": {"text": "Hola"}}`, string(val.([]byte))) + + var bt flows.BroadcastTranslations + err = bt.Scan([]byte(`{"spa": {"text": "Adios"}}`)) + assert.NoError(t, err) + assert.Equal(t, flows.BroadcastTranslations{"spa": {Text: "Adios"}}, bt) +} diff --git a/flows/runs/environment.go b/flows/runs/environment.go index f9b0b7765..53572e2ad 100644 --- a/flows/runs/environment.go +++ b/flows/runs/environment.go @@ -5,6 +5,7 @@ import ( "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/flows" + "golang.org/x/exp/slices" ) // an extended environment which takes some values from a contact if there is one and if the have those values. @@ -36,7 +37,7 @@ func (e *runEnvironment) DefaultLanguage() envs.Language { contact := e.run.Contact() // if we have a contact and they have a language and it's an allowed language that overrides the base environment's languuage - if contact != nil && contact.Language() != envs.NilLanguage && isAllowedLanguage(e, contact.Language()) { + if contact != nil && contact.Language() != envs.NilLanguage && slices.Contains(e.AllowedLanguages(), contact.Language()) { return contact.Language() } return e.Environment.DefaultLanguage() @@ -58,12 +59,3 @@ func (e *runEnvironment) DefaultCountry() envs.Country { func (e *runEnvironment) DefaultLocale() envs.Locale { return envs.NewLocale(e.DefaultLanguage(), e.DefaultCountry()) } - -func isAllowedLanguage(e envs.Environment, language envs.Language) bool { - for _, l := range e.AllowedLanguages() { - if language == l { - return true - } - } - return false -}