Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion flows/actions/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,6 @@ func TestConstructors(t *testing.T) {
actions.NewStartSession(
actionUUID,
assets.NewFlowReference(assets.FlowUUID("fece6eac-9127-4343-9269-56e88f391562"), "Parent"),

[]*assets.GroupReference{
assets.NewGroupReference(assets.GroupUUID("b7cf0d83-f1c9-411c-96fd-c511a4cfa86d"), "Testers"),
},
Expand Down Expand Up @@ -734,6 +733,28 @@ func TestConstructors(t *testing.T) {
"create_contact": true
}`,
},
{
actions.NewTriggerSession(
actionUUID,
assets.NewFlowReference(assets.FlowUUID("fece6eac-9127-4343-9269-56e88f391562"), "Parent"),
flows.NewContactReference(flows.ContactUUID("cbe87f5c-cda2-4f90-b5dd-0ac93a884950"), "Bob Smith"),
"",
true,
),
`{
"type": "trigger_session",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"flow": {
"uuid": "fece6eac-9127-4343-9269-56e88f391562",
"name": "Parent"
},
"contact": {
"uuid": "cbe87f5c-cda2-4f90-b5dd-0ac93a884950",
"name": "Bob Smith"
},
"interrupt": true
}`,
},
}

for _, tc := range tests {
Expand Down
5 changes: 1 addition & 4 deletions flows/actions/start_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import (
"github.com/nyaruka/goflow/flows/events"
)

// max number of times a session can trigger another session without there being input from the contact
const maxAncestorsSinceInput = 5

func init() {
registerType(TypeStartSession, func() flows.Action { return &StartSessionAction{} })
}
Expand Down Expand Up @@ -97,6 +94,6 @@ func (a *StartSessionAction) Execute(run flows.Run, step flows.Step, logModifier

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

logEvent(events.NewSessionTriggered(flow.Reference(false), groupRefs, contactRefs, contactQuery, a.Exclusions, a.CreateContact, urnList, runSnapshot, history))
logEvent(events.NewLegacySessionTriggered(flow.Reference(false), groupRefs, contactRefs, contactQuery, a.Exclusions, a.CreateContact, urnList, runSnapshot, history))
return nil
}
203 changes: 203 additions & 0 deletions flows/actions/testdata/trigger_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
[
{
"description": "Error event and NOOP if flow missing",
"action": {
"type": "trigger_session",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"flow": {
"uuid": "dede1e50-db55-4b50-8929-2116bfc56148",
"name": "Missing"
},
"contact": {
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
"name": "Stavros"
},
"interrupt": true
},
"events": [
{
"type": "error",
"created_on": "2018-10-18T14:20:30.000123456Z",
"step_uuid": "59d74b86-3e2f-4a93-aece-b05d2fdcde0c",
"text": "missing dependency: flow[uuid=dede1e50-db55-4b50-8929-2116bfc56148,name=Missing]"
}
],
"inspection": {
"dependencies": [
{
"uuid": "dede1e50-db55-4b50-8929-2116bfc56148",
"name": "Missing",
"type": "flow",
"missing": true
},
{
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
"name": "Stavros",
"type": "contact"
}
],
"issues": [
{
"type": "missing_dependency",
"node_uuid": "72a1f5df-49f9-45df-94c9-d86f7ea064e5",
"action_uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"description": "missing flow dependency 'dede1e50-db55-4b50-8929-2116bfc56148'",
"dependency": {
"uuid": "dede1e50-db55-4b50-8929-2116bfc56148",
"name": "Missing",
"type": "flow"
}
}
],
"results": [],
"waiting_exits": [],
"parent_refs": []
}
},
{
"description": "Session triggered event with concrete contact reference",
"action": {
"type": "trigger_session",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"flow": {
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
"name": "Collect Age"
},
"contact": {
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
"name": "Stavros"
},
"interrupt": true
},
"events": [
{
"type": "session_triggered",
"created_on": "2018-10-18T14:20:30.000123456Z",
"step_uuid": "59d74b86-3e2f-4a93-aece-b05d2fdcde0c",
"flow": {
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
"name": "Collect Age"
},
"contact": {
"uuid": "945493e3-933f-4668-9761-ce990fae5e5c",
"name": "Stavros"
},
"interrupt": true,
"exclusions": {},
"run_summary": {
"uuid": "e7187099-7d38-4f60-955c-325957214c42",
"flow": {
"uuid": "bead76f5-dac4-4c9d-996c-c62b326e8c0a",
"name": "Action Tester",
"revision": 123
},
"contact": {
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
"name": "Ryan Lewis",
"language": "eng",
"last_seen_on": "2018-10-18T14:20:30.000123456Z",
"status": "active",
"timezone": "America/Guayaquil",
"created_on": "2018-06-20T11:40:30.123456789Z",
"urns": [
"tel:+12065551212?channel=57f1078f-88aa-46f4-a59a-948a5739c03d&id=123",
"twitterid:54784326227#nyaruka"
],
"groups": [
{
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
"name": "Testers"
},
{
"uuid": "0ec97956-c451-48a0-a180-1ce766623e31",
"name": "Males"
}
],
"fields": {
"gender": {
"text": "Male"
}
}
},
"status": "active",
"results": {}
},
"history": {
"parent_uuid": "1ae96956-4b34-433e-8d1a-f05fe6923d6d",
"ancestors": 1,
"ancestors_since_input": 0
}
}
]
},
{
"description": "Session triggered event with URN",
"action": {
"type": "trigger_session",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"flow": {
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
"name": "Collect Age"
},
"urn": "@(\"tel:+593979123456\")",
"interrupt": true
},
"events": [
{
"type": "session_triggered",
"created_on": "2018-10-18T14:20:30.000123456Z",
"step_uuid": "59d74b86-3e2f-4a93-aece-b05d2fdcde0c",
"flow": {
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
"name": "Collect Age"
},
"urn": "tel:+593979123456",
"interrupt": true,
"exclusions": {},
"run_summary": {
"uuid": "e7187099-7d38-4f60-955c-325957214c42",
"flow": {
"uuid": "bead76f5-dac4-4c9d-996c-c62b326e8c0a",
"name": "Action Tester",
"revision": 123
},
"contact": {
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
"name": "Ryan Lewis",
"language": "eng",
"last_seen_on": "2018-10-18T14:20:30.000123456Z",
"status": "active",
"timezone": "America/Guayaquil",
"created_on": "2018-06-20T11:40:30.123456789Z",
"urns": [
"tel:+12065551212?channel=57f1078f-88aa-46f4-a59a-948a5739c03d&id=123",
"twitterid:54784326227#nyaruka"
],
"groups": [
{
"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d",
"name": "Testers"
},
{
"uuid": "0ec97956-c451-48a0-a180-1ce766623e31",
"name": "Males"
}
],
"fields": {
"gender": {
"text": "Male"
}
}
},
"status": "active",
"results": {}
},
"history": {
"parent_uuid": "1ae96956-4b34-433e-8d1a-f05fe6923d6d",
"ancestors": 1,
"ancestors_since_input": 0
}
}
]
}
]
125 changes: 125 additions & 0 deletions flows/actions/trigger_session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package actions

import (
"fmt"

"github.com/nyaruka/gocommon/jsonx"
"github.com/nyaruka/gocommon/urns"
"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/goflow/flows"
"github.com/nyaruka/goflow/flows/events"
"github.com/nyaruka/goflow/utils"
)

// max number of times a session can trigger another session without there being input from the contact
const maxAncestorsSinceInput = 5

func init() {
registerType(TypeTriggerSession, func() flows.Action { return &TriggerSessionAction{} })
}

// TypeTriggerSession is the type for the trigger session action
const TypeTriggerSession string = "trigger_session"

// TriggerSessionAction can be used to trigger sessions for another contact. A [event:session_triggered] event will be
// created and it's the responsibility of the caller to act on that by initiating a new session with the flow engine.
// The contact can be specified via a reference or as a URN. In the latter case the contact will be created if they
// don't exist.
//
// {
// "uuid": "8eebd020-1af5-431c-b943-aa670fc74da9",
// "type": "trigger_session",
// "flow": {"uuid": "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d", "name": "Registration"},
// "contact": {"uuid": "1e1ce1e1-9288-4504-869e-022d1003c72a", "name": "Bob"},
// "interrupt": true
// }
//
// @action start_session
type TriggerSessionAction struct {
baseAction
onlineAction

Flow *assets.FlowReference `json:"flow" validate:"required"`
Contact *flows.ContactReference `json:"contact,omitempty"`
URN string `json:"urn,omitempty" engine:"evaluated"`
Interrupt bool `json:"interrupt"`
}

// NewTriggerSession creates a new trigger session action
func NewTriggerSession(uuid flows.ActionUUID, flow *assets.FlowReference, contact *flows.ContactReference, urn string, interrupt bool) *TriggerSessionAction {
return &TriggerSessionAction{
baseAction: newBaseAction(TypeTriggerSession, uuid),
Flow: flow,
Contact: contact,
URN: urn,
Interrupt: interrupt,
}
}

// Validate validates our action is valid
func (a *TriggerSessionAction) Validate() error {
if (a.Contact != nil && a.URN != "") || (a.Contact == nil && a.URN == "") {
return fmt.Errorf("must specify either contact or urn")
}
return nil
}

// Execute runs our action
func (a *TriggerSessionAction) Execute(run flows.Run, step flows.Step, logModifier flows.ModifierCallback, logEvent flows.EventCallback) error {
urn := a.resolveURN(run, logEvent)

if urn == urns.NilURN && a.Contact == nil {
return nil
}

// check that flow exists - error event if not
flow, err := run.Session().Assets().Flows().Get(a.Flow.UUID)
if err != nil {
logEvent(events.NewDependencyError(a.Flow))
return nil
}

// loop footgun prevention
ref := run.Session().History()
if ref.AncestorsSinceInput >= maxAncestorsSinceInput {
logEvent(events.NewErrorf("too many sessions have been spawned since the last time input was received"))
return nil
}

runSnapshot, err := jsonx.Marshal(run.Snapshot())
if err != nil {
return err
}

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

logEvent(events.NewSessionTriggered(flow.Reference(false), a.Contact, urn, a.Interrupt, runSnapshot, history))
return nil
}

func (a *TriggerSessionAction) resolveURN(run flows.Run, logEvent flows.EventCallback) urns.URN {
if a.URN == "" {
return urns.NilURN
}

// otherwise this is a variable reference so evaluate it
evaluatedURN, ok := run.EvaluateTemplate(a.URN, logEvent)
if !ok {
return urns.NilURN
}

// if we have a valid URN now, return it
urn := urns.URN(evaluatedURN)
if urn.Validate() == nil {
return urn.Normalize()
}

// otherwise try to parse as phone number
parsedTel := utils.ParsePhoneNumber(evaluatedURN, run.Session().MergedEnvironment().DefaultCountry())
if parsedTel != "" {
urn, _ := urns.New(urns.Phone, parsedTel)
return urn.Normalize()
}

return urns.NilURN
}
5 changes: 4 additions & 1 deletion flows/definition/migrations/specdata/templates.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@
".groups[*].name_match",
".legacy_vars[*]"
],
"transfer_airtime": []
"transfer_airtime": [],
"trigger_session": [
".urn"
]
},
"routers": {
"random": [
Expand Down
Loading