mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
[messages] add data messages support
This commit is contained in:
parent
e4046c5865
commit
8f01332869
4
go.mod
4
go.mod
@ -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
20
go.sum
@ -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=
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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{})
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -7,8 +7,11 @@ import (
|
||||
)
|
||||
|
||||
type MessageIn struct {
|
||||
ID string
|
||||
Message string
|
||||
ID string
|
||||
|
||||
TextContent *TextMessageContent
|
||||
DataContent *DataMessageContent
|
||||
|
||||
PhoneNumbers []string
|
||||
IsEncrypted bool
|
||||
|
||||
|
||||
127
internal/sms-gateway/modules/messages/models.go
Normal file
127
internal/sms-gateway/modules/messages/models.go
Normal 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{})
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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: "",
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user