Skip to content

Commit

Permalink
Add new triger_session action to replace start_session
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Oct 1, 2024
1 parent f5d91b4 commit 6b43aa0
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 20 deletions.
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
}
3 changes: 1 addition & 2 deletions flows/actions/testdata/send_broadcast.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,7 @@
"name": "Stavros"
},
{
"uuid": "11708c34-d4ab-4b04-b82a-2578f6e0013c",
"name": ""
"uuid": "11708c34-d4ab-4b04-b82a-2578f6e0013c"
}
],
"contact_query": "name = \"Bob\"",
Expand Down
133 changes: 133 additions & 0 deletions flows/actions/testdata/trigger_session.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
[
{
"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
}
}
]
}
]
114 changes: 114 additions & 0 deletions flows/actions/trigger_session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package actions

import (
"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 concrete reference or as a URN via the scheme and path fields. 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" validate:"required"`
Interrupt bool `json:"interrupt"`
}

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

Check warning on line 51 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L46-L51

Added lines #L46 - L51 were not covered by tests
}
}

// Execute runs our action
func (a *TriggerSessionAction) Execute(run flows.Run, step flows.Step, logModifier flows.ModifierCallback, logEvent flows.EventCallback) error {
contact := a.resolveContact(run, logEvent)
if contact == nil {
logEvent(events.NewDependencyError(a.Contact))
return nil

Check warning on line 60 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L59-L60

Added lines #L59 - L60 were not covered by tests
}

// 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

Check warning on line 74 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L73-L74

Added lines #L73 - L74 were not covered by tests
}

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

Check warning on line 79 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L79

Added line #L79 was not covered by tests
}

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

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

func (a *TriggerSessionAction) resolveContact(run flows.Run, logEvent flows.EventCallback) *flows.ContactReference {
// if this is a concrete reference, return as is
if !a.Contact.Variable() {
return a.Contact
}

// otherwise this is a variable reference so evaluate it
evaluatedURN, ok := run.EvaluateTemplate(a.Contact.URNMatch, logEvent)
if !ok {
return nil

Check warning on line 97 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L95-L97

Added lines #L95 - L97 were not covered by tests
}

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

Check warning on line 103 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L101-L103

Added lines #L101 - L103 were not covered by tests
}

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

Check warning on line 110 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L107-L110

Added lines #L107 - L110 were not covered by tests
}

return nil

Check warning on line 113 in flows/actions/trigger_session.go

View check run for this annotation

Codecov / codecov/patch

flows/actions/trigger_session.go#L113

Added line #L113 was not covered by tests
}
5 changes: 3 additions & 2 deletions flows/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,9 @@ var _ contactql.Queryable = (*Contact)(nil)

// ContactReference is used to reference a contact
type ContactReference struct {
UUID ContactUUID `json:"uuid" validate:"required,uuid4"`
Name string `json:"name"`
UUID ContactUUID `json:"uuid,omitempty" validate:"omitempty,uuid4"`
Name string `json:"name,omitempty"`
URNMatch string `json:"urn_match,omitempty" engine:"evaluated"`
}

// NewContactReference creates a new contact reference with the given UUID and name
Expand Down
9 changes: 3 additions & 6 deletions flows/definition/legacy/testdata/actions.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,7 @@
"name": "Horatio"
},
{
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42",
"name": ""
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42"
}
],
"groups": [
Expand Down Expand Up @@ -596,12 +595,10 @@
"uuid": "5a4d00aa-807e-44af-9693-64b9fdedd352",
"contacts": [
{
"uuid": "879ace1b-740b-45f1-9198-c2f2f08a825f",
"name": ""
"uuid": "879ace1b-740b-45f1-9198-c2f2f08a825f"
},
{
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42",
"name": ""
"uuid": "cd0d8605-5abc-428c-b34b-c6f6e7a3ef42"
}
],
"groups": [
Expand Down
7 changes: 6 additions & 1 deletion flows/definition/migrations/specdata/templates.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"send_broadcast": [
".attachments[*]",
".contact_query",
".contacts[*].urn_match",
".groups[*].name_match",
".legacy_vars[*]",
".quick_replies[*]",
Expand Down Expand Up @@ -72,10 +73,14 @@
],
"start_session": [
".contact_query",
".contacts[*].urn_match",
".groups[*].name_match",
".legacy_vars[*]"
],
"transfer_airtime": []
"transfer_airtime": [],
"trigger_session": [
".contact.urn_match"
]
},
"routers": {
"random": [
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 @@ -561,7 +561,7 @@ func TestEventMarshaling(t *testing.T) {
}`,
},
{
events.NewSessionTriggered(
events.NewLegacySessionTriggered(
assets.NewFlowReference(assets.FlowUUID("e4d441f0-24e3-4627-85fb-1e99e733baf0"), "Collect Age"),
[]*assets.GroupReference{
assets.NewGroupReference(assets.GroupUUID("5f9fd4f7-4b0f-462a-a598-18bfc7810412"), "Supervisors"),
Expand Down
24 changes: 20 additions & 4 deletions flows/events/session_triggered.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,35 @@ type Exclusions struct {
type SessionTriggeredEvent struct {
BaseEvent

Flow *assets.FlowReference `json:"flow" validate:"required"`
Flow *assets.FlowReference `json:"flow" validate:"required"`
Contact *flows.ContactReference `json:"contact,omitempty"`
Interrupt bool `json:"interrupt,omitempty"`
RunSummary json.RawMessage `json:"run_summary"`
History *flows.SessionHistory `json:"history"`

// deprecated (used by StartSessionAction)
Groups []*assets.GroupReference `json:"groups,omitempty" validate:"dive"`
Contacts []*flows.ContactReference `json:"contacts,omitempty" validate:"dive"`
ContactQuery string `json:"contact_query,omitempty"`
Exclusions Exclusions `json:"exclusions"`
CreateContact bool `json:"create_contact,omitempty"`
URNs []urns.URN `json:"urns,omitempty" validate:"dive,urn"`
RunSummary json.RawMessage `json:"run_summary"`
History *flows.SessionHistory `json:"history"`
}

// NewSessionTriggered returns a new session triggered event
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 {
func NewSessionTriggered(flow *assets.FlowReference, contact *flows.ContactReference, interrupt bool, runSummary json.RawMessage, history *flows.SessionHistory) *SessionTriggeredEvent {
return &SessionTriggeredEvent{
BaseEvent: NewBaseEvent(TypeSessionTriggered),
Flow: flow,
Contact: contact,
Interrupt: interrupt,
RunSummary: runSummary,
History: history,

Check warning on line 82 in flows/events/session_triggered.go

View check run for this annotation

Codecov / codecov/patch

flows/events/session_triggered.go#L75-L82

Added lines #L75 - L82 were not covered by tests
}
}

// NewLegacySessionTriggered returns a new session triggered event
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 {
return &SessionTriggeredEvent{
BaseEvent: NewBaseEvent(TypeSessionTriggered),
Flow: flow,
Expand Down

0 comments on commit 6b43aa0

Please sign in to comment.