Skip to content

Commit 3677294

Browse files
committed
[messages] add get sent messages endpoint
1 parent f496d67 commit 3677294

File tree

14 files changed

+498
-49
lines changed

14 files changed

+498
-49
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ toolchain go1.23.2
66

77
require (
88
firebase.google.com/go/v4 v4.12.1
9-
github.com/android-sms-gateway/client-go v1.9.1
9+
github.com/android-sms-gateway/client-go v1.9.2
1010
github.com/ansrivas/fiberprometheus/v2 v2.6.1
1111
github.com/capcom6/go-helpers v0.3.0
1212
github.com/capcom6/go-infra-fx v0.2.3

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
2828
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
2929
github.com/android-sms-gateway/client-go v1.9.1 h1:i9hKf+kgaJo9ykWKoeno3MliOThoIlwO0N2eed6bY+Q=
3030
github.com/android-sms-gateway/client-go v1.9.1/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
31+
github.com/android-sms-gateway/client-go v1.9.2-0.20250805133721-9ebe535d038a h1:ibYxk2m1Qcwydi/GDHpDnUCcLORqLpYGxm6XModDFnM=
32+
github.com/android-sms-gateway/client-go v1.9.2-0.20250805133721-9ebe535d038a/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
33+
github.com/android-sms-gateway/client-go v1.9.2-0.20250805235002-a840b364bc1d h1:gurnY2hszJ1Yn8vt81Qn9yF/8zcmJK4YzhZAgsYUt/4=
34+
github.com/android-sms-gateway/client-go v1.9.2-0.20250805235002-a840b364bc1d/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
35+
github.com/android-sms-gateway/client-go v1.9.2 h1:e9HFgvR+LRMV0dOJvFkxt998UxOMWNf8hfnXwMIc39I=
36+
github.com/android-sms-gateway/client-go v1.9.2/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
3137
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
3238
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
3339
github.com/ansrivas/fiberprometheus/v2 v2.6.1 h1:wac3pXaE6BYYTF04AC6K0ktk6vCD+MnDOJZ3SK66kXM=

internal/sms-gateway/handlers/converters/messages.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages"
66
)
77

8-
func MessageToDTO(m messages.MessageOut) smsgateway.MobileMessage {
8+
func MessageToMobileDTO(m messages.MessageOut) smsgateway.MobileMessage {
99
var message string
1010
var textMessage *smsgateway.TextMessage
1111
var dataMessage *smsgateway.DataMessage
@@ -41,3 +41,15 @@ func MessageToDTO(m messages.MessageOut) smsgateway.MobileMessage {
4141
CreatedAt: m.CreatedAt,
4242
}
4343
}
44+
45+
func MessageStateToDTO(state messages.MessageStateOut) smsgateway.MessageState {
46+
return smsgateway.MessageState{
47+
ID: state.ID,
48+
DeviceID: state.DeviceID,
49+
State: smsgateway.ProcessingState(state.State),
50+
IsHashed: state.IsHashed,
51+
IsEncrypted: state.IsEncrypted,
52+
Recipients: state.Recipients,
53+
States: state.States,
54+
}
55+
}

internal/sms-gateway/handlers/converters/messages_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func TestMessageToDTO(t *testing.T) {
7979
for _, tc := range tests {
8080
t.Run(tc.name, func(t *testing.T) {
8181
// Call the function under test
82-
result := converters.MessageToDTO(tc.input)
82+
result := converters.MessageToMobileDTO(tc.input)
8383

8484
// Assert the results
8585
assert.Equal(t, tc.expected, result)

internal/sms-gateway/handlers/messages/3rdparty.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package messages
33
import (
44
"errors"
55
"fmt"
6+
"strconv"
67
"time"
78

89
"github.com/android-sms-gateway/client-go/smsgateway"
910
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
11+
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/converters"
1012
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
1113
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
1214
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/devices"
@@ -169,6 +171,42 @@ func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error {
169171
})
170172
}
171173

174+
// @Summary Get messages
175+
// @Description Retrieves a list of messages with filtering and pagination
176+
// @Security ApiAuth
177+
// @Tags User, Messages
178+
// @Produce json
179+
// @Param from query string false "Start date in RFC3339 format" Format(date-time)
180+
// @Param to query string false "End date in RFC3339 format" Format(date-time)
181+
// @Param state query string false "Filter messages by processing state" Enum(Pending, Processed, Sent, Delivered, Failed)
182+
// @Param deviceId query string false "Filter by device ID" min(21) max(21)
183+
// @Param limit query int false "Pagination limit" default(50) min(1) max(100)
184+
// @Param offset query int false "Pagination offset" default(0)
185+
// @Success 200 {object} smsgateway.GetMessagesResponse "A list of messages"
186+
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
187+
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
188+
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
189+
// @Router /3rdparty/v1/messages [get]
190+
//
191+
// Get message history
192+
func (h *ThirdPartyController) list(user models.User, c *fiber.Ctx) error {
193+
params := getQueryParams{}
194+
if err := h.QueryParserValidator(c, &params); err != nil {
195+
return fiber.NewError(fiber.StatusBadRequest, err.Error())
196+
}
197+
198+
messages, total, err := h.messagesSvc.SelectStates(user, params.ToFilter(), params.ToOptions())
199+
if err != nil {
200+
h.Logger.Error("Failed to get message history", zap.Error(err), zap.String("user_id", user.ID))
201+
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve message history")
202+
}
203+
204+
c.Set("X-Total-Count", strconv.Itoa(int(total)))
205+
return c.JSON(
206+
slices.Map(messages, converters.MessageStateToDTO),
207+
)
208+
}
209+
172210
// @Summary Get message state
173211
// @Description Returns message state by ID
174212
// @Security ApiAuth
@@ -194,15 +232,7 @@ func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error {
194232
return err
195233
}
196234

197-
return c.JSON(smsgateway.GetMessageResponse{
198-
ID: state.ID,
199-
DeviceID: state.DeviceID,
200-
State: smsgateway.ProcessingState(state.State),
201-
IsHashed: state.IsHashed,
202-
IsEncrypted: state.IsEncrypted,
203-
Recipients: state.Recipients,
204-
States: state.States,
205-
})
235+
return c.JSON(converters.MessageStateToDTO(state))
206236
}
207237

208238
// @Summary Request inbox messages export
@@ -242,8 +272,9 @@ func (h *ThirdPartyController) postInboxExport(user models.User, c *fiber.Ctx) e
242272
}
243273

244274
func (h *ThirdPartyController) Register(router fiber.Router) {
275+
router.Get("", userauth.WithUser(h.list))
245276
router.Post("", userauth.WithUser(h.post))
246-
router.Get(":id", userauth.WithUser(h.get))
277+
router.Get(":id", userauth.WithUser(h.get)).Name(route3rdPartyGetMessage)
247278

248279
router.Post("inbox/export", userauth.WithUser(h.postInboxExport))
249280
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,75 @@
11
package messages
22

3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/android-sms-gateway/server/internal/sms-gateway/modules/messages"
8+
)
9+
310
type postQueryParams struct {
411
SkipPhoneValidation bool `query:"skipPhoneValidation"`
512
DeviceActiveWithin uint `query:"deviceActiveWithin"`
613
}
14+
15+
type getQueryParams struct {
16+
StartDate string `query:"from" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"`
17+
EndDate string `query:"to" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"`
18+
State string `query:"state" validate:"omitempty,oneof=Pending Processed Sent Delivered Failed"`
19+
DeviceID string `query:"deviceId" validate:"omitempty,len=21"`
20+
Limit int `query:"limit" validate:"omitempty,min=1,max=100"`
21+
Offset int `query:"offset" validate:"omitempty,min=0"`
22+
}
23+
24+
func (p *getQueryParams) Validate() error {
25+
if p.StartDate != "" && p.EndDate != "" && p.StartDate > p.EndDate {
26+
return fmt.Errorf("`from` date must be before `to` date")
27+
}
28+
29+
return nil
30+
}
31+
32+
func (p *getQueryParams) ToFilter() messages.MessagesSelectFilter {
33+
filter := messages.MessagesSelectFilter{}
34+
35+
if p.StartDate != "" {
36+
if t, err := time.Parse(time.RFC3339, p.StartDate); err == nil {
37+
filter.StartDate = t
38+
}
39+
}
40+
41+
if p.EndDate != "" {
42+
if t, err := time.Parse(time.RFC3339, p.EndDate); err == nil {
43+
filter.EndDate = t
44+
}
45+
}
46+
47+
if p.State != "" {
48+
filter.State = messages.ProcessingState(p.State)
49+
}
50+
51+
if p.DeviceID != "" {
52+
filter.DeviceID = p.DeviceID
53+
}
54+
55+
return filter
56+
}
57+
58+
func (p *getQueryParams) ToOptions() messages.MessagesSelectOptions {
59+
options := messages.MessagesSelectOptions{
60+
WithRecipients: true,
61+
WithStates: true,
62+
}
63+
64+
if p.Limit > 0 {
65+
options.Limit = min(p.Limit, 100)
66+
} else {
67+
options.Limit = 50
68+
}
69+
70+
if p.Offset > 0 {
71+
options.Offset = p.Offset
72+
}
73+
74+
return options
75+
}

internal/sms-gateway/handlers/mobile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func (h *mobileHandler) getMessage(device models.Device, c *fiber.Ctx) error {
172172
smsgateway.MobileGetMessagesResponse(
173173
slices.Map(
174174
msgs,
175-
converters.MessageToDTO,
175+
converters.MessageToMobileDTO,
176176
),
177177
),
178178
)

internal/sms-gateway/modules/messages/repository.go

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"errors"
7+
"fmt"
78
"time"
89

910
"github.com/go-sql-driver/mysql"
@@ -15,46 +16,108 @@ const hashingLockName = "36444143-1ace-4dbf-891c-cc505911497e"
1516

1617
var ErrMessageNotFound = gorm.ErrRecordNotFound
1718
var ErrMessageAlreadyExists = errors.New("duplicate id")
19+
var ErrMultipleMessagesFound = errors.New("multiple messages found")
1820

1921
type repository struct {
2022
db *gorm.DB
2123
}
2224

23-
func (r *repository) SelectPending(deviceID string) (messages []Message, err error) {
24-
err = r.db.
25-
Where("device_id = ? AND state = ?", deviceID, ProcessingStatePending).
26-
Order("priority DESC, id DESC").
27-
Limit(100).
28-
Preload("Recipients").
29-
Find(&messages).
30-
Error
25+
func (r *repository) Select(filter MessagesSelectFilter, options MessagesSelectOptions) ([]Message, int64, error) {
26+
query := r.db.Model(&Message{})
3127

32-
return
33-
}
28+
// Apply date range filter
29+
if !filter.StartDate.IsZero() {
30+
query = query.Where("messages.created_at >= ?", filter.StartDate)
31+
}
32+
if !filter.EndDate.IsZero() {
33+
query = query.Where("messages.created_at < ?", filter.EndDate)
34+
}
35+
36+
// Apply ID filter
37+
if filter.ExtID != "" {
38+
query = query.Where("messages.ext_id = ?", filter.ExtID)
39+
}
40+
41+
// Apply user filter
42+
if filter.UserID != "" {
43+
query = query.
44+
Joins("JOIN devices ON messages.device_id = devices.id").
45+
Where("devices.user_id = ?", filter.UserID)
46+
}
3447

35-
func (r *repository) Get(ID string, filter MessagesSelectFilter, options ...MessagesSelectOptions) (message Message, err error) {
36-
query := r.db.Model(&message).
37-
Where("ext_id = ?", ID)
48+
// Apply state filter
49+
if filter.State != "" {
50+
query = query.Where("messages.state = ?", filter.State)
51+
}
3852

53+
// Apply device filter
3954
if filter.DeviceID != "" {
40-
query = query.Where("device_id = ?", filter.DeviceID)
55+
query = query.Where("messages.device_id = ?", filter.DeviceID)
4156
}
4257

43-
if len(options) > 0 {
44-
if options[0].WithRecipients {
45-
query = query.Preload("Recipients")
46-
}
47-
if options[0].WithDevice {
48-
query = query.Joins("Device")
49-
}
50-
if options[0].WithStates {
51-
query = query.Preload("States")
52-
}
58+
// Get total count
59+
var total int64
60+
if err := query.Count(&total).Error; err != nil {
61+
return nil, 0, err
62+
}
63+
64+
// Apply pagination
65+
if options.Limit > 0 {
66+
query = query.Limit(options.Limit)
67+
}
68+
if options.Offset > 0 {
69+
query = query.Offset(options.Offset)
70+
}
71+
72+
// Apply ordering
73+
query = query.Order("messages.priority DESC, messages.id DESC")
74+
75+
// Preload related data
76+
if options.WithRecipients {
77+
query = query.Preload("Recipients")
78+
}
79+
if filter.UserID == "" && options.WithDevice {
80+
query = query.Joins("Device")
81+
}
82+
if options.WithStates {
83+
query = query.Preload("States")
84+
}
85+
86+
messages := make([]Message, 0, min(options.Limit, int(total)))
87+
if err := query.Find(&messages).Error; err != nil {
88+
return nil, 0, fmt.Errorf("can't select messages: %w", err)
89+
}
90+
91+
return messages, total, nil
92+
}
93+
94+
func (r *repository) SelectPending(deviceID string) ([]Message, error) {
95+
messages, _, err := r.Select(MessagesSelectFilter{
96+
DeviceID: deviceID,
97+
State: ProcessingStatePending,
98+
}, MessagesSelectOptions{
99+
WithRecipients: true,
100+
Limit: 100,
101+
})
102+
103+
return messages, err
104+
}
105+
106+
func (r *repository) Get(filter MessagesSelectFilter, options MessagesSelectOptions) (Message, error) {
107+
messages, _, err := r.Select(filter, options)
108+
if err != nil {
109+
return Message{}, fmt.Errorf("can't get message: %w", err)
110+
}
111+
112+
if len(messages) == 0 {
113+
return Message{}, ErrMessageNotFound
53114
}
54115

55-
err = query.Take(&message).Error
116+
if len(messages) > 1 {
117+
return Message{}, ErrMultipleMessagesFound
118+
}
56119

57-
return
120+
return messages[0], nil
58121
}
59122

60123
func (r *repository) Insert(message *Message) error {
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
package messages
22

3+
import "time"
4+
35
type MessagesSelectFilter struct {
4-
DeviceID string
6+
ExtID string
7+
UserID string
8+
DeviceID string
9+
StartDate time.Time
10+
EndDate time.Time
11+
State ProcessingState
512
}
613

714
type MessagesSelectOptions struct {
815
WithRecipients bool
916
WithDevice bool
1017
WithStates bool
18+
19+
Limit int
20+
Offset int
1121
}

0 commit comments

Comments
 (0)