Skip to content

Commit

Permalink
Add support for resolving broadcast translations for different contacts
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Feb 8, 2023
1 parent 665640e commit f3b6705
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 30 deletions.
2 changes: 1 addition & 1 deletion cmd/flowrunner/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'`},
Expand Down
4 changes: 2 additions & 2 deletions flows/actions/send_broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,15 @@ 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
for _, language := range languages {
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,
Expand Down
2 changes: 1 addition & 1 deletion flows/events/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
Expand Down
22 changes: 7 additions & 15 deletions flows/events/broadcast_created.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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.
//
// {
Expand All @@ -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,
Expand Down
44 changes: 44 additions & 0 deletions flows/msg.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package flows

import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"

"github.com/go-playground/validator/v10"
Expand All @@ -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() {
Expand Down Expand Up @@ -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) }
38 changes: 37 additions & 1 deletion flows/msg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
12 changes: 2 additions & 10 deletions flows/runs/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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
}

0 comments on commit f3b6705

Please sign in to comment.