From f50b85bdba99ee584cefc487f7c24aef518bbf20 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Wed, 13 Aug 2025 08:57:22 +0700 Subject: [PATCH] [mobile] add order param for `GET /message` endpoint --- .../sms-gateway/handlers/messages/3rdparty.go | 4 +- .../sms-gateway/handlers/messages/mobile.go | 122 ++++++++++++++++++ .../sms-gateway/handlers/messages/params.go | 22 +++- internal/sms-gateway/handlers/mobile.go | 118 ++++------------- internal/sms-gateway/handlers/module.go | 1 + .../modules/messages/repository.go | 12 +- .../modules/messages/repository_filter.go | 15 +++ .../sms-gateway/modules/messages/service.go | 8 +- pkg/swagger/docs/swagger.json | 19 +++ pkg/swagger/docs/swagger.yaml | 13 ++ 10 files changed, 231 insertions(+), 103 deletions(-) create mode 100644 internal/sms-gateway/handlers/messages/mobile.go diff --git a/internal/sms-gateway/handlers/messages/3rdparty.go b/internal/sms-gateway/handlers/messages/3rdparty.go index c4e31fe..d14df87 100644 --- a/internal/sms-gateway/handlers/messages/3rdparty.go +++ b/internal/sms-gateway/handlers/messages/3rdparty.go @@ -60,7 +60,7 @@ 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()) } @@ -190,7 +190,7 @@ 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()) } diff --git a/internal/sms-gateway/handlers/messages/mobile.go b/internal/sms-gateway/handlers/messages/mobile.go new file mode 100644 index 0000000..6814546 --- /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 fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + 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 { + var req smsgateway.MobilePatchMessageRequest + if err := h.BodyParserValidator(c, &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.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 b8ab73d..b98de6a 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 fa77636..9c9914f 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 @@ -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 @@ -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 8b121f6..ea64187 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 db5f2cf..3c553da 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 9ff1c95..6b9b921 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 7466971..1fe0bff 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 95a4be2..d3e91d6 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 a5214d1..f5c9bde 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: