[handlers] separate message status DTOs

This commit is contained in:
Aleksandr Soloshenko 2025-07-09 07:00:47 +07:00 committed by Aleksandr
parent 26e205d73b
commit 18dabe504b
9 changed files with 264 additions and 155 deletions

2
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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 {

View File

@ -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))
}

View File

@ -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
}

View File

@ -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 },
),
},
}
}

View File

@ -43,7 +43,7 @@ Content-Type: application/json
[
{
"id": "2dcIAhcLg81cez7GE_Pdp",
"id": "NBjsgnVp72pvcdonJm7a5",
"state": "Failed",
"recipients": [
{

View File

@ -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": [

View File

@ -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