Skip to content

Commit f6d4ea2

Browse files
authored
Merge pull request #1482 from nyaruka/seen_modifier
Add modifier for `last_seen_on`
2 parents 4c24884 + 72d492a commit f6d4ea2

File tree

5 files changed

+170
-0
lines changed

5 files changed

+170
-0
lines changed

flows/events/base_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ func TestEventMarshaling(t *testing.T) {
157157
},
158158
`contact_language_changed`,
159159
},
160+
{
161+
func() flows.Event {
162+
return events.NewContactLastSeenChanged(time.Date(2022, 2, 3, 13, 45, 30, 0, time.UTC))
163+
},
164+
`contact_last_seen_changed`,
165+
},
160166
{
161167
func() flows.Event {
162168
return events.NewContactNameChanged("Bryan")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/nyaruka/goflow/flows"
7+
)
8+
9+
func init() {
10+
registerType(TypeContactLastSeenChanged, func() flows.Event { return &ContactLastSeenChanged{} })
11+
}
12+
13+
// TypeContactLastSeenChanged is the type of our contact last seen changed event
14+
const TypeContactLastSeenChanged string = "contact_last_seen_changed"
15+
16+
// ContactLastSeenChanged events are created when the last seen on of the contact has been changed.
17+
//
18+
// {
19+
// "uuid": "0197b335-6ded-79a4-95a6-3af85b57f108",
20+
// "type": "contact_last_seen_changed",
21+
// "created_on": "2006-01-02T15:04:05Z",
22+
// "seen_on": "2026-02-03T15:04:05Z"
23+
// }
24+
//
25+
// @event contact_last_seen_changed
26+
type ContactLastSeenChanged struct {
27+
BaseEvent
28+
29+
LastSeenOn time.Time `json:"last_seen_on"`
30+
}
31+
32+
// NewContactLastSeenChanged returns a new contact last seen changed event
33+
func NewContactLastSeenChanged(seen time.Time) *ContactLastSeenChanged {
34+
return &ContactLastSeenChanged{
35+
BaseEvent: NewBaseEvent(TypeContactLastSeenChanged),
36+
LastSeenOn: seen,
37+
}
38+
}

flows/modifiers/seen.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package modifiers
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/nyaruka/gocommon/jsonx"
8+
"github.com/nyaruka/goflow/assets"
9+
"github.com/nyaruka/goflow/envs"
10+
"github.com/nyaruka/goflow/flows"
11+
"github.com/nyaruka/goflow/flows/events"
12+
"github.com/nyaruka/goflow/utils"
13+
)
14+
15+
func init() {
16+
registerType(TypeSeen, readSeen)
17+
}
18+
19+
// TypeSeen is the type of our seen modifier
20+
const TypeSeen string = "seen"
21+
22+
// Seen modifies the last seen of a contact
23+
type Seen struct {
24+
baseModifier
25+
26+
seenOn time.Time
27+
}
28+
29+
// NewSeen creates a new seen modifier
30+
func NewSeen(seenOn time.Time) *Seen {
31+
return &Seen{
32+
baseModifier: newBaseModifier(TypeSeen),
33+
seenOn: seenOn,
34+
}
35+
}
36+
37+
// Apply applies this modification to the given contact
38+
func (m *Seen) Apply(ctx context.Context, eng flows.Engine, env envs.Environment, sa flows.SessionAssets, contact *flows.Contact, log flows.EventLogger) (bool, error) {
39+
if contact.LastSeenOn() == nil || m.seenOn.After(*contact.LastSeenOn()) {
40+
contact.SetLastSeenOn(m.seenOn)
41+
log(events.NewContactLastSeenChanged(m.seenOn))
42+
return true, nil
43+
}
44+
45+
return false, nil
46+
}
47+
48+
var _ flows.Modifier = (*Seen)(nil)
49+
50+
//------------------------------------------------------------------------------------------
51+
// JSON Encoding / Decoding
52+
//------------------------------------------------------------------------------------------
53+
54+
type seenEnvelope struct {
55+
utils.TypedEnvelope
56+
57+
SeenOn time.Time `json:"seen_on"`
58+
}
59+
60+
func readSeen(sa flows.SessionAssets, data []byte, missing assets.MissingCallback) (flows.Modifier, error) {
61+
e := &seenEnvelope{}
62+
if err := utils.UnmarshalAndValidate(data, e); err != nil {
63+
return nil, err
64+
}
65+
66+
return NewSeen(e.SeenOn), nil
67+
}
68+
69+
func (m *Seen) MarshalJSON() ([]byte, error) {
70+
return jsonx.Marshal(&seenEnvelope{
71+
TypedEnvelope: utils.TypedEnvelope{Type: m.Type()},
72+
SeenOn: m.seenOn,
73+
})
74+
}

flows/modifiers/testdata/_assets.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@
9696
"uuid": "b4546d78-e202-4d59-9114-9bcaa2b6ee86",
9797
"name": "Open Tickets",
9898
"query": "tickets > 0"
99+
},
100+
{
101+
"uuid": "23d0f9a4-5bb8-4577-b518-d404caef8cf3",
102+
"name": "Seen In 2026",
103+
"query": "last_seen_on >= 2026-01-01"
99104
}
100105
],
101106
"topics": [

flows/modifiers/testdata/seen.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[
2+
{
3+
"description": "last seen changed event if changed",
4+
"contact_before": {
5+
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
6+
"name": "Bob",
7+
"status": "active",
8+
"created_on": "2018-06-20T11:40:30.123456789Z"
9+
},
10+
"modifier": {
11+
"type": "seen",
12+
"seen_on": "2026-02-03T11:40:30.123456789Z"
13+
},
14+
"contact_after": {
15+
"uuid": "5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f",
16+
"name": "Bob",
17+
"status": "active",
18+
"created_on": "2018-06-20T11:40:30.123456789Z",
19+
"last_seen_on": "2026-02-03T11:40:30.123456789Z",
20+
"groups": [
21+
{
22+
"uuid": "23d0f9a4-5bb8-4577-b518-d404caef8cf3",
23+
"name": "Seen In 2026"
24+
}
25+
]
26+
},
27+
"events": [
28+
{
29+
"uuid": "01969b47-0583-76f8-ae7f-f8b243c49ff5",
30+
"type": "contact_last_seen_changed",
31+
"created_on": "2025-05-04T12:30:46.123456789Z",
32+
"last_seen_on": "2026-02-03T11:40:30.123456789Z"
33+
},
34+
{
35+
"uuid": "01969b47-0d53-76f8-bd38-d266ec8d3716",
36+
"type": "contact_groups_changed",
37+
"created_on": "2025-05-04T12:30:48.123456789Z",
38+
"groups_added": [
39+
{
40+
"uuid": "23d0f9a4-5bb8-4577-b518-d404caef8cf3",
41+
"name": "Seen In 2026"
42+
}
43+
]
44+
}
45+
]
46+
}
47+
]

0 commit comments

Comments
 (0)