diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ca00135c..953cc2b9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+v8.0.0 (2023-01-09)
+-------------------------
+ * Update test database to latest schema
+
+v7.5.36 (2023-01-02)
+-------------------------
+ * Update to latest goflow which adds locale field to MsgOut
+ * Improve error reporting when courier call fails
+
v7.5.35 (2022-12-05)
-------------------------
* Retry messages which fail to queue to courier
diff --git a/core/models/msgs.go b/core/models/msgs.go
index 3ca93a580..6960d87f6 100644
--- a/core/models/msgs.go
+++ b/core/models/msgs.go
@@ -418,7 +418,23 @@ func buildMsgMetadata(m *flows.MsgOut) map[string]interface{} {
metadata["quick_replies"] = m.QuickReplies()
}
if m.Templating() != nil {
- metadata["templating"] = m.Templating()
+ mLanguage, mCountry := m.Locale().ToParts()
+
+ // TODO once we're queuing messages with locale and courier is reading that, can just add templating directly
+ // without language and country
+ metadata["templating"] = struct {
+ Template *assets.TemplateReference `json:"template"`
+ Language envs.Language `json:"language"`
+ Country envs.Country `json:"country"`
+ Variables []string `json:"variables,omitempty"`
+ Namespace string `json:"namespace"`
+ }{
+ Template: m.Templating_.Template(),
+ Language: mLanguage,
+ Country: mCountry,
+ Variables: m.Templating().Variables(),
+ Namespace: m.Templating().Namespace(),
+ }
}
if m.Topic() != flows.NilMsgTopic {
metadata["topic"] = string(m.Topic())
@@ -1017,12 +1033,14 @@ func (b *BroadcastBatch) CreateMessages(ctx context.Context, rt *runtime.Runtime
// not found? try org default language
if t == nil {
- t = trans[oa.Env().DefaultLanguage()]
+ lang = oa.Env().DefaultLanguage()
+ t = trans[lang]
}
// not found? use broadcast base language
if t == nil {
- t = trans[b.BaseLanguage]
+ lang = b.BaseLanguage
+ t = trans[lang]
}
if t == nil {
@@ -1066,7 +1084,7 @@ func (b *BroadcastBatch) CreateMessages(ctx context.Context, rt *runtime.Runtime
}
// create our outgoing message
- out := flows.NewMsgOut(urn, channel.ChannelReference(), text, t.Attachments, t.QuickReplies, nil, flows.NilMsgTopic, unsendableReason)
+ out := flows.NewMsgOut(urn, channel.ChannelReference(), text, t.Attachments, t.QuickReplies, nil, flows.NilMsgTopic, envs.NewLocale(lang, envs.NilCountry), unsendableReason)
msg, err := NewOutgoingBroadcastMsg(rt, oa.Org(), channel, contact, out, time.Now(), b.BroadcastID)
if err != nil {
return nil, errors.Wrapf(err, "error creating outgoing message")
diff --git a/core/models/msgs_test.go b/core/models/msgs_test.go
index 77db04a2e..967dd7805 100644
--- a/core/models/msgs_test.go
+++ b/core/models/msgs_test.go
@@ -22,7 +22,6 @@ import (
"github.com/nyaruka/mailroom/testsuite/testdata"
"github.com/nyaruka/null"
"github.com/nyaruka/redisx/assertredis"
-
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -176,7 +175,7 @@ func TestNewOutgoingFlowMsg(t *testing.T) {
session.SetIncomingMsg(tc.ResponseTo, null.NullString)
}
- flowMsg := flows.NewMsgOut(tc.URN, assets.NewChannelReference(tc.ChannelUUID, "Test Channel"), tc.Text, tc.Attachments, tc.QuickReplies, nil, tc.Topic, tc.Unsendable)
+ flowMsg := flows.NewMsgOut(tc.URN, assets.NewChannelReference(tc.ChannelUUID, "Test Channel"), tc.Text, tc.Attachments, tc.QuickReplies, nil, tc.Topic, envs.NilLocale, tc.Unsendable)
msg, err := models.NewOutgoingFlowMsg(rt, oa.Org(), channel, session, flow, flowMsg, now)
assert.NoError(t, err)
@@ -221,7 +220,7 @@ func TestNewOutgoingFlowMsg(t *testing.T) {
// check that msg loop detection triggers after 20 repeats of the same text
newOutgoing := func(text string) *models.Msg {
- flowMsg := flows.NewMsgOut(urns.URN(fmt.Sprintf("tel:+250700000001?id=%d", testdata.Cathy.URNID)), assets.NewChannelReference(testdata.TwilioChannel.UUID, "Twilio"), text, nil, nil, nil, flows.NilMsgTopic, flows.NilUnsendableReason)
+ flowMsg := flows.NewMsgOut(urns.URN(fmt.Sprintf("tel:+250700000001?id=%d", testdata.Cathy.URNID)), assets.NewChannelReference(testdata.TwilioChannel.UUID, "Twilio"), text, nil, nil, nil, flows.NilMsgTopic, envs.NilLocale, flows.NilUnsendableReason)
msg, err := models.NewOutgoingFlowMsg(rt, oa.Org(), channel, session, flow, flowMsg, now)
require.NoError(t, err)
return msg
@@ -266,6 +265,7 @@ func TestMarshalMsg(t *testing.T) {
[]string{"yes", "no"},
nil,
flows.MsgTopicPurchase,
+ envs.NilLocale,
flows.NilUnsendableReason,
)
@@ -324,6 +324,7 @@ func TestMarshalMsg(t *testing.T) {
"Hi there",
nil, nil, nil,
flows.NilMsgTopic,
+ envs.NilLocale,
flows.NilUnsendableReason,
)
in1 := testdata.InsertIncomingMsg(db, testdata.Org1, testdata.TwilioChannel, testdata.Cathy, "test", models.MsgStatusHandled)
@@ -366,7 +367,7 @@ func TestMarshalMsg(t *testing.T) {
// try a broadcast message which won't have session and flow fields set
bcastID := testdata.InsertBroadcast(db, testdata.Org1, `eng`, map[envs.Language]string{`eng`: "Blast"}, models.NilScheduleID, []*testdata.Contact{testdata.Cathy}, nil)
- bcastMsg1 := flows.NewMsgOut(urn, assets.NewChannelReference(testdata.TwilioChannel.UUID, "Test Channel"), "Blast", nil, nil, nil, flows.NilMsgTopic, flows.NilUnsendableReason)
+ bcastMsg1 := flows.NewMsgOut(urn, assets.NewChannelReference(testdata.TwilioChannel.UUID, "Test Channel"), "Blast", nil, nil, nil, flows.NilMsgTopic, envs.NilLocale, flows.NilUnsendableReason)
msg3, err := models.NewOutgoingBroadcastMsg(rt, oa.Org(), channel, cathy, bcastMsg1, time.Date(2021, 11, 9, 14, 3, 30, 0, time.UTC), bcastID)
require.NoError(t, err)
@@ -526,8 +527,8 @@ func TestGetMsgRepetitions(t *testing.T) {
oa := testdata.Org1.Load(rt)
_, cathy := testdata.Cathy.Load(db, oa)
- msg1 := flows.NewMsgOut(testdata.Cathy.URN, nil, "foo", nil, nil, nil, flows.NilMsgTopic, flows.NilUnsendableReason)
- msg2 := flows.NewMsgOut(testdata.Cathy.URN, nil, "bar", nil, nil, nil, flows.NilMsgTopic, flows.NilUnsendableReason)
+ msg1 := flows.NewMsgOut(testdata.Cathy.URN, nil, "foo", nil, nil, nil, flows.NilMsgTopic, envs.NilLocale, flows.NilUnsendableReason)
+ msg2 := flows.NewMsgOut(testdata.Cathy.URN, nil, "bar", nil, nil, nil, flows.NilMsgTopic, envs.NilLocale, flows.NilUnsendableReason)
assertRepetitions := func(m *flows.MsgOut, expected int) {
count, err := models.GetMsgRepetitions(rp, cathy, m)
@@ -694,7 +695,7 @@ func TestNewOutgoingIVR(t *testing.T) {
createdOn := time.Date(2021, 7, 26, 12, 6, 30, 0, time.UTC)
- flowMsg := flows.NewIVRMsgOut(testdata.Cathy.URN, vonage.ChannelReference(), "Hello", "eng", "http://example.com/hi.mp3")
+ flowMsg := flows.NewIVRMsgOut(testdata.Cathy.URN, vonage.ChannelReference(), "Hello", "http://example.com/hi.mp3", "eng")
dbMsg := models.NewOutgoingIVR(rt.Config, testdata.Org1.ID, conn, flowMsg, createdOn)
assert.Equal(t, flowMsg.UUID(), dbMsg.UUID())
diff --git a/core/models/templates.go b/core/models/templates.go
index 5259f72c2..6e51781b7 100644
--- a/core/models/templates.go
+++ b/core/models/templates.go
@@ -56,11 +56,12 @@ func (t *TemplateTranslation) UnmarshalJSON(data []byte) error { return json.Unm
func (t *TemplateTranslation) MarshalJSON() ([]byte, error) { return json.Marshal(t.t) }
func (t *TemplateTranslation) Channel() assets.ChannelReference { return t.t.Channel }
-func (t *TemplateTranslation) Language() envs.Language { return t.t.Language }
-func (t *TemplateTranslation) Country() envs.Country { return envs.Country(t.t.Country) }
-func (t *TemplateTranslation) Content() string { return t.t.Content }
-func (t *TemplateTranslation) Namespace() string { return t.t.Namespace }
-func (t *TemplateTranslation) VariableCount() int { return t.t.VariableCount }
+func (t *TemplateTranslation) Locale() envs.Locale {
+ return envs.NewLocale(t.t.Language, envs.Country(t.t.Country))
+}
+func (t *TemplateTranslation) Content() string { return t.t.Content }
+func (t *TemplateTranslation) Namespace() string { return t.t.Namespace }
+func (t *TemplateTranslation) VariableCount() int { return t.t.VariableCount }
// loads the templates for the passed in org
func loadTemplates(ctx context.Context, db sqlx.Queryer, orgID OrgID) ([]assets.Template, error) {
diff --git a/core/models/templates_test.go b/core/models/templates_test.go
index 5361e1bd4..5b494067f 100644
--- a/core/models/templates_test.go
+++ b/core/models/templates_test.go
@@ -27,16 +27,14 @@ func TestTemplates(t *testing.T) {
assert.Equal(t, 1, len(templates[0].Translations()))
tt := templates[0].Translations()[0]
- assert.Equal(t, envs.Language("fra"), tt.Language())
- assert.Equal(t, envs.NilCountry, tt.Country())
+ assert.Equal(t, envs.Locale("fra"), tt.Locale())
assert.Equal(t, "", tt.Namespace())
assert.Equal(t, testdata.TwitterChannel.UUID, tt.Channel().UUID)
assert.Equal(t, "Salut!", tt.Content())
assert.Equal(t, 1, len(templates[1].Translations()))
tt = templates[1].Translations()[0]
- assert.Equal(t, envs.Language("eng"), tt.Language())
- assert.Equal(t, envs.Country("US"), tt.Country())
+ assert.Equal(t, envs.Locale("eng-US"), tt.Locale())
assert.Equal(t, "2d40b45c_25cd_4965_9019_f05d0124c5fa", tt.Namespace())
assert.Equal(t, testdata.TwitterChannel.UUID, tt.Channel().UUID)
assert.Equal(t, "Hi {{1}}, are you still experiencing problems with {{2}}?", tt.Content())
diff --git a/core/msgio/courier.go b/core/msgio/courier.go
index 09eae1544..254b1a51b 100644
--- a/core/msgio/courier.go
+++ b/core/msgio/courier.go
@@ -180,8 +180,11 @@ func FetchAttachment(ctx context.Context, rt *runtime.Runtime, ch *models.Channe
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rt.Config.CourierAuthToken))
resp, err := httpx.DoTrace(courierHttpClient, req, nil, nil, -1)
- if err != nil || resp.Response.StatusCode != 200 {
- return "", "", errors.New("error calling courier endpoint")
+ if err != nil {
+ return "", "", errors.Wrap(err, "error calling courier endpoint")
+ }
+ if resp.Response.StatusCode != 200 {
+ return "", "", errors.Errorf("error calling courier endpoint, got non-200 status: %s", string(resp.ResponseTrace))
}
fa := &fetchAttachmentResponse{}
if err := json.Unmarshal(resp.ResponseBody, fa); err != nil {
diff --git a/go.mod b/go.mod
index 8006d198c..b6b6827a6 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
github.com/lib/pq v1.10.7
github.com/nyaruka/ezconf v0.2.1
github.com/nyaruka/gocommon v1.33.1
- github.com/nyaruka/goflow v0.175.0
+ github.com/nyaruka/goflow v0.178.1
github.com/nyaruka/logrus_sentry v0.8.2-0.20190129182604-c2962b80ba7d
github.com/nyaruka/null v1.2.0
github.com/nyaruka/redisx v0.2.2
diff --git a/go.sum b/go.sum
index 085a885a1..0adbaf7ba 100644
--- a/go.sum
+++ b/go.sum
@@ -221,8 +221,8 @@ github.com/nyaruka/ezconf v0.2.1 h1:TDXWoqjqYya1uhou1mAJZg7rgFYL98EB0Tb3+BWtUh0=
github.com/nyaruka/ezconf v0.2.1/go.mod h1:ey182kYkw2MIi4XiWe1FR/mzI33WCmTWuceDYYxgnQw=
github.com/nyaruka/gocommon v1.33.1 h1:RUy1O5Ly4tAaQDDpahds8z+4uewwsXg6SNCH0hYm7pE=
github.com/nyaruka/gocommon v1.33.1/go.mod h1:gusIA2aNC8EPB3ozlP4O0PaBiHUNq5+f1peRNvcn0DI=
-github.com/nyaruka/goflow v0.175.0 h1:ofrTm5qlf19oR1mjg8wFCmvNS9faFyDIQiFNs039kss=
-github.com/nyaruka/goflow v0.175.0/go.mod h1:C3Hj+jvJ2RY6w/ANx4zjcbVjYzd8gzOcryyPW2OEa8E=
+github.com/nyaruka/goflow v0.178.1 h1:ubVQXcrlFIebDnfJOvDRMaGc3CyGpngrtJLiVDgsHDc=
+github.com/nyaruka/goflow v0.178.1/go.mod h1:C3Hj+jvJ2RY6w/ANx4zjcbVjYzd8gzOcryyPW2OEa8E=
github.com/nyaruka/librato v1.0.0 h1:Vznj9WCeC1yZXbBYyYp40KnbmXLbEkjKmHesV/v2SR0=
github.com/nyaruka/librato v1.0.0/go.mod h1:pkRNLFhFurOz0QqBz6/DuTFhHHxAubWxs4Jx+J7yUgg=
github.com/nyaruka/logrus_sentry v0.8.2-0.20190129182604-c2962b80ba7d h1:hyp9u36KIwbTCo2JAJ+TuJcJBc+UZzEig7RI/S5Dvkc=
diff --git a/mailroom_test.dump b/mailroom_test.dump
index 06c861800..91d0cad12 100644
Binary files a/mailroom_test.dump and b/mailroom_test.dump differ
diff --git a/services/ivr/twiml/service.go b/services/ivr/twiml/service.go
index a1517c368..b2bf0e9de 100644
--- a/services/ivr/twiml/service.go
+++ b/services/ivr/twiml/service.go
@@ -16,7 +16,6 @@ import (
"github.com/nyaruka/gocommon/httpx"
"github.com/nyaruka/gocommon/urns"
- "github.com/nyaruka/goflow/envs"
"github.com/nyaruka/goflow/flows"
"github.com/nyaruka/goflow/flows/events"
"github.com/nyaruka/goflow/flows/routers/waits/hints"
@@ -461,11 +460,8 @@ func ResponseForSprint(cfg *runtime.Config, urn urns.URN, resumeURL string, es [
switch event := e.(type) {
case *events.IVRCreatedEvent:
if len(event.Msg.Attachments()) == 0 {
- urnCountry := envs.DeriveCountryFromTel(urn.Path())
- msgLocale := envs.NewLocale(event.Msg.TextLanguage, urnCountry)
-
// only send locale if it's a supported say language for Twilio
- msgLocaleCode := msgLocale.ToBCP47()
+ msgLocaleCode := event.Msg.Locale().ToBCP47()
if _, valid := supportedSayLanguages[msgLocaleCode]; !valid {
msgLocaleCode = ""
}
diff --git a/services/ivr/twiml/service_test.go b/services/ivr/twiml/service_test.go
index cd4c2355c..abbf272fb 100644
--- a/services/ivr/twiml/service_test.go
+++ b/services/ivr/twiml/service_test.go
@@ -48,21 +48,21 @@ func TestResponseForSprint(t *testing.T) {
{
// ivr msg, supported text language specified
events: []flows.Event{
- events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "Hi there", "eng", "")),
+ events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "Hi there", "", "eng-US")),
},
expected: `Hi there`,
},
{
// ivr msg, unsupported text language specified
events: []flows.Event{
- events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "Amakuru", "kin", "")),
+ events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "Amakuru", "", "kin")),
},
expected: `Amakuru`,
},
{
// ivr msg with audio attachment, text language ignored
events: []flows.Event{
- events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "Hi there", "eng", "/recordings/foo.wav")),
+ events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "Hi there", "/recordings/foo.wav", "eng-US")),
},
expected: `https://mailroom.io/recordings/foo.wav`,
},
diff --git a/services/ivr/vonage/service_test.go b/services/ivr/vonage/service_test.go
index fbbcddf77..3ddd11129 100644
--- a/services/ivr/vonage/service_test.go
+++ b/services/ivr/vonage/service_test.go
@@ -81,13 +81,13 @@ func TestResponseForSprint(t *testing.T) {
},
{
[]flows.Event{
- events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "hello world", "", "/recordings/foo.wav")),
+ events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "hello world", "/recordings/foo.wav", "")),
},
`[{"action":"stream","streamUrl":["/recordings/foo.wav"]}]`,
},
{
[]flows.Event{
- events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "hello world", "", "https://temba.io/recordings/foo.wav")),
+ events.NewIVRCreated(flows.NewIVRMsgOut(urn, channelRef, "hello world", "https://temba.io/recordings/foo.wav", "")),
},
`[{"action":"stream","streamUrl":["https://temba.io/recordings/foo.wav"]}]`,
},
diff --git a/testsuite/testdata/flows.go b/testsuite/testdata/flows.go
index 8c258c59b..55e35d148 100644
--- a/testsuite/testdata/flows.go
+++ b/testsuite/testdata/flows.go
@@ -34,8 +34,8 @@ func InsertFlow(db *sqlx.DB, org *Org, definition []byte) *Flow {
var id models.FlowID
must(db.Get(&id,
- `INSERT INTO flows_flow(org_id, uuid, name, flow_type, version_number, expires_after_minutes, ignore_triggers, has_issues, is_active, is_archived, is_system, created_by_id, created_on, modified_by_id, modified_on, saved_on, saved_by_id)
- VALUES($1, $2, $3, 'M', 1, 10, FALSE, FALSE, TRUE, FALSE, FALSE, $4, NOW(), $4, NOW(), NOW(), $4) RETURNING id`, org.ID, uuid, name, Admin.ID,
+ `INSERT INTO flows_flow(org_id, uuid, name, flow_type, version_number, base_language, expires_after_minutes, ignore_triggers, has_issues, is_active, is_archived, is_system, created_by_id, created_on, modified_by_id, modified_on, saved_on, saved_by_id)
+ VALUES($1, $2, $3, 'M', '13.1.0', 'eng', 10, FALSE, FALSE, TRUE, FALSE, FALSE, $4, NOW(), $4, NOW(), NOW(), $4) RETURNING id`, org.ID, uuid, name, Admin.ID,
))
db.MustExec(`INSERT INTO flows_flowrevision(flow_id, definition, spec_version, revision, is_active, created_by_id, created_on, modified_by_id, modified_on)
diff --git a/testsuite/testdata/msgs.go b/testsuite/testdata/msgs.go
index e6c84b204..44d7069c4 100644
--- a/testsuite/testdata/msgs.go
+++ b/testsuite/testdata/msgs.go
@@ -54,7 +54,7 @@ func insertOutgoingMsg(db *sqlx.DB, org *Org, channel *Channel, contact *Contact
channelID = channel.ID
}
- msg := flows.NewMsgOut(contact.URN, channelRef, text, attachments, nil, nil, flows.NilMsgTopic, flows.NilUnsendableReason)
+ msg := flows.NewMsgOut(contact.URN, channelRef, text, attachments, nil, nil, flows.NilMsgTopic, envs.NilLocale, flows.NilUnsendableReason)
var sentOn *time.Time
if status == models.MsgStatusWired || status == models.MsgStatusSent || status == models.MsgStatusDelivered {
diff --git a/testsuite/testsuite.go b/testsuite/testsuite.go
index bca5c0a22..f5192fa47 100644
--- a/testsuite/testsuite.go
+++ b/testsuite/testsuite.go
@@ -197,7 +197,7 @@ DELETE FROM msgs_msg;
DELETE FROM flows_flowrun;
DELETE FROM flows_flowpathcount;
DELETE FROM flows_flownodecount;
-DELETE FROM flows_flowruncount;
+DELETE FROM flows_flowrunstatuscount;
DELETE FROM flows_flowcategorycount;
DELETE FROM flows_flowsession;
DELETE FROM flows_flowrevision WHERE flow_id >= 30000;
diff --git a/web/ivr/ivr_test.go b/web/ivr/ivr_test.go
index a880c48df..2ee986197 100644
--- a/web/ivr/ivr_test.go
+++ b/web/ivr/ivr_test.go
@@ -199,7 +199,7 @@ func TestTwilioIVR(t *testing.T) {
`You said`,
`I hope hearing that makes you feel better. Good day and good bye.`,
`2065551212`,
+ `>+12065551212`,
},
expectedConnStatus: map[string]string{"Call1": "I", "Call2": "W", "Call3": "W"},
},
@@ -364,7 +364,7 @@ func mockVonageHandler(w http.ResponseWriter, r *http.Request) {
} else if form.To[0].Number == "16055743333" {
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{ "uuid": "Call2","status": "started","direction": "outbound","conversation_uuid": "Conversation2"}`))
- } else if form.To[0].Number == "2065551212" {
+ } else if form.To[0].Number == "12065551212" {
// start of a transfer leg
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{ "uuid": "Call3","status": "started","direction": "outbound","conversation_uuid": "Conversation3"}`))