Skip to content

Commit 06b3a71

Browse files
committed
Add events when heartbeats is offline or online
1 parent 8b4a025 commit 06b3a71

14 files changed

Lines changed: 221 additions & 34 deletions

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,24 @@ git clone https://github.com/NdoleStudio/httpsms.git
172172

173173
### 4. Setup the environment variables
174174

175-
- Copy the `.env.docker` file in the `web` directory into `.env` and update the environment variables with your firebase web SDK configuration.
175+
- Copy the `.env.docker` file in the `web` directory into `.env`
176176

177177
```bash
178178
cp web/.env.local.docker web/.env.local
179179
```
180180

181+
- Update the environment variables in the `.env` file in the `web` directory with your firebase web SDK configuration in step 1 above
182+
183+
```dotenv
184+
FIREBASE_API_KEY=
185+
FIREBASE_AUTH_DOMAIN=
186+
FIREBASE_PROJECT_ID=
187+
FIREBASE_STORAGE_BUCKET=
188+
FIREBASE_MESSAGING_SENDER_ID=
189+
FIREBASE_APP_ID=
190+
FIREBASE_MEASUREMENT_ID=
191+
```
192+
181193
- Copy the `.env.docker` file in the `api` directory into `.env`
182194

183195
```bash

api/pkg/entities/heartbeat_monitor.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,22 @@ import (
88

99
// HeartbeatMonitor is used to monitor heartbeats of a phone
1010
type HeartbeatMonitor struct {
11-
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
12-
PhoneID uuid.UUID `json:"phone_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
13-
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
14-
QueueID string `json:"queue_id" example:"0360259236613675274"`
15-
Owner string `json:"owner" example:"+18005550199"`
16-
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
17-
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
11+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
12+
PhoneID uuid.UUID `json:"phone_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
13+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
14+
QueueID string `json:"queue_id" example:"0360259236613675274"`
15+
Owner string `json:"owner" example:"+18005550199"`
16+
PhoneOnline bool `json:"phone_online" example:"true" default:"true"`
17+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
18+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
1819
}
1920

2021
// RequiresCheck returns true if the heartbeat monitor requires a check
2122
func (h *HeartbeatMonitor) RequiresCheck() bool {
2223
return h.UpdatedAt.Add(2 * time.Hour).Before(time.Now())
2324
}
25+
26+
// PhoneIsOffline returns true if the phone is offline
27+
func (h *HeartbeatMonitor) PhoneIsOffline() bool {
28+
return !h.PhoneOnline
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
"github.com/google/uuid"
8+
)
9+
10+
// EventTypePhoneHeartbeatOffline is emitted when the phone is missing a heartbeat
11+
const EventTypePhoneHeartbeatOffline = "phone.heartbeat.offline"
12+
13+
// PhoneHeartbeatOfflinePayload is the payload of the EventTypePhoneHeartbeatOffline event
14+
type PhoneHeartbeatOfflinePayload struct {
15+
PhoneID uuid.UUID `json:"phone_id"`
16+
UserID entities.UserID `json:"user_id"`
17+
LastHeartbeatTimestamp time.Time `json:"last_heartbeat_timestamp"`
18+
Timestamp time.Time `json:"timestamp"`
19+
MonitorID uuid.UUID `json:"monitor_id"`
20+
Owner string `json:"owner"`
21+
}

api/pkg/events/phone_heartbeat_dead_event.go renamed to api/pkg/events/phone_heartbeat_online_event.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import (
77
"github.com/google/uuid"
88
)
99

10-
// EventTypePhoneHeartbeatDead is emitted when the phone is missing a heartbeat
11-
const EventTypePhoneHeartbeatDead = "phone.heartbeat.dead"
10+
// EventTypePhoneHeartbeatOnline is emitted when the phone is missing a heartbeat
11+
const EventTypePhoneHeartbeatOnline = "phone.heartbeat.online"
1212

13-
// PhoneHeartbeatDeadPayload is the payload of the EventTypePhoneHeartbeatDead event
14-
type PhoneHeartbeatDeadPayload struct {
13+
// PhoneHeartbeatOnlinePayload is the payload of the EventTypePhoneHeartbeatOnline event
14+
type PhoneHeartbeatOnlinePayload struct {
1515
PhoneID uuid.UUID `json:"phone_id"`
1616
UserID entities.UserID `json:"user_id"`
1717
LastHeartbeatTimestamp time.Time `json:"last_heartbeat_timestamp"`

api/pkg/handlers/heartbeat_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func (h *HeartbeatHandler) Store(c *fiber.Ctx) error {
121121
return h.responseUnprocessableEntity(c, errors, "validation errors while storing heartbeat")
122122
}
123123

124-
heartbeat, err := h.service.Store(ctx, request.ToStoreParams(h.userFromContext(c), c.Get("X-Client-Version")))
124+
heartbeat, err := h.service.Store(ctx, request.ToStoreParams(h.userFromContext(c), c.OriginalURL(), c.Get("X-Client-Version")))
125125
if err != nil {
126126
msg := fmt.Sprintf("cannot store heartbeat with params [%+#v]", request)
127127
ctxLogger.Error(stacktrace.Propagate(err, msg))

api/pkg/listeners/heartbeat_listener.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ func NewHeartbeatListener(
3333
}
3434

3535
return l, map[string]events.EventListener{
36-
events.EventTypePhoneUpdated: l.onPhoneUpdated,
37-
events.EventTypePhoneDeleted: l.onPhoneDeleted,
38-
events.EventTypePhoneHeartbeatCheck: l.onPhoneHeartbeatCheck,
36+
events.EventTypePhoneUpdated: l.onPhoneUpdated,
37+
events.EventTypePhoneDeleted: l.onPhoneDeleted,
38+
events.EventTypePhoneHeartbeatCheck: l.onPhoneHeartbeatCheck,
39+
events.EventTypePhoneHeartbeatOffline: l.onPhoneHeartbeatOffline,
3940
}
4041
}
4142

@@ -110,3 +111,22 @@ func (listener *HeartbeatListener) onPhoneHeartbeatCheck(ctx context.Context, ev
110111

111112
return nil
112113
}
114+
115+
// onPhoneDeleted handles the events.EventTypePhoneDeleted event
116+
func (listener *HeartbeatListener) onPhoneHeartbeatOffline(ctx context.Context, event cloudevents.Event) error {
117+
ctx, span := listener.tracer.Start(ctx)
118+
defer span.End()
119+
120+
var payload events.PhoneHeartbeatOfflinePayload
121+
if err := event.DataAs(&payload); err != nil {
122+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
123+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
124+
}
125+
126+
if err := listener.service.UpdatePhoneOnline(ctx, payload.UserID, payload.MonitorID, false); err != nil {
127+
msg := fmt.Sprintf("cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID())
128+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
129+
}
130+
131+
return nil
132+
}

api/pkg/listeners/user_listener.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,20 @@ func NewUserListener(
3333
}
3434

3535
return l, map[string]events.EventListener{
36-
events.EventTypePhoneHeartbeatDead: l.onPhoneHeartbeatDead,
37-
events.UserSubscriptionCreated: l.OnUserSubscriptionCreated,
38-
events.UserSubscriptionCancelled: l.OnUserSubscriptionCancelled,
39-
events.UserSubscriptionUpdated: l.OnUserSubscriptionUpdated,
40-
events.UserSubscriptionExpired: l.OnUserSubscriptionExpired,
36+
events.EventTypePhoneHeartbeatOffline: l.onPhoneHeartbeatDead,
37+
events.UserSubscriptionCreated: l.OnUserSubscriptionCreated,
38+
events.UserSubscriptionCancelled: l.OnUserSubscriptionCancelled,
39+
events.UserSubscriptionUpdated: l.OnUserSubscriptionUpdated,
40+
events.UserSubscriptionExpired: l.OnUserSubscriptionExpired,
4141
}
4242
}
4343

44-
// onPhoneHeartbeatDead handles the events.EventTypePhoneHeartbeatDead event
44+
// onPhoneHeartbeatDead handles the events.EventTypePhoneHeartbeatOffline event
4545
func (listener *UserListener) onPhoneHeartbeatDead(ctx context.Context, event cloudevents.Event) error {
4646
ctx, span := listener.tracer.Start(ctx)
4747
defer span.End()
4848

49-
var payload events.PhoneHeartbeatDeadPayload
49+
var payload events.PhoneHeartbeatOfflinePayload
5050
if err := event.DataAs(&payload); err != nil {
5151
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
5252
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))

api/pkg/listeners/webhook_listener.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ func NewWebhookListener(
3636
events.EventTypeMessagePhoneDelivered: l.OnMessagePhoneDelivered,
3737
events.EventTypeMessageSendFailed: l.OnMessageSendFailed,
3838
events.EventTypeMessagePhoneSent: l.OnMessagePhoneSent,
39+
events.EventTypePhoneHeartbeatOnline: l.onPhoneHeartbeatOnline,
40+
events.EventTypePhoneHeartbeatOffline: l.onPhoneHeartbeatOffline,
3941
}
4042
}
4143

@@ -133,3 +135,41 @@ func (listener *WebhookListener) OnMessagePhoneDelivered(ctx context.Context, ev
133135

134136
return nil
135137
}
138+
139+
// OnMessagePhoneDelivered handles the events.EventTypeMessagePhoneDelivered event
140+
func (listener *WebhookListener) onPhoneHeartbeatOffline(ctx context.Context, event cloudevents.Event) error {
141+
ctx, span := listener.tracer.Start(ctx)
142+
defer span.End()
143+
144+
var payload events.PhoneHeartbeatOfflinePayload
145+
if err := event.DataAs(&payload); err != nil {
146+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
147+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
148+
}
149+
150+
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
151+
msg := fmt.Sprintf("cannot process [%s] event with ID [%s]", event.Type(), event.ID())
152+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
153+
}
154+
155+
return nil
156+
}
157+
158+
// OnMessagePhoneDelivered handles the events.EventTypeMessagePhoneDelivered event
159+
func (listener *WebhookListener) onPhoneHeartbeatOnline(ctx context.Context, event cloudevents.Event) error {
160+
ctx, span := listener.tracer.Start(ctx)
161+
defer span.End()
162+
163+
var payload events.PhoneHeartbeatOnlinePayload
164+
if err := event.DataAs(&payload); err != nil {
165+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
166+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
167+
}
168+
169+
if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil {
170+
msg := fmt.Sprintf("cannot process [%s] event with ID [%s]", event.Type(), event.ID())
171+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
172+
}
173+
174+
return nil
175+
}

api/pkg/repositories/gorm_heartbeat_monitor_repository.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,29 @@ type gormHeartbeatMonitorRepository struct {
2222
db *gorm.DB
2323
}
2424

25+
// UpdatePhoneOnline updates the online status of a phone
26+
func (repository *gormHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context.Context, userID entities.UserID, monitorID uuid.UUID, isOnline bool) error {
27+
ctx, span := repository.tracer.Start(ctx)
28+
defer span.End()
29+
30+
ctx, cancel := context.WithTimeout(ctx, dbOperationDuration)
31+
defer cancel()
32+
33+
err := repository.db.
34+
Model(&entities.HeartbeatMonitor{}).
35+
Where("id = ?", monitorID).
36+
Where("user_id = ?", userID).
37+
Updates(map[string]any{
38+
"phone_online": isOnline,
39+
"updated_at": time.Now().UTC(),
40+
}).Error
41+
if err != nil {
42+
msg := fmt.Sprintf("cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID)
43+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
44+
}
45+
return nil
46+
}
47+
2548
// UpdateQueueID updates the queueID of a monitor
2649
func (repository *gormHeartbeatMonitorRepository) UpdateQueueID(ctx context.Context, monitorID uuid.UUID, queueID string) error {
2750
ctx, span := repository.tracer.Start(ctx)

api/pkg/repositories/heartbeat_monitor_repository.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ type HeartbeatMonitorRepository interface {
2424

2525
// Delete an entities.HeartbeatMonitor
2626
Delete(ctx context.Context, userID entities.UserID, phoneNumber string) error
27+
28+
// UpdatePhoneOnline updates the phone online status of a monitor
29+
UpdatePhoneOnline(ctx context.Context, userID entities.UserID, monitorID uuid.UUID, online bool) error
2730
}

0 commit comments

Comments
 (0)