From 18dabe504b0db2900103dfe4734170c6d729e940 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Wed, 9 Jul 2025 07:00:47 +0700 Subject: [PATCH] [handlers] separate message status DTOs --- go.mod | 2 +- go.sum | 2 + .../sms-gateway/handlers/messages/3rdparty.go | 55 ++++--- internal/sms-gateway/handlers/mobile.go | 20 ++- .../sms-gateway/modules/messages/domain.go | 22 +++ .../sms-gateway/modules/messages/service.go | 49 +++--- pkg/swagger/docs/mobile.http | 2 +- pkg/swagger/docs/swagger.json | 154 +++++++++++------- pkg/swagger/docs/swagger.yaml | 113 ++++++++----- 9 files changed, 264 insertions(+), 155 deletions(-) diff --git a/go.mod b/go.mod index 62b1ef6..f7e9f95 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.23.2 require ( firebase.google.com/go/v4 v4.12.1 - github.com/android-sms-gateway/client-go v1.8.2 + github.com/android-sms-gateway/client-go v1.8.3-0.20250708235905-d5c9b879467b github.com/ansrivas/fiberprometheus/v2 v2.6.1 github.com/capcom6/go-helpers v0.3.0 github.com/capcom6/go-infra-fx v0.2.1 diff --git a/go.sum b/go.sum index a1a2daf..afe80a4 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/android-sms-gateway/client-go v1.8.2-0.20250703013756-220a21ca308a h1 github.com/android-sms-gateway/client-go v1.8.2-0.20250703013756-220a21ca308a/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= github.com/android-sms-gateway/client-go v1.8.2 h1:uywKRE9j1UL+u9e8k4MDg7qri0RFN+4lSdR9ZYd7vSo= github.com/android-sms-gateway/client-go v1.8.2/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.8.3-0.20250708235905-d5c9b879467b h1:1gbDvRyHzSx3A9l9A1G1xPIcHjswLoi3Q5/5W1Izn4s= +github.com/android-sms-gateway/client-go v1.8.3-0.20250708235905-d5c9b879467b/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= diff --git a/internal/sms-gateway/handlers/messages/3rdparty.go b/internal/sms-gateway/handlers/messages/3rdparty.go index f43d207..c7478d7 100644 --- a/internal/sms-gateway/handlers/messages/3rdparty.go +++ b/internal/sms-gateway/handlers/messages/3rdparty.go @@ -45,16 +45,16 @@ type ThirdPartyController struct { // @Tags User, Messages // @Accept json // @Produce json -// @Param skipPhoneValidation query bool false "Skip phone validation" -// @Param deviceActiveWithin query int false "Filter devices active within the specified number of hours" default(0) -// @Param request body smsgateway.Message true "Send message request" -// @Success 202 {object} smsgateway.MessageState "Message enqueued" -// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" -// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" -// @Failure 409 {object} smsgateway.ErrorResponse "Message with such ID already exists" -// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" -// @Header 202 {string} Location "Get message state URL" -// @Router /3rdparty/v1/messages [post] +// @Param skipPhoneValidation query bool false "Skip phone validation" +// @Param deviceActiveWithin query int false "Filter devices active within the specified number of hours" default(0) +// @Param request body smsgateway.Message true "Send message request" +// @Success 202 {object} smsgateway.GetMessageResponse "Message enqueued" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 409 {object} smsgateway.ErrorResponse "Message with such ID already exists" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Header 202 {string} Location "Get message state URL" +// @Router /3rdparty/v1/messages [post] // // Enqueue message func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error { @@ -157,7 +157,16 @@ func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error { c.Location(location) } - return c.Status(fiber.StatusAccepted).JSON(state) + return c.Status(fiber.StatusAccepted). + JSON(smsgateway.GetMessageResponse{ + ID: state.ID, + DeviceID: state.DeviceID, + State: smsgateway.ProcessingState(state.State), + IsHashed: state.IsHashed, + IsEncrypted: state.IsEncrypted, + Recipients: state.Recipients, + States: state.States, + }) } // @Summary Get message state @@ -165,12 +174,12 @@ func (h *ThirdPartyController) post(user models.User, c *fiber.Ctx) error { // @Security ApiAuth // @Tags User, Messages // @Produce json -// @Param id path string true "Message ID" -// @Success 200 {object} smsgateway.MessageState "Message state" -// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" -// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" -// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" -// @Router /3rdparty/v1/messages/{id} [get] +// @Param id path string true "Message ID" +// @Success 200 {object} smsgateway.GetMessageResponse "Message state" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Router /3rdparty/v1/messages/{id} [get] // // Get message state func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error { @@ -185,7 +194,15 @@ func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error { return err } - return c.JSON(state) + return c.JSON(smsgateway.GetMessageResponse{ + ID: state.ID, + DeviceID: state.DeviceID, + State: smsgateway.ProcessingState(state.State), + IsHashed: state.IsHashed, + IsEncrypted: state.IsEncrypted, + Recipients: state.Recipients, + States: state.States, + }) } // @Summary Request inbox messages export @@ -199,7 +216,7 @@ func (h *ThirdPartyController) get(user models.User, c *fiber.Ctx) error { // @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" // @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized" // @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" -// @Router /3rdparty/v1/inbox/export [post] +// @Router /3rdparty/v1/inbox/export [post] // // Export inbox func (h *ThirdPartyController) postInboxExport(user models.User, c *fiber.Ctx) error { diff --git a/internal/sms-gateway/handlers/mobile.go b/internal/sms-gateway/handlers/mobile.go index caac407..dfd1ea9 100644 --- a/internal/sms-gateway/handlers/mobile.go +++ b/internal/sms-gateway/handlers/mobile.go @@ -182,25 +182,27 @@ func (h *mobileHandler) getMessage(device models.Device, c *fiber.Ctx) error { // @Tags Device, Messages // @Accept json // @Produce json -// @Param request body []smsgateway.MessageState true "New message state" -// @Success 204 {object} nil "Successfully updated" -// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" -// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" +// @Param request body smsgateway.MobilePatchMessageRequest true "New message state" +// @Success 204 {object} nil "Successfully updated" +// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request" +// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error" // @Router /mobile/v1/message [patch] // // Update message state func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error { - req := []smsgateway.MessageState{} + var req smsgateway.MobilePatchMessageRequest if err := c.BodyParser(&req); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } + var messageState messages.MessageStateIn for _, v := range req { - if err := h.ValidateStruct(v); err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } + messageState.ID = v.ID + messageState.State = messages.ProcessingState(v.State) + messageState.Recipients = v.Recipients + messageState.States = v.States - err := h.messagesSvc.UpdateState(device.ID, v) + err := h.messagesSvc.UpdateState(device.ID, messageState) if err != nil && !errors.Is(err, messages.ErrMessageNotFound) { h.Logger.Error("Can't update message status", zap.Error(err)) } diff --git a/internal/sms-gateway/modules/messages/domain.go b/internal/sms-gateway/modules/messages/domain.go index 2cfd7f9..9e70ac7 100644 --- a/internal/sms-gateway/modules/messages/domain.go +++ b/internal/sms-gateway/modules/messages/domain.go @@ -27,3 +27,25 @@ type MessageOut struct { CreatedAt time.Time } + +type MessageStateIn struct { + // Message ID + ID string + // State + State ProcessingState + // Recipients states + Recipients []smsgateway.RecipientState + // History of states + States map[string]time.Time +} + +type MessageStateOut struct { + // Device ID + DeviceID string + // Hashed + IsHashed bool + // Encrypted + IsEncrypted bool + + MessageStateIn +} diff --git a/internal/sms-gateway/modules/messages/service.go b/internal/sms-gateway/modules/messages/service.go index 5cf8731..4735b64 100644 --- a/internal/sms-gateway/modules/messages/service.go +++ b/internal/sms-gateway/modules/messages/service.go @@ -104,17 +104,17 @@ func (s *Service) SelectPending(deviceID string) ([]MessageOut, error) { return slices.MapOrError(messages, messageToDomain) } -func (s *Service) UpdateState(deviceID string, message smsgateway.MessageState) error { +func (s *Service) UpdateState(deviceID string, message MessageStateIn) error { existing, err := s.messages.Get(message.ID, MessagesSelectFilter{DeviceID: deviceID}) if err != nil { return err } - if message.State == smsgateway.ProcessingStatePending { - message.State = smsgateway.ProcessingStateProcessed + if message.State == ProcessingStatePending { + message.State = ProcessingStateProcessed } - existing.State = ProcessingState(message.State) + existing.State = message.State existing.States = slices.Map(maps.Keys(message.States), func(key string) MessageState { return MessageState{ MessageID: existing.ID, @@ -135,28 +135,30 @@ func (s *Service) UpdateState(deviceID string, message smsgateway.MessageState) return nil } -func (s *Service) GetState(user models.User, ID string) (smsgateway.MessageState, error) { +func (s *Service) GetState(user models.User, ID string) (MessageStateOut, error) { message, err := s.messages.Get( ID, MessagesSelectFilter{}, MessagesSelectOptions{WithRecipients: true, WithDevice: true, WithStates: true}, ) if err != nil { - return smsgateway.MessageState{}, ErrMessageNotFound + return MessageStateOut{}, ErrMessageNotFound } if message.Device.UserID != user.ID { - return smsgateway.MessageState{}, ErrMessageNotFound + return MessageStateOut{}, ErrMessageNotFound } return modelToMessageState(message), nil } -func (s *Service) Enqueue(device models.Device, message MessageIn, opts EnqueueOptions) (smsgateway.MessageState, error) { - state := smsgateway.MessageState{ - DeviceID: device.ID, - State: smsgateway.ProcessingStatePending, - Recipients: make([]smsgateway.RecipientState, len(message.PhoneNumbers)), +func (s *Service) Enqueue(device models.Device, message MessageIn, opts EnqueueOptions) (MessageStateOut, error) { + state := MessageStateOut{ + DeviceID: device.ID, + MessageStateIn: MessageStateIn{ + State: ProcessingStatePending, + Recipients: make([]smsgateway.RecipientState, len(message.PhoneNumbers)), + }, } var phone string @@ -293,19 +295,22 @@ func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash return output } -func modelToMessageState(input Message) smsgateway.MessageState { - return smsgateway.MessageState{ - ID: input.ExtID, +func modelToMessageState(input Message) MessageStateOut { + return MessageStateOut{ DeviceID: input.DeviceID, - State: smsgateway.ProcessingState(input.State), IsHashed: input.IsHashed, IsEncrypted: input.IsEncrypted, - Recipients: slices.Map(input.Recipients, modelToRecipientState), - States: slices.Associate( - input.States, - func(state MessageState) string { return string(state.State) }, - func(state MessageState) time.Time { return state.UpdatedAt }, - ), + + MessageStateIn: MessageStateIn{ + ID: input.ExtID, + State: input.State, + Recipients: slices.Map(input.Recipients, modelToRecipientState), + States: slices.Associate( + input.States, + func(state MessageState) string { return string(state.State) }, + func(state MessageState) time.Time { return state.UpdatedAt }, + ), + }, } } diff --git a/pkg/swagger/docs/mobile.http b/pkg/swagger/docs/mobile.http index 04e0a0c..b640ede 100644 --- a/pkg/swagger/docs/mobile.http +++ b/pkg/swagger/docs/mobile.http @@ -43,7 +43,7 @@ Content-Type: application/json [ { - "id": "2dcIAhcLg81cez7GE_Pdp", + "id": "NBjsgnVp72pvcdonJm7a5", "state": "Failed", "recipients": [ { diff --git a/pkg/swagger/docs/swagger.json b/pkg/swagger/docs/swagger.json index b4b59f9..10ba474 100644 --- a/pkg/swagger/docs/swagger.json +++ b/pkg/swagger/docs/swagger.json @@ -311,7 +311,7 @@ "202": { "description": "Message enqueued", "schema": { - "$ref": "#/definitions/smsgateway.MessageState" + "$ref": "#/definitions/smsgateway.GetMessageResponse" }, "headers": { "Location": { @@ -376,7 +376,7 @@ "200": { "description": "Message state", "schema": { - "$ref": "#/definitions/smsgateway.MessageState" + "$ref": "#/definitions/smsgateway.GetMessageResponse" } }, "400": { @@ -900,7 +900,43 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/smsgateway.MessageState" + "type": "object", + "required": [ + "recipients", + "state" + ], + "properties": { + "id": { + "description": "Message ID", + "type": "string", + "maxLength": 36, + "example": "PyDmBQZZXYmyxMwED8Fzy" + }, + "recipients": { + "description": "Recipients states", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/smsgateway.RecipientState" + } + }, + "state": { + "description": "State", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.ProcessingState" + } + ], + "example": "Pending" + }, + "states": { + "description": "History of states", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } } } } @@ -1266,6 +1302,62 @@ } } }, + "smsgateway.GetMessageResponse": { + "type": "object", + "required": [ + "deviceId", + "recipients", + "state" + ], + "properties": { + "deviceId": { + "description": "Device ID", + "type": "string", + "maxLength": 21, + "example": "PyDmBQZZXYmyxMwED8Fzy" + }, + "id": { + "description": "Message ID", + "type": "string", + "maxLength": 36, + "example": "PyDmBQZZXYmyxMwED8Fzy" + }, + "isEncrypted": { + "description": "Encrypted", + "type": "boolean", + "example": false + }, + "isHashed": { + "description": "Hashed", + "type": "boolean", + "example": false + }, + "recipients": { + "description": "Recipients states", + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/smsgateway.RecipientState" + } + }, + "state": { + "description": "State", + "allOf": [ + { + "$ref": "#/definitions/smsgateway.ProcessingState" + } + ], + "example": "Pending" + }, + "states": { + "description": "History of states", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "smsgateway.HealthCheck": { "type": "object", "properties": { @@ -1516,62 +1608,6 @@ "PriorityMaximum" ] }, - "smsgateway.MessageState": { - "type": "object", - "required": [ - "deviceId", - "recipients", - "state" - ], - "properties": { - "deviceId": { - "description": "Device ID", - "type": "string", - "maxLength": 21, - "example": "PyDmBQZZXYmyxMwED8Fzy" - }, - "id": { - "description": "Message ID", - "type": "string", - "maxLength": 36, - "example": "PyDmBQZZXYmyxMwED8Fzy" - }, - "isEncrypted": { - "description": "Encrypted", - "type": "boolean", - "example": false - }, - "isHashed": { - "description": "Hashed", - "type": "boolean", - "example": false - }, - "recipients": { - "description": "Recipients states", - "type": "array", - "minItems": 1, - "items": { - "$ref": "#/definitions/smsgateway.RecipientState" - } - }, - "state": { - "description": "State", - "allOf": [ - { - "$ref": "#/definitions/smsgateway.ProcessingState" - } - ], - "example": "Pending" - }, - "states": { - "description": "History of states", - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - }, "smsgateway.MessagesExportRequest": { "type": "object", "required": [ diff --git a/pkg/swagger/docs/swagger.yaml b/pkg/swagger/docs/swagger.yaml index 30cb2bb..c202538 100644 --- a/pkg/swagger/docs/swagger.yaml +++ b/pkg/swagger/docs/swagger.yaml @@ -80,6 +80,47 @@ definitions: example: An error occurred type: string type: object + smsgateway.GetMessageResponse: + properties: + deviceId: + description: Device ID + example: PyDmBQZZXYmyxMwED8Fzy + maxLength: 21 + type: string + id: + description: Message ID + example: PyDmBQZZXYmyxMwED8Fzy + maxLength: 36 + type: string + isEncrypted: + description: Encrypted + example: false + type: boolean + isHashed: + description: Hashed + example: false + type: boolean + recipients: + description: Recipients states + items: + $ref: '#/definitions/smsgateway.RecipientState' + minItems: 1 + type: array + state: + allOf: + - $ref: '#/definitions/smsgateway.ProcessingState' + description: State + example: Pending + states: + additionalProperties: + type: string + description: History of states + type: object + required: + - deviceId + - recipients + - state + type: object smsgateway.HealthCheck: properties: description: @@ -267,47 +308,6 @@ definitions: - PriorityDefault - PriorityBypassThreshold - PriorityMaximum - smsgateway.MessageState: - properties: - deviceId: - description: Device ID - example: PyDmBQZZXYmyxMwED8Fzy - maxLength: 21 - type: string - id: - description: Message ID - example: PyDmBQZZXYmyxMwED8Fzy - maxLength: 36 - type: string - isEncrypted: - description: Encrypted - example: false - type: boolean - isHashed: - description: Hashed - example: false - type: boolean - recipients: - description: Recipients states - items: - $ref: '#/definitions/smsgateway.RecipientState' - minItems: 1 - type: array - state: - allOf: - - $ref: '#/definitions/smsgateway.ProcessingState' - description: State - example: Pending - states: - additionalProperties: - type: string - description: History of states - type: object - required: - - deviceId - - recipients - - state - type: object smsgateway.MessagesExportRequest: properties: deviceId: @@ -921,7 +921,7 @@ paths: description: Get message state URL type: string schema: - $ref: '#/definitions/smsgateway.MessageState' + $ref: '#/definitions/smsgateway.GetMessageResponse' "400": description: Invalid request schema: @@ -959,7 +959,7 @@ paths: "200": description: Message state schema: - $ref: '#/definitions/smsgateway.MessageState' + $ref: '#/definitions/smsgateway.GetMessageResponse' "400": description: Invalid request schema: @@ -1292,7 +1292,32 @@ paths: required: true schema: items: - $ref: '#/definitions/smsgateway.MessageState' + properties: + id: + description: Message ID + example: PyDmBQZZXYmyxMwED8Fzy + maxLength: 36 + type: string + recipients: + description: Recipients states + items: + $ref: '#/definitions/smsgateway.RecipientState' + minItems: 1 + type: array + state: + allOf: + - $ref: '#/definitions/smsgateway.ProcessingState' + description: State + example: Pending + states: + additionalProperties: + type: string + description: History of states + type: object + required: + - recipients + - state + type: object type: array produces: - application/json