Skip to content

Commit 509c601

Browse files
committed
[chore] improve errors handling and logging
1 parent c9cb324 commit 509c601

File tree

16 files changed

+356
-77
lines changed

16 files changed

+356
-77
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.5.9-0.20250522134006-6e8b4dd3057a
9+
github.com/android-sms-gateway/client-go v1.5.9-0.20250522231449-9e0855eff19f
1010
github.com/ansrivas/fiberprometheus/v2 v2.6.1
1111
github.com/capcom6/go-helpers v0.2.0
1212
github.com/capcom6/go-infra-fx v0.2.1

go.sum

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,10 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
2626
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
2727
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
2828
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
29-
github.com/android-sms-gateway/client-go v1.5.7 h1:1L9Ot3yc+5DtGaDOCUj4/8DEECWyfo4IoPyL+oXnzyE=
30-
github.com/android-sms-gateway/client-go v1.5.7/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
31-
github.com/android-sms-gateway/client-go v1.5.8-0.20250516025314-5876d8deb355 h1:fctR5OH1c7g1zWEfp4K+fCZkY4+tZwTiKr/rN5N2yS8=
32-
github.com/android-sms-gateway/client-go v1.5.8-0.20250516025314-5876d8deb355/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
33-
github.com/android-sms-gateway/client-go v1.5.8 h1:t9630c1Hv8u/MjwQ8epJ0iDpt3VXurSNFC91CFEjM/M=
34-
github.com/android-sms-gateway/client-go v1.5.8/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
3529
github.com/android-sms-gateway/client-go v1.5.9-0.20250522134006-6e8b4dd3057a h1:TSmfm+KOsR1Ie10nZEjCVDepa1bEPin0NAgEUOSJiqw=
3630
github.com/android-sms-gateway/client-go v1.5.9-0.20250522134006-6e8b4dd3057a/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
31+
github.com/android-sms-gateway/client-go v1.5.9-0.20250522231449-9e0855eff19f h1:VYrL6YbkQ49pcyiXTYcR5LN1WpNy1Tc684XjeE1UCvw=
32+
github.com/android-sms-gateway/client-go v1.5.9-0.20250522231449-9e0855eff19f/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
3733
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
3834
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
3935
github.com/ansrivas/fiberprometheus/v2 v2.6.1 h1:wac3pXaE6BYYTF04AC6K0ktk6vCD+MnDOJZ3SK66kXM=

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ func TestDeviceToDTO(t *testing.T) {
3232
ID: "test-id",
3333
Name: anys.AsPointer("test-name"),
3434
LastSeen: lastSeenAt,
35-
TimedModel: models.TimedModel{
36-
CreatedAt: createdAt,
37-
UpdatedAt: updatedAt,
35+
SoftDeletableModel: models.SoftDeletableModel{
36+
TimedModel: models.TimedModel{
37+
CreatedAt: createdAt,
38+
UpdatedAt: updatedAt,
39+
},
3840
},
3941
},
4042
expected: smsgateway.Device{

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

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ type ThirdPartyController struct {
2929
devicesSvc *devices.Service
3030
}
3131

32-
// @Summary Get settings
33-
// @Description Returns settings for a specific user
34-
// @Security ApiAuth
35-
// @Tags User, Settings
36-
// @Produce json
37-
// @Success 200 {object} smsgateway.DeviceSettings "Settings"
38-
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
39-
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
40-
// @Router /3rdparty/v1/settings [get]
32+
// @Summary Get settings
33+
// @Description Returns settings for a specific user
34+
// @Security ApiAuth
35+
// @Tags User, Settings
36+
// @Produce json
37+
// @Success 200 {object} smsgateway.DeviceSettings "Settings"
38+
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
39+
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
40+
// @Router /3rdparty/v1/settings [get]
4141
//
4242
// Get settings
4343
func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error {
@@ -49,29 +49,29 @@ func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error {
4949
return c.JSON(settings)
5050
}
5151

52-
// @Summary Update settings
53-
// @Description Updates settings for a specific user
54-
// @Security ApiAuth
55-
// @Tags User, Settings
56-
// @Accept json
57-
// @Produce json
58-
// @Param request body smsgateway.DeviceSettings true "Settings"
59-
// @Success 200 {object} object "Settings updated"
60-
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
61-
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
62-
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
63-
// @Router /3rdparty/v1/settings [put]
52+
// @Summary Update settings
53+
// @Description Updates settings for a specific user
54+
// @Security ApiAuth
55+
// @Tags User, Settings
56+
// @Accept json
57+
// @Produce json
58+
// @Param request body smsgateway.DeviceSettings true "Settings"
59+
// @Success 200 {object} object "Settings updated"
60+
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
61+
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
62+
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
63+
// @Router /3rdparty/v1/settings [put]
6464
//
6565
// Update settings
6666
func (h *ThirdPartyController) put(user models.User, c *fiber.Ctx) error {
6767
if err := h.BodyParserValidator(c, &smsgateway.DeviceSettings{}); err != nil {
68-
return fiber.NewError(fiber.StatusBadRequest, err.Error())
68+
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid settings format: %v", err))
6969
}
7070

7171
settings := make(map[string]any, 8)
7272

7373
if err := c.BodyParser(&settings); err != nil {
74-
return err
74+
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to parse request body: %v", err))
7575
}
7676

7777
updated, err := h.devicesSvc.ReplaceSettings(user.ID, settings)
@@ -83,15 +83,29 @@ func (h *ThirdPartyController) put(user models.User, c *fiber.Ctx) error {
8383
return c.JSON(updated)
8484
}
8585

86+
// @Summary Partially update settings
87+
// @Description Partially updates settings for a specific user
88+
// @Security ApiAuth
89+
// @Tags User, Settings
90+
// @Accept json
91+
// @Produce json
92+
// @Param request body smsgateway.DeviceSettings true "Settings"
93+
// @Success 200 {object} object "Settings updated"
94+
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
95+
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
96+
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
97+
// @Router /3rdparty/v1/settings [patch]
98+
//
99+
// Partially update settings
86100
func (h *ThirdPartyController) patch(user models.User, c *fiber.Ctx) error {
87101
if err := h.BodyParserValidator(c, &smsgateway.DeviceSettings{}); err != nil {
88-
return fiber.NewError(fiber.StatusBadRequest, err.Error())
102+
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid settings format: %v", err))
89103
}
90104

91105
settings := make(map[string]any, 8)
92106

93107
if err := c.BodyParser(&settings); err != nil {
94-
return err
108+
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to parse request body: %v", err))
95109
}
96110

97111
updated, err := h.devicesSvc.UpdateSettings(user.ID, settings)

internal/sms-gateway/handlers/settings/mobile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type MobileController struct {
4040
func (h *MobileController) get(device models.Device, c *fiber.Ctx) error {
4141
settings, err := h.devicesSvc.GetSettings(device.UserID)
4242
if err != nil {
43-
return fmt.Errorf("can't get settings: %w", err)
43+
return fmt.Errorf("can't get settings for device %s (user ID: %s): %w", device.ID, device.UserID, err)
4444
}
4545

4646
return c.JSON(settings)

internal/sms-gateway/models/migrations/mysql/20250521225803_add_device_settings.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
CREATE TABLE `device_settings` (
44
`user_id` varchar(32) NOT NULL,
55
`settings` json NOT NULL,
6+
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
7+
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
68
PRIMARY KEY (`user_id`),
79
CONSTRAINT `fk_device_settings_user` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
810
);

internal/sms-gateway/models/models.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ const (
1515
)
1616

1717
type TimedModel struct {
18-
CreatedAt time.Time `gorm:"->;not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"`
19-
UpdatedAt time.Time `gorm:"->;not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`
18+
CreatedAt time.Time `gorm:"->;not null;autocreatetime:false;default:CURRENT_TIMESTAMP(3)"`
19+
UpdatedAt time.Time `gorm:"->;not null;autoupdatetime:false;default:CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"`
20+
}
21+
22+
type SoftDeletableModel struct {
23+
TimedModel
2024
DeletedAt *time.Time `gorm:"<-:update"`
2125
}
2226

@@ -25,7 +29,7 @@ type User struct {
2529
PasswordHash string `gorm:"not null;type:varchar(72)"`
2630
Devices []Device `gorm:"-,foreignKey:UserID;constraint:OnDelete:CASCADE"`
2731

28-
TimedModel
32+
SoftDeletableModel
2933
}
3034

3135
type Device struct {
@@ -38,7 +42,7 @@ type Device struct {
3842

3943
UserID string `gorm:"not null;type:varchar(32)"`
4044

41-
TimedModel
45+
SoftDeletableModel
4246
}
4347

4448
func (d *Device) IsEmpty() bool {
@@ -67,7 +71,7 @@ type Message struct {
6771
Recipients []MessageRecipient `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`
6872
States []MessageState `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`
6973

70-
TimedModel
74+
SoftDeletableModel
7175
}
7276

7377
type MessageRecipient struct {

internal/sms-gateway/modules/devices/models.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package devices
22

33
import (
4+
"fmt"
5+
46
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
57
"gorm.io/gorm"
68
)
@@ -10,8 +12,13 @@ type DeviceSettings struct {
1012
Settings map[string]any `gorm:"not null;type:json;serializer:json"`
1113

1214
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
15+
16+
models.TimedModel
1317
}
1418

1519
func Migrate(db *gorm.DB) error {
16-
return db.AutoMigrate(&DeviceSettings{})
20+
if err := db.AutoMigrate(&DeviceSettings{}); err != nil {
21+
return fmt.Errorf("device_settings migration failed: %w", err)
22+
}
23+
return nil
1724
}

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

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,26 +111,21 @@ func (r *repository) UpdateSettings(settings *DeviceSettings) error {
111111
return err
112112
}
113113

114+
if source.Settings == nil {
115+
source.Settings = map[string]any{}
116+
}
117+
114118
settings.Settings = appendMap(source.Settings, settings.Settings, rules)
115119

116120
return r.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(settings).Error
117121
})
118-
119-
// return r.db.
120-
// Clauses(clause.OnConflict{
121-
// DoUpdates: clause.Assignments(
122-
// map[string]interface{}{
123-
// "settings": gorm.Expr("JSON_MERGE_PATCH(settings, VALUES(settings))"),
124-
// },
125-
// ),
126-
// }).
127-
// Create(settings).
128-
// Error
129-
130122
}
131123

132124
func (r *repository) ReplaceSettings(settings *DeviceSettings) (*DeviceSettings, error) {
133-
return settings, r.db.Save(settings).Error
125+
err := r.db.Transaction(func(tx *gorm.DB) error {
126+
return tx.Save(settings).Error
127+
})
128+
return settings, err
134129
}
135130

136131
func newDevicesRepository(db *gorm.DB) *repository {

internal/sms-gateway/modules/devices/utils.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package devices
22

3-
import "errors"
3+
import "fmt"
44

55
var rules = map[string]any{
66
"encryption": map[string]any{
@@ -76,7 +76,7 @@ func filterMap(m map[string]any, r map[string]any) (map[string]any, error) {
7676
} else if m[field] == nil {
7777
continue
7878
} else {
79-
return nil, errors.New("The field: '" + field + "' is not a map to dive")
79+
return nil, fmt.Errorf("the field: '%s' is not a map to dive", field)
8080
}
8181
} else if _, ok := rule.(string); ok {
8282
if _, ok := m[field]; !ok {
@@ -94,7 +94,13 @@ func appendMap(m1, m2 map[string]any, rules map[string]any) map[string]any {
9494
for field, rule := range rules {
9595
if ruleObj, ok := rule.(map[string]any); ok {
9696
if dataObj, ok := m2[field].(map[string]any); ok {
97-
m1[field] = appendMap(m1[field].(map[string]any), dataObj, ruleObj)
97+
if m1Field, ok := m1[field].(map[string]any); ok {
98+
m1[field] = appendMap(m1Field, dataObj, ruleObj)
99+
} else {
100+
// Initialize if not present or not a map
101+
newMap := make(map[string]any)
102+
m1[field] = appendMap(newMap, dataObj, ruleObj)
103+
}
98104
} else if m2[field] == nil {
99105
continue
100106
}

internal/sms-gateway/modules/push/service.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ func (s *Service) Notify(userID string, deviceID *string, event *domain.Event) e
168168
}
169169

170170
errs := make([]error, 0, len(devices))
171+
notifiedCount := 0
171172
for _, device := range devices {
172173
if device.PushToken == nil {
173174
s.logger.Info("Device has no push token", zap.String("user_id", userID), zap.String("device_id", device.ID))
@@ -177,10 +178,12 @@ func (s *Service) Notify(userID string, deviceID *string, event *domain.Event) e
177178
if err := s.Enqueue(*device.PushToken, event); err != nil {
178179
s.logger.Error("Failed to send push notification", zap.String("user_id", userID), zap.String("device_id", device.ID), zap.Error(err))
179180
errs = append(errs, err)
181+
} else {
182+
notifiedCount++
180183
}
181184
}
182185

183-
s.logger.Info("Notified devices", append(logFields, zap.Int("count", len(devices)))...)
186+
s.logger.Info("Notified devices", append(logFields, zap.Int("count", notifiedCount), zap.Int("total", len(devices)))...)
184187

185188
return errors.Join(errs...)
186189
}

internal/sms-gateway/modules/push/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ func NewMessagesExportRequestedEvent(since, until time.Time) *domain.Event {
4747
},
4848
)
4949
}
50+
51+
func NewSettingsUpdatedEvent() *domain.Event {
52+
return domain.NewEvent(smsgateway.PushSettingsUpdated, nil)
53+
}

internal/sms-gateway/modules/webhooks/models.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type Webhook struct {
1919
User models.User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"`
2020
Device *models.Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"`
2121

22-
models.TimedModel
22+
models.SoftDeletableModel
2323
}
2424

2525
func Migrate(db *gorm.DB) error {

pkg/swagger/docs/requests.http

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ Content-Type: application/json
118118
},
119119
"messages": {
120120
"send_interval_min": null,
121-
"send_interval_max": -1
121+
"send_interval_max": 1
122122
}
123123
}
124124

0 commit comments

Comments
 (0)