[mobile] add order param for GET /message endpoint

This commit is contained in:
Aleksandr Soloshenko 2025-08-13 08:57:22 +07:00 committed by Aleksandr
parent 6dac509305
commit f50b85bdba
10 changed files with 231 additions and 103 deletions

View File

@ -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, &params); 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, &params); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}

View File

@ -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, &params); 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,
}
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -26,6 +26,7 @@ var Module = fx.Module(
fx.Provide(
newHealthHandler,
messages.NewThirdPartyController,
messages.NewMobileController,
webhooks.NewThirdPartyController,
webhooks.NewMobileController,
devices.NewThirdPartyController,

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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": {

View File

@ -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: