From 568c8c90b6dee7c921fb7222d5dc15264118e038 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Fri, 10 Jul 2026 11:13:38 +0700 Subject: [PATCH] [messages] add sort order option --- api/requests.http | 4 ++ go.mod | 2 +- go.sum | 4 +- .../sms-gateway/handlers/messages/3rdparty.go | 19 ++++-- .../sms-gateway/handlers/messages/params.go | 64 ++++++++----------- .../modules/messages/repository.go | 9 ++- .../modules/messages/repository_filter.go | 17 +++++ internal/sms-gateway/openapi/docs.go | 23 +++++++ 8 files changed, 93 insertions(+), 49 deletions(-) diff --git a/api/requests.http b/api/requests.http index 7edf6a5d..bb189888 100644 --- a/api/requests.http +++ b/api/requests.http @@ -121,6 +121,10 @@ Authorization: Basic {{credentials}} GET {{baseUrl}}/3rdparty/v1/messages?from=2025-01-01T00:00:00.000Z&to=2025-12-31T23:59:59Z&state=Pending&deviceId=fL2m4IirEvh9BvTf6TIB0&limit=50&offset=0 HTTP/1.1 Authorization: Basic {{credentials}} +### +GET {{baseUrl}}/3rdparty/v1/messages?sort=created_at HTTP/1.1 +Authorization: Basic {{credentials}} + ### POST {{baseUrl}}/3rdparty/v1/messages/inbox/export HTTP/1.1 Authorization: Basic {{credentials}} diff --git a/go.mod b/go.mod index b453623a..e5678fc8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.8 require ( firebase.google.com/go/v4 v4.20.0 - github.com/android-sms-gateway/client-go v1.14.0 + github.com/android-sms-gateway/client-go v1.14.2-0.20260709051643-3f84f44a4653 github.com/ansrivas/fiberprometheus/v2 v2.17.0 github.com/capcom6/go-helpers v0.4.0 github.com/capcom6/go-infra-fx v0.5.8 diff --git a/go.sum b/go.sum index 53380b59..11b9d15d 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= -github.com/android-sms-gateway/client-go v1.14.0 h1:CyKb8NgmOJZ+xOpBjY+Qv5Cj3vSSy6MoaDuI6G5oRLY= -github.com/android-sms-gateway/client-go v1.14.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.14.2-0.20260709051643-3f84f44a4653 h1:tBUdjLI28WlkpbJSUkpba023B78Ectlf48cSSbFWGvA= +github.com/android-sms-gateway/client-go v1.14.2-0.20260709051643-3f84f44a4653/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.17.0 h1:p0gqs5LsSCWGoSFF44fCJkyU+XcE6TLRqEMu80b2iCo= diff --git a/internal/sms-gateway/handlers/messages/3rdparty.go b/internal/sms-gateway/handlers/messages/3rdparty.go index e2e329fd..e087fb33 100644 --- a/internal/sms-gateway/handlers/messages/3rdparty.go +++ b/internal/sms-gateway/handlers/messages/3rdparty.go @@ -181,13 +181,14 @@ func (h *ThirdPartyController) post(userID string, c *fiber.Ctx) error { // @Security JWTAuth // @Tags User, Messages // @Produce json -// @Param from query string false "Start date in RFC3339 format" Format(date-time) -// @Param to query string false "End date in RFC3339 format" Format(date-time) -// @Param state query string false "Filter messages by processing state" Enum(Pending, Processed, Sent, Delivered, Failed) -// @Param deviceId query string false "Filter by device ID" min(21) max(21) -// @Param limit query int false "Pagination limit" default(50) min(1) max(100) -// @Param offset query int false "Pagination offset" default(0) -// @Param includeContent query bool false "Include textMessage/dataMessage content for each message. Default is false" default(false) +// @Param from query string false "Start date in RFC3339 format" Format(date-time) +// @Param to query string false "End date in RFC3339 format" Format(date-time) +// @Param state query string false "Filter messages by processing state" Enum(Pending, Processed, Sent, Delivered, Failed) +// @Param deviceId query string false "Filter by device ID" min(21) max(21) +// @Param limit query int false "Pagination limit" default(50) min(1) max(100) +// @Param offset query int false "Pagination offset" default(0) +// @Param includeContent query bool false "Include textMessage/dataMessage content for each message. Default is false" default(false) +// @Param sort query string false "Sort order per JSON:API spec. Use created_at (ascending) or -created_at (descending)" Enums(created_at, -created_at) default(-created_at) // @Success 200 {object} smsgateway.GetMessagesResponse "A list of messages" // @Header 200 {integer} X-Total-Count "Total number of items available" // @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" @@ -203,6 +204,10 @@ func (h *ThirdPartyController) list(userID string, c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } + if params.Sort == nil { + params.Sort = lo.ToPtr(smsgateway.CreatedAtDescending) + } + messages, total, err := h.messagesSvc.SelectStates(userID, params.ToFilter(), params.ToOptions()) if err != nil { h.Logger.Error("failed to get message history", zap.Error(err), zap.String("user_id", userID)) diff --git a/internal/sms-gateway/handlers/messages/params.go b/internal/sms-gateway/handlers/messages/params.go index ebe52413..a3124a43 100644 --- a/internal/sms-gateway/handlers/messages/params.go +++ b/internal/sms-gateway/handlers/messages/params.go @@ -1,9 +1,7 @@ package messages import ( - "errors" - "time" - + "github.com/android-sms-gateway/client-go/smsgateway" "github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages" ) @@ -12,45 +10,25 @@ type thirdPartyPostQueryParams struct { DeviceActiveWithin int `query:"deviceActiveWithin" validate:"omitempty,min=1"` } -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 Cancelling Cancelled Processed Sent Delivered Failed"` - DeviceID string `query:"deviceId" validate:"omitempty,len=21"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100"` - Offset int `query:"offset" validate:"omitempty,min=0"` - IncludeContent bool `query:"includeContent"` -} - -func (p *thirdPartyGetQueryParams) Validate() error { - if p.StartDate != "" && p.EndDate != "" && p.StartDate > p.EndDate { - return errors.New("`from` date must be before `to` date") //nolint:err113 // won't be used directly - } - - return nil -} +type thirdPartyGetQueryParams smsgateway.ListMessagesOptions func (p *thirdPartyGetQueryParams) ToFilter() messages.SelectFilter { var filter messages.SelectFilter - if p.StartDate != "" { - if t, err := time.Parse(time.RFC3339, p.StartDate); err == nil { - filter.StartDate = t - } + if p.From != nil { + filter.StartDate = *p.From } - if p.EndDate != "" { - if t, err := time.Parse(time.RFC3339, p.EndDate); err == nil { - filter.EndDate = t - } + if p.To != nil { + filter.EndDate = *p.To } - if p.State != "" { - filter.State = append(filter.State, messages.ProcessingState(p.State)) + if p.State != nil { + filter.State = append(filter.State, messages.ProcessingState(*p.State)) } - if p.DeviceID != "" { - filter.DeviceID = p.DeviceID + if p.DeviceID != nil { + filter.DeviceID = *p.DeviceID } return filter @@ -62,16 +40,28 @@ func (p *thirdPartyGetQueryParams) ToOptions() messages.SelectOptions { var options messages.SelectOptions options.WithRecipients = true options.WithStates = true - options.WithContent = p.IncludeContent - if p.Limit > 0 { - options.Limit = min(p.Limit, maxLimit) + if p.IncludeContent != nil { + options.WithContent = *p.IncludeContent + } + + if p.Limit != nil { + options.Limit = max(min(*p.Limit, maxLimit), 1) } else { options.Limit = 50 } - if p.Offset > 0 { - options.Offset = p.Offset + if p.Offset != nil { + options.Offset = max(*p.Offset, 0) + } + + if p.Sort != nil { + switch *p.Sort { + case smsgateway.CreatedAtAscending: + options.SortField = messages.SortFieldCreatedAtAsc + case smsgateway.CreatedAtDescending: + options.SortField = messages.SortFieldCreatedAtDesc + } } return options diff --git a/internal/sms-gateway/modules/messages/repository.go b/internal/sms-gateway/modules/messages/repository.go index c23659ec..77aa751a 100644 --- a/internal/sms-gateway/modules/messages/repository.go +++ b/internal/sms-gateway/modules/messages/repository.go @@ -71,9 +71,14 @@ func (r *Repository) list(filter SelectFilter, options SelectOptions) ([]message } // Apply ordering - if options.OrderBy == MessagesOrderFIFO { + switch { + case options.SortField == SortFieldCreatedAtAsc: + query = query.Order("messages.created_at ASC") + case options.SortField == SortFieldCreatedAtDesc: + query = query.Order("messages.created_at DESC") + case options.OrderBy == MessagesOrderFIFO: query = query.Order("messages.schedule_at ASC, messages.priority DESC, messages.id ASC") - } else { + default: query = query.Order("messages.schedule_at ASC, messages.priority DESC, messages.id DESC") } diff --git a/internal/sms-gateway/modules/messages/repository_filter.go b/internal/sms-gateway/modules/messages/repository_filter.go index 21154e31..0a3b3953 100644 --- a/internal/sms-gateway/modules/messages/repository_filter.go +++ b/internal/sms-gateway/modules/messages/repository_filter.go @@ -13,6 +13,19 @@ const ( MessagesOrderFIFO Order = "fifo" ) +// SortField defines the allowed column sort directions. +// Only values defined here may be used in SelectOptions.SortField. +type SortField string + +const ( + // SortFieldNone is the zero value (no explicit sort; falls through to OrderBy/default). + SortFieldNone SortField = "" + // SortFieldCreatedAtAsc sorts by created_at ascending. + SortFieldCreatedAtAsc SortField = "created_at ASC" + // SortFieldCreatedAtDesc sorts by created_at descending. + SortFieldCreatedAtDesc SortField = "created_at DESC" +) + type SelectFilter struct { ExtID string UserID string @@ -58,6 +71,10 @@ type SelectOptions struct { // Empty (zero) value defaults to "lifo". OrderBy Order + // SortField selects a column sort direction. + // When non-zero, it overrides OrderBy. + SortField SortField + Limit int Offset int } diff --git a/internal/sms-gateway/openapi/docs.go b/internal/sms-gateway/openapi/docs.go index ddc987dd..c0f53c8c 100644 --- a/internal/sms-gateway/openapi/docs.go +++ b/internal/sms-gateway/openapi/docs.go @@ -673,6 +673,17 @@ const docTemplate = `{ "description": "Include textMessage/dataMessage content for each message. Default is false", "name": "includeContent", "in": "query" + }, + { + "enum": [ + "created_at", + "-created_at" + ], + "type": "string", + "default": "-created_at", + "description": "Sort order per JSON:API spec. Use created_at (ascending) or -created_at (descending)", + "name": "sort", + "in": "query" } ], "responses": { @@ -2289,6 +2300,18 @@ const docTemplate = `{ "$ref": "#/definitions/smsgateway.SimSelectionMode" } ] + }, + "work_hours_enabled": { + "description": "WorkHoursEnabled enables restricting message delivery to a configurable time window.", + "type": "boolean" + }, + "work_hours_end": { + "description": "WorkHoursEnd is the end of the working hours window in HH:mm format (24-hour).", + "type": "string" + }, + "work_hours_start": { + "description": "WorkHoursStart is the start of the working hours window in HH:mm format (24-hour).", + "type": "string" } } },