[messages] add messages priority

This commit is contained in:
Aleksandr Soloshenko 2025-03-29 07:44:22 +07:00 committed by Aleksandr
parent a3da29b56d
commit 11bdf0e033
13 changed files with 288 additions and 65 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,8 +20,9 @@ Authorization: Basic {{credentials}}
"phoneNumbers": [
"{{phone}}"
],
"simNumber": {{$randomInt 1 2}},
"withDeliveryReport": true
"withDeliveryReport": true,
"priority": 128,
"simNumber": {{$randomInt 1 2}}
}
###

View File

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

View File

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