diff --git a/go.mod b/go.mod index fcc29083..258703c1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( firebase.google.com/go/v4 v4.19.0 - github.com/android-sms-gateway/client-go v1.12.5 + github.com/android-sms-gateway/client-go v1.12.7-0.20260506065831-72a3c7972539 github.com/ansrivas/fiberprometheus/v2 v2.6.1 github.com/capcom6/go-helpers v0.3.0 github.com/capcom6/go-infra-fx v0.5.2 diff --git a/go.sum b/go.sum index ec451ae5..697ab8bc 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/android-sms-gateway/client-go v1.12.5 h1:LcfTIvtRxOujPxmtFfCjHuFiX5wFyjN9yqGE+82jZdw= -github.com/android-sms-gateway/client-go v1.12.5/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.12.7-0.20260506065831-72a3c7972539 h1:TbYUr0uHTkQZe7wqX93fWjnVb+f6fHnYShdu1VeZ+dM= +github.com/android-sms-gateway/client-go v1.12.7-0.20260506065831-72a3c7972539/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/ansrivas/fiberprometheus/v2 v2.6.1 h1:wac3pXaE6BYYTF04AC6K0ktk6vCD+MnDOJZ3SK66kXM= diff --git a/internal/sms-gateway/handlers/converters/devices.go b/internal/sms-gateway/handlers/converters/devices.go index 5e1861ba..40ad9192 100644 --- a/internal/sms-gateway/handlers/converters/devices.go +++ b/internal/sms-gateway/handlers/converters/devices.go @@ -2,11 +2,12 @@ package converters import ( "github.com/android-sms-gateway/client-go/smsgateway" - "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" "github.com/capcom6/go-helpers/anys" + "github.com/samber/lo" ) -func DeviceToDTO(device models.Device) smsgateway.Device { +func DeviceToDTO(device devices.Device) smsgateway.Device { return smsgateway.Device{ ID: device.ID, Name: anys.OrDefault(device.Name, ""), @@ -14,5 +15,22 @@ func DeviceToDTO(device models.Device) smsgateway.Device { UpdatedAt: device.UpdatedAt, DeletedAt: device.DeletedAt, LastSeen: device.LastSeen, + SimCards: mapSimCards(device.SimCards), } } + +func mapSimCards(simCards []devices.SimCard) []smsgateway.SimCard { + if simCards == nil { + return nil + } + + return lo.Map(simCards, func(sc devices.SimCard, _ int) smsgateway.SimCard { + return smsgateway.SimCard{ + SlotIndex: sc.SlotIndex, + SimNumber: sc.SimNumber, + PhoneNumber: sc.PhoneNumber, + CarrierName: sc.CarrierName, + ICCID: sc.ICCID, + } + }) +} diff --git a/internal/sms-gateway/handlers/converters/devices_test.go b/internal/sms-gateway/handlers/converters/devices_test.go index 06b78c08..ff2c005c 100644 --- a/internal/sms-gateway/handlers/converters/devices_test.go +++ b/internal/sms-gateway/handlers/converters/devices_test.go @@ -6,9 +6,9 @@ import ( "github.com/android-sms-gateway/client-go/smsgateway" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/converters" - "github.com/android-sms-gateway/server/internal/sms-gateway/models" - "github.com/capcom6/go-helpers/anys" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" "github.com/go-playground/assert/v2" + "github.com/samber/lo" ) func TestDeviceToDTO(t *testing.T) { @@ -18,26 +18,27 @@ func TestDeviceToDTO(t *testing.T) { tests := []struct { name string - device models.Device + device devices.Device expected smsgateway.Device }{ { name: "empty device", - device: models.Device{}, + device: devices.Device{}, expected: smsgateway.Device{}, }, { name: "non-empty device", - device: models.Device{ - ID: "test-id", - Name: anys.AsPointer("test-name"), - LastSeen: lastSeenAt, - SoftDeletableModel: models.SoftDeletableModel{ - TimedModel: models.TimedModel{ - CreatedAt: createdAt, - UpdatedAt: updatedAt, + device: devices.Device{ + DeviceInput: devices.DeviceInput{ + DeviceInfo: devices.DeviceInfo{ + DeviceUpdate: devices.DeviceUpdate{}, + Name: lo.ToPtr("test-name"), }, + ID: "test-id", }, + LastSeen: lastSeenAt, + CreatedAt: createdAt, + UpdatedAt: updatedAt, }, expected: smsgateway.Device{ ID: "test-id", @@ -49,15 +50,52 @@ func TestDeviceToDTO(t *testing.T) { }, { name: "device with nil name", - device: models.Device{ - ID: "test-id", - Name: nil, + device: devices.Device{ + DeviceInput: devices.DeviceInput{ + DeviceInfo: devices.DeviceInfo{ + Name: nil, + }, + ID: "test-id", + }, }, expected: smsgateway.Device{ ID: "test-id", Name: "", }, }, + { + name: "device with sim cards", + device: devices.Device{ + DeviceInput: devices.DeviceInput{ + DeviceInfo: devices.DeviceInfo{ + DeviceUpdate: devices.DeviceUpdate{ + SimCards: []devices.SimCard{ + { + SlotIndex: 0, + SimNumber: 1, + PhoneNumber: lo.ToPtr("+79990001234"), + CarrierName: lo.ToPtr("Carrier"), + ICCID: lo.ToPtr("8901260000000000000"), + }, + }, + }, + }, + ID: "test-id", + }, + }, + expected: smsgateway.Device{ + ID: "test-id", + SimCards: []smsgateway.SimCard{ + { + SlotIndex: 0, + SimNumber: 1, + PhoneNumber: lo.ToPtr("+79990001234"), + CarrierName: lo.ToPtr("Carrier"), + ICCID: lo.ToPtr("8901260000000000000"), + }, + }, + }, + }, } for _, test := range tests { diff --git a/internal/sms-gateway/handlers/devices/3rdparty.go b/internal/sms-gateway/handlers/devices/3rdparty.go index a4937c57..09d153f6 100644 --- a/internal/sms-gateway/handlers/devices/3rdparty.go +++ b/internal/sms-gateway/handlers/devices/3rdparty.go @@ -4,14 +4,15 @@ import ( "errors" "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/converters" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/permissions" "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" - "github.com/capcom6/go-helpers/slices" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + "github.com/samber/lo" "go.uber.org/zap" ) @@ -50,14 +51,17 @@ func NewThirdPartyController( // // List devices. func (h *ThirdPartyController) get(userID string, c *fiber.Ctx) error { - devices, err := h.devicesSvc.Select(userID) + items, err := h.devicesSvc.Select(c.Context(), userID) if err != nil { return fmt.Errorf("failed to select devices: %w", err) } - response := slices.Map(devices, converters.DeviceToDTO) - - return c.JSON(response) + return c.JSON(lo.Map( + items, + func(device devices.Device, _ int) smsgateway.Device { + return converters.DeviceToDTO(device) + }, + )) } // @Summary Remove device @@ -79,7 +83,7 @@ func (h *ThirdPartyController) get(userID string, c *fiber.Ctx) error { func (h *ThirdPartyController) remove(userID string, c *fiber.Ctx) error { id := c.Params("id") - err := h.devicesSvc.Remove(userID, devices.WithID(id)) + err := h.devicesSvc.Remove(c.Context(), userID, devices.WithID(id)) if errors.Is(err, devices.ErrNotFound) { return fiber.NewError(fiber.StatusNotFound, err.Error()) } diff --git a/internal/sms-gateway/handlers/events/mobile.go b/internal/sms-gateway/handlers/events/mobile.go index 8cbb6a04..c12769a7 100644 --- a/internal/sms-gateway/handlers/events/mobile.go +++ b/internal/sms-gateway/handlers/events/mobile.go @@ -3,7 +3,7 @@ package events import ( "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/sse" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -42,7 +42,7 @@ func NewMobileController(sseService *sse.Service, validator *validator.Validate, // @Router /mobile/v1/events [get] // // Get events. -func (h *MobileController) get(device models.Device, c *fiber.Ctx) error { +func (h *MobileController) get(device devices.Device, c *fiber.Ctx) error { return h.sseSvc.Handler(device.ID, c) //nolint:wrapcheck //wrapped internally } diff --git a/internal/sms-gateway/handlers/messages/3rdparty.go b/internal/sms-gateway/handlers/messages/3rdparty.go index 5976677c..c8e6717d 100644 --- a/internal/sms-gateway/handlers/messages/3rdparty.go +++ b/internal/sms-gateway/handlers/messages/3rdparty.go @@ -90,6 +90,7 @@ func (h *ThirdPartyController) post(userID string, c *fiber.Ctx) error { } device, err := h.devicesSvc.GetAny( + c.Context(), userID, req.DeviceID, time.Duration(params.DeviceActiveWithin)*time.Hour, @@ -251,17 +252,17 @@ func (h *ThirdPartyController) get(userID string, c *fiber.Ctx) error { // @Tags User, Messages // @Accept json // @Produce json -// @Param request body smsgateway.MessagesExportRequest true "Export inbox request" -// @Success 202 {object} object "Inbox export request accepted" -// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" -// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" -// @Failure 403 {object} smsgateway.ErrorResponse "Forbidden" -// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Param request body smsgateway.InboxRefreshRequest true "Export inbox request" +// @Success 202 {object} object "Inbox export request accepted" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 403 {object} smsgateway.ErrorResponse "Forbidden" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" // @Router /3rdparty/v1/messages/inbox/export [post] // // Export inbox. func (h *ThirdPartyController) postInboxExport(userID string, c *fiber.Ctx) error { - req := new(smsgateway.MessagesExportRequest) + req := new(smsgateway.InboxRefreshRequest) if err := h.BodyParserValidator(c, req); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/internal/sms-gateway/handlers/messages/mobile.go b/internal/sms-gateway/handlers/messages/mobile.go index 99434770..5076c487 100644 --- a/internal/sms-gateway/handlers/messages/mobile.go +++ b/internal/sms-gateway/handlers/messages/mobile.go @@ -8,7 +8,7 @@ import ( "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base" "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/models" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages" "github.com/capcom6/go-helpers/slices" "github.com/go-playground/validator/v10" @@ -55,7 +55,7 @@ func NewMobileController(params mobileControllerParams) *MobileController { // @Router /mobile/v1/message [get] // // Get messages for sending. -func (h *MobileController) list(device models.Device, c *fiber.Ctx) error { +func (h *MobileController) list(device devices.Device, c *fiber.Ctx) error { // Get and validate order parameter params := new(mobileGetQueryParams) if err := h.QueryParserValidator(c, params); err != nil { @@ -90,7 +90,7 @@ func (h *MobileController) list(device models.Device, c *fiber.Ctx) error { // @Router /mobile/v1/message [patch] // // Update message state. -func (h *MobileController) patch(device models.Device, c *fiber.Ctx) error { +func (h *MobileController) patch(device devices.Device, c *fiber.Ctx) error { req := smsgateway.MobilePatchMessageRequest{} if err := h.BodyParserValidator(c, &req); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) diff --git a/internal/sms-gateway/handlers/middlewares/deviceauth/deviceauth.go b/internal/sms-gateway/handlers/middlewares/deviceauth/deviceauth.go index 240f6811..ef523599 100644 --- a/internal/sms-gateway/handlers/middlewares/deviceauth/deviceauth.go +++ b/internal/sms-gateway/handlers/middlewares/deviceauth/deviceauth.go @@ -4,7 +4,6 @@ import ( "errors" "strings" - "github.com/android-sms-gateway/server/internal/sms-gateway/models" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/auth" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" "github.com/gofiber/fiber/v2" @@ -29,7 +28,7 @@ func New(authSvc *auth.Service) fiber.Handler { // Get the token token := auth[7:] - device, err := authSvc.AuthorizeDevice(token) + device, err := authSvc.AuthorizeDevice(c.Context(), token) if errors.Is(err, devices.ErrNotFound) { return c.Next() } @@ -37,7 +36,7 @@ func New(authSvc *auth.Service) fiber.Handler { return fiber.NewError(fiber.StatusUnauthorized, err.Error()) } - c.Locals(LocalsDevice, device) + c.Locals(LocalsDevice, *device) return c.Next() } @@ -52,10 +51,10 @@ func HasDevice(c *fiber.Ctx) bool { // GetDevice returns the device stored in the Locals under the key LocalsDevice. // If the Locals do not contain a device, it returns an empty device. -func GetDevice(c *fiber.Ctx) models.Device { - device, ok := c.Locals(LocalsDevice).(models.Device) +func GetDevice(c *fiber.Ctx) devices.Device { + device, ok := c.Locals(LocalsDevice).(devices.Device) if !ok { - return models.Device{} + return devices.Device{} } return device @@ -80,7 +79,7 @@ func DeviceRequired() fiber.Handler { // // It is a convenience function that wraps the call to GetDevice and calls the // handler with the device as the first argument. -func WithDevice(handler func(models.Device, *fiber.Ctx) error) fiber.Handler { +func WithDevice(handler func(devices.Device, *fiber.Ctx) error) fiber.Handler { return func(c *fiber.Ctx) error { return handler(GetDevice(c), c) } diff --git a/internal/sms-gateway/handlers/mobile.go b/internal/sms-gateway/handlers/mobile.go index 505cb825..b46ee06e 100644 --- a/internal/sms-gateway/handlers/mobile.go +++ b/internal/sms-gateway/handlers/mobile.go @@ -13,15 +13,14 @@ import ( "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" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" "github.com/android-sms-gateway/server/internal/sms-gateway/users" - "github.com/capcom6/go-helpers/anys" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/keyauth" "github.com/jaevor/go-nanoid" + "github.com/samber/lo" "go.uber.org/zap" ) @@ -74,6 +73,57 @@ func newMobileHandler( } } +func (h *mobileHandler) Register(router fiber.Router) { + router = router.Group("/mobile/v1") + + router.Post("/device", + userauth.NewBasic(h.usersSvc), + userauth.NewCode(h.authSvc), + keyauth.New(keyauth.Config{ + Next: func(c *fiber.Ctx) bool { + // Skip server key authorization in the following cases: + // 1. Public mode is enabled - allowing open registration + // 2. User is already authenticated - allowing device registration for existing users + return h.authSvc.IsPublic() || userauth.HasUser(c) + }, + Validator: func(_ *fiber.Ctx, token string) (bool, error) { + err := h.authSvc.AuthorizeRegistration(token) + if err != nil { + return false, fmt.Errorf("authorization failed: %w", err) + } + + return true, nil + }, + }), + h.postDevice, + ) + + router.Get("/user/code", + userauth.NewBasic(h.usersSvc), + userauth.UserRequired(), + userauth.WithUserID(h.getUserCode), + ) + + router.Use( + deviceauth.New(h.authSvc), + ) + + router.Get("/device", deviceauth.WithDevice(h.getDevice)) + + router.Use(deviceauth.DeviceRequired()) + + router.Patch("/device", deviceauth.WithDevice(h.patchDevice)) + + // Should be under `userauth.NewBasic` protection instead of `deviceauth` + router.Patch("/user/password", deviceauth.WithDevice(h.changePassword)) + + h.messagesCtrl.Register(router.Group("/message")) + h.messagesCtrl.Register(router.Group("/messages")) + h.webhooksCtrl.Register(router.Group("/webhooks")) + h.settingsCtrl.Register(router.Group("/settings")) + h.eventsCtrl.Register(router.Group("/events")) +} + // @Summary Get device information // @Description Returns device information // @Tags Device @@ -83,14 +133,14 @@ func newMobileHandler( // @Router /mobile/v1/device [get] // // Get device information. -func (h *mobileHandler) getDevice(device models.Device, c *fiber.Ctx) error { +func (h *mobileHandler) getDevice(device devices.Device, c *fiber.Ctx) error { res := smsgateway.MobileDeviceResponse{ ExternalIP: c.IP(), Device: nil, } if !device.IsEmpty() { - res.Device = anys.AsPointer(converters.DeviceToDTO(device)) + res.Device = lo.ToPtr(converters.DeviceToDTO(device)) } return c.JSON(res) @@ -138,7 +188,17 @@ func (h *mobileHandler) postDevice(c *fiber.Ctx) error { } } - device, err := h.authSvc.RegisterDevice(userID, req.Name, req.PushToken) + device, err := h.authSvc.RegisterDevice( + c.Context(), + userID, + devices.DeviceInfo{ + DeviceUpdate: devices.DeviceUpdate{ + PushToken: req.PushToken, + SimCards: h.simCardsToDomain(req.SimCards), + }, + Name: req.Name, + }, + ) if err != nil { return fmt.Errorf("failed to register device: %w", err) } @@ -165,7 +225,7 @@ func (h *mobileHandler) postDevice(c *fiber.Ctx) error { // @Router /mobile/v1/device [patch] // // Update device. -func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error { +func (h *mobileHandler) patchDevice(device devices.Device, c *fiber.Ctx) error { req := new(smsgateway.MobileUpdateRequest) if err := h.BodyParserValidator(c, req); err != nil { @@ -176,9 +236,12 @@ func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error { return fiber.ErrForbidden } - if err := h.devicesSvc.UpdatePushToken(req.Id, req.PushToken); err != nil { - h.Logger.Error("failed to update device", zap.Error(err), zap.String("device_id", req.Id)) - return fiber.NewError(fiber.StatusInternalServerError, "failed to update device") + err := h.devicesSvc.Update(c.Context(), req.Id, devices.DeviceUpdate{ + PushToken: lo.EmptyableToPtr(req.PushToken), + SimCards: h.simCardsToDomain(req.SimCards), + }) + if err != nil { + return fmt.Errorf("failed to update device: %w", err) } return c.SendStatus(fiber.StatusNoContent) @@ -222,7 +285,7 @@ func (h *mobileHandler) getUserCode(userID string, c *fiber.Ctx) error { // @Router /mobile/v1/user/password [patch] // // Change password. -func (h *mobileHandler) changePassword(device models.Device, c *fiber.Ctx) error { +func (h *mobileHandler) changePassword(device devices.Device, c *fiber.Ctx) error { req := new(smsgateway.MobileChangePasswordRequest) if err := h.BodyParserValidator(c, req); err != nil { @@ -237,53 +300,14 @@ func (h *mobileHandler) changePassword(device models.Device, c *fiber.Ctx) error return c.SendStatus(fiber.StatusNoContent) } -func (h *mobileHandler) Register(router fiber.Router) { - router = router.Group("/mobile/v1") - - router.Post("/device", - userauth.NewBasic(h.usersSvc), - userauth.NewCode(h.authSvc), - keyauth.New(keyauth.Config{ - Next: func(c *fiber.Ctx) bool { - // Skip server key authorization in the following cases: - // 1. Public mode is enabled - allowing open registration - // 2. User is already authenticated - allowing device registration for existing users - return h.authSvc.IsPublic() || userauth.HasUser(c) - }, - Validator: func(_ *fiber.Ctx, token string) (bool, error) { - err := h.authSvc.AuthorizeRegistration(token) - if err != nil { - return false, fmt.Errorf("authorization failed: %w", err) - } - - return true, nil - }, - }), - h.postDevice, - ) - - router.Get("/user/code", - userauth.NewBasic(h.usersSvc), - userauth.UserRequired(), - userauth.WithUserID(h.getUserCode), - ) - - router.Use( - deviceauth.New(h.authSvc), - ) - - router.Get("/device", deviceauth.WithDevice(h.getDevice)) - - router.Use(deviceauth.DeviceRequired()) - - router.Patch("/device", deviceauth.WithDevice(h.patchDevice)) - - // Should be under `userauth.NewBasic` protection instead of `deviceauth` - router.Patch("/user/password", deviceauth.WithDevice(h.changePassword)) - - h.messagesCtrl.Register(router.Group("/message")) - h.messagesCtrl.Register(router.Group("/messages")) - h.webhooksCtrl.Register(router.Group("/webhooks")) - h.settingsCtrl.Register(router.Group("/settings")) - h.eventsCtrl.Register(router.Group("/events")) +func (h *mobileHandler) simCardsToDomain(simCards []smsgateway.SimCard) []devices.SimCard { + return lo.Map(simCards, func(sc smsgateway.SimCard, _ int) devices.SimCard { + return devices.SimCard{ + SlotIndex: sc.SlotIndex, + SimNumber: sc.SimNumber, + PhoneNumber: sc.PhoneNumber, + CarrierName: sc.CarrierName, + ICCID: sc.ICCID, + } + }) } diff --git a/internal/sms-gateway/handlers/settings/mobile.go b/internal/sms-gateway/handlers/settings/mobile.go index 38c4fb32..708f4a63 100644 --- a/internal/sms-gateway/handlers/settings/mobile.go +++ b/internal/sms-gateway/handlers/settings/mobile.go @@ -3,7 +3,6 @@ package settings import ( "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/go-playground/validator/v10" @@ -14,12 +13,10 @@ import ( type MobileController struct { base.Handler - devicesSvc *devices.Service settingsSvc *settings.Service } func NewMobileController( - devicesSvc *devices.Service, settingsSvc *settings.Service, logger *zap.Logger, validator *validator.Validate, @@ -29,7 +26,6 @@ func NewMobileController( Logger: logger, Validator: validator, }, - devicesSvc: devicesSvc, settingsSvc: settingsSvc, } } @@ -45,7 +41,7 @@ func NewMobileController( // @Router /mobile/v1/settings [get] // // Get settings. -func (h *MobileController) get(device models.Device, c *fiber.Ctx) error { +func (h *MobileController) get(device devices.Device, c *fiber.Ctx) error { settings, err := h.settingsSvc.GetSettings(device.UserID, false) if err != nil { h.Logger.Error( diff --git a/internal/sms-gateway/handlers/webhooks/3rdparty.go b/internal/sms-gateway/handlers/webhooks/3rdparty.go index 036e74fe..313f4a2c 100644 --- a/internal/sms-gateway/handlers/webhooks/3rdparty.go +++ b/internal/sms-gateway/handlers/webhooks/3rdparty.go @@ -84,7 +84,7 @@ func (h *ThirdPartyController) post(userID string, c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } - if err := h.webhooksSvc.Replace(userID, dto); err != nil { + if err := h.webhooksSvc.Replace(c.Context(), userID, dto); err != nil { if webhooks.IsValidationError(err) { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/internal/sms-gateway/handlers/webhooks/mobile.go b/internal/sms-gateway/handlers/webhooks/mobile.go index bfbaefd7..07e78aa5 100644 --- a/internal/sms-gateway/handlers/webhooks/mobile.go +++ b/internal/sms-gateway/handlers/webhooks/mobile.go @@ -5,7 +5,7 @@ import ( "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/webhooks" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -43,7 +43,7 @@ func NewMobileController( // @Router /mobile/v1/webhooks [get] // // List webhooks. -func (h *MobileController) get(device models.Device, c *fiber.Ctx) error { +func (h *MobileController) get(device devices.Device, c *fiber.Ctx) error { items, err := h.webhooksSvc.Select(device.UserID, webhooks.WithDeviceID(device.ID, false)) if err != nil { return fmt.Errorf("failed to select webhooks: %w", err) diff --git a/internal/sms-gateway/jwt/metrics.go b/internal/sms-gateway/jwt/metrics.go index b0f0a1fc..c5e6ddf2 100644 --- a/internal/sms-gateway/jwt/metrics.go +++ b/internal/sms-gateway/jwt/metrics.go @@ -7,6 +7,9 @@ import ( // Metric constants. const ( + metricsNamespace = "sms" + metricsSubsystem = "auth" + MetricTokensIssuedTotal = "jwt_tokens_issued_total" //nolint:gosec // false positive MetricTokensValidatedTotal = "jwt_tokens_validated_total" //nolint:gosec // false positive MetricTokensRevokedTotal = "jwt_tokens_revoked_total" //nolint:gosec // false positive @@ -37,67 +40,64 @@ type Metrics struct { // NewMetrics creates and initializes all JWT metrics. func NewMetrics() *Metrics { - const namespace = "sms" - const subsystem = "auth" - var defBuckets = []float64{.0005, .001, .0025, .005, .01, .025, .05, .1, .25, .5, 1} return &Metrics{ tokensIssuedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricTokensIssuedTotal, Help: "Total number of JWT tokens issued", }, []string{labelStatus}), tokensValidatedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricTokensValidatedTotal, Help: "Total number of JWT tokens validated", }, []string{labelStatus}), tokensRevokedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricTokensRevokedTotal, Help: "Total number of JWT tokens revoked", }, []string{labelStatus}), tokensRefreshedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricTokensRefreshedTotal, Help: "Total number of JWT tokens refreshed", }, []string{labelStatus}), issuanceDurationHistogram: promauto.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricIssuanceDurationSeconds, Help: "JWT issuance duration in seconds", Buckets: defBuckets, }), validationDurationHistogram: promauto.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricValidationDurationSeconds, Help: "JWT validation duration in seconds", Buckets: defBuckets, }), revocationDurationHistogram: promauto.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricRevocationDurationSeconds, Help: "JWT revocation duration in seconds", Buckets: defBuckets, }), refreshDurationHistogram: promauto.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricRefreshDurationSeconds, Help: "JWT refresh duration in seconds", Buckets: defBuckets, diff --git a/internal/sms-gateway/models/migration.go b/internal/sms-gateway/models/migration.go index e4d08a58..c2b36f9f 100644 --- a/internal/sms-gateway/models/migration.go +++ b/internal/sms-gateway/models/migration.go @@ -2,17 +2,7 @@ package models import ( "embed" - "fmt" - - "gorm.io/gorm" ) //go:embed migrations var migrations embed.FS - -func Migrate(db *gorm.DB) error { - if err := db.AutoMigrate(new(Device)); err != nil { - return fmt.Errorf("models migration failed: %w", err) - } - return nil -} diff --git a/internal/sms-gateway/models/migrations/mysql/20260507035019_device_sim_cards.sql b/internal/sms-gateway/models/migrations/mysql/20260507035019_device_sim_cards.sql new file mode 100644 index 00000000..29faaa9f --- /dev/null +++ b/internal/sms-gateway/models/migrations/mysql/20260507035019_device_sim_cards.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE `devices` +ADD `sim_cards` json NOT NULL DEFAULT '[]'; +-- +goose StatementEnd +--- +-- +goose Down +-- +goose StatementBegin +ALTER TABLE `devices` DROP `sim_cards`; +-- +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 b719c33c..bde3796e 100644 --- a/internal/sms-gateway/models/models.go +++ b/internal/sms-gateway/models/models.go @@ -14,32 +14,3 @@ type SoftDeletableModel struct { DeletedAt *time.Time `gorm:"<-:update"` } - -type Device struct { - SoftDeletableModel - - ID string `gorm:"primaryKey;type:char(21)"` - Name *string `gorm:"type:varchar(128)"` - AuthToken string `gorm:"not null;uniqueIndex;type:char(21)"` - PushToken *string `gorm:"type:varchar(256)"` - - LastSeen time.Time `gorm:"not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3);index:idx_devices_last_seen"` - - UserID string `gorm:"not null;type:varchar(32)"` -} - -func NewDevice(name, pushToken *string) *Device { - //nolint:exhaustruct // partial constructor - return &Device{ - Name: name, - PushToken: pushToken, - } -} - -func (d *Device) IsEmpty() bool { - if d == nil { - return true - } - - return d.ID == "" -} diff --git a/internal/sms-gateway/models/models_test.go b/internal/sms-gateway/models/models_test.go deleted file mode 100644 index 2ce82db0..00000000 --- a/internal/sms-gateway/models/models_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/android-sms-gateway/server/internal/sms-gateway/models" -) - -func TestDevice_IsEmpty(t *testing.T) { - tests := []struct { - name string - d *models.Device - want bool - }{ - { - name: "nil Device", - d: nil, - want: true, - }, - { - name: "empty ID", - d: &models.Device{ - ID: "", - }, - want: true, - }, - { - name: "non-empty ID", - d: &models.Device{ - ID: "some-id", - }, - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.d.IsEmpty(); got != tt.want { - t.Errorf("IsEmpty() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/sms-gateway/models/module.go b/internal/sms-gateway/models/module.go index 62582b38..6566ec4c 100644 --- a/internal/sms-gateway/models/module.go +++ b/internal/sms-gateway/models/module.go @@ -6,6 +6,5 @@ import ( //nolint:gochecknoinits // framework-specific func init() { - db.RegisterMigration(Migrate) db.RegisterGoose(migrations) } diff --git a/internal/sms-gateway/modules/auth/service.go b/internal/sms-gateway/modules/auth/service.go index 09f9a7db..07a56245 100644 --- a/internal/sms-gateway/modules/auth/service.go +++ b/internal/sms-gateway/modules/auth/service.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - "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/online" "github.com/android-sms-gateway/server/internal/sms-gateway/otp" @@ -61,13 +60,14 @@ func (s *Service) GenerateUserCode(ctx context.Context, userID string) (*otp.Cod return code, nil } -func (s *Service) RegisterDevice(userID string, name, pushToken *string) (*models.Device, error) { - device := models.NewDevice( - name, - pushToken, - ) +func (s *Service) RegisterDevice( + ctx context.Context, + userID string, + info devices.DeviceInfo, +) (*devices.Device, error) { + device, err := s.devicesSvc.Insert(ctx, userID, info) - if err := s.devicesSvc.Insert(userID, device); err != nil { + if err != nil { return device, fmt.Errorf("failed to create device: %w", err) } @@ -90,17 +90,18 @@ func (s *Service) AuthorizeRegistration(token string) error { return ErrAuthorizationFailed } -func (s *Service) AuthorizeDevice(token string) (models.Device, error) { - device, err := s.devicesSvc.GetByToken(token) +func (s *Service) AuthorizeDevice(ctx context.Context, token string) (*devices.Device, error) { + device, err := s.devicesSvc.GetByToken(ctx, token) if err != nil { return device, fmt.Errorf("%w: %w", ErrAuthorizationFailed, err) } + //nolint:gosec // background online-update goroutine go func(id string) { const timeout = 5 * time.Second - ctx, cancel := context.WithTimeout(context.Background(), timeout) + subCtx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - s.onlineSvc.SetOnline(ctx, id) + s.onlineSvc.SetOnline(subCtx, id) }(device.ID) device.LastSeen = time.Now() diff --git a/internal/sms-gateway/modules/devices/cache.go b/internal/sms-gateway/modules/devices/cache.go index 2f33cee2..c90f830d 100644 --- a/internal/sms-gateway/modules/devices/cache.go +++ b/internal/sms-gateway/modules/devices/cache.go @@ -5,25 +5,24 @@ import ( "fmt" "time" - "github.com/android-sms-gateway/server/internal/sms-gateway/models" cacheImpl "github.com/capcom6/go-helpers/cache" ) type cache struct { - byID *cacheImpl.Cache[*models.Device] - byToken *cacheImpl.Cache[*models.Device] + byID *cacheImpl.Cache[*Device] + byToken *cacheImpl.Cache[*Device] } func newCache() *cache { const ttl = 10 * time.Minute return &cache{ - byID: cacheImpl.New[*models.Device](cacheImpl.Config{TTL: ttl}), - byToken: cacheImpl.New[*models.Device](cacheImpl.Config{TTL: ttl}), + byID: cacheImpl.New[*Device](cacheImpl.Config{TTL: ttl}), + byToken: cacheImpl.New[*Device](cacheImpl.Config{TTL: ttl}), } } -func (c *cache) Set(device models.Device) error { +func (c *cache) Set(device Device) error { err := errors.Join(c.byID.Set(device.ID, &device), c.byToken.Set(device.AuthToken, &device)) if err != nil { return fmt.Errorf("failed to cache device: %w", err) @@ -32,19 +31,19 @@ func (c *cache) Set(device models.Device) error { return nil } -func (c *cache) GetByID(id string) (models.Device, error) { +func (c *cache) GetByID(id string) (Device, error) { device, err := c.byID.Get(id) if err != nil { - return models.Device{}, fmt.Errorf("failed to get device by ID: %w", err) + return Device{}, fmt.Errorf("failed to get device by ID: %w", err) } return *device, nil } -func (c *cache) GetByToken(token string) (models.Device, error) { +func (c *cache) GetByToken(token string) (Device, error) { device, err := c.byToken.Get(token) if err != nil { - return models.Device{}, fmt.Errorf("failed to get device by token: %w", err) + return Device{}, fmt.Errorf("failed to get device by token: %w", err) } return *device, nil diff --git a/internal/sms-gateway/modules/devices/domain.go b/internal/sms-gateway/modules/devices/domain.go new file mode 100644 index 00000000..b9c4de8e --- /dev/null +++ b/internal/sms-gateway/modules/devices/domain.go @@ -0,0 +1,44 @@ +package devices + +import "time" + +type DeviceInput struct { + DeviceInfo + + ID string + UserID string + + AuthToken string `json:"-"` +} + +type DeviceInfo struct { + DeviceUpdate + + Name *string +} + +type DeviceUpdate struct { + PushToken *string + SimCards []SimCard +} + +type Device struct { + DeviceInput + + LastSeen time.Time + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time +} + +func (d Device) IsEmpty() bool { + return d.ID == "" +} + +type SimCard struct { + SlotIndex int // Zero-based index of the physical SIM slot (0, 1, ...). + SimNumber int // One-based number used by the application. + PhoneNumber *string + CarrierName *string + ICCID *string +} diff --git a/internal/sms-gateway/modules/devices/models.go b/internal/sms-gateway/modules/devices/models.go new file mode 100644 index 00000000..fa0ff37b --- /dev/null +++ b/internal/sms-gateway/modules/devices/models.go @@ -0,0 +1,105 @@ +package devices + +import ( + "fmt" + "time" + + "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/samber/lo" + "gorm.io/gorm" +) + +type DeviceModel struct { + models.SoftDeletableModel + + ID string `gorm:"primaryKey;type:char(21)"` + Name *string `gorm:"type:varchar(128)"` + AuthToken string `gorm:"not null;uniqueIndex;type:char(21)"` + PushToken *string `gorm:"type:varchar(256)"` + + LastSeen time.Time `gorm:"not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3);index:idx_devices_last_seen"` + + UserID string `gorm:"not null;type:varchar(32)"` + + SimCards []simCardModel `gorm:"serializer:json;type:json"` +} + +func newDeviceModel(device DeviceInput) *DeviceModel { + now := time.Now() + return &DeviceModel{ + SoftDeletableModel: models.SoftDeletableModel{ + TimedModel: models.TimedModel{ + CreatedAt: now, + UpdatedAt: now, + }, + DeletedAt: nil, + }, + + ID: device.ID, + Name: device.Name, + AuthToken: device.AuthToken, + PushToken: device.PushToken, + LastSeen: now, + UserID: device.UserID, + SimCards: lo.Map( + device.SimCards, + func(simCard SimCard, _ int) simCardModel { return newSimCardModel(simCard) }, + ), + } +} + +func (*DeviceModel) TableName() string { + return "devices" +} + +func (m *DeviceModel) toDomain() *Device { + if m == nil { + return nil + } + + return &Device{ + DeviceInput: DeviceInput{ + DeviceInfo: DeviceInfo{ + DeviceUpdate: DeviceUpdate{ + PushToken: m.PushToken, + SimCards: lo.Map(m.SimCards, func(m simCardModel, _ int) SimCard { return m.toDomain() }), + }, + + Name: m.Name, + }, + + ID: m.ID, + UserID: m.UserID, + + AuthToken: m.AuthToken, + }, + + LastSeen: m.LastSeen, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + DeletedAt: m.DeletedAt, + } +} + +type simCardModel struct { + SlotIndex int `json:"slotIndex"` + SimNumber int `json:"simNumber"` + PhoneNumber *string `json:"phoneNumber,omitempty"` + CarrierName *string `json:"carrierName,omitempty"` + ICCID *string `json:"iccid,omitempty"` +} + +func newSimCardModel(simCard SimCard) simCardModel { + return simCardModel(simCard) +} + +func (m simCardModel) toDomain() SimCard { + return SimCard(m) +} + +func Migrate(db *gorm.DB) error { + if err := db.AutoMigrate(new(DeviceModel)); err != nil { + return fmt.Errorf("devices migration failed: %w", err) + } + return nil +} diff --git a/internal/sms-gateway/modules/devices/module.go b/internal/sms-gateway/modules/devices/module.go index 445d1ae9..a57101d7 100644 --- a/internal/sms-gateway/modules/devices/module.go +++ b/internal/sms-gateway/modules/devices/module.go @@ -1,6 +1,7 @@ package devices import ( + "github.com/capcom6/go-infra-fx/db" "github.com/go-core-fx/logger" "go.uber.org/fx" ) @@ -16,3 +17,8 @@ func Module() fx.Option { fx.Provide(NewService), ) } + +//nolint:gochecknoinits // framework-specific +func init() { + db.RegisterMigration(Migrate) +} diff --git a/internal/sms-gateway/modules/devices/repository.go b/internal/sms-gateway/modules/devices/repository.go index 070601b9..13617d78 100644 --- a/internal/sms-gateway/modules/devices/repository.go +++ b/internal/sms-gateway/modules/devices/repository.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/samber/lo" "gorm.io/gorm" ) @@ -26,15 +26,18 @@ func NewRepository(db *gorm.DB) *Repository { } } -func (r *Repository) Select(filter ...SelectFilter) ([]models.Device, error) { +func (r *Repository) Select(ctx context.Context, filter ...SelectFilter) ([]Device, error) { if len(filter) == 0 { return nil, ErrInvalidFilter } f := newFilter(filter...) - devices := []models.Device{} + devices := []DeviceModel{} + if err := f.apply(r.db.WithContext(ctx)).Find(&devices).Error; err != nil { + return nil, fmt.Errorf("failed to select devices: %w", err) + } - return devices, f.apply(r.db).Find(&devices).Error + return lo.Map(devices, func(m DeviceModel, _ int) Device { return *m.toDomain() }), nil } // Exists checks if there exists a device with the given filters. @@ -42,8 +45,8 @@ func (r *Repository) Select(filter ...SelectFilter) ([]models.Device, error) { // 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(new(models.Device)).Error +func (r *Repository) Exists(ctx context.Context, filters ...SelectFilter) (bool, error) { + err := newFilter(filters...).apply(r.db.WithContext(ctx)).Take(new(DeviceModel)).Error if errors.Is(err, gorm.ErrRecordNotFound) { return false, nil } @@ -53,31 +56,48 @@ func (r *Repository) Exists(filters ...SelectFilter) (bool, error) { return true, nil } -func (r *Repository) Get(filter ...SelectFilter) (models.Device, error) { - devices, err := r.Select(filter...) +func (r *Repository) Get(ctx context.Context, filter ...SelectFilter) (*Device, error) { + devices, err := r.Select(ctx, filter...) if err != nil { - return models.Device{}, fmt.Errorf("failed to get device: %w", err) + return nil, fmt.Errorf("failed to get device: %w", err) } if len(devices) == 0 { - return models.Device{}, ErrNotFound + return nil, ErrNotFound } if len(devices) > 1 { - return models.Device{}, ErrMoreThanOne + return nil, ErrMoreThanOne } - return devices[0], nil + return &devices[0], nil } -func (r *Repository) Insert(device *models.Device) error { - return r.db.Create(device).Error +func (r *Repository) Insert(ctx context.Context, device DeviceInput) (*Device, error) { + model := newDeviceModel(device) + + if err := r.db.WithContext(ctx).Create(model).Error; err != nil { + return nil, fmt.Errorf("failed to insert device: %w", err) + } + + return model.toDomain(), nil } -func (r *Repository) UpdatePushToken(id string, token *string) error { - res := r.db.Model((*models.Device)(nil)).Where("id = ?", id).Update("push_token", token) - if res.Error != nil { - return fmt.Errorf("failed to update device: %w", res.Error) +func (r *Repository) Update(ctx context.Context, id string, device DeviceUpdate) error { + err := r.db. + WithContext(ctx). + Model((*DeviceModel)(nil)). + Where("id = ?", id). + Updates(map[string]any{ + "push_token": device.PushToken, + "sim_cards": lo.Map( + device.SimCards, + func(simCard SimCard, _ int) simCardModel { return newSimCardModel(simCard) }, + ), + }). + Error + if err != nil { + return fmt.Errorf("failed to update device: %w", err) } return nil @@ -87,32 +107,34 @@ func (r *Repository) SetLastSeen(ctx context.Context, id string, lastSeen time.T if lastSeen.IsZero() { return nil // ignore zero timestamps } - res := r.db.WithContext(ctx). - Model((*models.Device)(nil)). + + err := r.db. + WithContext(ctx). + Model((*DeviceModel)(nil)). Where("id = ? AND last_seen < ?", id, lastSeen). - UpdateColumn("last_seen", lastSeen) - if res.Error != nil { - return res.Error + UpdateColumn("last_seen", lastSeen). + Error + if err != nil { + return fmt.Errorf("failed to set last seen: %w", err) } - // RowsAffected==0 => not found or stale timestamp; treat as no-op. return nil } -func (r *Repository) Remove(filter ...SelectFilter) error { +func (r *Repository) Remove(ctx context.Context, filter ...SelectFilter) error { if len(filter) == 0 { return ErrInvalidFilter } f := newFilter(filter...) - return f.apply(r.db).Delete(new(models.Device)).Error + return f.apply(r.db.WithContext(ctx)).Delete(new(DeviceModel)).Error } func (r *Repository) Cleanup(ctx context.Context, until time.Time) (int64, error) { res := r.db. WithContext(ctx). Where("last_seen < ?", until). - Delete(new(models.Device)) + Delete(new(DeviceModel)) return res.RowsAffected, res.Error } diff --git a/internal/sms-gateway/modules/devices/service.go b/internal/sms-gateway/modules/devices/service.go index 13aca073..eaac32cd 100644 --- a/internal/sms-gateway/modules/devices/service.go +++ b/internal/sms-gateway/modules/devices/service.go @@ -7,9 +7,7 @@ import ( "math/rand/v2" "time" - "github.com/android-sms-gateway/server/internal/sms-gateway/models" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/db" - "github.com/samber/lo" "go.uber.org/zap" ) @@ -42,19 +40,22 @@ func NewService( } } -func (s *Service) Insert(userID string, device *models.Device) error { - device.ID = s.idGen() - device.AuthToken = s.idGen() - device.UserID = userID +func (s *Service) Insert(ctx context.Context, userID string, device DeviceInfo) (*Device, error) { + input := DeviceInput{ + DeviceInfo: device, + ID: s.idGen(), + UserID: userID, + AuthToken: s.idGen(), + } - return s.devices.Insert(device) + return s.devices.Insert(ctx, input) } // Select returns a list of devices for a specific user that match the provided filters. -func (s *Service) Select(userID string, filter ...SelectFilter) ([]models.Device, error) { +func (s *Service) Select(ctx context.Context, userID string, filter ...SelectFilter) ([]Device, error) { filter = append(filter, WithUserID(userID)) - return s.devices.Select(filter...) + return s.devices.Select(ctx, filter...) } // Exists checks if there exists a device that matches the provided filters. @@ -62,23 +63,23 @@ func (s *Service) Select(userID string, filter ...SelectFilter) ([]models.Device // 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) { +func (s *Service) Exists(ctx context.Context, userID string, filter ...SelectFilter) (bool, error) { filter = append(filter, WithUserID(userID)) - return s.devices.Exists(filter...) + return s.devices.Exists(ctx, 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 // ErrMoreThanOne. -func (s *Service) Get(userID string, filter ...SelectFilter) (models.Device, error) { +func (s *Service) Get(ctx context.Context, userID string, filter ...SelectFilter) (*Device, error) { filter = append(filter, WithUserID(userID)) - return s.devices.Get(filter...) + return s.devices.Get(ctx, filter...) } -func (s *Service) GetAny(userID string, deviceID string, duration time.Duration) (*models.Device, error) { +func (s *Service) GetAny(ctx context.Context, userID string, deviceID string, duration time.Duration) (*Device, error) { filter := []SelectFilter{ WithUserID(userID), } @@ -89,7 +90,7 @@ func (s *Service) GetAny(userID string, deviceID string, duration time.Duration) filter = append(filter, ActiveWithin(duration)) } - devices, err := s.devices.Select(filter...) + devices, err := s.devices.Select(ctx, filter...) if err != nil { return nil, err } @@ -111,34 +112,36 @@ func (s *Service) GetAny(userID string, deviceID string, duration time.Duration) // // This method is used to retrieve a device by its auth token. If the device // does not exist, it returns ErrNotFound. -func (s *Service) GetByToken(token string) (models.Device, error) { +func (s *Service) GetByToken(ctx context.Context, token string) (*Device, error) { device, err := s.cache.GetByToken(token) + if err == nil { + return &device, nil + } + + devicePtr, err := s.devices.Get(ctx, WithToken(token)) if err != nil { - device, err = s.devices.Get(WithToken(token)) - if err != nil { - return device, err - } + return nil, err + } - if setErr := s.cache.Set(device); setErr != nil { - s.logger.Error("failed to cache device", zap.String("device_id", device.ID), zap.Error(setErr)) - } + if setErr := s.cache.Set(*devicePtr); setErr != nil { + s.logger.Error("failed to cache device", zap.String("device_id", devicePtr.ID), zap.Error(setErr)) } - return device, nil + return devicePtr, nil } -func (s *Service) UpdatePushToken(id string, token string) error { - if err := s.cache.DeleteByID(id); err != nil { +func (s *Service) Update(ctx context.Context, id string, device DeviceUpdate) error { + if err := s.devices.Update(ctx, id, device); err != nil { + return err + } + + if cacheErr := s.cache.DeleteByID(id); cacheErr != nil { s.logger.Error("failed to invalidate cache", zap.String("device_id", id), - zap.Error(err), + zap.Error(cacheErr), ) } - if err := s.devices.UpdatePushToken(id, lo.EmptyableToPtr(token)); err != nil { - return err - } - return nil } @@ -166,10 +169,10 @@ func (s *Service) SetLastSeen(ctx context.Context, batch map[string]time.Time) e // Remove removes devices for a specific user that match the provided filters. // It ensures that the filter includes the user's ID. -func (s *Service) Remove(userID string, filter ...SelectFilter) error { +func (s *Service) Remove(ctx context.Context, userID string, filter ...SelectFilter) error { filter = append(filter, WithUserID(userID)) - devices, err := s.devices.Select(filter...) + devices, err := s.devices.Select(ctx, filter...) if err != nil { return err } @@ -186,7 +189,7 @@ func (s *Service) Remove(userID string, filter ...SelectFilter) error { } } - if rmErr := s.devices.Remove(filter...); rmErr != nil { + if rmErr := s.devices.Remove(ctx, filter...); rmErr != nil { return rmErr } diff --git a/internal/sms-gateway/modules/events/metrics.go b/internal/sms-gateway/modules/events/metrics.go index 9b6bc423..84e64e17 100644 --- a/internal/sms-gateway/modules/events/metrics.go +++ b/internal/sms-gateway/modules/events/metrics.go @@ -7,6 +7,9 @@ import ( // Metric constants. const ( + metricsNamespace = "sms" + metricsSubsystem = "events" + MetricEnqueuedTotal = "enqueued_total" MetricSentTotal = "sent_total" MetricFailedTotal = "failed_total" @@ -35,25 +38,22 @@ type metrics struct { // newMetrics creates and initializes all events metrics. func newMetrics() *metrics { - const namespace = "sms" - const subsystem = "events" - return &metrics{ enqueuedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricEnqueuedTotal, Help: "Total number of events enqueued", }, []string{LabelEvent}), sentCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricSentTotal, Help: "Total number of events sent", }, []string{LabelEvent, LabelDeliveryType}), failedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricFailedTotal, Help: "Total number of failed notifications", }, []string{LabelEvent, LabelDeliveryType, LabelReason}), diff --git a/internal/sms-gateway/modules/events/service.go b/internal/sms-gateway/modules/events/service.go index 8ee7b766..2d2f2418 100644 --- a/internal/sms-gateway/modules/events/service.go +++ b/internal/sms-gateway/modules/events/service.go @@ -105,19 +105,19 @@ func (s *Service) Run(ctx context.Context) error { s.logger.Error("failed to deserialize event wrapper", zap.Error(jsonErr)) continue } - s.processEvent(wrapper) + s.processEvent(ctx, wrapper) } } } -func (s *Service) processEvent(wrapper *eventWrapper) { +func (s *Service) processEvent(ctx context.Context, wrapper *eventWrapper) { // Load devices from database filters := []devices.SelectFilter{} if wrapper.DeviceID != nil { filters = append(filters, devices.WithID(*wrapper.DeviceID)) } - devices, err := s.deviceSvc.Select(wrapper.UserID, filters...) + devices, err := s.deviceSvc.Select(ctx, wrapper.UserID, filters...) if err != nil { s.logger.Error("failed to select devices", zap.String("user_id", wrapper.UserID), zap.Error(err)) return diff --git a/internal/sms-gateway/modules/messages/models.go b/internal/sms-gateway/modules/messages/models.go index 771ebc54..37f51738 100644 --- a/internal/sms-gateway/modules/messages/models.go +++ b/internal/sms-gateway/modules/messages/models.go @@ -7,6 +7,7 @@ import ( "github.com/android-sms-gateway/client-go/smsgateway" "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" "github.com/samber/lo" "gorm.io/gorm" ) @@ -43,7 +44,7 @@ type messageModel struct { IsHashed bool `gorm:"not null;type:tinyint(1) unsigned;default:0"` IsEncrypted bool `gorm:"not null;type:tinyint(1) unsigned;default:0"` - Device models.Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"` + Device devices.DeviceModel `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"` Recipients []messageRecipientModel `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"` States []messageStateModel `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"` } diff --git a/internal/sms-gateway/modules/messages/service.go b/internal/sms-gateway/modules/messages/service.go index a6e3587a..d7007816 100644 --- a/internal/sms-gateway/modules/messages/service.go +++ b/internal/sms-gateway/modules/messages/service.go @@ -9,8 +9,8 @@ import ( "time" "github.com/android-sms-gateway/client-go/smsgateway" - "github.com/android-sms-gateway/server/internal/sms-gateway/models" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/db" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/events" "github.com/capcom6/go-helpers/anys" "github.com/capcom6/go-helpers/slices" @@ -90,7 +90,7 @@ func (s *Service) SelectPending(deviceID string, order Order) ([]Message, error) return slices.MapOrError(messages, messageToDomain) //nolint:wrapcheck // already wrapped } -func (s *Service) UpdateState(device *models.Device, message MessageStateInput) error { +func (s *Service) UpdateState(device *devices.Device, message MessageStateInput) error { existing, err := s.messages.get( *new(SelectFilter).WithExtID(message.ID).WithDeviceID(device.ID), *new(SelectOptions).IncludeContent(), @@ -206,7 +206,7 @@ func (s *Service) GetState(userID string, id string) (*MessageState, error) { func (s *Service) Enqueue( ctx context.Context, - device models.Device, + device devices.Device, message MessageInput, opts EnqueueOptions, ) (*MessageState, error) { @@ -258,7 +258,7 @@ func (s *Service) Enqueue( } func (s *Service) prepareMessage( - device models.Device, + device devices.Device, message MessageInput, opts EnqueueOptions, ) (*messageModel, error) { diff --git a/internal/sms-gateway/modules/push/metrics.go b/internal/sms-gateway/modules/push/metrics.go index 6bcfac19..53638b8f 100644 --- a/internal/sms-gateway/modules/push/metrics.go +++ b/internal/sms-gateway/modules/push/metrics.go @@ -8,6 +8,9 @@ import ( type BlacklistOperation string const ( + metricsNamespace = "sms" + metricsSubsystem = "push" + BlacklistOperationAdded BlacklistOperation = "added" BlacklistOperationSkipped BlacklistOperation = "skipped" ) @@ -20,34 +23,31 @@ type metrics struct { } func newMetrics() *metrics { - const namespace = "sms" - const subsystem = "push" - return &metrics{ enqueuedCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: "enqueued_total", Help: "Total number of messages enqueued", }, []string{"event"}), retriesCounter: promauto.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: "retries_total", Help: "Total retry attempts", }), blacklistCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: "blacklist_total", Help: "Blacklist operations", }, []string{"operation"}), errorsCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: "errors_total", Help: "Total number of errors", }, []string{}), diff --git a/internal/sms-gateway/modules/sse/metrics.go b/internal/sms-gateway/modules/sse/metrics.go index 5c845e23..5042a143 100644 --- a/internal/sms-gateway/modules/sse/metrics.go +++ b/internal/sms-gateway/modules/sse/metrics.go @@ -7,6 +7,9 @@ import ( // Metric constants. const ( + metricsNamespace = "sms" + metricsSubsystem = "sse" + MetricActiveConnections = "active_connections" MetricEventsSent = "events_sent_total" MetricConnectionErrors = "connection_errors_total" @@ -33,39 +36,37 @@ type metrics struct { // newMetrics creates and initializes all SSE metrics. func newMetrics() *metrics { - const namespace = "sms" - const subsystem = "sse" var defBuckets = []float64{1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, .001, .005, .01, .05, .1} metrics := &metrics{ activeConnections: promauto.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricActiveConnections, Help: "Current number of active SSE connections", }, []string{}), eventsSent: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricEventsSent, Help: "Total number of SSE events sent, labeled by event type", }, []string{LabelEventType}), connectionErrors: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricConnectionErrors, Help: "Total number of SSE connection errors, labeled by error type", }, []string{LabelErrorType}), eventDeliveryLatency: promauto.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricEventLatency, Help: "Event delivery latency in seconds", Buckets: defBuckets, }, []string{}), keepalivesSent: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: MetricKeepalivesSent, Help: "Total keepalive messages sent", }, []string{}), diff --git a/internal/sms-gateway/modules/webhooks/models.go b/internal/sms-gateway/modules/webhooks/models.go index 15c7736c..13433db1 100644 --- a/internal/sms-gateway/modules/webhooks/models.go +++ b/internal/sms-gateway/modules/webhooks/models.go @@ -5,6 +5,7 @@ import ( "github.com/android-sms-gateway/client-go/smsgateway" "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/users" "gorm.io/gorm" ) @@ -16,13 +17,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"` + DeviceID *string `json:"device_id,omitempty" gorm:"type:char(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 users.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"` - Device *models.Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"` + User users.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"` + Device *devices.DeviceModel `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"` } func newWebhook(extID string, url string, event smsgateway.WebhookEvent, userID string, deviceID *string) *Webhook { diff --git a/internal/sms-gateway/modules/webhooks/service.go b/internal/sms-gateway/modules/webhooks/service.go index 28dc5b39..8ae7bc4a 100644 --- a/internal/sms-gateway/modules/webhooks/service.go +++ b/internal/sms-gateway/modules/webhooks/service.go @@ -1,6 +1,7 @@ package webhooks import ( + "context" "fmt" "github.com/android-sms-gateway/client-go/smsgateway" @@ -69,7 +70,7 @@ func (s *Service) Select(userID string, filters ...SelectFilter) ([]smsgateway.W // Replace creates or updates a webhook for a given user. After replacing the webhook, // it asynchronously notifies all the user's devices. Returns an error if the operation fails. -func (s *Service) Replace(userID string, webhook *smsgateway.Webhook) error { +func (s *Service) Replace(ctx context.Context, userID string, webhook *smsgateway.Webhook) error { if !smsgateway.IsValidWebhookEvent(webhook.Event) { return newValidationError("event", webhook.Event, ErrInvalidEvent) } @@ -80,7 +81,7 @@ func (s *Service) Replace(userID string, webhook *smsgateway.Webhook) error { // Check device ownership if deviceID is provided if webhook.DeviceID != nil { - ok, err := s.devicesSvc.Exists(userID, devices.WithID(*webhook.DeviceID)) + ok, err := s.devicesSvc.Exists(ctx, userID, devices.WithID(*webhook.DeviceID)) if err != nil { return fmt.Errorf("failed to verify device ownership: %w", err) } diff --git a/internal/sms-gateway/online/metrics.go b/internal/sms-gateway/online/metrics.go index 007e608b..19f50147 100644 --- a/internal/sms-gateway/online/metrics.go +++ b/internal/sms-gateway/online/metrics.go @@ -7,6 +7,9 @@ import ( // Metric constants. const ( + metricsNamespace = "sms" + metricsSubsystem = "online" + metricStatusSetTotal = "status_set_total" metricCacheOperations = "cache_operations_total" metricCacheLatency = "cache_latency_seconds" @@ -36,52 +39,50 @@ type metrics struct { // newMetrics creates and initializes all online metrics. func newMetrics() *metrics { - const namespace = "sms" - const subsystem = "online" var memBuckets = []float64{1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, .001, .005, .01, .05, .1} var dbBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} return &metrics{ statusSetCounter: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: metricStatusSetTotal, Help: "Total number of online status updates", }, []string{labelStatus}), cacheOperations: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: metricCacheOperations, Help: "Total cache operations by type", }, []string{labelOperation, labelStatus}), cacheLatency: promauto.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: metricCacheLatency, Help: "Cache operation latency in seconds", Buckets: memBuckets, }), persistenceLatency: promauto.NewHistogram(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: metricPersistenceLatency, Help: "Persistence operation latency in seconds", Buckets: dbBuckets, }), persistenceErrors: promauto.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: metricPersistenceErrors, Help: "Total persistence errors by type", }), batchSize: promauto.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: metricBatchSize, Help: "Current batch size", }), diff --git a/internal/sms-gateway/openapi/docs.go b/internal/sms-gateway/openapi/docs.go index 0549f910..626676bd 100644 --- a/internal/sms-gateway/openapi/docs.go +++ b/internal/sms-gateway/openapi/docs.go @@ -853,7 +853,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/smsgateway.MessagesExportRequest" + "$ref": "#/definitions/smsgateway.InboxRefreshRequest" } } ], @@ -1406,32 +1406,39 @@ const docTemplate = `{ "type": "object", "properties": { "createdAt": { - "description": "Created at (read only)", + "description": "Time at which the device was created, read only.", "type": "string", "example": "2020-01-01T00:00:00Z" }, "deletedAt": { - "description": "Deleted at (read only)", + "description": "Time at which the device was deleted, read only.", "type": "string", "example": "2020-01-01T00:00:00Z" }, "id": { - "description": "ID", + "description": "Device ID, read only.", "type": "string", "example": "PyDmBQZZXYmyxMwED8Fzy" }, "lastSeen": { - "description": "Last seen at (read only)", + "description": "Time at which the device was last seen, read only.", "type": "string", "example": "2020-01-01T00:00:00Z" }, "name": { - "description": "Name", + "description": "Device name.", "type": "string", "example": "My Device" }, + "simCards": { + "description": "List of SIM cards in the device.", + "type": "array", + "items": { + "$ref": "#/definitions/smsgateway.SimCard" + } + }, "updatedAt": { - "description": "Updated at (read only)", + "description": "Time at which the device was last updated, read only.", "type": "string", "example": "2020-01-01T00:00:00Z" } @@ -1673,6 +1680,45 @@ const docTemplate = `{ "HealthStatusFail" ] }, + "smsgateway.InboxRefreshRequest": { + "type": "object", + "required": [ + "deviceId", + "since", + "until" + ], + "properties": { + "deviceId": { + "description": "DeviceID is the ID of the device to export messages for.", + "type": "string", + "maxLength": 21, + "example": "PyDmBQZZXYmyxMwED8Fzy" + }, + "messageTypes": { + "description": "MessageTypes is the list of message types to export. By default, SMS messages are exported.", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/smsgateway.IncomingMessageType" + } + }, + "since": { + "description": "Since is the start of the time range to export.", + "type": "string", + "example": "2024-01-01T00:00:00Z" + }, + "triggerWebhooks": { + "description": "TriggerWebhooks indicates whether to trigger webhooks for the exported messages.", + "type": "boolean", + "example": true + }, + "until": { + "description": "Until is the end of the time range to export.", + "type": "string", + "example": "2024-01-01T23:59:59Z" + } + } + }, "smsgateway.IncomingMessage": { "type": "object", "required": [ @@ -2038,11 +2084,24 @@ const docTemplate = `{ "maxLength": 21, "example": "PyDmBQZZXYmyxMwED8Fzy" }, + "messageTypes": { + "description": "MessageTypes is the list of message types to export. By default, SMS messages are exported.", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/smsgateway.IncomingMessageType" + } + }, "since": { "description": "Since is the start of the time range to export.", "type": "string", "example": "2024-01-01T00:00:00Z" }, + "triggerWebhooks": { + "description": "TriggerWebhooks indicates whether to trigger webhooks for the exported messages.", + "type": "boolean", + "example": true + }, "until": { "description": "Until is the end of the time range to export.", "type": "string", @@ -2254,6 +2313,31 @@ const docTemplate = `{ } } }, + "smsgateway.SimCard": { + "type": "object", + "properties": { + "carrierName": { + "description": "Carrier/network operator name (may be null).", + "type": "string" + }, + "iccid": { + "description": "Integrated Circuit Card Identifier (may be null).", + "type": "string" + }, + "phoneNumber": { + "description": "Phone number associated with the SIM.", + "type": "string" + }, + "simNumber": { + "description": "1-based slot number (1, 2, or 3).", + "type": "integer" + }, + "slotIndex": { + "description": "0-based physical slot index.", + "type": "integer" + } + } + }, "smsgateway.SimSelectionMode": { "type": "string", "enum": [ diff --git a/internal/worker/executor/metrics.go b/internal/worker/executor/metrics.go index 216e5c4d..8d46881a 100644 --- a/internal/worker/executor/metrics.go +++ b/internal/worker/executor/metrics.go @@ -10,6 +10,9 @@ import ( type metricsTaskResult string const ( + metricsNamespace = "worker" + metricsSubsystem = "executor" + metricsTaskResultSuccess metricsTaskResult = "success" metricsTaskResultError metricsTaskResult = "error" ) @@ -21,27 +24,24 @@ type metrics struct { } func newMetrics() *metrics { - const namespace = "worker" - const subsystem = "executor" - var defBuckets = []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25} return &metrics{ activeTasksCounter: promauto.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: "active_tasks", Help: "Number of active tasks", }), taskResult: promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: "task_result_total", Help: "Task result, labeled by task name and result", }, []string{"task", "result"}), taskDuration: promauto.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, + Namespace: metricsNamespace, + Subsystem: metricsSubsystem, Name: "task_duration_seconds", Help: "Task duration in seconds", Buckets: defBuckets,