From 11bdf0e03383e03103471dbc0f6537dce05b6ccb Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Sat, 29 Mar 2025 07:44:22 +0700 Subject: [PATCH] [messages] add messages priority --- .../handlers/converters/messages.go | 23 +++++ .../sms-gateway/handlers/messages/3rdparty.go | 14 +++- internal/sms-gateway/handlers/mobile.go | 16 +++- .../20250330003657_add_messages_priority.sql | 10 +++ internal/sms-gateway/models/models.go | 1 + .../modules/messages/converters.go | 37 ++++++++ .../sms-gateway/modules/messages/domain.go | 26 ++++++ .../modules/messages/repository.go | 2 +- .../sms-gateway/modules/messages/service.go | 64 ++++---------- .../sms-gateway/modules/messages/tasks.go | 3 +- pkg/swagger/docs/requests.http | 5 +- pkg/swagger/docs/swagger.json | 84 ++++++++++++++++++- pkg/swagger/docs/swagger.yaml | 68 ++++++++++++++- 13 files changed, 288 insertions(+), 65 deletions(-) create mode 100644 internal/sms-gateway/handlers/converters/messages.go create mode 100644 internal/sms-gateway/models/migrations/mysql/20250330003657_add_messages_priority.sql create mode 100644 internal/sms-gateway/modules/messages/converters.go create mode 100644 internal/sms-gateway/modules/messages/domain.go diff --git a/internal/sms-gateway/handlers/converters/messages.go b/internal/sms-gateway/handlers/converters/messages.go new file mode 100644 index 0000000..dfa0c1b --- /dev/null +++ b/internal/sms-gateway/handlers/converters/messages.go @@ -0,0 +1,23 @@ +package converters + +import ( + "github.com/android-sms-gateway/client-go/smsgateway" + "github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages" +) + +func MessageToDTO(m messages.MessageOut) smsgateway.MobileMessage { + return smsgateway.MobileMessage{ + Message: smsgateway.Message{ + ID: m.ID, + Message: m.Message, + SimNumber: m.SimNumber, + WithDeliveryReport: m.WithDeliveryReport, + IsEncrypted: m.IsEncrypted, + PhoneNumbers: m.PhoneNumbers, + TTL: m.TTL, + ValidUntil: m.ValidUntil, + Priority: m.Priority, + }, + CreatedAt: m.CreatedAt, + } +} diff --git a/internal/sms-gateway/handlers/messages/3rdparty.go b/internal/sms-gateway/handlers/messages/3rdparty.go index 5eeaaef..1cb6b6e 100644 --- a/internal/sms-gateway/handlers/messages/3rdparty.go +++ b/internal/sms-gateway/handlers/messages/3rdparty.go @@ -78,7 +78,19 @@ func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error { return fmt.Errorf("can't get random device: %w", err) } - state, err := h.messagesSvc.Enqeue(device, req, messages.EnqueueOptions{SkipPhoneValidation: skipPhoneValidation}) + msg := messages.MessageIn{ + ID: req.ID, + Message: req.Message, + PhoneNumbers: req.PhoneNumbers, + IsEncrypted: req.IsEncrypted, + + SimNumber: req.SimNumber, + WithDeliveryReport: req.WithDeliveryReport, + TTL: req.TTL, + ValidUntil: req.ValidUntil, + Priority: req.Priority, + } + state, err := h.messagesSvc.Enqueue(device, msg, messages.EnqueueOptions{SkipPhoneValidation: skipPhoneValidation}) if err != nil { var errValidation messages.ErrValidation if isBadRequest := errors.As(err, &errValidation); isBadRequest { diff --git a/internal/sms-gateway/handlers/mobile.go b/internal/sms-gateway/handlers/mobile.go index c81d58f..927c51f 100644 --- a/internal/sms-gateway/handlers/mobile.go +++ b/internal/sms-gateway/handlers/mobile.go @@ -16,6 +16,7 @@ import ( "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" @@ -152,18 +153,25 @@ func (h *mobileHandler) patchDevice(device models.Device, c *fiber.Ctx) error { // @Tags Device, Messages // @Accept json // @Produce json -// @Success 200 {array} smsgateway.Message "List of pending messages" -// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @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 { - messages, err := h.messagesSvc.SelectPending(device.ID) + msgs, err := h.messagesSvc.SelectPending(device.ID) if err != nil { return fmt.Errorf("can't get messages: %w", err) } - return c.JSON(messages) + return c.JSON( + smsgateway.MobileGetMessagesResponse( + slices.Map( + msgs, + converters.MessageToDTO, + ), + ), + ) } // @Summary Update message state diff --git a/internal/sms-gateway/models/migrations/mysql/20250330003657_add_messages_priority.sql b/internal/sms-gateway/models/migrations/mysql/20250330003657_add_messages_priority.sql new file mode 100644 index 0000000..99f764d --- /dev/null +++ b/internal/sms-gateway/models/migrations/mysql/20250330003657_add_messages_priority.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE `messages` +ADD `priority` tinyint NOT NULL DEFAULT 0; +-- +goose StatementEnd +--- +-- +goose Down +-- +goose StatementBegin +ALTER TABLE `messages` DROP `priority`; +-- +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 dccc10e..f9ea409 100644 --- a/internal/sms-gateway/models/models.go +++ b/internal/sms-gateway/models/models.go @@ -58,6 +58,7 @@ type Message struct { ValidUntil *time.Time `gorm:"type:datetime"` SimNumber *uint8 `gorm:"type:tinyint(1) unsigned"` WithDeliveryReport bool `gorm:"not null;type:tinyint(1) unsigned"` + Priority int8 `gorm:"not null;type:tinyint;default:0"` IsHashed bool `gorm:"not null;type:tinyint(1) unsigned;default:0"` IsEncrypted bool `gorm:"not null;type:tinyint(1) unsigned;default:0"` diff --git a/internal/sms-gateway/modules/messages/converters.go b/internal/sms-gateway/modules/messages/converters.go new file mode 100644 index 0000000..8ffd5e5 --- /dev/null +++ b/internal/sms-gateway/modules/messages/converters.go @@ -0,0 +1,37 @@ +package messages + +import ( + "math" + "time" + + "github.com/android-sms-gateway/client-go/smsgateway" + "github.com/android-sms-gateway/server/internal/sms-gateway/models" + "github.com/capcom6/go-helpers/slices" +) + +func messageToDomain(input models.Message) MessageOut { + var ttl *uint64 = nil + if input.ValidUntil != nil { + secondsUntil := uint64(math.Max(0, time.Until(*input.ValidUntil).Seconds())) + ttl = &secondsUntil + } + + return MessageOut{ + MessageIn: MessageIn{ + ID: input.ExtID, + Message: input.Message, + PhoneNumbers: slices.Map(input.Recipients, recipientToDomain), + IsEncrypted: input.IsEncrypted, + SimNumber: input.SimNumber, + WithDeliveryReport: &input.WithDeliveryReport, + TTL: ttl, + ValidUntil: input.ValidUntil, + Priority: smsgateway.MessagePriority(input.Priority), + }, + CreatedAt: input.CreatedAt, + } +} + +func recipientToDomain(input models.MessageRecipient) string { + return input.PhoneNumber +} diff --git a/internal/sms-gateway/modules/messages/domain.go b/internal/sms-gateway/modules/messages/domain.go new file mode 100644 index 0000000..ad03783 --- /dev/null +++ b/internal/sms-gateway/modules/messages/domain.go @@ -0,0 +1,26 @@ +package messages + +import ( + "time" + + "github.com/android-sms-gateway/client-go/smsgateway" +) + +type MessageIn struct { + ID string + Message string + PhoneNumbers []string + IsEncrypted bool + + SimNumber *uint8 + WithDeliveryReport *bool + TTL *uint64 + ValidUntil *time.Time + Priority smsgateway.MessagePriority +} + +type MessageOut struct { + MessageIn + + CreatedAt time.Time +} diff --git a/internal/sms-gateway/modules/messages/repository.go b/internal/sms-gateway/modules/messages/repository.go index 0d572a1..06b8475 100644 --- a/internal/sms-gateway/modules/messages/repository.go +++ b/internal/sms-gateway/modules/messages/repository.go @@ -24,7 +24,7 @@ type repository struct { func (r *repository) SelectPending(deviceID string) (messages []models.Message, err error) { err = r.db. Where("device_id = ? AND state = ?", deviceID, models.ProcessingStatePending). - Order("id DESC"). + Order("priority DESC, id DESC"). Limit(100). Preload("Recipients"). Find(&messages). diff --git a/internal/sms-gateway/modules/messages/service.go b/internal/sms-gateway/modules/messages/service.go index d0c3d88..9689816 100644 --- a/internal/sms-gateway/modules/messages/service.go +++ b/internal/sms-gateway/modules/messages/service.go @@ -95,39 +95,13 @@ func (s *Service) RunBackgroundTasks(ctx context.Context, wg *sync.WaitGroup) { }() } -func (s *Service) SelectPending(deviceID string) ([]smsgateway.Message, error) { +func (s *Service) SelectPending(deviceID string) ([]MessageOut, error) { messages, err := s.messages.SelectPending(deviceID) if err != nil { return nil, err } - result := make([]smsgateway.Message, len(messages)) - for i, v := range messages { - var ttl *uint64 = nil - if v.ValidUntil != nil { - delta := time.Until(*v.ValidUntil).Seconds() - if delta > 0 { - deltaInt := uint64(delta) - ttl = &deltaInt - } else { - deltaInt := uint64(0) - ttl = &deltaInt - } - } - - result[i] = smsgateway.Message{ - ID: v.ExtID, - Message: v.Message, - SimNumber: v.SimNumber, - WithDeliveryReport: anys.AsPointer[bool](v.WithDeliveryReport), - IsEncrypted: v.IsEncrypted, - PhoneNumbers: s.recipientsToDomain(v.Recipients), - TTL: ttl, - ValidUntil: v.ValidUntil, - } - } - - return result, nil + return slices.Map(messages, messageToDomain), nil } func (s *Service) UpdateState(deviceID string, message smsgateway.MessageState) error { @@ -154,7 +128,7 @@ func (s *Service) UpdateState(deviceID string, message smsgateway.MessageState) return err } - s.hashingTask.Enqeue(existing.ID) + s.hashingTask.Enqueue(existing.ID) s.messagesCounter.WithLabelValues(string(existing.State)).Inc() @@ -178,7 +152,7 @@ func (s *Service) GetState(user models.User, ID string) (smsgateway.MessageState return modelToMessageState(message), nil } -func (s *Service) Enqeue(device models.Device, message smsgateway.Message, opts EnqueueOptions) (smsgateway.MessageState, error) { +func (s *Service) Enqueue(device models.Device, message MessageIn, opts EnqueueOptions) (smsgateway.MessageState, error) { state := smsgateway.MessageState{ ID: "", State: smsgateway.ProcessingStatePending, @@ -210,16 +184,18 @@ func (s *Service) Enqeue(device models.Device, message smsgateway.Message, opts } msg := models.Message{ - DeviceID: device.ID, - ExtID: message.ID, - Message: message.Message, - ValidUntil: validUntil, + ExtID: message.ID, + Message: message.Message, + Recipients: s.recipientsToModel(message.PhoneNumbers), + IsEncrypted: message.IsEncrypted, + + DeviceID: device.ID, + SimNumber: message.SimNumber, - WithDeliveryReport: anys.OrDefault[bool](message.WithDeliveryReport, true), - IsEncrypted: message.IsEncrypted, - Device: device, - Recipients: s.recipientsToModel(message.PhoneNumbers), - TimedModel: models.TimedModel{}, + WithDeliveryReport: anys.OrDefault(message.WithDeliveryReport, true), + + Priority: int8(message.Priority), + ValidUntil: validUntil, } if msg.ExtID == "" { msg.ExtID = s.idgen() @@ -265,16 +241,6 @@ func (s *Service) Clean(ctx context.Context) error { /////////////////////////////////////////////////////////////////////////////// -func (s *Service) recipientsToDomain(input []models.MessageRecipient) []string { - output := make([]string, len(input)) - - for i, v := range input { - output[i] = v.PhoneNumber - } - - return output -} - func (s *Service) recipientsToModel(input []string) []models.MessageRecipient { output := make([]models.MessageRecipient, len(input)) diff --git a/internal/sms-gateway/modules/messages/tasks.go b/internal/sms-gateway/modules/messages/tasks.go index 6ce6730..e0b7514 100644 --- a/internal/sms-gateway/modules/messages/tasks.go +++ b/internal/sms-gateway/modules/messages/tasks.go @@ -53,7 +53,8 @@ func (t *HashingTask) Run(ctx context.Context) { } } -func (t *HashingTask) Enqeue(id uint64) { +// Enqueue adds a message ID to the processing queue to be hashed in the next batch +func (t *HashingTask) Enqueue(id uint64) { t.mux.Lock() t.queue[id] = struct{}{} t.mux.Unlock() diff --git a/pkg/swagger/docs/requests.http b/pkg/swagger/docs/requests.http index e32ac81..7d99959 100644 --- a/pkg/swagger/docs/requests.http +++ b/pkg/swagger/docs/requests.http @@ -20,8 +20,9 @@ Authorization: Basic {{credentials}} "phoneNumbers": [ "{{phone}}" ], - "simNumber": {{$randomInt 1 2}}, - "withDeliveryReport": true + "withDeliveryReport": true, + "priority": 128, + "simNumber": {{$randomInt 1 2}} } ### diff --git a/pkg/swagger/docs/swagger.json b/pkg/swagger/docs/swagger.json index 831293d..c20192f 100644 --- a/pkg/swagger/docs/swagger.json +++ b/pkg/swagger/docs/swagger.json @@ -704,7 +704,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/smsgateway.Message" + "$ref": "#/definitions/smsgateway.MobileMessage" } } }, @@ -1164,6 +1164,7 @@ }, "priority": { "description": "Priority, messages with values greater than `99` will bypass limits and delays", + "default": 0, "maximum": 127, "minimum": -128, "allOf": [ @@ -1206,12 +1207,12 @@ 127 ], "x-enum-comments": { - "PriorityExpedited": "This and higher priority messages will bypass limits and delays" + "PriorityBypassThreshold": "Threshold at which messages bypass limits and delays" }, "x-enum-varnames": [ "PriorityMinimum", "PriorityDefault", - "PriorityExpedited", + "PriorityBypassThreshold", "PriorityMaximum" ] }, @@ -1327,6 +1328,83 @@ } } }, + "smsgateway.MobileMessage": { + "type": "object", + "required": [ + "message", + "phoneNumbers" + ], + "properties": { + "createdAt": { + "description": "Message creation time", + "type": "string", + "example": "2020-01-01T00:00:00Z" + }, + "id": { + "description": "ID (if not set - will be generated)", + "type": "string", + "maxLength": 36, + "example": "PyDmBQZZXYmyxMwED8Fzy" + }, + "isEncrypted": { + "description": "Is encrypted", + "type": "boolean", + "example": true + }, + "message": { + "description": "Content", + "type": "string", + "maxLength": 65535, + "example": "Hello World!" + }, + "phoneNumbers": { + "description": "Recipients (phone numbers)", + "type": "array", + "maxItems": 100, + "minItems": 1, + "items": { + "type": "string" + }, + "example": [ + "79990001234" + ] + }, + "priority": { + "description": "Priority, messages with values greater than `99` will bypass limits and delays", + "default": 0, + "maximum": 127, + "minimum": -128, + "allOf": [ + { + "$ref": "#/definitions/smsgateway.MessagePriority" + } + ], + "example": 0 + }, + "simNumber": { + "description": "SIM card number (1-3), if not set - default SIM will be used", + "type": "integer", + "maximum": 3, + "example": 1 + }, + "ttl": { + "description": "Time to live in seconds (conflicts with `validUntil`)", + "type": "integer", + "minimum": 5, + "example": 86400 + }, + "validUntil": { + "description": "Valid until (conflicts with `ttl`)", + "type": "string", + "example": "2020-01-01T00:00:00Z" + }, + "withDeliveryReport": { + "description": "With delivery report", + "type": "boolean", + "example": true + } + } + }, "smsgateway.MobileRegisterRequest": { "type": "object", "properties": { diff --git a/pkg/swagger/docs/swagger.yaml b/pkg/swagger/docs/swagger.yaml index 385751a..8d3faf8 100644 --- a/pkg/swagger/docs/swagger.yaml +++ b/pkg/swagger/docs/swagger.yaml @@ -157,6 +157,7 @@ definitions: priority: allOf: - $ref: '#/definitions/smsgateway.MessagePriority' + default: 0 description: Priority, messages with values greater than `99` will bypass limits and delays example: 0 @@ -192,12 +193,11 @@ definitions: - 127 type: integer x-enum-comments: - PriorityExpedited: This and higher priority messages will bypass limits and - delays + PriorityBypassThreshold: Threshold at which messages bypass limits and delays x-enum-varnames: - PriorityMinimum - PriorityDefault - - PriorityExpedited + - PriorityBypassThreshold - PriorityMaximum smsgateway.MessageState: properties: @@ -280,6 +280,66 @@ definitions: description: External IP type: string type: object + smsgateway.MobileMessage: + properties: + createdAt: + description: Message creation time + example: "2020-01-01T00:00:00Z" + type: string + id: + description: ID (if not set - will be generated) + example: PyDmBQZZXYmyxMwED8Fzy + maxLength: 36 + type: string + isEncrypted: + description: Is encrypted + example: true + type: boolean + message: + description: Content + example: Hello World! + maxLength: 65535 + type: string + phoneNumbers: + description: Recipients (phone numbers) + example: + - "79990001234" + items: + type: string + maxItems: 100 + minItems: 1 + type: array + priority: + allOf: + - $ref: '#/definitions/smsgateway.MessagePriority' + default: 0 + description: Priority, messages with values greater than `99` will bypass + limits and delays + example: 0 + maximum: 127 + minimum: -128 + simNumber: + description: SIM card number (1-3), if not set - default SIM will be used + example: 1 + maximum: 3 + type: integer + ttl: + description: Time to live in seconds (conflicts with `validUntil`) + example: 86400 + minimum: 5 + type: integer + validUntil: + description: Valid until (conflicts with `ttl`) + example: "2020-01-01T00:00:00Z" + type: string + withDeliveryReport: + description: With delivery report + example: true + type: boolean + required: + - message + - phoneNumbers + type: object smsgateway.MobileRegisterRequest: properties: name: @@ -899,7 +959,7 @@ paths: description: List of pending messages schema: items: - $ref: '#/definitions/smsgateway.Message' + $ref: '#/definitions/smsgateway.MobileMessage' type: array "500": description: Internal server error