[messages] add data messages support

This commit is contained in:
Aleksandr Soloshenko 2025-06-30 07:00:44 +07:00 committed by Aleksandr
parent e4046c5865
commit 8f01332869
22 changed files with 836 additions and 503 deletions

4
go.mod
View File

@ -6,9 +6,9 @@ toolchain go1.23.2
require (
firebase.google.com/go/v4 v4.12.1
github.com/android-sms-gateway/client-go v1.6.0
github.com/android-sms-gateway/client-go v1.8.1
github.com/ansrivas/fiberprometheus/v2 v2.6.1
github.com/capcom6/go-helpers v0.2.0
github.com/capcom6/go-helpers v0.3.0
github.com/capcom6/go-infra-fx v0.2.1
github.com/go-playground/assert/v2 v2.2.0
github.com/go-playground/validator/v10 v10.16.0

20
go.sum
View File

@ -26,14 +26,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/android-sms-gateway/client-go v1.5.9-0.20250522134006-6e8b4dd3057a h1:TSmfm+KOsR1Ie10nZEjCVDepa1bEPin0NAgEUOSJiqw=
github.com/android-sms-gateway/client-go v1.5.9-0.20250522134006-6e8b4dd3057a/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.5.9-0.20250522231449-9e0855eff19f h1:VYrL6YbkQ49pcyiXTYcR5LN1WpNy1Tc684XjeE1UCvw=
github.com/android-sms-gateway/client-go v1.5.9-0.20250522231449-9e0855eff19f/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.5.9-0.20250524095300-2e41cae07049 h1:kdyVkqrgKDSI13JOKXVFz1al3IxfJPcbUaJvSXF6z+0=
github.com/android-sms-gateway/client-go v1.5.9-0.20250524095300-2e41cae07049/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.6.0 h1:3hN0XEUnNrweBl5Xx3IfE5zyq5ihm7fB0dhuTZBKlns=
github.com/android-sms-gateway/client-go v1.6.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.7.1-0.20250629114454-6a0c4d8bb90a h1:dAMTNI56fW8l5RrrwYUrvibIkpRCFw9jEFkjEw6mDMQ=
github.com/android-sms-gateway/client-go v1.7.1-0.20250629114454-6a0c4d8bb90a/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.8.1-0.20250701232650-4956a99b0da7 h1:rOBF445neI27pKcndrC3lH7buN8HrRvYvdVWCjFIsHg=
github.com/android-sms-gateway/client-go v1.8.1-0.20250701232650-4956a99b0da7/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.8.1 h1:cakQc4aw7oBKbcdCJsbP1HjM2BZouYSIKHRwHzBo2TY=
github.com/android-sms-gateway/client-go v1.8.1/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/ansrivas/fiberprometheus/v2 v2.6.1 h1:wac3pXaE6BYYTF04AC6K0ktk6vCD+MnDOJZ3SK66kXM=
@ -43,8 +41,10 @@ github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/capcom6/go-helpers v0.2.0 h1:OUcUnVbjBiwaTzvyaxkxqRKtrOXv1ifYalQ1NXzFBNM=
github.com/capcom6/go-helpers v0.2.0/go.mod h1:WDqc7HZNqHxUTisArkYIBZtqUfJBVyPWeQI+FMwEzAw=
github.com/capcom6/go-helpers v0.2.1-0.20250630235533-8457c7435058 h1:tt64ezShwdmcUk04gBVL1BD49FDAfVZ4ELiw2rrJp+I=
github.com/capcom6/go-helpers v0.2.1-0.20250630235533-8457c7435058/go.mod h1:WDqc7HZNqHxUTisArkYIBZtqUfJBVyPWeQI+FMwEzAw=
github.com/capcom6/go-helpers v0.3.0 h1:ae18fLfluoPubiB2V+j4cIpfZaTuK4acS2entamaDkE=
github.com/capcom6/go-helpers v0.3.0/go.mod h1:WDqc7HZNqHxUTisArkYIBZtqUfJBVyPWeQI+FMwEzAw=
github.com/capcom6/go-infra-fx v0.2.1 h1:8rqr2ZV+YC2R07amHMdlE1XKLUhMe5yO+ffCJ/xXlNY=
github.com/capcom6/go-infra-fx v0.2.1/go.mod h1:klScvB8QAKgJ19FfJOnUKK5tI0o9b79Aj2RmCJHfbN0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=

View File

@ -6,10 +6,29 @@ import (
)
func MessageToDTO(m messages.MessageOut) smsgateway.MobileMessage {
var message string
var textMessage *smsgateway.TextMessage
var dataMessage *smsgateway.DataMessage
if m.TextContent != nil {
textMessage = &smsgateway.TextMessage{
Text: m.TextContent.Text,
}
} else if m.DataContent != nil {
dataMessage = &smsgateway.DataMessage{
Data: m.DataContent.Data,
Port: m.DataContent.Port,
}
}
return smsgateway.MobileMessage{
Message: smsgateway.Message{
ID: m.ID,
Message: m.Message,
ID: m.ID,
Message: message,
TextMessage: textMessage,
DataMessage: dataMessage,
SimNumber: m.SimNumber,
WithDeliveryReport: m.WithDeliveryReport,
IsEncrypted: m.IsEncrypted,

View File

@ -26,7 +26,7 @@ func TestMessageToDTO(t *testing.T) {
input: messages.MessageOut{
MessageIn: messages.MessageIn{
ID: "msg-123",
Message: "Test message content",
TextContent: &messages.TextMessageContent{Text: "Test message content"},
PhoneNumbers: []string{"+1234567890", "+9876543210"},
IsEncrypted: true,
SimNumber: anys.AsPointer(uint8(2)),
@ -57,7 +57,7 @@ func TestMessageToDTO(t *testing.T) {
input: messages.MessageOut{
MessageIn: messages.MessageIn{
ID: "msg-456",
Message: "Another test message",
TextContent: &messages.TextMessageContent{Text: "Another test message"},
PhoneNumbers: []string{"+1122334455"},
},
CreatedAt: now,

View File

@ -78,9 +78,27 @@ func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error {
return fmt.Errorf("can't get random device: %w", err)
}
var textContent *messages.TextMessageContent
var dataContent *messages.DataMessageContent
if text := req.GetTextMessage(); text != nil {
textContent = &messages.TextMessageContent{
Text: text.Text,
}
} else if data := req.GetDataMessage(); data != nil {
dataContent = &messages.DataMessageContent{
Data: data.Data,
Port: data.Port,
}
} else {
return fiber.NewError(fiber.StatusBadRequest, "No message content provided")
}
msg := messages.MessageIn{
ID: req.ID,
Message: req.Message,
ID: req.ID,
TextContent: textContent,
DataContent: dataContent,
PhoneNumbers: req.PhoneNumbers,
IsEncrypted: req.IsEncrypted,

View File

@ -10,5 +10,5 @@ import (
var migrations embed.FS
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Device{}, &Message{}, &MessageRecipient{}, &MessageState{})
return db.AutoMigrate(&User{}, &Device{})
}

View File

@ -0,0 +1,37 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE `messages`
ADD `type` enum('Text', 'Data') NOT NULL DEFAULT 'Text',
ADD `content` text NOT NULL,
MODIFY `message` text NULL;
-- +goose StatementEnd
-- +goose StatementBegin
UPDATE `messages`
SET `content` = json_object('text', `message`)
WHERE `is_hashed` = 0;
-- +goose StatementEnd
-- +goose StatementBegin
UPDATE `messages`
SET `content` = `message`
WHERE `is_hashed` = 1;
-- +goose StatementEnd
---
-- +goose Down
-- +goose StatementBegin
UPDATE `messages`
SET `message` = COALESCE(
json_value(`content`, '$.text'),
json_value(`content`, '$.data')
)
WHERE `is_hashed` = 0;
-- +goose StatementEnd
-- +goose StatementBegin
UPDATE `messages`
SET `message` = `content`
WHERE `is_hashed` = 1;
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE `messages` DROP `type`,
DROP `content`,
MODIFY `message` text NOT NULL;
-- +goose StatementEnd

View File

@ -4,16 +4,6 @@ import (
"time"
)
type ProcessingState string
const (
ProcessingStatePending ProcessingState = "Pending"
ProcessingStateProcessed ProcessingState = "Processed"
ProcessingStateSent ProcessingState = "Sent"
ProcessingStateDelivered ProcessingState = "Delivered"
ProcessingStateFailed ProcessingState = "Failed"
)
type TimedModel struct {
CreatedAt time.Time `gorm:"->;not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"`
UpdatedAt time.Time `gorm:"->;not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`
@ -52,39 +42,3 @@ func (d *Device) IsEmpty() bool {
return d.ID == ""
}
type Message struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
DeviceID string `gorm:"not null;type:char(21);uniqueIndex:unq_messages_id_device,priority:2;index:idx_messages_device_state"`
ExtID string `gorm:"not null;type:varchar(36);uniqueIndex:unq_messages_id_device,priority:1"`
Message string `gorm:"not null;type:text"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending;index:idx_messages_device_state"`
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"`
Device Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"`
Recipients []MessageRecipient `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`
States []MessageState `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`
SoftDeletableModel
}
type MessageRecipient struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
MessageID uint64 `gorm:"uniqueIndex:unq_message_recipients_message_id_phone_number,priority:1;type:BIGINT UNSIGNED"`
PhoneNumber string `gorm:"uniqueIndex:unq_message_recipients_message_id_phone_number,priority:2;type:varchar(128)"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending"`
Error *string `gorm:"type:varchar(256)"`
}
type MessageState struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
MessageID uint64 `gorm:"not null;type:BIGINT UNSIGNED;uniqueIndex:unq_message_states_message_id_state,priority:1"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');uniqueIndex:unq_message_states_message_id_state,priority:2"`
UpdatedAt time.Time `gorm:"<-:create;not null;autoupdatetime:false"`
}

View File

@ -23,7 +23,11 @@ func testHealth(shutdowner fx.Shutdowner, logger *zap.Logger, config http.Config
}
return
}
defer res.Body.Close()
defer func() {
if err := res.Body.Close(); err != nil {
logger.Error("Failed to close body", zap.Error(err))
}
}()
body, err := io.ReadAll(res.Body)
if err != nil {

View File

@ -48,9 +48,10 @@ func (s *Service) HealthCheck(ctx context.Context) (Check, error) {
for name, detail := range healthChecks {
check.Checks[p.Name()+":"+name] = detail
if detail.Status == StatusFail {
switch detail.Status {
case StatusFail:
level = max(level, levelFail)
} else if detail.Status == StatusWarn {
case StatusWarn:
level = max(level, levelWarn)
}
}

View File

@ -1,25 +1,37 @@
package messages
import (
"fmt"
"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 {
func messageToDomain(input Message) (MessageOut, error) {
var ttl *uint64 = nil
if input.ValidUntil != nil {
secondsUntil := uint64(math.Max(0, time.Until(*input.ValidUntil).Seconds()))
ttl = &secondsUntil
}
textContent, err := input.GetTextContent()
if err != nil {
return MessageOut{}, fmt.Errorf("can't get text content: %w", err)
}
dataContent, err := input.GetDataContent()
if err != nil {
return MessageOut{}, fmt.Errorf("can't get data content: %w", err)
}
return MessageOut{
MessageIn: MessageIn{
ID: input.ExtID,
Message: input.Message,
ID: input.ExtID,
TextContent: textContent,
DataContent: dataContent,
PhoneNumbers: slices.Map(input.Recipients, recipientToDomain),
IsEncrypted: input.IsEncrypted,
SimNumber: input.SimNumber,
@ -29,9 +41,9 @@ func messageToDomain(input models.Message) MessageOut {
Priority: smsgateway.MessagePriority(input.Priority),
},
CreatedAt: input.CreatedAt,
}
}, nil
}
func recipientToDomain(input models.MessageRecipient) string {
func recipientToDomain(input MessageRecipient) string {
return input.PhoneNumber
}

View File

@ -7,8 +7,11 @@ import (
)
type MessageIn struct {
ID string
Message string
ID string
TextContent *TextMessageContent
DataContent *DataMessageContent
PhoneNumbers []string
IsEncrypted bool

View File

@ -0,0 +1,127 @@
package messages
import (
"encoding/json"
"time"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"gorm.io/gorm"
)
type ProcessingState string
type MessageType string
const (
ProcessingStatePending ProcessingState = "Pending"
ProcessingStateProcessed ProcessingState = "Processed"
ProcessingStateSent ProcessingState = "Sent"
ProcessingStateDelivered ProcessingState = "Delivered"
ProcessingStateFailed ProcessingState = "Failed"
MessageTypeText MessageType = "Text"
MessageTypeData MessageType = "Data"
)
type TextMessageContent struct {
Text string `json:"text"`
}
type DataMessageContent struct {
Data string `json:"data"`
Port uint16 `json:"port"`
}
type Message struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
DeviceID string `gorm:"not null;type:char(21);uniqueIndex:unq_messages_id_device,priority:2;index:idx_messages_device_state"`
ExtID string `gorm:"not null;type:varchar(36);uniqueIndex:unq_messages_id_device,priority:1"`
Type MessageType `gorm:"not null;type:enum('Text','Data');default:Text"`
Content string `gorm:"not null;type:text"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending;index:idx_messages_device_state"`
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"`
Device models.Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"`
Recipients []MessageRecipient `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`
States []MessageState `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`
models.SoftDeletableModel
}
func (m *Message) SetTextContent(content TextMessageContent) error {
contentJSON, err := json.Marshal(content)
if err != nil {
return err
}
m.Type = MessageTypeText
m.Content = string(contentJSON)
return nil
}
func (m *Message) GetTextContent() (*TextMessageContent, error) {
if m.Type != MessageTypeText {
return nil, nil
}
content := TextMessageContent{}
err := json.Unmarshal([]byte(m.Content), &content)
if err != nil {
return nil, err
}
return &content, nil
}
func (m *Message) SetDataContent(content DataMessageContent) error {
contentJSON, err := json.Marshal(content)
if err != nil {
return err
}
m.Type = MessageTypeData
m.Content = string(contentJSON)
return nil
}
func (m *Message) GetDataContent() (*DataMessageContent, error) {
if m.Type != MessageTypeData {
return nil, nil
}
content := DataMessageContent{}
err := json.Unmarshal([]byte(m.Content), &content)
if err != nil {
return nil, err
}
return &content, nil
}
type MessageRecipient struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
MessageID uint64 `gorm:"uniqueIndex:unq_message_recipients_message_id_phone_number,priority:1;type:BIGINT UNSIGNED"`
PhoneNumber string `gorm:"uniqueIndex:unq_message_recipients_message_id_phone_number,priority:2;type:varchar(128)"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending"`
Error *string `gorm:"type:varchar(256)"`
}
type MessageState struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
MessageID uint64 `gorm:"not null;type:BIGINT UNSIGNED;uniqueIndex:unq_message_states_message_id_state,priority:1"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');uniqueIndex:unq_message_states_message_id_state,priority:2"`
UpdatedAt time.Time `gorm:"<-:create;not null;autoupdatetime:false"`
}
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&Message{}, &MessageRecipient{}, &MessageState{})
}

View File

@ -2,6 +2,7 @@ package messages
import (
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/cleaner"
"github.com/capcom6/go-infra-fx/db"
"go.uber.org/fx"
"go.uber.org/zap"
)
@ -31,3 +32,7 @@ var Module = fx.Module(
fx.Provide(newRepository),
fx.Provide(NewHashingTask, fx.Private),
)
func init() {
db.RegisterMigration(Migrate)
}

View File

@ -6,7 +6,6 @@ import (
"errors"
"time"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
"github.com/go-sql-driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@ -21,9 +20,9 @@ type repository struct {
db *gorm.DB
}
func (r *repository) SelectPending(deviceID string) (messages []models.Message, err error) {
func (r *repository) SelectPending(deviceID string) (messages []Message, err error) {
err = r.db.
Where("device_id = ? AND state = ?", deviceID, models.ProcessingStatePending).
Where("device_id = ? AND state = ?", deviceID, ProcessingStatePending).
Order("priority DESC, id DESC").
Limit(100).
Preload("Recipients").
@ -33,7 +32,7 @@ func (r *repository) SelectPending(deviceID string) (messages []models.Message,
return
}
func (r *repository) Get(ID string, filter MessagesSelectFilter, options ...MessagesSelectOptions) (message models.Message, err error) {
func (r *repository) Get(ID string, filter MessagesSelectFilter, options ...MessagesSelectOptions) (message Message, err error) {
query := r.db.Model(&message).
Where("ext_id = ?", ID)
@ -58,7 +57,7 @@ func (r *repository) Get(ID string, filter MessagesSelectFilter, options ...Mess
return
}
func (r *repository) Insert(message *models.Message) error {
func (r *repository) Insert(message *Message) error {
err := r.db.Omit("Device").Create(message).Error
if err == nil {
return nil
@ -70,7 +69,7 @@ func (r *repository) Insert(message *models.Message) error {
return err
}
func (r *repository) UpdateState(message *models.Message) error {
func (r *repository) UpdateState(message *Message) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(message).Select("State").Updates(message).Error; err != nil {
return err
@ -97,7 +96,7 @@ func (r *repository) UpdateState(message *models.Message) error {
func (r *repository) HashProcessed(ids []uint64) error {
rawSQL := "UPDATE `messages` `m`, `message_recipients` `r`\n" +
"SET `m`.`is_hashed` = true, `m`.`message` = SHA2(m.message, 256), `r`.`phone_number` = LEFT(SHA2(phone_number, 256), 16)\n" +
"SET `m`.`is_hashed` = true, `m`.`content` = SHA2(COALESCE(JSON_VALUE(`content`, '$.text'), JSON_VALUE(`content`, '$.data')), 256), `r`.`phone_number` = LEFT(SHA2(phone_number, 256), 16)\n" +
"WHERE `m`.`id` = `r`.`message_id` AND `m`.`is_hashed` = false AND `m`.`is_encrypted` = false AND `m`.`state` <> 'Pending'"
params := []interface{}{}
if len(ids) > 0 {
@ -130,9 +129,9 @@ func (r *repository) HashProcessed(ids []uint64) error {
func (r *repository) removeProcessed(ctx context.Context, until time.Time) (int64, error) {
res := r.db.
WithContext(ctx).
Where("state <> ?", models.ProcessingStatePending).
Where("state <> ?", ProcessingStatePending).
Where("created_at < ?", until).
Delete(&models.Message{})
Delete(&Message{})
return res.RowsAffected, res.Error
}

View File

@ -101,7 +101,7 @@ func (s *Service) SelectPending(deviceID string) ([]MessageOut, error) {
return nil, err
}
return slices.Map(messages, messageToDomain), nil
return slices.MapOrError(messages, messageToDomain)
}
func (s *Service) UpdateState(deviceID string, message smsgateway.MessageState) error {
@ -114,11 +114,11 @@ func (s *Service) UpdateState(deviceID string, message smsgateway.MessageState)
message.State = smsgateway.ProcessingStateProcessed
}
existing.State = models.ProcessingState(message.State)
existing.States = slices.Map(maps.Keys(message.States), func(key string) models.MessageState {
return models.MessageState{
existing.State = ProcessingState(message.State)
existing.States = slices.Map(maps.Keys(message.States), func(key string) MessageState {
return MessageState{
MessageID: existing.ID,
State: models.ProcessingState(key),
State: ProcessingState(key),
UpdatedAt: message.States[key],
}
})
@ -178,14 +178,13 @@ func (s *Service) Enqueue(device models.Device, message MessageIn, opts EnqueueO
}
}
var validUntil *time.Time = message.ValidUntil
validUntil := message.ValidUntil
if message.TTL != nil && *message.TTL > 0 {
validUntil = anys.AsPointer(time.Now().Add(time.Duration(*message.TTL) * time.Second))
}
msg := models.Message{
msg := Message{
ExtID: message.ID,
Message: message.Message,
Recipients: s.recipientsToModel(message.PhoneNumbers),
IsEncrypted: message.IsEncrypted,
@ -197,6 +196,19 @@ func (s *Service) Enqueue(device models.Device, message MessageIn, opts EnqueueO
Priority: int8(message.Priority),
ValidUntil: validUntil,
}
if message.TextContent != nil {
if err := msg.SetTextContent(*message.TextContent); err != nil {
return state, fmt.Errorf("can't set text content: %w", err)
}
} else if message.DataContent != nil {
if err := msg.SetDataContent(*message.DataContent); err != nil {
return state, fmt.Errorf("can't set data content: %w", err)
}
} else {
return state, errors.New("no text or data content")
}
if msg.ExtID == "" {
msg.ExtID = s.idgen()
}
@ -241,11 +253,11 @@ func (s *Service) Clean(ctx context.Context) error {
///////////////////////////////////////////////////////////////////////////////
func (s *Service) recipientsToModel(input []string) []models.MessageRecipient {
output := make([]models.MessageRecipient, len(input))
func (s *Service) recipientsToModel(input []string) []MessageRecipient {
output := make([]MessageRecipient, len(input))
for i, v := range input {
output[i] = models.MessageRecipient{
output[i] = MessageRecipient{
PhoneNumber: v,
}
}
@ -253,8 +265,8 @@ func (s *Service) recipientsToModel(input []string) []models.MessageRecipient {
return output
}
func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash bool) []models.MessageRecipient {
output := make([]models.MessageRecipient, len(input))
func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash bool) []MessageRecipient {
output := make([]MessageRecipient, len(input))
for i, v := range input {
phoneNumber := v.PhoneNumber
@ -271,9 +283,9 @@ func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash
phoneNumber = fmt.Sprintf("%x", sha256.Sum256([]byte(phoneNumber)))[:16]
}
output[i] = models.MessageRecipient{
output[i] = MessageRecipient{
PhoneNumber: phoneNumber,
State: models.ProcessingState(v.State),
State: ProcessingState(v.State),
Error: v.Error,
}
}
@ -281,7 +293,7 @@ func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash
return output
}
func modelToMessageState(input models.Message) smsgateway.MessageState {
func modelToMessageState(input Message) smsgateway.MessageState {
return smsgateway.MessageState{
ID: input.ExtID,
State: smsgateway.ProcessingState(input.State),
@ -290,13 +302,13 @@ func modelToMessageState(input models.Message) smsgateway.MessageState {
Recipients: slices.Map(input.Recipients, modelToRecipientState),
States: slices.Associate(
input.States,
func(state models.MessageState) string { return string(state.State) },
func(state models.MessageState) time.Time { return state.UpdatedAt },
func(state MessageState) string { return string(state.State) },
func(state MessageState) time.Time { return state.UpdatedAt },
),
}
}
func modelToRecipientState(input models.MessageRecipient) smsgateway.RecipientState {
func modelToRecipientState(input MessageRecipient) smsgateway.RecipientState {
return smsgateway.RecipientState{
PhoneNumber: input.PhoneNumber,
State: smsgateway.ProcessingState(input.State),

View File

@ -5,7 +5,6 @@ import (
"testing"
"github.com/android-sms-gateway/client-go/smsgateway"
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
)
func TestService_recipientsStateToModel(t *testing.T) {
@ -17,7 +16,7 @@ func TestService_recipientsStateToModel(t *testing.T) {
name string
s *Service
args args
want []models.MessageRecipient
want []MessageRecipient
}{
{
name: "Without +",
@ -30,7 +29,7 @@ func TestService_recipientsStateToModel(t *testing.T) {
},
},
},
want: []models.MessageRecipient{
want: []MessageRecipient{
{
MessageID: 0,
PhoneNumber: "+79990001234",
@ -49,7 +48,7 @@ func TestService_recipientsStateToModel(t *testing.T) {
},
},
},
want: []models.MessageRecipient{
want: []MessageRecipient{
{
MessageID: 0,
PhoneNumber: "+79990001234",
@ -69,7 +68,7 @@ func TestService_recipientsStateToModel(t *testing.T) {
},
hash: true,
},
want: []models.MessageRecipient{
want: []MessageRecipient{
{
MessageID: 0,
PhoneNumber: "62d17792b45c5307",
@ -88,7 +87,7 @@ func TestService_recipientsStateToModel(t *testing.T) {
},
},
},
want: []models.MessageRecipient{
want: []MessageRecipient{
{
MessageID: 0,
PhoneNumber: "",

View File

@ -17,11 +17,12 @@ var Module = fx.Module(
}),
fx.Provide(
func(cfg Config, lc fx.Lifecycle) (c client, err error) {
if cfg.Mode == ModeFCM {
switch cfg.Mode {
case ModeFCM:
c, err = fcm.New(cfg.ClientOptions)
} else if cfg.Mode == ModeUpstream {
case ModeUpstream:
c, err = upstream.New(cfg.ClientOptions)
} else {
default:
return nil, errors.New("invalid push mode")
}

View File

@ -29,6 +29,35 @@ POST {{localUrl}}/message HTTP/1.1
Content-Type: application/json
Authorization: Basic {{localCredentials}}
{
"textMessage": {
"text": "{{$localDatetime iso8601}}"
},
"phoneNumbers": [
"{{phone}}"
]
}
###
POST {{localUrl}}/message HTTP/1.1
Content-Type: application/json
Authorization: Basic {{localCredentials}}
{
"dataMessage": {
"data": "SGVsbG8gV29ybGQh",
"port": 12345
},
"phoneNumbers": [
"{{phone}}"
]
}
###
POST {{localUrl}}/message HTTP/1.1
Content-Type: application/json
Authorization: Basic {{localCredentials}}
{
"message": "{{$localDatetime iso8601}}",
"ttl": 86400,

View File

@ -30,6 +30,35 @@ POST {{baseUrl}}/3rdparty/v1/messages HTTP/1.1
Content-Type: application/json
Authorization: Basic {{credentials}}
{
"textMessage": {
"text": "{{$localDatetime iso8601}}"
},
"phoneNumbers": [
"{{phone}}"
]
}
###
POST {{baseUrl}}/3rdparty/v1/messages HTTP/1.1
Content-Type: application/json
Authorization: Basic {{credentials}}
{
"dataMessage": {
"data": "SGVsbG8gRGF0YSBXb3JsZCE=",
"port": 53739
},
"phoneNumbers": [
"{{phone}}"
]
}
###
POST {{baseUrl}}/3rdparty/v1/messages HTTP/1.1
Content-Type: application/json
Authorization: Basic {{credentials}}
{
"message": "$aes-256-cbc/pbkdf2-sha1$i=75000$pb+tpPcF0nabV46wDeDMig==$ucdVkMrRYLQ0LAeoXQsWhrD36I9nnop8rRIh3dNmBhvg7Wc4Cwu3h9Petvp1dN3x",
"ttl": 600,

View File

@ -1138,6 +1138,30 @@
}
},
"definitions": {
"smsgateway.DataMessage": {
"type": "object",
"required": [
"data",
"port"
],
"properties": {
"data": {
"description": "Base64-encoded payload",
"type": "string",
"format": "byte",
"maxLength": 65535,
"minLength": 4,
"example": "SGVsbG8gV29ybGQh"
},
"port": {
"description": "Destination port",
"type": "integer",
"maximum": 65535,
"minimum": 1,
"example": 53739
}
}
},
"smsgateway.Device": {
"type": "object",
"properties": {
@ -1943,6 +1967,21 @@
"Random"
]
},
"smsgateway.TextMessage": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"description": "Message text",
"type": "string",
"maxLength": 65535,
"minLength": 1,
"example": "Hello World!"
}
}
},
"smsgateway.Webhook": {
"type": "object",
"required": [

File diff suppressed because it is too large Load Diff