Skip to content

Commit 632ef46

Browse files
committed
[webhooks] set webhooks per-device
1 parent 99791de commit 632ef46

File tree

13 files changed

+137
-20
lines changed

13 files changed

+137
-20
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ toolchain go1.23.2
66

77
require (
88
firebase.google.com/go/v4 v4.12.1
9-
github.com/android-sms-gateway/client-go v1.5.7
9+
github.com/android-sms-gateway/client-go v1.5.8
1010
github.com/ansrivas/fiberprometheus/v2 v2.6.1
1111
github.com/capcom6/go-helpers v0.2.0
1212
github.com/capcom6/go-infra-fx v0.2.1

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
2828
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
2929
github.com/android-sms-gateway/client-go v1.5.7 h1:1L9Ot3yc+5DtGaDOCUj4/8DEECWyfo4IoPyL+oXnzyE=
3030
github.com/android-sms-gateway/client-go v1.5.7/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
31+
github.com/android-sms-gateway/client-go v1.5.8-0.20250516025314-5876d8deb355 h1:fctR5OH1c7g1zWEfp4K+fCZkY4+tZwTiKr/rN5N2yS8=
32+
github.com/android-sms-gateway/client-go v1.5.8-0.20250516025314-5876d8deb355/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
33+
github.com/android-sms-gateway/client-go v1.5.8 h1:t9630c1Hv8u/MjwQ8epJ0iDpt3VXurSNFC91CFEjM/M=
34+
github.com/android-sms-gateway/client-go v1.5.8/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
3135
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
3236
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
3337
github.com/ansrivas/fiberprometheus/v2 v2.6.1 h1:wac3pXaE6BYYTF04AC6K0ktk6vCD+MnDOJZ3SK66kXM=

internal/sms-gateway/handlers/webhooks/mobile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type MobileController struct {
3838
//
3939
// List webhooks
4040
func (h *MobileController) get(device models.Device, c *fiber.Ctx) error {
41-
items, err := h.webhooksSvc.Select(device.UserID)
41+
items, err := h.webhooksSvc.Select(device.UserID, webhooks.WithDeviceID(device.ID, false))
4242
if err != nil {
4343
return fmt.Errorf("can't select webhooks: %w", err)
4444
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- +goose Up
2+
-- +goose StatementBegin
3+
ALTER TABLE `webhooks`
4+
ADD `device_id` char(21);
5+
-- +goose StatementEnd
6+
-- +goose StatementBegin
7+
ALTER TABLE `webhooks`
8+
ADD CONSTRAINT `fk_webhooks_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON DELETE CASCADE;
9+
-- +goose StatementEnd
10+
-- +goose StatementBegin
11+
CREATE INDEX `idx_webhooks_device` ON `webhooks`(`device_id`);
12+
-- +goose StatementEnd
13+
---
14+
-- +goose Down
15+
-- +goose StatementBegin
16+
ALTER TABLE `webhooks` DROP FOREIGN KEY `fk_webhooks_device`;
17+
-- +goose StatementEnd
18+
-- +goose StatementBegin
19+
ALTER TABLE `webhooks` DROP `device_id`;
20+
-- +goose StatementEnd

internal/sms-gateway/modules/devices/repository.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ func (r *repository) Select(filter ...SelectFilter) ([]models.Device, error) {
3030
return devices, f.apply(r.db).Find(&devices).Error
3131
}
3232

33+
// Exists checks if there exists a device with the given filters.
34+
//
35+
// If the device does not exist, it returns false and nil error. If there is an
36+
// error during the query, it returns false and the error. Otherwise, it returns
37+
// true and nil error.
38+
func (r *repository) Exists(filters ...SelectFilter) (bool, error) {
39+
err := newFilter(filters...).apply(r.db).Take(&models.Device{}).Error
40+
if errors.Is(err, gorm.ErrRecordNotFound) {
41+
return false, nil
42+
}
43+
if err != nil {
44+
return false, err
45+
}
46+
return true, nil
47+
}
48+
3349
func (r *repository) Get(filter ...SelectFilter) (models.Device, error) {
3450
devices, err := r.Select(filter...)
3551
if err != nil {

internal/sms-gateway/modules/devices/service.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ func (s *Service) Select(userID string, filter ...SelectFilter) ([]models.Device
5252
return s.devices.Select(filter...)
5353
}
5454

55+
// Exists checks if there exists a device that matches the provided filters.
56+
//
57+
// If the device does not exist, it returns false and nil error. If there is an
58+
// error during the query, it returns false and the error. Otherwise, it returns
59+
// true and nil error.
60+
func (s *Service) Exists(userID string, filter ...SelectFilter) (bool, error) {
61+
filter = append(filter, WithUserID(userID))
62+
63+
return s.devices.Exists(filter...)
64+
}
65+
5566
// Get returns a single device based on the provided filters for a specific user.
5667
// It ensures that the filter includes the user's ID. If no device matches the
5768
// criteria, it returns ErrNotFound. If more than one device matches, it returns

internal/sms-gateway/modules/webhooks/converters.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66

77
func webhookToDTO(model *Webhook) smsgateway.Webhook {
88
return smsgateway.Webhook{
9-
ID: model.ExtID,
10-
URL: model.URL,
11-
Event: model.Event,
9+
ID: model.ExtID,
10+
DeviceID: model.DeviceID,
11+
URL: model.URL,
12+
Event: model.Event,
1213
}
1314
}

internal/sms-gateway/modules/webhooks/models.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ type Webhook struct {
1111
ExtID string `json:"id" gorm:"not null;type:varchar(36);uniqueIndex:unq_webhooks_user_extid,priority:2"`
1212
UserID string `json:"-" gorm:"<-:create;not null;type:varchar(32);uniqueIndex:unq_webhooks_user_extid,priority:1"`
1313

14+
DeviceID *string `json:"device_id,omitempty" gorm:"type:varchar(21);index:idx_webhooks_device"`
15+
1416
URL string `json:"url" validate:"required,http_url" gorm:"not null;type:varchar(256)"`
1517
Event smsgateway.WebhookEvent `json:"event" gorm:"not null;type:varchar(32)"`
1618

17-
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
19+
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
20+
Device *models.Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"`
1821

1922
models.TimedModel
2023
}

internal/sms-gateway/modules/webhooks/repository_filter.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ func WithUserID(userID string) SelectFilter {
1717
}
1818

1919
type selectFilter struct {
20-
userID string
21-
extID *string
20+
userID string
21+
extID *string
22+
deviceID *string
23+
deviceIDExact bool
2224
}
2325

2426
func newFilter(filters ...SelectFilter) *selectFilter {
@@ -33,10 +35,27 @@ func (f *selectFilter) merge(filters ...SelectFilter) {
3335
}
3436
}
3537

38+
// WithDeviceID creates a SelectFilter that filters by device ID.
39+
// If exact is true, only records with the exact device ID are matched.
40+
// If exact is false, records with the device ID or with a null device ID are matched.
41+
func WithDeviceID(deviceID string, exact bool) SelectFilter {
42+
return func(f *selectFilter) {
43+
f.deviceID = &deviceID
44+
f.deviceIDExact = exact
45+
}
46+
}
47+
3648
func (f *selectFilter) apply(query *gorm.DB) *gorm.DB {
3749
query = query.Where("user_id = ?", f.userID)
3850
if f.extID != nil {
3951
query = query.Where("ext_id = ?", *f.extID)
4052
}
53+
if f.deviceID != nil {
54+
if f.deviceIDExact {
55+
query = query.Where("device_id = ?", *f.deviceID)
56+
} else {
57+
query = query.Where("device_id = ? OR device_id IS NULL", *f.deviceID)
58+
}
59+
}
4160
return query
4261
}

internal/sms-gateway/modules/webhooks/service.go

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,30 @@ func (s *Service) Replace(userID string, webhook smsgateway.Webhook) error {
7575
webhook.ID = s.idgen()
7676
}
7777

78+
// Check device ownership if deviceID is provided
79+
if webhook.DeviceID != nil {
80+
ok, err := s.devicesSvc.Exists(userID, devices.WithID(*webhook.DeviceID))
81+
if err != nil {
82+
return fmt.Errorf("failed to select devices: %w", err)
83+
}
84+
if !ok {
85+
return newValidationError("device_id", *webhook.DeviceID, devices.ErrNotFound)
86+
}
87+
}
88+
7889
model := Webhook{
79-
ExtID: webhook.ID,
80-
UserID: userID,
81-
URL: webhook.URL,
82-
Event: webhook.Event,
90+
ExtID: webhook.ID,
91+
UserID: userID,
92+
DeviceID: webhook.DeviceID,
93+
URL: webhook.URL,
94+
Event: webhook.Event,
8395
}
8496

8597
if err := s.webhooks.Replace(&model); err != nil {
8698
return fmt.Errorf("can't replace webhook: %w", err)
8799
}
88100

89-
go s.notifyDevices(userID)
101+
go s.notifyDevices(userID, webhook.DeviceID)
90102

91103
return nil
92104
}
@@ -99,30 +111,48 @@ func (s *Service) Delete(userID string, filters ...SelectFilter) error {
99111
return fmt.Errorf("can't delete webhooks: %w", err)
100112
}
101113

102-
go s.notifyDevices(userID)
114+
go s.notifyDevices(userID, nil)
103115

104116
return nil
105117
}
106118

107119
// notifyDevices sends a push notification to all devices associated with the given user.
108-
func (s *Service) notifyDevices(userID string) {
109-
s.logger.Info("Notifying devices", zap.String("user_id", userID))
120+
func (s *Service) notifyDevices(userID string, deviceID *string) {
121+
logFields := []zap.Field{
122+
zap.String("user_id", userID),
123+
}
124+
if deviceID != nil {
125+
logFields = append(logFields, zap.String("device_id", *deviceID))
126+
}
110127

111-
devices, err := s.devicesSvc.Select(userID)
128+
s.logger.Info("Notifying devices", logFields...)
129+
130+
var filters []devices.SelectFilter
131+
if deviceID != nil {
132+
filters = []devices.SelectFilter{devices.WithID(*deviceID)}
133+
}
134+
135+
devices, err := s.devicesSvc.Select(userID, filters...)
112136
if err != nil {
113-
s.logger.Error("Failed to select devices", zap.String("user_id", userID), zap.Error(err))
137+
s.logger.Error("Failed to select devices", append(logFields, zap.Error(err))...)
138+
return
139+
}
140+
141+
if len(devices) == 0 {
142+
s.logger.Info("No devices found", logFields...)
114143
return
115144
}
116145

117146
for _, device := range devices {
118147
if device.PushToken == nil {
148+
s.logger.Info("Device has no push token", zap.String("user_id", userID), zap.String("device_id", device.ID))
119149
continue
120150
}
121151

122152
if err := s.pushSvc.Enqueue(*device.PushToken, push.NewWebhooksUpdatedEvent()); err != nil {
123-
s.logger.Error("Failed to send push notification", zap.String("user_id", userID), zap.Error(err))
153+
s.logger.Error("Failed to send push notification", zap.String("user_id", userID), zap.String("device_id", device.ID), zap.Error(err))
124154
}
125155
}
126156

127-
s.logger.Info("Notified devices", zap.String("user_id", userID), zap.Int("count", len(devices)))
157+
s.logger.Info("Notified devices", append(logFields, zap.Int("count", len(devices)))...)
128158
}

0 commit comments

Comments
 (0)