mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
[webhooks] set webhooks per-device
This commit is contained in:
parent
99791deebe
commit
632ef462ef
2
go.mod
2
go.mod
@ -6,7 +6,7 @@ toolchain go1.23.2
|
||||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.12.1
|
||||
github.com/android-sms-gateway/client-go v1.5.7
|
||||
github.com/android-sms-gateway/client-go v1.5.8
|
||||
github.com/ansrivas/fiberprometheus/v2 v2.6.1
|
||||
github.com/capcom6/go-helpers v0.2.0
|
||||
github.com/capcom6/go-infra-fx v0.2.1
|
||||
|
||||
4
go.sum
4
go.sum
@ -28,6 +28,10 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/android-sms-gateway/client-go v1.5.7 h1:1L9Ot3yc+5DtGaDOCUj4/8DEECWyfo4IoPyL+oXnzyE=
|
||||
github.com/android-sms-gateway/client-go v1.5.7/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/android-sms-gateway/client-go v1.5.8-0.20250516025314-5876d8deb355 h1:fctR5OH1c7g1zWEfp4K+fCZkY4+tZwTiKr/rN5N2yS8=
|
||||
github.com/android-sms-gateway/client-go v1.5.8-0.20250516025314-5876d8deb355/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/android-sms-gateway/client-go v1.5.8 h1:t9630c1Hv8u/MjwQ8epJ0iDpt3VXurSNFC91CFEjM/M=
|
||||
github.com/android-sms-gateway/client-go v1.5.8/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/ansrivas/fiberprometheus/v2 v2.6.1 h1:wac3pXaE6BYYTF04AC6K0ktk6vCD+MnDOJZ3SK66kXM=
|
||||
|
||||
@ -38,7 +38,7 @@ type MobileController struct {
|
||||
//
|
||||
// List webhooks
|
||||
func (h *MobileController) get(device models.Device, c *fiber.Ctx) error {
|
||||
items, err := h.webhooksSvc.Select(device.UserID)
|
||||
items, err := h.webhooksSvc.Select(device.UserID, webhooks.WithDeviceID(device.ID, false))
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't select webhooks: %w", err)
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `webhooks`
|
||||
ADD `device_id` char(21);
|
||||
-- +goose StatementEnd
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `webhooks`
|
||||
ADD CONSTRAINT `fk_webhooks_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON DELETE CASCADE;
|
||||
-- +goose StatementEnd
|
||||
-- +goose StatementBegin
|
||||
CREATE INDEX `idx_webhooks_device` ON `webhooks`(`device_id`);
|
||||
-- +goose StatementEnd
|
||||
---
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `webhooks` DROP FOREIGN KEY `fk_webhooks_device`;
|
||||
-- +goose StatementEnd
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `webhooks` DROP `device_id`;
|
||||
-- +goose StatementEnd
|
||||
@ -30,6 +30,22 @@ func (r *repository) Select(filter ...SelectFilter) ([]models.Device, error) {
|
||||
return devices, f.apply(r.db).Find(&devices).Error
|
||||
}
|
||||
|
||||
// Exists checks if there exists a device with the given filters.
|
||||
//
|
||||
// If the device does not exist, it returns false and nil error. If there is an
|
||||
// error during the query, it returns false and the error. Otherwise, it returns
|
||||
// true and nil error.
|
||||
func (r *repository) Exists(filters ...SelectFilter) (bool, error) {
|
||||
err := newFilter(filters...).apply(r.db).Take(&models.Device{}).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *repository) Get(filter ...SelectFilter) (models.Device, error) {
|
||||
devices, err := r.Select(filter...)
|
||||
if err != nil {
|
||||
|
||||
@ -52,6 +52,17 @@ func (s *Service) Select(userID string, filter ...SelectFilter) ([]models.Device
|
||||
return s.devices.Select(filter...)
|
||||
}
|
||||
|
||||
// Exists checks if there exists a device that matches the provided filters.
|
||||
//
|
||||
// If the device does not exist, it returns false and nil error. If there is an
|
||||
// error during the query, it returns false and the error. Otherwise, it returns
|
||||
// true and nil error.
|
||||
func (s *Service) Exists(userID string, filter ...SelectFilter) (bool, error) {
|
||||
filter = append(filter, WithUserID(userID))
|
||||
|
||||
return s.devices.Exists(filter...)
|
||||
}
|
||||
|
||||
// Get returns a single device based on the provided filters for a specific user.
|
||||
// It ensures that the filter includes the user's ID. If no device matches the
|
||||
// criteria, it returns ErrNotFound. If more than one device matches, it returns
|
||||
|
||||
@ -6,8 +6,9 @@ import (
|
||||
|
||||
func webhookToDTO(model *Webhook) smsgateway.Webhook {
|
||||
return smsgateway.Webhook{
|
||||
ID: model.ExtID,
|
||||
URL: model.URL,
|
||||
Event: model.Event,
|
||||
ID: model.ExtID,
|
||||
DeviceID: model.DeviceID,
|
||||
URL: model.URL,
|
||||
Event: model.Event,
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,13 @@ type Webhook struct {
|
||||
ExtID string `json:"id" gorm:"not null;type:varchar(36);uniqueIndex:unq_webhooks_user_extid,priority:2"`
|
||||
UserID string `json:"-" gorm:"<-:create;not null;type:varchar(32);uniqueIndex:unq_webhooks_user_extid,priority:1"`
|
||||
|
||||
DeviceID *string `json:"device_id,omitempty" gorm:"type:varchar(21);index:idx_webhooks_device"`
|
||||
|
||||
URL string `json:"url" validate:"required,http_url" gorm:"not null;type:varchar(256)"`
|
||||
Event smsgateway.WebhookEvent `json:"event" gorm:"not null;type:varchar(32)"`
|
||||
|
||||
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
|
||||
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
|
||||
Device *models.Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"`
|
||||
|
||||
models.TimedModel
|
||||
}
|
||||
|
||||
@ -17,8 +17,10 @@ func WithUserID(userID string) SelectFilter {
|
||||
}
|
||||
|
||||
type selectFilter struct {
|
||||
userID string
|
||||
extID *string
|
||||
userID string
|
||||
extID *string
|
||||
deviceID *string
|
||||
deviceIDExact bool
|
||||
}
|
||||
|
||||
func newFilter(filters ...SelectFilter) *selectFilter {
|
||||
@ -33,10 +35,27 @@ func (f *selectFilter) merge(filters ...SelectFilter) {
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeviceID creates a SelectFilter that filters by device ID.
|
||||
// If exact is true, only records with the exact device ID are matched.
|
||||
// If exact is false, records with the device ID or with a null device ID are matched.
|
||||
func WithDeviceID(deviceID string, exact bool) SelectFilter {
|
||||
return func(f *selectFilter) {
|
||||
f.deviceID = &deviceID
|
||||
f.deviceIDExact = exact
|
||||
}
|
||||
}
|
||||
|
||||
func (f *selectFilter) apply(query *gorm.DB) *gorm.DB {
|
||||
query = query.Where("user_id = ?", f.userID)
|
||||
if f.extID != nil {
|
||||
query = query.Where("ext_id = ?", *f.extID)
|
||||
}
|
||||
if f.deviceID != nil {
|
||||
if f.deviceIDExact {
|
||||
query = query.Where("device_id = ?", *f.deviceID)
|
||||
} else {
|
||||
query = query.Where("device_id = ? OR device_id IS NULL", *f.deviceID)
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
@ -75,18 +75,30 @@ func (s *Service) Replace(userID string, webhook smsgateway.Webhook) error {
|
||||
webhook.ID = s.idgen()
|
||||
}
|
||||
|
||||
// Check device ownership if deviceID is provided
|
||||
if webhook.DeviceID != nil {
|
||||
ok, err := s.devicesSvc.Exists(userID, devices.WithID(*webhook.DeviceID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select devices: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return newValidationError("device_id", *webhook.DeviceID, devices.ErrNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
model := Webhook{
|
||||
ExtID: webhook.ID,
|
||||
UserID: userID,
|
||||
URL: webhook.URL,
|
||||
Event: webhook.Event,
|
||||
ExtID: webhook.ID,
|
||||
UserID: userID,
|
||||
DeviceID: webhook.DeviceID,
|
||||
URL: webhook.URL,
|
||||
Event: webhook.Event,
|
||||
}
|
||||
|
||||
if err := s.webhooks.Replace(&model); err != nil {
|
||||
return fmt.Errorf("can't replace webhook: %w", err)
|
||||
}
|
||||
|
||||
go s.notifyDevices(userID)
|
||||
go s.notifyDevices(userID, webhook.DeviceID)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -99,30 +111,48 @@ func (s *Service) Delete(userID string, filters ...SelectFilter) error {
|
||||
return fmt.Errorf("can't delete webhooks: %w", err)
|
||||
}
|
||||
|
||||
go s.notifyDevices(userID)
|
||||
go s.notifyDevices(userID, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyDevices sends a push notification to all devices associated with the given user.
|
||||
func (s *Service) notifyDevices(userID string) {
|
||||
s.logger.Info("Notifying devices", zap.String("user_id", userID))
|
||||
func (s *Service) notifyDevices(userID string, deviceID *string) {
|
||||
logFields := []zap.Field{
|
||||
zap.String("user_id", userID),
|
||||
}
|
||||
if deviceID != nil {
|
||||
logFields = append(logFields, zap.String("device_id", *deviceID))
|
||||
}
|
||||
|
||||
devices, err := s.devicesSvc.Select(userID)
|
||||
s.logger.Info("Notifying devices", logFields...)
|
||||
|
||||
var filters []devices.SelectFilter
|
||||
if deviceID != nil {
|
||||
filters = []devices.SelectFilter{devices.WithID(*deviceID)}
|
||||
}
|
||||
|
||||
devices, err := s.devicesSvc.Select(userID, filters...)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to select devices", zap.String("user_id", userID), zap.Error(err))
|
||||
s.logger.Error("Failed to select devices", append(logFields, zap.Error(err))...)
|
||||
return
|
||||
}
|
||||
|
||||
if len(devices) == 0 {
|
||||
s.logger.Info("No devices found", logFields...)
|
||||
return
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
if device.PushToken == nil {
|
||||
s.logger.Info("Device has no push token", zap.String("user_id", userID), zap.String("device_id", device.ID))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.pushSvc.Enqueue(*device.PushToken, push.NewWebhooksUpdatedEvent()); err != nil {
|
||||
s.logger.Error("Failed to send push notification", zap.String("user_id", userID), zap.Error(err))
|
||||
s.logger.Error("Failed to send push notification", zap.String("user_id", userID), zap.String("device_id", device.ID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Notified devices", zap.String("user_id", userID), zap.Int("count", len(devices)))
|
||||
s.logger.Info("Notified devices", append(logFields, zap.Int("count", len(devices)))...)
|
||||
}
|
||||
|
||||
@ -90,6 +90,7 @@ Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "MYofX8bTd5Bov0wWFZLRP",
|
||||
"deviceId": "C0ZGtCNf7-sXTbCtF6JXm",
|
||||
"url": "https://webhook.site/280a6655-eb68-40b9-b857-af5be37c5303",
|
||||
"event": "sms:received"
|
||||
}
|
||||
|
||||
@ -1587,6 +1587,12 @@
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"deviceId": {
|
||||
"description": "The unique identifier of the device the webhook is associated with.",
|
||||
"type": "string",
|
||||
"maxLength": 21,
|
||||
"example": "PyDmBQZZXYmyxMwED8Fzy"
|
||||
},
|
||||
"event": {
|
||||
"description": "The type of event the webhook is triggered for.",
|
||||
"allOf": [
|
||||
|
||||
@ -472,6 +472,12 @@ definitions:
|
||||
type: object
|
||||
smsgateway.Webhook:
|
||||
properties:
|
||||
deviceId:
|
||||
description: The unique identifier of the device the webhook is associated
|
||||
with.
|
||||
example: PyDmBQZZXYmyxMwED8Fzy
|
||||
maxLength: 21
|
||||
type: string
|
||||
event:
|
||||
allOf:
|
||||
- $ref: '#/definitions/smsgateway.WebhookEvent'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user