From d8f9864e522196e89e6dd0537654fc8417ab8842 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Thu, 22 May 2025 06:51:34 +0700 Subject: [PATCH] [settings] introduce settings module --- go.mod | 2 +- go.sum | 14 +- internal/sms-gateway/app.go | 2 + internal/sms-gateway/handlers/3rdparty.go | 6 + .../handlers/converters/devices_test.go | 8 +- internal/sms-gateway/handlers/mobile.go | 6 + internal/sms-gateway/handlers/module.go | 3 + .../sms-gateway/handlers/settings/3rdparty.go | 137 ++++++ .../sms-gateway/handlers/settings/mobile.go | 64 +++ .../20250521225803_add_device_settings.sql | 16 + internal/sms-gateway/models/models.go | 14 +- internal/sms-gateway/modules/push/service.go | 5 +- internal/sms-gateway/modules/push/types.go | 4 + .../sms-gateway/modules/settings/models.go | 24 ++ .../sms-gateway/modules/settings/module.go | 25 ++ .../modules/settings/repository.go | 67 +++ .../sms-gateway/modules/settings/service.go | 93 +++++ .../sms-gateway/modules/settings/utils.go | 112 +++++ .../sms-gateway/modules/webhooks/models.go | 2 +- pkg/swagger/docs/local.http | 53 +++ pkg/swagger/docs/mobile.http | 4 + pkg/swagger/docs/requests.http | 46 +++ pkg/swagger/docs/swagger.json | 390 +++++++++++++++++- pkg/swagger/docs/swagger.yaml | 274 ++++++++++++ 24 files changed, 1351 insertions(+), 20 deletions(-) create mode 100644 internal/sms-gateway/handlers/settings/3rdparty.go create mode 100644 internal/sms-gateway/handlers/settings/mobile.go create mode 100644 internal/sms-gateway/models/migrations/mysql/20250521225803_add_device_settings.sql create mode 100644 internal/sms-gateway/modules/settings/models.go create mode 100644 internal/sms-gateway/modules/settings/module.go create mode 100644 internal/sms-gateway/modules/settings/repository.go create mode 100644 internal/sms-gateway/modules/settings/service.go create mode 100644 internal/sms-gateway/modules/settings/utils.go diff --git a/go.mod b/go.mod index bc6990a..835ca7b 100644 --- a/go.mod +++ b/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.8 + github.com/android-sms-gateway/client-go v1.6.0 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 diff --git a/go.sum b/go.sum index 9e80d69..faa9ed3 100644 --- a/go.sum +++ b/go.sum @@ -26,12 +26,14 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= 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/android-sms-gateway/client-go v1.5.9-0.20250522134006-6e8b4dd3057a h1:TSmfm+KOsR1Ie10nZEjCVDepa1bEPin0NAgEUOSJiqw= +github.com/android-sms-gateway/client-go v1.5.9-0.20250522134006-6e8b4dd3057a/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.5.9-0.20250522231449-9e0855eff19f h1:VYrL6YbkQ49pcyiXTYcR5LN1WpNy1Tc684XjeE1UCvw= +github.com/android-sms-gateway/client-go v1.5.9-0.20250522231449-9e0855eff19f/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.5.9-0.20250524095300-2e41cae07049 h1:kdyVkqrgKDSI13JOKXVFz1al3IxfJPcbUaJvSXF6z+0= +github.com/android-sms-gateway/client-go v1.5.9-0.20250524095300-2e41cae07049/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.6.0 h1:3hN0XEUnNrweBl5Xx3IfE5zyq5ihm7fB0dhuTZBKlns= +github.com/android-sms-gateway/client-go v1.6.0/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= diff --git a/internal/sms-gateway/app.go b/internal/sms-gateway/app.go index 725c7d1..966156e 100644 --- a/internal/sms-gateway/app.go +++ b/internal/sms-gateway/app.go @@ -14,6 +14,7 @@ import ( "github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/metrics" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/push" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/settings" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/webhooks" "github.com/capcom6/go-infra-fx/cli" "github.com/capcom6/go-infra-fx/db" @@ -40,6 +41,7 @@ var Module = fx.Module( messages.Module, health.Module, webhooks.Module, + settings.Module, devices.Module, metrics.Module, cleaner.Module, diff --git a/internal/sms-gateway/handlers/3rdparty.go b/internal/sms-gateway/handlers/3rdparty.go index c81152c..cd84712 100644 --- a/internal/sms-gateway/handlers/3rdparty.go +++ b/internal/sms-gateway/handlers/3rdparty.go @@ -6,6 +6,7 @@ import ( "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/logs" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/messages" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth" + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/settings" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/webhooks" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth" "github.com/go-playground/validator/v10" @@ -21,6 +22,7 @@ type ThirdPartyHandlerParams struct { MessagesHandler *messages.ThirdPartyController WebhooksHandler *webhooks.ThirdPartyController DevicesHandler *devices.ThirdPartyController + SettingsHandler *settings.ThirdPartyController LogsHandler *logs.ThirdPartyController AuthSvc *auth.Service @@ -36,6 +38,7 @@ type thirdPartyHandler struct { messagesHandler *messages.ThirdPartyController webhooksHandler *webhooks.ThirdPartyController devicesHandler *devices.ThirdPartyController + settingsHandler *settings.ThirdPartyController logsHandler *logs.ThirdPartyController authSvc *auth.Service @@ -57,6 +60,8 @@ func (h *thirdPartyHandler) Register(router fiber.Router) { h.devicesHandler.Register(router.Group("/device")) // TODO: remove after 2025-07-11 h.devicesHandler.Register(router.Group("/devices")) + h.settingsHandler.Register(router.Group("/settings")) + h.webhooksHandler.Register(router.Group("/webhooks")) h.logsHandler.Register(router.Group("/logs")) @@ -69,6 +74,7 @@ func newThirdPartyHandler(params ThirdPartyHandlerParams) *thirdPartyHandler { messagesHandler: params.MessagesHandler, webhooksHandler: params.WebhooksHandler, devicesHandler: params.DevicesHandler, + settingsHandler: params.SettingsHandler, logsHandler: params.LogsHandler, authSvc: params.AuthSvc, } diff --git a/internal/sms-gateway/handlers/converters/devices_test.go b/internal/sms-gateway/handlers/converters/devices_test.go index 8983425..06b78c0 100644 --- a/internal/sms-gateway/handlers/converters/devices_test.go +++ b/internal/sms-gateway/handlers/converters/devices_test.go @@ -32,9 +32,11 @@ func TestDeviceToDTO(t *testing.T) { ID: "test-id", Name: anys.AsPointer("test-name"), LastSeen: lastSeenAt, - TimedModel: models.TimedModel{ - CreatedAt: createdAt, - UpdatedAt: updatedAt, + SoftDeletableModel: models.SoftDeletableModel{ + TimedModel: models.TimedModel{ + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, }, }, expected: smsgateway.Device{ diff --git a/internal/sms-gateway/handlers/mobile.go b/internal/sms-gateway/handlers/mobile.go index 927c51f..caac407 100644 --- a/internal/sms-gateway/handlers/mobile.go +++ b/internal/sms-gateway/handlers/mobile.go @@ -10,6 +10,7 @@ import ( "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/converters" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/deviceauth" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth" + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/settings" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/webhooks" "github.com/android-sms-gateway/server/internal/sms-gateway/models" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth" @@ -33,6 +34,7 @@ type mobileHandler struct { messagesSvc *messages.Service webhooksCtrl *webhooks.MobileController + settingsCtrl *settings.MobileController idGen func() string } @@ -303,6 +305,8 @@ func (h *mobileHandler) Register(router fiber.Router) { router.Patch("/user/password", deviceauth.WithDevice(h.changePassword)) h.webhooksCtrl.Register(router.Group("/webhooks")) + + h.settingsCtrl.Register(router.Group("/settings")) } type mobileHandlerParams struct { @@ -316,6 +320,7 @@ type mobileHandlerParams struct { MessagesSvc *messages.Service WebhooksCtrl *webhooks.MobileController + SettingsCtrl *settings.MobileController } func newMobileHandler(params mobileHandlerParams) *mobileHandler { @@ -327,6 +332,7 @@ func newMobileHandler(params mobileHandlerParams) *mobileHandler { devicesSvc: params.DevicesSvc, messagesSvc: params.MessagesSvc, webhooksCtrl: params.WebhooksCtrl, + settingsCtrl: params.SettingsCtrl, idGen: idGen, } } diff --git a/internal/sms-gateway/handlers/module.go b/internal/sms-gateway/handlers/module.go index b5fb41b..27df706 100644 --- a/internal/sms-gateway/handlers/module.go +++ b/internal/sms-gateway/handlers/module.go @@ -4,6 +4,7 @@ import ( "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/devices" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/logs" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/messages" + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/settings" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/webhooks" "github.com/capcom6/go-infra-fx/http" "go.uber.org/fx" @@ -27,6 +28,8 @@ var Module = fx.Module( webhooks.NewThirdPartyController, webhooks.NewMobileController, devices.NewThirdPartyController, + settings.NewThirdPartyController, + settings.NewMobileController, logs.NewThirdPartyController, fx.Private, ), diff --git a/internal/sms-gateway/handlers/settings/3rdparty.go b/internal/sms-gateway/handlers/settings/3rdparty.go new file mode 100644 index 0000000..c330cc0 --- /dev/null +++ b/internal/sms-gateway/handlers/settings/3rdparty.go @@ -0,0 +1,137 @@ +package settings + +import ( + "fmt" + + "github.com/android-sms-gateway/client-go/smsgateway" + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base" + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth" + "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/settings" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" + "go.uber.org/zap" +) + +type thirdPartyControllerParams struct { + fx.In + + DevicesSvc *devices.Service + SettingsSvc *settings.Service + + Validator *validator.Validate + Logger *zap.Logger +} + +type ThirdPartyController struct { + base.Handler + + devicesSvc *devices.Service + settingsSvc *settings.Service +} + +// @Summary Get settings +// @Description Returns settings for a specific user +// @Security ApiAuth +// @Tags User, Settings +// @Produce json +// @Success 200 {object} smsgateway.DeviceSettings "Settings" +// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Router /3rdparty/v1/settings [get] +// +// Get settings +func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error { + settings, err := h.settingsSvc.GetSettings(user.ID, true) + if err != nil { + return fmt.Errorf("can't get settings: %w", err) + } + + return c.JSON(settings) +} + +// @Summary Update settings +// @Description Updates settings for a specific user +// @Security ApiAuth +// @Tags User, Settings +// @Accept json +// @Produce json +// @Param request body smsgateway.DeviceSettings true "Settings" +// @Success 200 {object} object "Settings updated" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Router /3rdparty/v1/settings [put] +// +// Update settings +func (h *ThirdPartyController) put(user models.User, c *fiber.Ctx) error { + if err := h.BodyParserValidator(c, &smsgateway.DeviceSettings{}); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid settings format: %v", err)) + } + + settings := make(map[string]any, 8) + + if err := c.BodyParser(&settings); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to parse request body: %v", err)) + } + + updated, err := h.settingsSvc.ReplaceSettings(user.ID, settings) + + if err != nil { + return fmt.Errorf("can't update settings: %w", err) + } + + return c.JSON(updated) +} + +// @Summary Partially update settings +// @Description Partially updates settings for a specific user +// @Security ApiAuth +// @Tags User, Settings +// @Accept json +// @Produce json +// @Param request body smsgateway.DeviceSettings true "Settings" +// @Success 200 {object} object "Settings updated" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Router /3rdparty/v1/settings [patch] +// +// Partially update settings +func (h *ThirdPartyController) patch(user models.User, c *fiber.Ctx) error { + if err := h.BodyParserValidator(c, &smsgateway.DeviceSettings{}); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid settings format: %v", err)) + } + + settings := make(map[string]any, 8) + + if err := c.BodyParser(&settings); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to parse request body: %v", err)) + } + + updated, err := h.settingsSvc.UpdateSettings(user.ID, settings) + if err != nil { + return fmt.Errorf("can't update settings: %w", err) + } + + return c.JSON(updated) +} + +func (h *ThirdPartyController) Register(app fiber.Router) { + app.Get("", userauth.WithUser(h.get)) + app.Patch("", userauth.WithUser(h.patch)) + app.Put("", userauth.WithUser(h.put)) +} + +func NewThirdPartyController(params thirdPartyControllerParams) *ThirdPartyController { + return &ThirdPartyController{ + Handler: base.Handler{ + Logger: params.Logger.Named("settings"), + Validator: params.Validator, + }, + devicesSvc: params.DevicesSvc, + settingsSvc: params.SettingsSvc, + } +} diff --git a/internal/sms-gateway/handlers/settings/mobile.go b/internal/sms-gateway/handlers/settings/mobile.go new file mode 100644 index 0000000..11596f9 --- /dev/null +++ b/internal/sms-gateway/handlers/settings/mobile.go @@ -0,0 +1,64 @@ +package settings + +import ( + "fmt" + + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base" + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/deviceauth" + "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/settings" + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" + "go.uber.org/zap" +) + +type mobileControllerParams struct { + fx.In + + DevicesSvc *devices.Service + SettingsSvc *settings.Service + + Logger *zap.Logger +} + +type MobileController struct { + base.Handler + + devicesSvc *devices.Service + settingsSvc *settings.Service +} + +// @Summary Get settings +// @Description Returns settings for a device +// @Security MobileToken +// @Tags Device, Settings +// @Produce json +// @Success 200 {object} smsgateway.DeviceSettings "Settings" +// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Router /mobile/v1/settings [get] +// +// Get settings +func (h *MobileController) get(device models.Device, c *fiber.Ctx) error { + settings, err := h.settingsSvc.GetSettings(device.UserID, false) + if err != nil { + return fmt.Errorf("can't get settings for device %s (user ID: %s): %w", device.ID, device.UserID, err) + } + + return c.JSON(settings) +} + +func (h *MobileController) Register(router fiber.Router) { + router.Get("", deviceauth.WithDevice(h.get)) +} + +func NewMobileController(params mobileControllerParams) *MobileController { + return &MobileController{ + Handler: base.Handler{ + Logger: params.Logger.Named("settings"), + }, + devicesSvc: params.DevicesSvc, + settingsSvc: params.SettingsSvc, + } +} diff --git a/internal/sms-gateway/models/migrations/mysql/20250521225803_add_device_settings.sql b/internal/sms-gateway/models/migrations/mysql/20250521225803_add_device_settings.sql new file mode 100644 index 0000000..b9cc366 --- /dev/null +++ b/internal/sms-gateway/models/migrations/mysql/20250521225803_add_device_settings.sql @@ -0,0 +1,16 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE `device_settings` ( + `user_id` varchar(32) NOT NULL, + `settings` json NOT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`user_id`), + CONSTRAINT `fk_device_settings_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +); +-- +goose StatementEnd +--- +-- +goose Down +-- +goose StatementBegin +DROP TABLE `device_settings`; +-- +goose StatementEnd \ No newline at end of file diff --git a/internal/sms-gateway/models/models.go b/internal/sms-gateway/models/models.go index f9ea409..8213864 100644 --- a/internal/sms-gateway/models/models.go +++ b/internal/sms-gateway/models/models.go @@ -15,8 +15,12 @@ const ( ) type TimedModel struct { - CreatedAt time.Time `gorm:"->;not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"` - UpdatedAt time.Time `gorm:"->;not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"` + CreatedAt time.Time `gorm:"->;not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"` + UpdatedAt time.Time `gorm:"->;not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"` +} + +type SoftDeletableModel struct { + TimedModel DeletedAt *time.Time `gorm:"<-:update"` } @@ -25,7 +29,7 @@ type User struct { PasswordHash string `gorm:"not null;type:varchar(72)"` Devices []Device `gorm:"-,foreignKey:UserID;constraint:OnDelete:CASCADE"` - TimedModel + SoftDeletableModel } type Device struct { @@ -38,7 +42,7 @@ type Device struct { UserID string `gorm:"not null;type:varchar(32)"` - TimedModel + SoftDeletableModel } func (d *Device) IsEmpty() bool { @@ -67,7 +71,7 @@ type Message struct { Recipients []MessageRecipient `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"` States []MessageState `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"` - TimedModel + SoftDeletableModel } type MessageRecipient struct { diff --git a/internal/sms-gateway/modules/push/service.go b/internal/sms-gateway/modules/push/service.go index c6c1a8b..07ff5aa 100644 --- a/internal/sms-gateway/modules/push/service.go +++ b/internal/sms-gateway/modules/push/service.go @@ -168,6 +168,7 @@ func (s *Service) Notify(userID string, deviceID *string, event *domain.Event) e } errs := make([]error, 0, len(devices)) + notifiedCount := 0 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)) @@ -177,10 +178,12 @@ func (s *Service) Notify(userID string, deviceID *string, event *domain.Event) e if err := s.Enqueue(*device.PushToken, event); err != nil { s.logger.Error("Failed to send push notification", zap.String("user_id", userID), zap.String("device_id", device.ID), zap.Error(err)) errs = append(errs, err) + } else { + notifiedCount++ } } - s.logger.Info("Notified devices", append(logFields, zap.Int("count", len(devices)))...) + s.logger.Info("Notified devices", append(logFields, zap.Int("count", notifiedCount), zap.Int("total", len(devices)))...) return errors.Join(errs...) } diff --git a/internal/sms-gateway/modules/push/types.go b/internal/sms-gateway/modules/push/types.go index 2ebacd4..a0800eb 100644 --- a/internal/sms-gateway/modules/push/types.go +++ b/internal/sms-gateway/modules/push/types.go @@ -47,3 +47,7 @@ func NewMessagesExportRequestedEvent(since, until time.Time) *domain.Event { }, ) } + +func NewSettingsUpdatedEvent() *domain.Event { + return domain.NewEvent(smsgateway.PushSettingsUpdated, nil) +} diff --git a/internal/sms-gateway/modules/settings/models.go b/internal/sms-gateway/modules/settings/models.go new file mode 100644 index 0000000..f23f1a9 --- /dev/null +++ b/internal/sms-gateway/modules/settings/models.go @@ -0,0 +1,24 @@ +package settings + +import ( + "fmt" + + "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "gorm.io/gorm" +) + +type DeviceSettings struct { + UserID string `gorm:"primaryKey;not null;type:varchar(32)"` + Settings map[string]any `gorm:"not null;type:json;serializer:json"` + + User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"` + + models.TimedModel +} + +func Migrate(db *gorm.DB) error { + if err := db.AutoMigrate(&DeviceSettings{}); err != nil { + return fmt.Errorf("device_settings migration failed: %w", err) + } + return nil +} diff --git a/internal/sms-gateway/modules/settings/module.go b/internal/sms-gateway/modules/settings/module.go new file mode 100644 index 0000000..64fb2f1 --- /dev/null +++ b/internal/sms-gateway/modules/settings/module.go @@ -0,0 +1,25 @@ +package settings + +import ( + "github.com/capcom6/go-infra-fx/db" + "go.uber.org/fx" + "go.uber.org/zap" +) + +var Module = fx.Module( + "settings", + fx.Decorate(func(log *zap.Logger) *zap.Logger { + return log.Named("settings") + }), + fx.Provide( + newRepository, + fx.Private, + ), + fx.Provide( + NewService, + ), +) + +func init() { + db.RegisterMigration(Migrate) +} diff --git a/internal/sms-gateway/modules/settings/repository.go b/internal/sms-gateway/modules/settings/repository.go new file mode 100644 index 0000000..4278c40 --- /dev/null +++ b/internal/sms-gateway/modules/settings/repository.go @@ -0,0 +1,67 @@ +package settings + +import ( + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type repository struct { + db *gorm.DB +} + +// GetSettings retrieves the device settings for a user by their userID. +func (r *repository) GetSettings(userID string) (*DeviceSettings, error) { + settings := &DeviceSettings{} + err := r.db.Where("user_id = ?", userID).Limit(1).Find(settings).Error + if err != nil { + return nil, err + } + + return settings, nil +} + +// UpdateSettings updates the settings for a user. +func (r *repository) UpdateSettings(settings *DeviceSettings) (*DeviceSettings, error) { + var updatedSettings *DeviceSettings + err := r.db.Transaction(func(tx *gorm.DB) error { + source := &DeviceSettings{UserID: settings.UserID} + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Limit(1).Find(source).Error; err != nil { + return err + } + + if source.Settings == nil { + source.Settings = map[string]any{} + } + + var err error + settings.Settings, err = appendMap(source.Settings, settings.Settings, rules) + if err != nil { + return err + } + + if err := tx.Clauses(clause.OnConflict{UpdateAll: true}).Create(settings).Error; err != nil { + return err + } + + // Return the updated settings + updatedSettings = settings + return nil + }) + return updatedSettings, err +} + +// ReplaceSettings replaces the settings for a user. +// +// This function will overwrite all existing settings for the user. +func (r *repository) ReplaceSettings(settings *DeviceSettings) (*DeviceSettings, error) { + err := r.db.Transaction(func(tx *gorm.DB) error { + return tx.Save(settings).Error + }) + return settings, err +} + +func newRepository(db *gorm.DB) *repository { + return &repository{ + db: db, + } +} diff --git a/internal/sms-gateway/modules/settings/service.go b/internal/sms-gateway/modules/settings/service.go new file mode 100644 index 0000000..ef409e8 --- /dev/null +++ b/internal/sms-gateway/modules/settings/service.go @@ -0,0 +1,93 @@ +package settings + +import ( + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/push" + "go.uber.org/fx" + "go.uber.org/zap" +) + +type ServiceParams struct { + fx.In + + Repository *repository + + Logger *zap.Logger + + PushSvc *push.Service +} + +type Service struct { + settings *repository + + logger *zap.Logger + + pushSvc *push.Service +} + +func (s *Service) GetSettings(userID string, public bool) (map[string]any, error) { + settings, err := s.settings.GetSettings(userID) + if err != nil { + return nil, err + } + + if !public { + return settings.Settings, nil + } + + return filterMap(settings.Settings, rulesPublic) +} + +func (s *Service) UpdateSettings(userID string, settings map[string]any) (map[string]any, error) { + filtered, err := filterMap(settings, rules) + if err != nil { + return nil, err + } + + updatedSettings, err := s.settings.UpdateSettings(&DeviceSettings{ + UserID: userID, + Settings: filtered, + }) + if err != nil { + return nil, err + } + + s.notifyDevices(userID) + + return filterMap(updatedSettings.Settings, rulesPublic) +} + +func (s *Service) ReplaceSettings(userID string, settings map[string]any) (map[string]any, error) { + filtered, err := filterMap(settings, rules) + if err != nil { + return nil, err + } + + updated, err := s.settings.ReplaceSettings(&DeviceSettings{ + UserID: userID, + Settings: filtered, + }) + if err != nil { + return nil, err + } + + s.notifyDevices(userID) + + return filterMap(updated.Settings, rulesPublic) +} + +// notifyDevices asynchronously notifies all the user's devices. +func (s *Service) notifyDevices(userID string) { + go func(userID string) { + if err := s.pushSvc.Notify(userID, nil, push.NewSettingsUpdatedEvent()); err != nil { + s.logger.Error("can't notify devices", zap.Error(err)) + } + }(userID) +} + +func NewService(params ServiceParams) *Service { + return &Service{ + settings: params.Repository, + logger: params.Logger.Named("service"), + pushSvc: params.PushSvc, + } +} diff --git a/internal/sms-gateway/modules/settings/utils.go b/internal/sms-gateway/modules/settings/utils.go new file mode 100644 index 0000000..58c0cf6 --- /dev/null +++ b/internal/sms-gateway/modules/settings/utils.go @@ -0,0 +1,112 @@ +package settings + +import "fmt" + +var rules = map[string]any{ + "encryption": map[string]any{ + "passphrase": "", + }, + "messages": map[string]any{ + "send_interval_min": "", + "send_interval_max": "", + "limit_period": "", + "limit_value": "", + "sim_selection_mode": "", + "log_lifetime_days": "", + }, + "ping": map[string]any{ + "interval_seconds": "", + }, + "logs": map[string]any{ + "lifetime_days": "", + }, + "webhooks": map[string]any{ + "internet_required": "", + "retry_count": "", + "signing_key": "", + }, +} + +var rulesPublic = map[string]any{ + "encryption": map[string]any{}, + "messages": map[string]any{ + "send_interval_min": "", + "send_interval_max": "", + "limit_period": "", + "limit_value": "", + "sim_selection_mode": "", + "log_lifetime_days": "", + }, + "ping": map[string]any{ + "interval_seconds": "", + }, + "logs": map[string]any{ + "lifetime_days": "", + }, + "webhooks": map[string]any{ + "internet_required": "", + "retry_count": "", + }, +} + +func filterMap(m map[string]any, r map[string]any) (map[string]any, error) { + var err error + + result := make(map[string]any) + for field, rule := range r { + if ruleObj, ok := rule.(map[string]any); ok { + if dataObj, ok := m[field].(map[string]any); ok { + result[field], err = filterMap(dataObj, ruleObj) + if err != nil { + return nil, err + } + } else if m[field] == nil { + continue + } else { + return nil, fmt.Errorf("the field: '%s' is not a map to dive", field) + } + } else if _, ok := rule.(string); ok { + if _, ok := m[field]; !ok { + continue + } + result[field] = m[field] + } + } + + return result, nil +} + +func appendMap(m1, m2 map[string]any, rules map[string]any) (map[string]any, error) { + var err error + + for field, rule := range rules { + if ruleObj, ok := rule.(map[string]any); ok { + if dataObj, ok := m2[field].(map[string]any); ok { + if m1Field, ok := m1[field].(map[string]any); ok { + m1[field], err = appendMap(m1Field, dataObj, ruleObj) + if err != nil { + return nil, err + } + } else { + // Initialize if not present or not a map + newMap := make(map[string]any) + m1[field], err = appendMap(newMap, dataObj, ruleObj) + if err != nil { + return nil, err + } + } + } else if m2[field] == nil { + continue + } else { + return nil, fmt.Errorf("expected field '%s' to be a map, but got %T", field, m2[field]) + } + } else if _, ok := rule.(string); ok { + if _, ok := m2[field]; !ok { + continue + } + m1[field] = m2[field] + } + } + + return m1, nil +} diff --git a/internal/sms-gateway/modules/webhooks/models.go b/internal/sms-gateway/modules/webhooks/models.go index a8e58be..a6a546a 100644 --- a/internal/sms-gateway/modules/webhooks/models.go +++ b/internal/sms-gateway/modules/webhooks/models.go @@ -19,7 +19,7 @@ type Webhook struct { User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"` Device *models.Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"` - models.TimedModel + models.SoftDeletableModel } func Migrate(db *gorm.DB) error { diff --git a/pkg/swagger/docs/local.http b/pkg/swagger/docs/local.http index 355f984..9648c21 100644 --- a/pkg/swagger/docs/local.http +++ b/pkg/swagger/docs/local.http @@ -80,3 +80,56 @@ Authorization: Basic {{localCredentials}} ### GET {{localUrl}}/logs?from=2025-02-05T20:39:46.190%2B07:00 HTTP/1.1 Authorization: Basic {{localCredentials}} + +### +GET {{localUrl}}/settings HTTP/1.1 +Authorization: Basic {{localCredentials}} + +### +PATCH {{localUrl}}/settings HTTP/1.1 +Authorization: Basic {{localCredentials}} +Content-Type: application/json + +{ + "encryption": { + "passphrase": null + }, + "gateway": { + "cloud_url": "https://api.sms-gate.app/mobile/v1", + "private_token": null + }, + "messages": { + "send_interval_min": null, + "send_interval_max": null, + "limit_period": "Disabled", + "limit_value": null, + "sim_selection_mode": "OSDefault", + "log_lifetime_days": null + }, + "localserver": { + "PORT": 8080 + }, + "ping": { + "interval_seconds": null + }, + "logs": { + "lifetime_days": 30 + }, + "webhooks": { + "internet_required": true, + "retry_count": 1, + "signing_key": null + } +} + +### +PATCH {{localUrl}}/settings HTTP/1.1 +Authorization: Basic {{localCredentials}} +Content-Type: application/json + +{ + "webhooks": { + "internet_required": true, + "retry_count": 1 + } +} \ No newline at end of file diff --git a/pkg/swagger/docs/mobile.http b/pkg/swagger/docs/mobile.http index d15e8ed..04e0a0c 100644 --- a/pkg/swagger/docs/mobile.http +++ b/pkg/swagger/docs/mobile.http @@ -71,3 +71,7 @@ Content-Type: application/json "currentPassword": "wsmgz1akhoo24o", "newPassword": "wsmgz1akhoo24o" } + +### +GET {{baseUrl}}/settings HTTP/1.1 +Authorization: Bearer {{mobileToken}} diff --git a/pkg/swagger/docs/requests.http b/pkg/swagger/docs/requests.http index e59d929..40fea78 100644 --- a/pkg/swagger/docs/requests.http +++ b/pkg/swagger/docs/requests.http @@ -103,6 +103,52 @@ Authorization: Basic {{credentials}} GET {{baseUrl}}/api/3rdparty/v1/logs HTTP/1.1 Authorization: Basic {{credentials}} +### +GET {{baseUrl}}/3rdparty/v1/settings HTTP/1.1 +Authorization: Basic {{credentials}} + +### +PATCH {{baseUrl}}/3rdparty/v1/settings HTTP/1.1 +Authorization: Basic {{credentials}} +Content-Type: application/json + +{ + "messages": { + "send_interval_min": null, + "send_interval_max": 1 + } +} + +### +PUT {{baseUrl}}/3rdparty/v1/settings HTTP/1.1 +Authorization: Basic {{credentials}} +Content-Type: application/json + +{ + "encryption": { + "passphrase": "{{$guid}}" + }, + "messages": { + "send_interval_min": null, + "send_interval_max": null, + "limit_period": "Disabled", + "limit_value": null, + "sim_selection_mode": "OSDefault", + "log_lifetime_days": null + }, + "ping": { + "interval_seconds": null + }, + "logs": { + "lifetime_days": 30 + }, + "webhooks": { + "internet_required": true, + "retry_count": 1, + "signing_key": "{{$guid}}" + } +} + ### GET http://localhost:3000/metrics HTTP/1.1 diff --git a/pkg/swagger/docs/swagger.json b/pkg/swagger/docs/swagger.json index 7164736..b987dbe 100644 --- a/pkg/swagger/docs/swagger.json +++ b/pkg/swagger/docs/swagger.json @@ -393,6 +393,156 @@ } } }, + "/3rdparty/v1/settings": { + "get": { + "security": [ + { + "ApiAuth": [] + } + ], + "description": "Returns settings for a specific user", + "produces": [ + "application/json" + ], + "tags": [ + "User", + "Settings" + ], + "summary": "Get settings", + "responses": { + "200": { + "description": "Settings", + "schema": { + "$ref": "#/definitions/smsgateway.DeviceSettings" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "ApiAuth": [] + } + ], + "description": "Updates settings for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User", + "Settings" + ], + "summary": "Update settings", + "parameters": [ + { + "description": "Settings", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/smsgateway.DeviceSettings" + } + } + ], + "responses": { + "200": { + "description": "Settings updated", + "schema": { + "type": "object" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "ApiAuth": [] + } + ], + "description": "Partially updates settings for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User", + "Settings" + ], + "summary": "Partially update settings", + "parameters": [ + { + "description": "Settings", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/smsgateway.DeviceSettings" + } + } + ], + "responses": { + "200": { + "description": "Settings updated", + "schema": { + "type": "object" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + } + } + } + }, "/3rdparty/v1/webhooks": { "get": { "security": [ @@ -767,6 +917,44 @@ } } }, + "/mobile/v1/settings": { + "get": { + "security": [ + { + "MobileToken": [] + } + ], + "description": "Returns settings for a device", + "produces": [ + "application/json" + ], + "tags": [ + "Device", + "Settings" + ], + "summary": "Get settings", + "responses": { + "200": { + "description": "Settings", + "schema": { + "$ref": "#/definitions/smsgateway.DeviceSettings" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + } + } + } + }, "/mobile/v1/user/code": { "get": { "security": [ @@ -985,6 +1173,59 @@ } } }, + "smsgateway.DeviceSettings": { + "type": "object", + "properties": { + "encryption": { + "description": "Encryption contains settings related to message encryption.", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.SettingsEncryption" + } + ] + }, + "gateway": { + "description": "Gateway contains settings related to the Cloud/Private server configuration.", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.SettingsGateway" + } + ] + }, + "logs": { + "description": "Logs contains settings related to logging.", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.SettingsLogs" + } + ] + }, + "messages": { + "description": "Messages contains settings related to message handling.", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.SettingsMessages" + } + ] + }, + "ping": { + "description": "Ping contains settings related to ping functionality.", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.SettingsPing" + } + ] + }, + "webhooks": { + "description": "Webhooks contains settings related to webhook functionality.", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.SettingsWebhooks" + } + ] + } + } + }, "smsgateway.ErrorResponse": { "type": "object", "properties": { @@ -1075,6 +1316,21 @@ "HealthStatusFail" ] }, + "smsgateway.LimitPeriod": { + "type": "string", + "enum": [ + "Disabled", + "PerMinute", + "PerHour", + "PerDay" + ], + "x-enum-varnames": [ + "Disabled", + "PerMinute", + "PerHour", + "PerDay" + ] + }, "smsgateway.LogEntry": { "type": "object", "properties": { @@ -1507,12 +1763,14 @@ "enum": [ "MessageEnqueued", "WebhooksUpdated", - "MessagesExportRequested" + "MessagesExportRequested", + "SettingsUpdated" ], "x-enum-varnames": [ "PushMessageEnqueued", "PushWebhooksUpdated", - "PushMessagesExportRequested" + "PushMessagesExportRequested", + "PushSettingsUpdated" ] }, "smsgateway.PushNotification": { @@ -1534,7 +1792,8 @@ "enum": [ "MessageEnqueued", "WebhooksUpdated", - "MessagesExportRequested" + "MessagesExportRequested", + "SettingsUpdated" ], "allOf": [ { @@ -1580,6 +1839,131 @@ } } }, + "smsgateway.SettingsEncryption": { + "type": "object", + "properties": { + "passphrase": { + "description": "Passphrase is the encryption passphrase. If nil or empty, encryption is disabled.", + "type": "string" + } + } + }, + "smsgateway.SettingsGateway": { + "type": "object", + "properties": { + "cloud_url": { + "description": "CloudURL is the URL of the server. If nil, the Cloud Public server is used.\nMust be a valid HTTPs URL when provided.", + "type": "string" + }, + "private_token": { + "description": "PrivateToken is the private token for authenticating with the Private Server.", + "type": "string" + } + } + }, + "smsgateway.SettingsLogs": { + "type": "object", + "properties": { + "lifetime_days": { + "description": "LifetimeDays is the number of days to retain logs.\nMust be at least 1 when provided.", + "type": "integer", + "minimum": 1 + } + } + }, + "smsgateway.SettingsMessages": { + "type": "object", + "properties": { + "limit_period": { + "description": "LimitPeriod defines the period for message sending limits.\nValid values are \"Disabled\", \"PerMinute\", \"PerHour\", or \"PerDay\".", + "enum": [ + "Disabled", + "PerMinute", + "PerHour", + "PerDay" + ], + "allOf": [ + { + "$ref": "#/definitions/smsgateway.LimitPeriod" + } + ] + }, + "limit_value": { + "description": "LimitValue is the maximum number of messages allowed per limit period.\nMust be at least 1 when provided.", + "type": "integer", + "minimum": 1 + }, + "log_lifetime_days": { + "description": "LogLifetimeDays is the number of days to retain message logs.\nMust be at least 1 when provided.", + "type": "integer", + "minimum": 1 + }, + "send_interval_max": { + "description": "SendIntervalMax is the maximum interval between message sends (in seconds).\nMust be at least 1 when provided and greater than or equal to SendIntervalMin.", + "type": "integer", + "minimum": 1 + }, + "send_interval_min": { + "description": "SendIntervalMin is the minimum interval between message sends (in seconds).\nMust be at least 1 when provided.", + "type": "integer", + "minimum": 1 + }, + "sim_selection_mode": { + "description": "SimSelectionMode defines how SIM cards are selected for sending messages.\nValid values are \"OSDefault\", \"RoundRobin\", or \"Random\".", + "enum": [ + "OSDefault", + "RoundRobin", + "Random" + ], + "allOf": [ + { + "$ref": "#/definitions/smsgateway.SimSelectionMode" + } + ] + } + } + }, + "smsgateway.SettingsPing": { + "type": "object", + "properties": { + "interval_seconds": { + "description": "IntervalSeconds is the interval between ping requests (in seconds).\nMust be at least 1 when provided.", + "type": "integer", + "minimum": 1 + } + } + }, + "smsgateway.SettingsWebhooks": { + "type": "object", + "properties": { + "internet_required": { + "description": "InternetRequired indicates whether internet access is required for webhooks.", + "type": "boolean" + }, + "retry_count": { + "description": "RetryCount is the number of times to retry failed webhook deliveries.\nMust be at least 1 when provided.", + "type": "integer", + "minimum": 1 + }, + "signing_key": { + "description": "SigningKey is the secret key used for signing webhook payloads.", + "type": "string" + } + } + }, + "smsgateway.SimSelectionMode": { + "type": "string", + "enum": [ + "OSDefault", + "RoundRobin", + "Random" + ], + "x-enum-varnames": [ + "OSDefault", + "RoundRobin", + "Random" + ] + }, "smsgateway.Webhook": { "type": "object", "required": [ diff --git a/pkg/swagger/docs/swagger.yaml b/pkg/swagger/docs/swagger.yaml index bdf610c..11ca0a9 100644 --- a/pkg/swagger/docs/swagger.yaml +++ b/pkg/swagger/docs/swagger.yaml @@ -26,6 +26,34 @@ definitions: example: "2020-01-01T00:00:00Z" type: string type: object + smsgateway.DeviceSettings: + properties: + encryption: + allOf: + - $ref: '#/definitions/smsgateway.SettingsEncryption' + description: Encryption contains settings related to message encryption. + gateway: + allOf: + - $ref: '#/definitions/smsgateway.SettingsGateway' + description: Gateway contains settings related to the Cloud/Private server + configuration. + logs: + allOf: + - $ref: '#/definitions/smsgateway.SettingsLogs' + description: Logs contains settings related to logging. + messages: + allOf: + - $ref: '#/definitions/smsgateway.SettingsMessages' + description: Messages contains settings related to message handling. + ping: + allOf: + - $ref: '#/definitions/smsgateway.SettingsPing' + description: Ping contains settings related to ping functionality. + webhooks: + allOf: + - $ref: '#/definitions/smsgateway.SettingsWebhooks' + description: Webhooks contains settings related to webhook functionality. + type: object smsgateway.ErrorResponse: properties: code: @@ -91,6 +119,18 @@ definitions: - HealthStatusPass - HealthStatusWarn - HealthStatusFail + smsgateway.LimitPeriod: + enum: + - Disabled + - PerMinute + - PerHour + - PerDay + type: string + x-enum-varnames: + - Disabled + - PerMinute + - PerHour + - PerDay smsgateway.LogEntry: properties: context: @@ -420,11 +460,13 @@ definitions: - MessageEnqueued - WebhooksUpdated - MessagesExportRequested + - SettingsUpdated type: string x-enum-varnames: - PushMessageEnqueued - PushWebhooksUpdated - PushMessagesExportRequested + - PushSettingsUpdated smsgateway.PushNotification: properties: data: @@ -441,6 +483,7 @@ definitions: - MessageEnqueued - WebhooksUpdated - MessagesExportRequested + - SettingsUpdated example: MessageEnqueued token: description: The token of the device that receives the notification. @@ -470,6 +513,117 @@ definitions: - phoneNumber - state type: object + smsgateway.SettingsEncryption: + properties: + passphrase: + description: Passphrase is the encryption passphrase. If nil or empty, encryption + is disabled. + type: string + type: object + smsgateway.SettingsGateway: + properties: + cloud_url: + description: |- + CloudURL is the URL of the server. If nil, the Cloud Public server is used. + Must be a valid HTTPs URL when provided. + type: string + private_token: + description: PrivateToken is the private token for authenticating with the + Private Server. + type: string + type: object + smsgateway.SettingsLogs: + properties: + lifetime_days: + description: |- + LifetimeDays is the number of days to retain logs. + Must be at least 1 when provided. + minimum: 1 + type: integer + type: object + smsgateway.SettingsMessages: + properties: + limit_period: + allOf: + - $ref: '#/definitions/smsgateway.LimitPeriod' + description: |- + LimitPeriod defines the period for message sending limits. + Valid values are "Disabled", "PerMinute", "PerHour", or "PerDay". + enum: + - Disabled + - PerMinute + - PerHour + - PerDay + limit_value: + description: |- + LimitValue is the maximum number of messages allowed per limit period. + Must be at least 1 when provided. + minimum: 1 + type: integer + log_lifetime_days: + description: |- + LogLifetimeDays is the number of days to retain message logs. + Must be at least 1 when provided. + minimum: 1 + type: integer + send_interval_max: + description: |- + SendIntervalMax is the maximum interval between message sends (in seconds). + Must be at least 1 when provided and greater than or equal to SendIntervalMin. + minimum: 1 + type: integer + send_interval_min: + description: |- + SendIntervalMin is the minimum interval between message sends (in seconds). + Must be at least 1 when provided. + minimum: 1 + type: integer + sim_selection_mode: + allOf: + - $ref: '#/definitions/smsgateway.SimSelectionMode' + description: |- + SimSelectionMode defines how SIM cards are selected for sending messages. + Valid values are "OSDefault", "RoundRobin", or "Random". + enum: + - OSDefault + - RoundRobin + - Random + type: object + smsgateway.SettingsPing: + properties: + interval_seconds: + description: |- + IntervalSeconds is the interval between ping requests (in seconds). + Must be at least 1 when provided. + minimum: 1 + type: integer + type: object + smsgateway.SettingsWebhooks: + properties: + internet_required: + description: InternetRequired indicates whether internet access is required + for webhooks. + type: boolean + retry_count: + description: |- + RetryCount is the number of times to retry failed webhook deliveries. + Must be at least 1 when provided. + minimum: 1 + type: integer + signing_key: + description: SigningKey is the secret key used for signing webhook payloads. + type: string + type: object + smsgateway.SimSelectionMode: + enum: + - OSDefault + - RoundRobin + - Random + type: string + x-enum-varnames: + - OSDefault + - RoundRobin + - Random smsgateway.Webhook: properties: deviceId: @@ -770,6 +924,102 @@ paths: tags: - User - Messages + /3rdparty/v1/settings: + get: + description: Returns settings for a specific user + produces: + - application/json + responses: + "200": + description: Settings + schema: + $ref: '#/definitions/smsgateway.DeviceSettings' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + security: + - ApiAuth: [] + summary: Get settings + tags: + - User + - Settings + patch: + consumes: + - application/json + description: Partially updates settings for a specific user + parameters: + - description: Settings + in: body + name: request + required: true + schema: + $ref: '#/definitions/smsgateway.DeviceSettings' + produces: + - application/json + responses: + "200": + description: Settings updated + schema: + type: object + "400": + description: Invalid request + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + security: + - ApiAuth: [] + summary: Partially update settings + tags: + - User + - Settings + put: + consumes: + - application/json + description: Updates settings for a specific user + parameters: + - description: Settings + in: body + name: request + required: true + schema: + $ref: '#/definitions/smsgateway.DeviceSettings' + produces: + - application/json + responses: + "200": + description: Settings updated + schema: + type: object + "400": + description: Invalid request + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + security: + - ApiAuth: [] + summary: Update settings + tags: + - User + - Settings /3rdparty/v1/webhooks: get: description: Returns list of registered webhooks @@ -1009,6 +1259,30 @@ paths: tags: - Device - Messages + /mobile/v1/settings: + get: + description: Returns settings for a device + produces: + - application/json + responses: + "200": + description: Settings + schema: + $ref: '#/definitions/smsgateway.DeviceSettings' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' + security: + - MobileToken: [] + summary: Get settings + tags: + - Device + - Settings /mobile/v1/user/code: get: consumes: