[api/mobile] write message states log to db

This commit is contained in:
Aleksandr Soloshenko 2024-05-15 23:37:42 +07:00
parent a6c23ec7c4
commit 63f0cb5960
7 changed files with 92 additions and 28 deletions

View File

@ -63,14 +63,18 @@ Content-Type: application/json
[
{
"id": "GKBw_tkVnN8NJz3hse9ue",
"id": "RqqnKoakAc82f6e4SwoMe",
"state": "Failed",
"recipients": [
{
"phoneNumber": "{{phone}}",
"state": "Failed"
}
]
],
"states": {
"Processed": "2024-05-13T16:49:17.357Z",
"Failed": "2024-05-13T16:49:17.357+04:00"
}
}
]

View File

@ -141,11 +141,11 @@ func (h *mobileHandler) patchMessage(device models.Device, c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if err := h.Validator.Var(req, "required,dive"); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
for _, v := range req {
if err := h.validateStruct(v); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
err := h.messagesSvc.UpdateState(device.ID, v)
if err != nil && !errors.Is(err, repositories.ErrMessageNotFound) {
h.Logger.Error("Can't update message status", zap.Error(err))

View File

@ -0,0 +1,23 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE `message_states` (
`id` BIGINT UNSIGNED AUTO_INCREMENT,
`message_id` BIGINT UNSIGNED NOT NULL,
`state` enum(
'Pending',
'Sent',
'Processed',
'Delivered',
'Failed'
) NOT NULL,
`updated_at` datetime(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `unq_message_states_message_id_state` (`message_id`, `state`),
CONSTRAINT `fk_messages_states` FOREIGN KEY (`message_id`) REFERENCES `messages`(`id`) ON DELETE CASCADE
);
-- +goose StatementEnd
---
-- +goose Down
-- +goose StatementBegin
DROP TABLE `message_states`;
-- +goose StatementEnd

View File

@ -17,6 +17,7 @@ import (
"github.com/nyaruka/phonenumbers"
"go.uber.org/fx"
"go.uber.org/zap"
"golang.org/x/exp/maps"
)
const (
@ -116,11 +117,18 @@ func (s *Service) UpdateState(deviceID string, message smsgateway.MessageState)
return err
}
if message.State == smsgateway.MessageStatePending {
message.State = smsgateway.MessageStateProcessed
if message.State == smsgateway.ProcessingStatePending {
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{
MessageID: existing.ID,
State: models.ProcessingState(key),
UpdatedAt: message.States[key],
}
})
existing.Recipients = s.recipientsStateToModel(message.Recipients, existing.IsHashed)
if err := s.Messages.UpdateState(&existing); err != nil {
@ -148,7 +156,7 @@ func (s *Service) GetState(user models.User, ID string) (smsgateway.MessageState
func (s *Service) Enqeue(device models.Device, message smsgateway.Message, opts EnqueueOptions) (smsgateway.MessageState, error) {
state := smsgateway.MessageState{
ID: "",
State: smsgateway.MessageStatePending,
State: smsgateway.ProcessingStatePending,
Recipients: make([]smsgateway.RecipientState, len(message.PhoneNumbers)),
}
@ -167,7 +175,7 @@ func (s *Service) Enqeue(device models.Device, message smsgateway.Message, opts
state.Recipients[i] = smsgateway.RecipientState{
PhoneNumber: phone,
State: smsgateway.MessageStatePending,
State: smsgateway.ProcessingStatePending,
}
}
@ -245,8 +253,8 @@ func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash
phoneNumber = "+" + phoneNumber
}
if v.State == smsgateway.MessageStatePending {
v.State = smsgateway.MessageStateProcessed
if v.State == smsgateway.ProcessingStatePending {
v.State = smsgateway.ProcessingStateProcessed
}
if hash {
@ -266,7 +274,7 @@ func (s *Service) recipientsStateToModel(input []smsgateway.RecipientState, hash
func modelToMessageState(input models.Message) smsgateway.MessageState {
return smsgateway.MessageState{
ID: input.ExtID,
State: smsgateway.ProcessState(input.State),
State: smsgateway.ProcessingState(input.State),
IsHashed: input.IsHashed,
IsEncrypted: input.IsEncrypted,
Recipients: slices.Map(input.Recipients, modelToRecipientState),
@ -276,7 +284,7 @@ func modelToMessageState(input models.Message) smsgateway.MessageState {
func modelToRecipientState(input models.MessageRecipient) smsgateway.RecipientState {
return smsgateway.RecipientState{
PhoneNumber: input.PhoneNumber,
State: smsgateway.ProcessState(input.State),
State: smsgateway.ProcessingState(input.State),
Error: input.Error,
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/capcom6/sms-gateway/internal/sms-gateway/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const HashingLockName = "36444143-1ace-4dbf-891c-cc505911497e"
@ -59,6 +60,15 @@ func (r *MessagesRepository) UpdateState(message *models.Message) error {
return err
}
for _, v := range message.States {
v.MessageID = message.ID
if err := tx.Model(&v).Clauses(clause.OnConflict{
DoNothing: true,
}).Create(&v).Error; err != nil {
return err
}
}
for _, v := range message.Recipients {
if err := tx.Model(&v).Where("message_id = ?", message.ID).Select("State", "Error").Updates(&v).Error; err != nil {
return err

View File

@ -6,13 +6,21 @@ import (
)
const (
MessageStatePending ProcessState = "Pending" // Pending
MessageStateProcessed ProcessState = "Processed" // Processed (received by device)
MessageStateSent ProcessState = "Sent" // Sent
MessageStateDelivered ProcessState = "Delivered" // Delivered
MessageStateFailed ProcessState = "Failed" // Failed
ProcessingStatePending ProcessingState = "Pending" // Pending
ProcessingStateProcessed ProcessingState = "Processed" // Processed (received by device)
ProcessingStateSent ProcessingState = "Sent" // Sent
ProcessingStateDelivered ProcessingState = "Delivered" // Delivered
ProcessingStateFailed ProcessingState = "Failed" // Failed
)
var allProcessStates = map[ProcessingState]struct{}{
ProcessingStatePending: {},
ProcessingStateProcessed: {},
ProcessingStateSent: {},
ProcessingStateDelivered: {},
ProcessingStateFailed: {},
}
// Device
type Device struct {
ID string `json:"id" example:"PyDmBQZZXYmyxMwED8Fzy"` // ID
@ -47,18 +55,29 @@ func (m Message) Validate() error {
// Message state
type MessageState struct {
ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Message ID
State ProcessState `json:"state" validate:"required" example:"Pending"` // State
IsHashed bool `json:"isHashed" example:"false"` // Hashed
IsEncrypted bool `json:"isEncrypted" example:"false"` // Encrypted
Recipients []RecipientState `json:"recipients" validate:"required,min=1,dive"` // Recipients states
ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Message ID
State ProcessingState `json:"state" validate:"required" example:"Pending"` // State
IsHashed bool `json:"isHashed" example:"false"` // Hashed
IsEncrypted bool `json:"isEncrypted" example:"false"` // Encrypted
Recipients []RecipientState `json:"recipients" validate:"required,min=1,dive"` // Recipients states
States map[string]time.Time `json:"states"` // History of states
}
func (m MessageState) Validate() error {
for k := range m.States {
if _, ok := allProcessStates[ProcessingState(k)]; !ok {
return fmt.Errorf("invalid state value: %s", k)
}
}
return nil
}
// Recipient state
type RecipientState struct {
PhoneNumber string `json:"phoneNumber" validate:"required,min=10,max=128" example:"79990001234"` // Phone number or first 16 symbols of SHA256 hash
State ProcessState `json:"state" validate:"required" example:"Pending"` // State
Error *string `json:"error,omitempty" example:"timeout"` // Error (for `Failed` state)
PhoneNumber string `json:"phoneNumber" validate:"required,min=10,max=128" example:"79990001234"` // Phone number or first 16 symbols of SHA256 hash
State ProcessingState `json:"state" validate:"required" example:"Pending"` // State
Error *string `json:"error,omitempty" example:"timeout"` // Error (for `Failed` state)
}
// Push notification

View File

@ -2,6 +2,6 @@ package smsgateway
import "errors"
type ProcessState string
type ProcessingState string
var ErrConflictFields = errors.New("conflict fields")