Skip to content

Commit

Permalink
Merge pull request #1185 from nyaruka/optin_action
Browse files Browse the repository at this point in the history
Optin action and event
  • Loading branch information
rowanseymour authored Sep 14, 2023
2 parents 4991791 + 18732e2 commit 73b5ede
Show file tree
Hide file tree
Showing 17 changed files with 271 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: CI
on: [push, pull_request]
env:
go-version: "1.19.x"
go-version: "1.20.x"
jobs:
test:
name: Test
Expand Down
8 changes: 8 additions & 0 deletions assets/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ const (
ChannelRoleUSSD ChannelRole = "ussd"
)

// ChannelFeature is a feature that a channel supports
type ChannelFeature string

const (
ChannelFeatureOptIns ChannelFeature = "optins"
)

// Channel is something that can send/receive messages.
//
// {
Expand All @@ -40,6 +47,7 @@ type Channel interface {
Address() string
Schemes() []string
Roles() []ChannelRole
Features() []ChannelFeature
Country() i18n.Country
MatchPrefixes() []string
AllowInternational() bool
Expand Down
24 changes: 15 additions & 9 deletions assets/static/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,26 @@ import (

// Channel is a JSON serializable implementation of a channel asset
type Channel struct {
UUID_ assets.ChannelUUID `json:"uuid" validate:"required,uuid"`
Name_ string `json:"name"`
Address_ string `json:"address"`
Schemes_ []string `json:"schemes" validate:"min=1"`
Roles_ []assets.ChannelRole `json:"roles" validate:"min=1,dive,eq=send|eq=receive|eq=call|eq=answer|eq=ussd"`
Country_ i18n.Country `json:"country,omitempty"`
MatchPrefixes_ []string `json:"match_prefixes,omitempty"`
AllowInternational_ bool `json:"allow_international,omitempty"`
UUID_ assets.ChannelUUID `json:"uuid" validate:"required,uuid"`
Name_ string `json:"name"`
Address_ string `json:"address"`
Schemes_ []string `json:"schemes" validate:"min=1"`
Roles_ []assets.ChannelRole `json:"roles" validate:"min=1,dive,eq=send|eq=receive|eq=call|eq=answer|eq=ussd"`
Features_ []assets.ChannelFeature `json:"features,omitempty"`
Country_ i18n.Country `json:"country,omitempty"`
MatchPrefixes_ []string `json:"match_prefixes,omitempty"`
AllowInternational_ bool `json:"allow_international,omitempty"`
}

// NewChannel creates a new channel
func NewChannel(uuid assets.ChannelUUID, name string, address string, schemes []string, roles []assets.ChannelRole) assets.Channel {
func NewChannel(uuid assets.ChannelUUID, name string, address string, schemes []string, roles []assets.ChannelRole, features []assets.ChannelFeature) assets.Channel {
return &Channel{
UUID_: uuid,
Name_: name,
Address_: address,
Schemes_: schemes,
Roles_: roles,
Features_: features,
AllowInternational_: true,
}
}
Expand All @@ -38,6 +40,7 @@ func NewTelChannel(uuid assets.ChannelUUID, name string, address string, roles [
Address_: address,
Schemes_: []string{urns.TelScheme},
Roles_: roles,
Features_: []assets.ChannelFeature{},
Country_: country,
MatchPrefixes_: matchPrefixes,
AllowInternational_: allowInternational,
Expand All @@ -59,6 +62,9 @@ func (c *Channel) Schemes() []string { return c.Schemes_ }
// Roles returns the roles of this channel
func (c *Channel) Roles() []assets.ChannelRole { return c.Roles_ }

// Features returnsthe featurs this channel supports
func (c *Channel) Features() []assets.ChannelFeature { return c.Features_ }

// Country returns this channel's associated country code (if any)
func (c *Channel) Country() i18n.Country { return c.Country_ }

Expand Down
2 changes: 2 additions & 0 deletions assets/static/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ func TestChannel(t *testing.T) {
"+234151",
[]string{"tel"},
[]assets.ChannelRole{assets.ChannelRoleSend},
[]assets.ChannelFeature{assets.ChannelFeatureOptIns},
)
assert.Equal(t, assets.ChannelUUID("ffffffff-9b24-92e1-ffff-ffffb207cdb4"), channel.UUID())
assert.Equal(t, "Android", channel.Name())
assert.Equal(t, "+234151", channel.Address())
assert.Equal(t, []string{"tel"}, channel.Schemes())
assert.Equal(t, []assets.ChannelRole{assets.ChannelRoleSend}, channel.Roles())
assert.Equal(t, []assets.ChannelFeature{assets.ChannelFeatureOptIns}, channel.Features())
assert.Equal(t, i18n.NilCountry, channel.Country())
assert.Nil(t, channel.MatchPrefixes())
assert.True(t, channel.AllowInternational())
Expand Down
38 changes: 27 additions & 11 deletions flows/actions/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ import (
"github.com/stretchr/testify/require"
)

var contactJSON = `{
var defaultContactJSON = []byte(`{
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
"name": "Ryan Lewis",
"language": "eng",
"timezone": "America/Guayaquil",
"urns": [],
"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"}
Expand All @@ -53,7 +56,7 @@ var contactJSON = `{
}
},
"created_on": "2018-06-20T11:40:30.123456789-00:00"
}`
}`)

func TestActionTypes(t *testing.T) {
assetsJSON, err := os.ReadFile("testdata/_assets.json")
Expand Down Expand Up @@ -81,7 +84,7 @@ func testActionType(t *testing.T, assetsJSON json.RawMessage, typeName string) {
HTTPMocks *httpx.MockRequestor `json:"http_mocks,omitempty"`
SMTPError string `json:"smtp_error,omitempty"`
NoContact bool `json:"no_contact,omitempty"`
NoURNs bool `json:"no_urns,omitempty"`
Contact json.RawMessage `json:"contact,omitempty"`
HasTicket bool `json:"has_ticket,omitempty"`
NoInput bool `json:"no_input,omitempty"`
RedactURNs bool `json:"redact_urns,omitempty"`
Expand Down Expand Up @@ -164,15 +167,14 @@ func testActionType(t *testing.T, assetsJSON json.RawMessage, typeName string) {
// optionally load our contact
var contact *flows.Contact
if !tc.NoContact {
contact, err = flows.ReadContact(sa, json.RawMessage(contactJSON), assets.PanicOnMissing)
contactJSON := defaultContactJSON
if tc.Contact != nil {
contactJSON = tc.Contact
}

contact, err = flows.ReadContact(sa, contactJSON, assets.PanicOnMissing)
require.NoError(t, err)

// optionally give our contact some URNs and a ticket
if !tc.NoURNs {
channel := sa.Channels().Get("57f1078f-88aa-46f4-a59a-948a5739c03d")
contact.AddURN(urns.URN("tel:+12065551212?channel=57f1078f-88aa-46f4-a59a-948a5739c03d&id=123"), channel)
contact.AddURN(urns.URN("twitterid:54784326227#nyaruka"), nil)
}
if tc.HasTicket {
ticketer := sa.Ticketers().Get("d605bb96-258d-4097-ad0a-080937db2212")
topic := sa.Topics().Get("0d9a2c56-6fc2-4f27-93c5-a6322e26b740")
Expand Down Expand Up @@ -568,6 +570,20 @@ func TestConstructors(t *testing.T) {
"body": "So I was thinking..."
}`,
},
{
actions.NewSendOptIn(
actionUUID,
assets.NewOptInReference("248be71d-78e9-4d71-a6c4-9981d369e5cb", "Joke Of The Day"),
),
`{
"type": "send_optin",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"optin": {
"uuid": "248be71d-78e9-4d71-a6c4-9981d369e5cb",
"name": "Joke Of The Day"
}
}`,
},
{
actions.NewSendMsg(
actionUUID,
Expand Down
55 changes: 55 additions & 0 deletions flows/actions/send_optin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package actions

import (
"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/goflow/flows"
"github.com/nyaruka/goflow/flows/events"
)

func init() {
registerType(TypeSendOptIn, func() flows.Action { return &SendOptInAction{} })
}

// TypeSendOptIn is the type for the send optin action
const TypeSendOptIn string = "send_optin"

// SendOptInAction can be used to send an optin to the contact if the channel supports that.
//
// An [event:optin_sent] event will be created if the optin was sent.
//
// {
// "uuid": "8eebd020-1af5-431c-b943-aa670fc74da9",
// "type": "send_optin",
// "optin": {
// "uuid": "248be71d-78e9-4d71-a6c4-9981d369e5cb",
// "name": "Joke Of The Day"
// }
// }
//
// @action send_optin
type SendOptInAction struct {
baseAction
onlineAction

OptIn *assets.OptInReference `json:"optin" validate:"required,dive"`
}

// NewSendOptIn creates a new send optin action
func NewSendOptIn(uuid flows.ActionUUID, optIn *assets.OptInReference) *SendOptInAction {
return &SendOptInAction{
baseAction: newBaseAction(TypeSendOptIn, uuid),
OptIn: optIn,
}
}

// Execute creates the optin events
func (a *SendOptInAction) Execute(run flows.Run, step flows.Step, logModifier flows.ModifierCallback, logEvent flows.EventCallback) error {
optIn := run.Session().Assets().OptIns().Get(a.OptIn.UUID)
destinations := run.Contact().ResolveDestinations(false)

if len(destinations) > 0 && destinations[0].Channel.HasFeature(assets.ChannelFeatureOptIns) {
logEvent(events.NewOptInSent(optIn))
}

return nil
}
21 changes: 21 additions & 0 deletions flows/actions/testdata/_assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,21 @@
"receive"
]
},
{
"uuid": "4bb288a0-7fca-4da1-abe8-59a593aff648",
"name": "Facebook Channel",
"address": "235326346322111",
"schemes": [
"facebook"
],
"roles": [
"send",
"receive"
],
"features": [
"optins"
]
},
{
"uuid": "8e21f093-99aa-413b-b55b-758b54308fcb",
"name": "Twitter Channel",
Expand Down Expand Up @@ -205,6 +220,12 @@
"name": "Spam"
}
],
"optins": [
{
"uuid": "248be71d-78e9-4d71-a6c4-9981d369e5cb",
"name": "Joke Of The Day"
}
],
"resthooks": [
{
"slug": "new-registration",
Expand Down
11 changes: 10 additions & 1 deletion flows/actions/testdata/call_resthook.json
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,16 @@
}
]
},
"no_urns": true,
"contact": {
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
"name": "Ryan Lewis",
"language": "eng",
"timezone": "America/Guayaquil",
"urns": [],
"groups": [],
"fields": {},
"created_on": "2018-06-20T11:40:30.123456789-00:00"
},
"no_input": true,
"action": {
"type": "call_resthook",
Expand Down
11 changes: 10 additions & 1 deletion flows/actions/testdata/send_msg.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,16 @@
},
{
"description": "Msg created event even if contact has no sendable URNs",
"no_urns": true,
"contact": {
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
"name": "Ryan Lewis",
"language": "eng",
"timezone": "America/Guayaquil",
"urns": [],
"groups": [],
"fields": {},
"created_on": "2018-06-20T11:40:30.123456789-00:00"
},
"action": {
"type": "send_msg",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
Expand Down
48 changes: 48 additions & 0 deletions flows/actions/testdata/send_optin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
[
{
"description": "NOOP when channel doesn't support optins feature",
"action": {
"type": "send_optin",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"optin": {
"uuid": "248be71d-78e9-4d71-a6c4-9981d369e5cb",
"name": "Joke Of The Day"
}
},
"events": []
},
{
"description": "Event created when channel does support optins feature",
"contact": {
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
"name": "Ryan Lewis",
"language": "eng",
"timezone": "America/Guayaquil",
"urns": [
"facebook:1234567890"
],
"groups": [],
"fields": {},
"created_on": "2018-06-20T11:40:30.123456789-00:00"
},
"action": {
"type": "send_optin",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
"optin": {
"uuid": "248be71d-78e9-4d71-a6c4-9981d369e5cb",
"name": "Joke Of The Day"
}
},
"events": [
{
"type": "optin_sent",
"created_on": "2018-10-18T14:20:30.000123456Z",
"step_uuid": "59d74b86-3e2f-4a93-aece-b05d2fdcde0c",
"optin": {
"name": "Joke Of The Day",
"uuid": "248be71d-78e9-4d71-a6c4-9981d369e5cb"
}
}
]
}
]
11 changes: 10 additions & 1 deletion flows/actions/testdata/transfer_airtime.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@
},
{
"description": "Error and failed transfer if contact has no tel urn",
"no_urns": true,
"contact": {
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
"name": "Ryan Lewis",
"language": "eng",
"timezone": "America/Guayaquil",
"urns": [],
"groups": [],
"fields": {},
"created_on": "2018-06-20T11:40:30.123456789-00:00"
},
"action": {
"type": "transfer_airtime",
"uuid": "ad154980-7bf7-4ab8-8728-545fd6378912",
Expand Down
Loading

0 comments on commit 73b5ede

Please sign in to comment.