diff --git a/internal/sms-gateway/handlers/base/handler.go b/internal/sms-gateway/handlers/base/handler.go index d79521d0..aaa8b1a5 100644 --- a/internal/sms-gateway/handlers/base/handler.go +++ b/internal/sms-gateway/handlers/base/handler.go @@ -43,7 +43,7 @@ func (h *Handler) ParamsParserValidator(c *fiber.Ctx, out any) error { func (h *Handler) ValidateStruct(out any) error { if h.Validator != nil { - if err := h.Validator.Struct(out); err != nil { + if err := h.Validator.Var(out, "required,dive"); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } } diff --git a/internal/sms-gateway/handlers/base/handler_test.go b/internal/sms-gateway/handlers/base/handler_test.go index 9fb44501..b1f8f5a7 100644 --- a/internal/sms-gateway/handlers/base/handler_test.go +++ b/internal/sms-gateway/handlers/base/handler_test.go @@ -11,7 +11,6 @@ import ( "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" - "go.uber.org/zap" "go.uber.org/zap/zaptest" ) @@ -25,6 +24,16 @@ type testRequestBodyNoValidate struct { Age int `json:"age" validate:"required"` } +type testRequestQuery struct { + Name string `query:"name" validate:"required"` + Age int `query:"age" validate:"required"` +} + +type testRequestParams struct { + ID string `params:"id" validate:"required"` + Name string `params:"name" validate:"required"` +} + func (t *testRequestBody) Validate() error { if t.Age < 18 { return fmt.Errorf("must be at least 18 years old") @@ -32,6 +41,20 @@ func (t *testRequestBody) Validate() error { return nil } +func (t *testRequestQuery) Validate() error { + if t.Age < 18 { + return fmt.Errorf("must be at least 18 years old") + } + return nil +} + +func (t *testRequestParams) Validate() error { + if t.ID == "invalid" { + return fmt.Errorf("invalid ID") + } + return nil +} + func TestHandler_BodyParserValidator(t *testing.T) { logger := zaptest.NewLogger(t) validate := validator.New() @@ -100,7 +123,10 @@ func TestHandler_BodyParserValidator(t *testing.T) { req = httptest.NewRequest("POST", test.path, nil) } - resp, _ := app.Test(req) + resp, err := app.Test(req) + if err != nil { + t.Fatalf("app.Test failed: %v", err) + } if test.expectedStatus != resp.StatusCode { t.Errorf("Expected status code %d, got %d", test.expectedStatus, resp.StatusCode) } @@ -109,89 +135,200 @@ func TestHandler_BodyParserValidator(t *testing.T) { } func TestHandler_QueryParserValidator(t *testing.T) { - type fields struct { - Logger *zap.Logger - Validator *validator.Validate - } - type args struct { - c *fiber.Ctx - out any + logger := zaptest.NewLogger(t) + validate := validator.New() + + handler := &base.Handler{ + Logger: logger, + Validator: validate, } + + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + var query testRequestQuery + return handler.QueryParserValidator(c, &query) + }) + tests := []struct { - name string - fields fields - args args - wantErr bool + description string + path string + expectedStatus int }{ - // TODO: Add test cases. + { + description: "Invalid query parameters - non-integer age", + path: "/test?name=John&age=abc", + expectedStatus: fiber.StatusBadRequest, + }, + { + description: "Valid query parameters", + path: "/test?name=John&age=25", + expectedStatus: fiber.StatusOK, + }, + { + description: "Invalid query parameters - missing name", + path: "/test?age=25", + expectedStatus: fiber.StatusBadRequest, + }, + { + description: "Invalid query parameters - age too low", + path: "/test?name=John&age=17", + expectedStatus: fiber.StatusBadRequest, + }, + { + description: "Invalid query parameters - missing age", + path: "/test?name=John", + expectedStatus: fiber.StatusBadRequest, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &base.Handler{ - Logger: tt.fields.Logger, - Validator: tt.fields.Validator, + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + req := httptest.NewRequest("GET", test.path, nil) + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("app.Test failed: %v", err) } - if err := h.QueryParserValidator(tt.args.c, tt.args.out); (err != nil) != tt.wantErr { - t.Errorf("Handler.QueryParserValidator() error = %v, wantErr %v", err, tt.wantErr) + if test.expectedStatus != resp.StatusCode { + t.Errorf("Expected status code %d, got %d", test.expectedStatus, resp.StatusCode) } }) } } func TestHandler_ParamsParserValidator(t *testing.T) { - type fields struct { - Logger *zap.Logger - Validator *validator.Validate - } - type args struct { - c *fiber.Ctx - out any + logger := zaptest.NewLogger(t) + validate := validator.New() + + handler := &base.Handler{ + Logger: logger, + Validator: validate, } + + app := fiber.New() + app.Get("/test/:id/:name", func(c *fiber.Ctx) error { + var params testRequestParams + return handler.ParamsParserValidator(c, ¶ms) + }) + tests := []struct { - name string - fields fields - args args - wantErr bool + description string + path string + expectedStatus int }{ - // TODO: Add test cases. + { + description: "Valid path parameters", + path: "/test/123/John", + expectedStatus: fiber.StatusOK, + }, + { + description: "Invalid path parameters - missing id", + path: "/test//John", + expectedStatus: fiber.StatusNotFound, + }, + { + description: "Invalid path parameters - missing name", + path: "/test/123/", + expectedStatus: fiber.StatusNotFound, + }, + { + description: "Invalid path parameters - invalid ID", + path: "/test/invalid/John", + expectedStatus: fiber.StatusBadRequest, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &base.Handler{ - Logger: tt.fields.Logger, - Validator: tt.fields.Validator, + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + req := httptest.NewRequest("GET", test.path, nil) + + resp, err := app.Test(req) + if err != nil { + t.Fatalf("app.Test failed: %v", err) } - if err := h.ParamsParserValidator(tt.args.c, tt.args.out); (err != nil) != tt.wantErr { - t.Errorf("Handler.ParamsParserValidator() error = %v, wantErr %v", err, tt.wantErr) + if test.expectedStatus != resp.StatusCode { + t.Errorf("Expected status code %d, got %d", test.expectedStatus, resp.StatusCode) } }) } } -func TestHandler_validateStruct(t *testing.T) { - type fields struct { - Logger *zap.Logger - Validator *validator.Validate +func TestHandler_ValidateStruct(t *testing.T) { + logger := zaptest.NewLogger(t) + validate := validator.New() + + // Test with validator + handlerWithValidator := &base.Handler{ + Logger: logger, + Validator: validate, } - type args struct { - out any + + // Test without validator + handlerWithoutValidator := &base.Handler{ + Logger: logger, + Validator: nil, } + tests := []struct { - name string - fields fields - args args - wantErr bool + description string + handler *base.Handler + input any + expectedStatus int }{ - // TODO: Add test cases. + { + description: "Valid struct with validator", + handler: handlerWithValidator, + input: &testRequestBody{Name: "John Doe", Age: 25}, + expectedStatus: fiber.StatusOK, + }, + { + description: "Invalid struct with validator - missing required field", + handler: handlerWithValidator, + input: &testRequestBody{Age: 25}, + expectedStatus: fiber.StatusBadRequest, + }, + { + description: "Invalid struct with validator - custom validation fails", + handler: handlerWithValidator, + input: &testRequestBody{Name: "John Doe", Age: 17}, + expectedStatus: fiber.StatusBadRequest, + }, + { + description: "Valid struct without validator", + handler: handlerWithoutValidator, + input: &testRequestBody{Name: "John Doe", Age: 25}, + expectedStatus: fiber.StatusOK, + }, + { + description: "Invalid struct without validator - custom validation fails", + handler: handlerWithoutValidator, + input: &testRequestBody{Name: "John Doe", Age: 17}, + expectedStatus: fiber.StatusBadRequest, + }, + { + description: "Valid struct with Validatable interface", + handler: handlerWithValidator, + input: &testRequestQuery{Name: "John", Age: 25}, + expectedStatus: fiber.StatusOK, + }, + { + description: "Invalid struct with Validatable interface", + handler: handlerWithValidator, + input: &testRequestQuery{Name: "John", Age: 17}, + expectedStatus: fiber.StatusBadRequest, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &base.Handler{ - Logger: tt.fields.Logger, - Validator: tt.fields.Validator, + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + err := test.handler.ValidateStruct(test.input) + + if test.expectedStatus == fiber.StatusOK && err != nil { + t.Errorf("Expected no error, got %v", err) } - if err := h.ValidateStruct(tt.args.out); (err != nil) != tt.wantErr { - t.Errorf("Handler.validateStruct() error = %v, wantErr %v", err, tt.wantErr) + + if test.expectedStatus == fiber.StatusBadRequest && err == nil { + t.Errorf("Expected error, got nil") } }) } diff --git a/internal/sms-gateway/handlers/messages/3rdparty.go b/internal/sms-gateway/handlers/messages/3rdparty.go index c4e31fee..631b7269 100644 --- a/internal/sms-gateway/handlers/messages/3rdparty.go +++ b/internal/sms-gateway/handlers/messages/3rdparty.go @@ -60,14 +60,14 @@ type ThirdPartyController struct { // // Enqueue message func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error { - var params postQueryParams + var params thirdPartyPostQueryParams if err := h.QueryParserValidator(c, ¶ms); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return err } var req smsgateway.Message if err := h.BodyParserValidator(c, &req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return err } var device models.Device @@ -190,9 +190,9 @@ func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error { // // Get message history func (h *ThirdPartyController) list(user models.User, c *fiber.Ctx) error { - params := getQueryParams{} + params := thirdPartyGetQueryParams{} if err := h.QueryParserValidator(c, ¶ms); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return err } messages, total, err := h.messagesSvc.SelectStates(user, params.ToFilter(), params.ToOptions()) @@ -252,7 +252,7 @@ func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error { func (h *ThirdPartyController) postInboxExport(user models.User, c *fiber.Ctx) error { req := smsgateway.MessagesExportRequest{} if err := h.BodyParserValidator(c, &req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return err } device, err := h.devicesSvc.Get(user.ID, devices.WithID(req.DeviceID)) diff --git a/internal/sms-gateway/handlers/messages/mobile.go b/internal/sms-gateway/handlers/messages/mobile.go new file mode 100644 index 00000000..5c4d8bba --- /dev/null +++ b/internal/sms-gateway/handlers/messages/mobile.go @@ -0,0 +1,122 @@ +package messages + +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/deviceauth" + "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages" + "github.com/capcom6/go-helpers/slices" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "go.uber.org/fx" + "go.uber.org/zap" +) + +type mobileControllerParams struct { + fx.In + + MessagesSvc *messages.Service + + Validator *validator.Validate + Logger *zap.Logger +} + +type MobileController struct { + base.Handler + + messagesSvc *messages.Service +} + +// @Summary Get messages for sending +// @Description Returns list of pending messages +// @Security MobileToken +// @Tags Device, Messages +// @Accept json +// @Produce json +// @Param order query string false "Message processing order: lifo (default) or fifo" Enums(lifo,fifo) default(lifo) +// @Success 200 {object} smsgateway.MobileGetMessagesResponse "List of pending messages" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Router /mobile/v1/message [get] +// +// Get messages for sending +func (h *MobileController) list(device models.Device, c *fiber.Ctx) error { + // Get and validate order parameter + params := mobileGetQueryParams{} + if err := h.QueryParserValidator(c, ¶ms); err != nil { + return err + } + + msgs, err := h.messagesSvc.SelectPending(device.ID, params.OrderOrDefault()) + if err != nil { + return fmt.Errorf("can't get messages: %w", err) + } + + return c.JSON( + smsgateway.MobileGetMessagesResponse( + slices.Map( + msgs, + converters.MessageToMobileDTO, + ), + ), + ) +} + +// @Summary Update message state +// @Description Updates message state +// @Security MobileToken +// @Tags Device, Messages +// @Accept json +// @Produce json +// @Param request body smsgateway.MobilePatchMessageRequest true "List of message state updates" +// @Success 204 {object} nil "Successfully updated" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Router /mobile/v1/message [patch] +// +// Update message state +func (h *MobileController) patch(device models.Device, c *fiber.Ctx) error { + req := smsgateway.MobilePatchMessageRequest{} + if err := h.BodyParserValidator(c, &req); err != nil { + return err + } + + for _, v := range req { + messageState := messages.MessageStateIn{ + ID: v.ID, + State: messages.ProcessingState(v.State), + Recipients: v.Recipients, + States: v.States, + } + + err := h.messagesSvc.UpdateState(device.ID, messageState) + if err != nil && !errors.Is(err, messages.ErrMessageNotFound) { + h.Logger.Error("Can't update message status", + zap.String("message_id", v.ID), + zap.Error(err), + ) + } + } + + return c.SendStatus(fiber.StatusNoContent) +} + +func (h *MobileController) Register(router fiber.Router) { + router.Get("", deviceauth.WithDevice(h.list)) + router.Patch("", deviceauth.WithDevice(h.patch)) +} + +func NewMobileController(params mobileControllerParams) *MobileController { + return &MobileController{ + Handler: base.Handler{ + Logger: params.Logger.Named("messages"), + Validator: params.Validator, + }, + messagesSvc: params.MessagesSvc, + } +} diff --git a/internal/sms-gateway/handlers/messages/params.go b/internal/sms-gateway/handlers/messages/params.go index b8ab73de..b98de6aa 100644 --- a/internal/sms-gateway/handlers/messages/params.go +++ b/internal/sms-gateway/handlers/messages/params.go @@ -7,12 +7,12 @@ import ( "github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages" ) -type postQueryParams struct { +type thirdPartyPostQueryParams struct { SkipPhoneValidation bool `query:"skipPhoneValidation"` DeviceActiveWithin uint `query:"deviceActiveWithin"` } -type getQueryParams struct { +type thirdPartyGetQueryParams struct { StartDate string `query:"from" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"` EndDate string `query:"to" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"` State string `query:"state" validate:"omitempty,oneof=Pending Processed Sent Delivered Failed"` @@ -21,7 +21,7 @@ type getQueryParams struct { Offset int `query:"offset" validate:"omitempty,min=0"` } -func (p *getQueryParams) Validate() error { +func (p *thirdPartyGetQueryParams) Validate() error { if p.StartDate != "" && p.EndDate != "" && p.StartDate > p.EndDate { return fmt.Errorf("`from` date must be before `to` date") } @@ -29,7 +29,7 @@ func (p *getQueryParams) Validate() error { return nil } -func (p *getQueryParams) ToFilter() messages.MessagesSelectFilter { +func (p *thirdPartyGetQueryParams) ToFilter() messages.MessagesSelectFilter { filter := messages.MessagesSelectFilter{} if p.StartDate != "" { @@ -55,7 +55,7 @@ func (p *getQueryParams) ToFilter() messages.MessagesSelectFilter { return filter } -func (p *getQueryParams) ToOptions() messages.MessagesSelectOptions { +func (p *thirdPartyGetQueryParams) ToOptions() messages.MessagesSelectOptions { options := messages.MessagesSelectOptions{ WithRecipients: true, WithStates: true, @@ -73,3 +73,15 @@ func (p *getQueryParams) ToOptions() messages.MessagesSelectOptions { return options } + +type mobileGetQueryParams struct { + Order messages.MessagesOrder `query:"order" validate:"omitempty,oneof=lifo fifo"` +} + +func (p *mobileGetQueryParams) OrderOrDefault() messages.MessagesOrder { + if p.Order != "" { + return p.Order + } + return messages.MessagesOrderLIFO + +} diff --git a/internal/sms-gateway/handlers/mobile.go b/internal/sms-gateway/handlers/mobile.go index fa776366..d9f04fc5 100644 --- a/internal/sms-gateway/handlers/mobile.go +++ b/internal/sms-gateway/handlers/mobile.go @@ -1,7 +1,6 @@ package handlers import ( - "errors" "fmt" "strings" @@ -9,6 +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/events" + "github.com/android-sms-gateway/server/internal/sms-gateway/handlers/messages" "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" @@ -16,9 +16,7 @@ import ( "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/modules/messages" "github.com/capcom6/go-helpers/anys" - "github.com/capcom6/go-helpers/slices" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/keyauth" @@ -27,13 +25,28 @@ import ( "go.uber.org/zap" ) +type mobileHandlerParams struct { + fx.In + + Logger *zap.Logger + Validator *validator.Validate + + AuthSvc *auth.Service + DevicesSvc *devices.Service + + MessagesCtrl *messages.MobileController + WebhooksCtrl *webhooks.MobileController + SettingsCtrl *settings.MobileController + EventsCtrl *events.MobileController +} + type mobileHandler struct { base.Handler - authSvc *auth.Service - devicesSvc *devices.Service - messagesSvc *messages.Service + authSvc *auth.Service + devicesSvc *devices.Service + messagesCtrl *messages.MobileController webhooksCtrl *webhooks.MobileController settingsCtrl *settings.MobileController eventsCtrl *events.MobileController @@ -83,7 +96,7 @@ func (h *mobileHandler) postDevice(c *fiber.Ctx) (err error) { req := smsgateway.MobileRegisterRequest{} if err = h.BodyParserValidator(c, &req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return err } var ( @@ -137,7 +150,7 @@ func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error { req := smsgateway.MobileUpdateRequest{} if err := h.BodyParserValidator(c, &req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return err } if req.Id != device.ID { @@ -151,69 +164,6 @@ func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error { return c.SendStatus(fiber.StatusNoContent) } -// @Summary Get messages for sending -// @Description Returns list of pending messages -// @Security MobileToken -// @Tags Device, Messages -// @Accept json -// @Produce json -// @Success 200 {object} smsgateway.MobileGetMessagesResponse "List of pending messages" -// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" -// @Router /mobile/v1/message [get] -// -// Get messages for sending -func (h *mobileHandler) getMessage(device models.Device, c *fiber.Ctx) error { - msgs, err := h.messagesSvc.SelectPending(device.ID) - if err != nil { - return fmt.Errorf("can't get messages: %w", err) - } - - return c.JSON( - smsgateway.MobileGetMessagesResponse( - slices.Map( - msgs, - converters.MessageToMobileDTO, - ), - ), - ) -} - -// @Summary Update message state -// @Description Updates message state -// @Security MobileToken -// @Tags Device, Messages -// @Accept json -// @Produce json -// @Param request body smsgateway.MobilePatchMessageRequest true "List of message state updates" -// @Success 204 {object} nil "Successfully updated" -// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" -// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" -// @Router /mobile/v1/message [patch] -// -// Update message state -func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error { - var req smsgateway.MobilePatchMessageRequest - if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - for _, v := range req { - messageState := messages.MessageStateIn{ - ID: v.ID, - State: messages.ProcessingState(v.State), - Recipients: v.Recipients, - States: v.States, - } - - err := h.messagesSvc.UpdateState(device.ID, messageState) - if err != nil && !errors.Is(err, messages.ErrMessageNotFound) { - h.Logger.Error("Can't update message status", zap.Error(err)) - } - } - - return c.SendStatus(fiber.StatusNoContent) -} - // @Summary Get one-time code for device registration // @Description Returns one-time code for device registration // @Security ApiAuth @@ -255,7 +205,7 @@ func (h *mobileHandler) changePassword(device models.Device, c *fiber.Ctx) error req := smsgateway.MobileChangePasswordRequest{} if err := h.BodyParserValidator(c, &req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return err } if err := h.authSvc.ChangePassword(device.UserID, req.CurrentPassword, req.NewPassword); err != nil { @@ -303,43 +253,29 @@ func (h *mobileHandler) Register(router fiber.Router) { router.Patch("/device", deviceauth.WithDevice(h.patchDevice)) - router.Get("/message", deviceauth.WithDevice(h.getMessage)) - router.Patch("/message", deviceauth.WithDevice(h.patchMessage)) - // 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")) } -type mobileHandlerParams struct { - fx.In - - Logger *zap.Logger - Validator *validator.Validate - - AuthSvc *auth.Service - DevicesSvc *devices.Service - MessagesSvc *messages.Service - - WebhooksCtrl *webhooks.MobileController - SettingsCtrl *settings.MobileController - EventsCtrl *events.MobileController -} - func newMobileHandler(params mobileHandlerParams) *mobileHandler { idGen, _ := nanoid.Standard(21) return &mobileHandler{ - Handler: base.Handler{Logger: params.Logger, Validator: params.Validator}, - authSvc: params.AuthSvc, + Handler: base.Handler{Logger: params.Logger, Validator: params.Validator}, + authSvc: params.AuthSvc, + + messagesCtrl: params.MessagesCtrl, devicesSvc: params.DevicesSvc, - messagesSvc: params.MessagesSvc, webhooksCtrl: params.WebhooksCtrl, settingsCtrl: params.SettingsCtrl, eventsCtrl: params.EventsCtrl, - idGen: idGen, + + idGen: idGen, } } diff --git a/internal/sms-gateway/handlers/module.go b/internal/sms-gateway/handlers/module.go index 8b121f6a..ea64187c 100644 --- a/internal/sms-gateway/handlers/module.go +++ b/internal/sms-gateway/handlers/module.go @@ -26,6 +26,7 @@ var Module = fx.Module( fx.Provide( newHealthHandler, messages.NewThirdPartyController, + messages.NewMobileController, webhooks.NewThirdPartyController, webhooks.NewMobileController, devices.NewThirdPartyController, diff --git a/internal/sms-gateway/modules/messages/repository.go b/internal/sms-gateway/modules/messages/repository.go index db5f2cfe..3c553daa 100644 --- a/internal/sms-gateway/modules/messages/repository.go +++ b/internal/sms-gateway/modules/messages/repository.go @@ -13,6 +13,7 @@ import ( ) const hashingLockName = "36444143-1ace-4dbf-891c-cc505911497e" +const maxPendingBatch = 100 var ErrMessageNotFound = gorm.ErrRecordNotFound var ErrMessageAlreadyExists = errors.New("duplicate id") @@ -70,7 +71,11 @@ func (r *repository) Select(filter MessagesSelectFilter, options MessagesSelectO } // Apply ordering - query = query.Order("messages.priority DESC, messages.id DESC") + if options.OrderBy == MessagesOrderFIFO { + query = query.Order("messages.priority DESC, messages.id ASC") + } else { + query = query.Order("messages.priority DESC, messages.id DESC") + } // Preload related data if options.WithRecipients { @@ -91,13 +96,14 @@ func (r *repository) Select(filter MessagesSelectFilter, options MessagesSelectO return messages, total, nil } -func (r *repository) SelectPending(deviceID string) ([]Message, error) { +func (r *repository) SelectPending(deviceID string, order MessagesOrder) ([]Message, error) { messages, _, err := r.Select(MessagesSelectFilter{ DeviceID: deviceID, State: ProcessingStatePending, }, MessagesSelectOptions{ WithRecipients: true, - Limit: 100, + Limit: maxPendingBatch, + OrderBy: order, }) return messages, err diff --git a/internal/sms-gateway/modules/messages/repository_filter.go b/internal/sms-gateway/modules/messages/repository_filter.go index 9ff1c95e..6b9b9216 100644 --- a/internal/sms-gateway/modules/messages/repository_filter.go +++ b/internal/sms-gateway/modules/messages/repository_filter.go @@ -2,6 +2,17 @@ package messages import "time" +// MessagesOrder defines supported ordering for message selection. +// Valid values: "lifo" (default), "fifo". +type MessagesOrder string + +const ( + // MessagesOrderLIFO orders messages newest-first within the same priority (default). + MessagesOrderLIFO MessagesOrder = "lifo" + // MessagesOrderFIFO orders messages oldest-first within the same priority. + MessagesOrderFIFO MessagesOrder = "fifo" +) + type MessagesSelectFilter struct { ExtID string UserID string @@ -16,6 +27,10 @@ type MessagesSelectOptions struct { WithDevice bool WithStates bool + // OrderBy sets the retrieval order for pending messages. + // Empty (zero) value defaults to "lifo". + OrderBy MessagesOrder + Limit int Offset int } diff --git a/internal/sms-gateway/modules/messages/service.go b/internal/sms-gateway/modules/messages/service.go index 7466971f..1fe0bff9 100644 --- a/internal/sms-gateway/modules/messages/service.go +++ b/internal/sms-gateway/modules/messages/service.go @@ -92,8 +92,12 @@ func (s *Service) RunBackgroundTasks(ctx context.Context, wg *sync.WaitGroup) { }() } -func (s *Service) SelectPending(deviceID string) ([]MessageOut, error) { - messages, err := s.messages.SelectPending(deviceID) +func (s *Service) SelectPending(deviceID string, order MessagesOrder) ([]MessageOut, error) { + if order == "" { + order = MessagesOrderLIFO + } + + messages, err := s.messages.SelectPending(deviceID, order) if err != nil { return nil, err } diff --git a/pkg/swagger/docs/swagger.json b/pkg/swagger/docs/swagger.json index 95a4be29..d3e91d6a 100644 --- a/pkg/swagger/docs/swagger.json +++ b/pkg/swagger/docs/swagger.json @@ -982,6 +982,19 @@ "Messages" ], "summary": "Get messages for sending", + "parameters": [ + { + "enum": [ + "lifo", + "fifo" + ], + "type": "string", + "default": "lifo", + "description": "Message processing order: lifo (default) or fifo", + "name": "order", + "in": "query" + } + ], "responses": { "200": { "description": "List of pending messages", @@ -992,6 +1005,12 @@ } } }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/smsgateway.ErrorResponse" + } + }, "500": { "description": "Internal server error", "schema": { diff --git a/pkg/swagger/docs/swagger.yaml b/pkg/swagger/docs/swagger.yaml index a5214d1d..f5c9bde4 100644 --- a/pkg/swagger/docs/swagger.yaml +++ b/pkg/swagger/docs/swagger.yaml @@ -1417,6 +1417,15 @@ paths: consumes: - application/json description: Returns list of pending messages + parameters: + - default: lifo + description: 'Message processing order: lifo (default) or fifo' + enum: + - lifo + - fifo + in: query + name: order + type: string produces: - application/json responses: @@ -1426,6 +1435,10 @@ paths: items: $ref: '#/definitions/smsgateway.MobileMessage' type: array + "400": + description: Invalid request + schema: + $ref: '#/definitions/smsgateway.ErrorResponse' "500": description: Internal server error schema: