Skip to content

Commit 6b43aa0

Browse files
committed
Add new triger_session action to replace start_session
1 parent f5d91b4 commit 6b43aa0

File tree

9 files changed

+282
-20
lines changed

9 files changed

+282
-20
lines changed

Diff for: flows/actions/start_session.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ import (
88
"github.com/nyaruka/goflow/flows/events"
99
)
1010

11-
// max number of times a session can trigger another session without there being input from the contact
12-
const maxAncestorsSinceInput = 5
13-
1411
func init() {
1512
registerType(TypeStartSession, func() flows.Action { return &StartSessionAction{} })
1613
}
@@ -97,6 +94,6 @@ func (a *StartSessionAction) Execute(run flows.Run, step flows.Step, logModifier
9794

9895
history := flows.NewChildHistory(run.Session())
9996

100-
logEvent(events.NewSessionTriggered(flow.Reference(false), groupRefs, contactRefs, contactQuery, a.Exclusions, a.CreateContact, urnList, runSnapshot, history))
97+
logEvent(events.NewLegacySessionTriggered(flow.Reference(false), groupRefs, contactRefs, contactQuery, a.Exclusions, a.CreateContact, urnList, runSnapshot, history))
10198
return nil
10299
}

Diff for: flows/actions/testdata/send_broadcast.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,7 @@
195195
"name": "Stavros"
196196
},
197197
{
198-
"uuid": "11708c34-d4ab-4b04-b82a-2578f6e0013c",
199-
"name": ""
198+
"uuid": "11708c34-d4ab-4b04-b82a-2578f6e0013c"
200199
}
201200
],
202201
"contact_query": "name = \"Bob\"",

Diff for: flows/actions/testdata/trigger_session.json

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
[
2+
{
3+
"description": "Error event and NOOP if flow missing",
4+
"action": {
5+
"type": "trigger_session",
6+
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
7+
"flow": {
8+
"uuid": "dede1e50-db55-4b50-8929-2116bfc56148",
9+
"name": "Missing"
10+
},
11+
"contact": {
12+
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
13+
"name": "Stavros"
14+
},
15+
"interrupt": true
16+
},
17+
"events": [
18+
{
19+
"type": "error",
20+
"created_on": "2018-10-18T14:20:30.000123456Z",
21+
"step_uuid": "59d74b86-3e2f-4a93-aece-b05d2fdcde0c",
22+
"text": "missing dependency: flow[uuid=dede1e50-db55-4b50-8929-2116bfc56148,name=Missing]"
23+
}
24+
],
25+
"inspection": {
26+
"dependencies": [
27+
{
28+
"uuid": "dede1e50-db55-4b50-8929-2116bfc56148",
29+
"name": "Missing",
30+
"type": "flow",
31+
"missing": true
32+
},
33+
{
34+
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
35+
"name": "Stavros",
36+
"type": "contact"
37+
}
38+
],
39+
"issues": [
40+
{
41+
"type": "missing_dependency",
42+
"node_uuid": "72a1f5df-49f9-45df-94c9-d86f7ea064e5",
43+
"action_uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
44+
"description": "missing flow dependency 'dede1e50-db55-4b50-8929-2116bfc56148'",
45+
"dependency": {
46+
"uuid": "dede1e50-db55-4b50-8929-2116bfc56148",
47+
"name": "Missing",
48+
"type": "flow"
49+
}
50+
}
51+
],
52+
"results": [],
53+
"waiting_exits": [],
54+
"parent_refs": []
55+
}
56+
},
57+
{
58+
"description": "Session triggered event with concrete contact reference",
59+
"action": {
60+
"type": "trigger_session",
61+
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
62+
"flow": {
63+
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
64+
"name": "Collect Age"
65+
},
66+
"contact": {
67+
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
68+
"name": "Stavros"
69+
},
70+
"interrupt": true
71+
},
72+
"events": [
73+
{
74+
"type": "session_triggered",
75+
"created_on": "2018-10-18T14:20:30.000123456Z",
76+
"step_uuid": "59d74b86-3e2f-4a93-aece-b05d2fdcde0c",
77+
"flow": {
78+
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
79+
"name": "Collect Age"
80+
},
81+
"contact": {
82+
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
83+
"name": "Stavros"
84+
},
85+
"interrupt": true,
86+
"exclusions": {},
87+
"run_summary": {
88+
"uuid": "e7187099-7d38-4f60-955c-325957214c42",
89+
"flow": {
90+
"uuid": "bead76f5-dac4-4c9d-996c-c62b326e8c0a",
91+
"name": "Action Tester",
92+
"revision": 123
93+
},
94+
"contact": {
95+
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
96+
"name": "Ryan Lewis",
97+
"language": "eng",
98+
"last_seen_on": "2018-10-18T14:20:30.000123456Z",
99+
"status": "active",
100+
"timezone": "America/Guayaquil",
101+
"created_on": "2018-06-20T11:40:30.123456789Z",
102+
"urns": [
103+
"tel:+12065551212?channel=57f1078f-88aa-46f4-a59a-948a5739c03d&id=123",
104+
"twitterid:54784326227#nyaruka"
105+
],
106+
"groups": [
107+
{
108+
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
109+
"name": "Testers"
110+
},
111+
{
112+
"uuid": "0ec97956-c451-48a0-a180-1ce766623e31",
113+
"name": "Males"
114+
}
115+
],
116+
"fields": {
117+
"gender": {
118+
"text": "Male"
119+
}
120+
}
121+
},
122+
"status": "active",
123+
"results": {}
124+
},
125+
"history": {
126+
"parent_uuid": "1ae96956-4b34-433e-8d1a-f05fe6923d6d",
127+
"ancestors": 1,
128+
"ancestors_since_input": 0
129+
}
130+
}
131+
]
132+
}
133+
]

Diff for: flows/actions/trigger_session.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package actions
2+
3+
import (
4+
"github.com/nyaruka/gocommon/jsonx"
5+
"github.com/nyaruka/gocommon/urns"
6+
"github.com/nyaruka/goflow/assets"
7+
"github.com/nyaruka/goflow/flows"
8+
"github.com/nyaruka/goflow/flows/events"
9+
"github.com/nyaruka/goflow/utils"
10+
)
11+
12+
// max number of times a session can trigger another session without there being input from the contact
13+
const maxAncestorsSinceInput = 5
14+
15+
func init() {
16+
registerType(TypeTriggerSession, func() flows.Action { return &TriggerSessionAction{} })
17+
}
18+
19+
// TypeTriggerSession is the type for the trigger session action
20+
const TypeTriggerSession string = "trigger_session"
21+
22+
// TriggerSessionAction can be used to trigger sessions for another contact. A [event:session_triggered] event will be
23+
// created and it's the responsibility of the caller to act on that by initiating a new session with the flow engine.
24+
// The contact can be specified via a concrete reference or as a URN via the scheme and path fields. In the latter case
25+
// the contact will be created if they don't exist.
26+
//
27+
// {
28+
// "uuid": "8eebd020-1af5-431c-b943-aa670fc74da9",
29+
// "type": "trigger_session",
30+
// "flow": {"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d", "name": "Registration"},
31+
// "contact": {"uuid": "1e1ce1e1-9288-4504-869e-022d1003c72a", "name": "Bob"},
32+
// "interrupt": true
33+
// }
34+
//
35+
// @action start_session
36+
type TriggerSessionAction struct {
37+
baseAction
38+
onlineAction
39+
40+
Flow *assets.FlowReference `json:"flow" validate:"required"`
41+
Contact *flows.ContactReference `json:"contact" validate:"required"`
42+
Interrupt bool `json:"interrupt"`
43+
}
44+
45+
// NewTriggerSession creates a new trigger session action
46+
func NewTriggerSession(uuid flows.ActionUUID, flow *assets.FlowReference, contact *flows.ContactReference, interrupt bool) *TriggerSessionAction {
47+
return &TriggerSessionAction{
48+
baseAction: newBaseAction(TypeTriggerSession, uuid),
49+
Flow: flow,
50+
Contact: contact,
51+
Interrupt: interrupt,
52+
}
53+
}
54+
55+
// Execute runs our action
56+
func (a *TriggerSessionAction) Execute(run flows.Run, step flows.Step, logModifier flows.ModifierCallback, logEvent flows.EventCallback) error {
57+
contact := a.resolveContact(run, logEvent)
58+
if contact == nil {
59+
logEvent(events.NewDependencyError(a.Contact))
60+
return nil
61+
}
62+
63+
// check that flow exists - error event if not
64+
flow, err := run.Session().Assets().Flows().Get(a.Flow.UUID)
65+
if err != nil {
66+
logEvent(events.NewDependencyError(a.Flow))
67+
return nil
68+
}
69+
70+
// loop footgun prevention
71+
ref := run.Session().History()
72+
if ref.AncestorsSinceInput >= maxAncestorsSinceInput {
73+
logEvent(events.NewErrorf("too many sessions have been spawned since the last time input was received"))
74+
return nil
75+
}
76+
77+
runSnapshot, err := jsonx.Marshal(run.Snapshot())
78+
if err != nil {
79+
return err
80+
}
81+
82+
history := flows.NewChildHistory(run.Session())
83+
84+
logEvent(events.NewSessionTriggered(flow.Reference(false), contact, a.Interrupt, runSnapshot, history))
85+
return nil
86+
}
87+
88+
func (a *TriggerSessionAction) resolveContact(run flows.Run, logEvent flows.EventCallback) *flows.ContactReference {
89+
// if this is a concrete reference, return as is
90+
if !a.Contact.Variable() {
91+
return a.Contact
92+
}
93+
94+
// otherwise this is a variable reference so evaluate it
95+
evaluatedURN, ok := run.EvaluateTemplate(a.Contact.URNMatch, logEvent)
96+
if !ok {
97+
return nil
98+
}
99+
100+
// if we have a valid URN now, return it
101+
urn := urns.URN(evaluatedURN)
102+
if urn.Validate() == nil {
103+
return &flows.ContactReference{URNMatch: string(urn.Normalize())}
104+
}
105+
106+
// otherwise try to parse as phone number
107+
parsedTel := utils.ParsePhoneNumber(evaluatedURN, run.Session().MergedEnvironment().DefaultCountry())
108+
if parsedTel != "" {
109+
urn, _ := urns.New(urns.Phone, parsedTel)
110+
return &flows.ContactReference{URNMatch: string(urn.Normalize())}
111+
}
112+
113+
return nil
114+
}

Diff for: flows/contact.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,9 @@ var _ contactql.Queryable = (*Contact)(nil)
544544

545545
// ContactReference is used to reference a contact
546546
type ContactReference struct {
547-
UUID ContactUUID `json:"uuid" validate:"required,uuid4"`
548-
Name string `json:"name"`
547+
UUID ContactUUID `json:"uuid,omitempty" validate:"omitempty,uuid4"`
548+
Name string `json:"name,omitempty"`
549+
URNMatch string `json:"urn_match,omitempty" engine:"evaluated"`
549550
}
550551

551552
// NewContactReference creates a new contact reference with the given UUID and name

Diff for: flows/definition/legacy/testdata/actions.json

+3-6
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,7 @@
474474
"name": "Horatio"
475475
},
476476
{
477-
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42",
478-
"name": ""
477+
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42"
479478
}
480479
],
481480
"groups": [
@@ -596,12 +595,10 @@
596595
"uuid": "5a4d00aa-807e-44af-9693-64b9fdedd352",
597596
"contacts": [
598597
{
599-
"uuid": "879ace1b-740b-45f1-9198-c2f2f08a825f",
600-
"name": ""
598+
"uuid": "879ace1b-740b-45f1-9198-c2f2f08a825f"
601599
},
602600
{
603-
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42",
604-
"name": ""
601+
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42"
605602
}
606603
],
607604
"groups": [

Diff for: flows/definition/migrations/specdata/templates.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"send_broadcast": [
3838
".attachments[*]",
3939
".contact_query",
40+
".contacts[*].urn_match",
4041
".groups[*].name_match",
4142
".legacy_vars[*]",
4243
".quick_replies[*]",
@@ -72,10 +73,14 @@
7273
],
7374
"start_session": [
7475
".contact_query",
76+
".contacts[*].urn_match",
7577
".groups[*].name_match",
7678
".legacy_vars[*]"
7779
],
78-
"transfer_airtime": []
80+
"transfer_airtime": [],
81+
"trigger_session": [
82+
".contact.urn_match"
83+
]
7984
},
8085
"routers": {
8186
"random": [

Diff for: flows/events/base_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ func TestEventMarshaling(t *testing.T) {
561561
}`,
562562
},
563563
{
564-
events.NewSessionTriggered(
564+
events.NewLegacySessionTriggered(
565565
assets.NewFlowReference(assets.FlowUUID("e4d441f0-24e3-4627-85fb-1e99e733baf0"), "Collect Age"),
566566
[]*assets.GroupReference{
567567
assets.NewGroupReference(assets.GroupUUID("5f9fd4f7-4b0f-462a-a598-18bfc7810412"), "Supervisors"),

Diff for: flows/events/session_triggered.go

+20-4
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,35 @@ type Exclusions struct {
5656
type SessionTriggeredEvent struct {
5757
BaseEvent
5858

59-
Flow *assets.FlowReference `json:"flow" validate:"required"`
59+
Flow *assets.FlowReference `json:"flow" validate:"required"`
60+
Contact *flows.ContactReference `json:"contact,omitempty"`
61+
Interrupt bool `json:"interrupt,omitempty"`
62+
RunSummary json.RawMessage `json:"run_summary"`
63+
History *flows.SessionHistory `json:"history"`
64+
65+
// deprecated (used by StartSessionAction)
6066
Groups []*assets.GroupReference `json:"groups,omitempty" validate:"dive"`
6167
Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"`
6268
ContactQuery string `json:"contact_query,omitempty"`
6369
Exclusions Exclusions `json:"exclusions"`
6470
CreateContact bool `json:"create_contact,omitempty"`
6571
URNs []urns.URN `json:"urns,omitempty" validate:"dive,urn"`
66-
RunSummary json.RawMessage `json:"run_summary"`
67-
History *flows.SessionHistory `json:"history"`
6872
}
6973

7074
// NewSessionTriggered returns a new session triggered event
71-
func NewSessionTriggered(flow *assets.FlowReference, groups []*assets.GroupReference, contacts []*flows.ContactReference, contactQuery string, exclusions Exclusions, createContact bool, urns []urns.URN, runSummary json.RawMessage, history *flows.SessionHistory) *SessionTriggeredEvent {
75+
func NewSessionTriggered(flow *assets.FlowReference, contact *flows.ContactReference, interrupt bool, runSummary json.RawMessage, history *flows.SessionHistory) *SessionTriggeredEvent {
76+
return &SessionTriggeredEvent{
77+
BaseEvent: NewBaseEvent(TypeSessionTriggered),
78+
Flow: flow,
79+
Contact: contact,
80+
Interrupt: interrupt,
81+
RunSummary: runSummary,
82+
History: history,
83+
}
84+
}
85+
86+
// NewLegacySessionTriggered returns a new session triggered event
87+
func NewLegacySessionTriggered(flow *assets.FlowReference, groups []*assets.GroupReference, contacts []*flows.ContactReference, contactQuery string, exclusions Exclusions, createContact bool, urns []urns.URN, runSummary json.RawMessage, history *flows.SessionHistory) *SessionTriggeredEvent {
7288
return &SessionTriggeredEvent{
7389
BaseEvent: NewBaseEvent(TypeSessionTriggered),
7490
Flow: flow,

0 commit comments

Comments
 (0)