Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion api/local.http
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Authorization: Basic {{localCredentials}}
"phoneNumbers": [
"{{phone}}"
],
"simNumber": {{$randomInt 0 3}},
"simNumber": {{$randomInt 1 3}},
"withDeliveryReport": true
}

Expand Down Expand Up @@ -113,6 +113,10 @@ Authorization: Basic {{localCredentials}}
GET {{localUrl}}/messages/{{messageId}} HTTP/1.1
Authorization: Basic {{localCredentials}}

###
DELETE {{localUrl}}/messages/{{messageId}} HTTP/1.1
Authorization: Basic {{localCredentials}}

###
GET {{localUrl}}/webhooks HTTP/1.1
Authorization: Basic {{localCredentials}}
Expand Down
6 changes: 6 additions & 0 deletions api/requests.http
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ GET {{baseUrl}}/3rdparty/v1/messages/{{messageId}} HTTP/1.1
Authorization: Basic {{credentials}}
# Authorization: Bearer {{jwtToken}}

###
DELETE {{baseUrl}}/3rdparty/v1/messages/{{messageId}} HTTP/1.1
Authorization: Basic {{credentials}}
# Authorization: Bearer {{jwtToken}}

###
GET {{baseUrl}}/3rdparty/v1/messages HTTP/1.1
Authorization: Basic {{credentials}}
Expand Down Expand Up @@ -234,6 +239,7 @@ Content-Type: application/json
"scopes": [
"messages:send",
"messages:read",
"messages:cancel",
"devices:list",
"devices:write",
"webhooks:list",
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.25.8

require (
firebase.google.com/go/v4 v4.20.0
github.com/android-sms-gateway/client-go v1.13.0
github.com/android-sms-gateway/client-go v1.13.1-0.20260617073313-8d7d78d0d762
github.com/ansrivas/fiberprometheus/v2 v2.17.0
github.com/capcom6/go-helpers v0.4.0
github.com/capcom6/go-infra-fx v0.5.7
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/android-sms-gateway/client-go v1.13.0 h1:EvKDi796R2ScCAiaekWYRekVjkjiJGK3d/LZcPKpfQQ=
github.com/android-sms-gateway/client-go v1.13.0/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/android-sms-gateway/client-go v1.13.1-0.20260617073313-8d7d78d0d762 h1:UEUGzQuuqp2ifPJVIoZgH7ZFms2MsAQR7ajUvJ5wk6s=
github.com/android-sms-gateway/client-go v1.13.1-0.20260617073313-8d7d78d0d762/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/ansrivas/fiberprometheus/v2 v2.17.0 h1:p0gqs5LsSCWGoSFF44fCJkyU+XcE6TLRqEMu80b2iCo=
Expand Down
1 change: 1 addition & 0 deletions internal/sms-gateway/handlers/converters/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func MessageToMobileDTO(m messages.Message) smsgateway.MobileMessage {
ScheduleAt: m.ScheduleAt,
Priority: m.Priority,
},
State: smsgateway.ProcessingState(m.State),
CreatedAt: m.CreatedAt,
}
}
Expand Down
35 changes: 30 additions & 5 deletions internal/sms-gateway/handlers/messages/3rdparty.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,32 @@ func (h *ThirdPartyController) get(userID string, c *fiber.Ctx) error {
return c.JSON(converters.MessageStateToDTO(*state))
}

// @Summary Cancel message
// @Description Cancels a pending message by ID. The message must be in Pending state.
// @Security ApiAuth
// @Security JWTAuth
// @Tags User, Messages
// @Param id path string true "Message ID"
// @Success 200 {object} smsgateway.GetMessageResponse "Message state after cancellation"
// @Failure 400 {object} smsgateway.ErrorResponse "Invalid request"
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
// @Failure 403 {object} smsgateway.ErrorResponse "Forbidden"
// @Failure 404 {object} smsgateway.ErrorResponse "Message not found"
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
// @Router /3rdparty/v1/messages/{id} [delete]
//
// Cancel message.
func (h *ThirdPartyController) delete(userID string, c *fiber.Ctx) error {
id := c.Params("id")

state, err := h.messagesSvc.CancelMessage(userID, id)
if err != nil {
return fmt.Errorf("failed to cancel message: %w", err)
}

return c.JSON(converters.MessageStateToDTO(*state))
}

// Export inbox.
//
// Deprecated: use /3rdparty/v1/inbox/refresh instead.
Expand Down Expand Up @@ -283,11 +309,9 @@ func (h *ThirdPartyController) errorHandler(c *fiber.Ctx) error {

var msgValidationError messages.ValidationError
switch {
case errors.As(err, &msgValidationError):
fallthrough
case errors.Is(err, messages.ErrMultipleMessagesFound):
fallthrough
case errors.Is(err, messages.ErrNoContent):
case errors.As(err, &msgValidationError),
errors.Is(err, messages.ErrMultipleMessagesFound),
errors.Is(err, messages.ErrNoContent):
return fiber.NewError(fiber.StatusBadRequest, err.Error())

case errors.Is(err, messages.ErrMessageNotFound):
Expand Down Expand Up @@ -319,6 +343,7 @@ func (h *ThirdPartyController) Register(router fiber.Router) {
router.Get("", permissions.RequireScope(ScopeList), userauth.WithUserID(h.list))
router.Post("", permissions.RequireScope(ScopeSend), userauth.WithUserID(h.post))
router.Get(":id", permissions.RequireScope(ScopeRead), userauth.WithUserID(h.get)).Name(route3rdPartyGetMessage)
router.Delete(":id", permissions.RequireScope(ScopeCancel), userauth.WithUserID(h.delete))

router.Post("inbox/export", permissions.RequireScope(ScopeExport), userauth.WithUserID(h.postInboxExport))
}
4 changes: 2 additions & 2 deletions internal/sms-gateway/handlers/messages/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type thirdPartyPostQueryParams struct {
type thirdPartyGetQueryParams struct {
StartDate string `query:"from" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"`
EndDate string `query:"to" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"`
State string `query:"state" validate:"omitempty,oneof=Pending Processed Sent Delivered Failed"`
State string `query:"state" validate:"omitempty,oneof=Pending Cancelling Cancelled Processed Sent Delivered Failed"`
DeviceID string `query:"deviceId" validate:"omitempty,len=21"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100"`
Offset int `query:"offset" validate:"omitempty,min=0"`
Expand Down Expand Up @@ -46,7 +46,7 @@ func (p *thirdPartyGetQueryParams) ToFilter() messages.SelectFilter {
}

if p.State != "" {
filter.State = messages.ProcessingState(p.State)
filter.State = append(filter.State, messages.ProcessingState(p.State))
}
Comment thread
capcom6 marked this conversation as resolved.

if p.DeviceID != "" {
Expand Down
2 changes: 2 additions & 0 deletions internal/sms-gateway/handlers/messages/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const (
ScopeRead = smsgateway.ScopeMessagesRead
// ScopeList is the permission scope required for listing messages.
ScopeList = smsgateway.ScopeMessagesList
// ScopeCancel is the permission scope required for cancelling messages.
ScopeCancel = smsgateway.ScopeMessagesCancel
// ScopeExport is the permission scope required for exporting messages.
ScopeExport = smsgateway.ScopeMessagesExport
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE `messages`
MODIFY COLUMN `state` enum(
'Pending',
'Cancelling',
'Cancelled',
'Processed',
'Sent',
'Delivered',
'Failed'
) NOT NULL DEFAULT 'Pending';
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE `message_recipients`
MODIFY COLUMN `state` enum(
'Pending',
'Cancelling',
'Cancelled',
'Processed',
'Sent',
'Delivered',
'Failed'
) NOT NULL DEFAULT 'Pending';
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE `message_states`
MODIFY COLUMN `state` enum(
'Pending',
'Cancelling',
'Cancelled',
'Processed',
'Sent',
'Delivered',
'Failed'
) NOT NULL;
-- +goose StatementEnd
---
-- +goose Down
-- +goose StatementBegin
UPDATE `messages`
SET `state` = CASE
WHEN `state` = 'Cancelling' THEN 'Pending'
WHEN `state` = 'Cancelled' THEN 'Failed'
ELSE `state`
END
WHERE `state` IN ('Cancelling', 'Cancelled');
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE `messages`
MODIFY COLUMN `state` enum(
'Pending',
'Processed',
'Sent',
'Delivered',
'Failed'
) NOT NULL DEFAULT 'Pending';
-- +goose StatementEnd
-- +goose StatementBegin
UPDATE `message_recipients`
SET `state` = CASE
WHEN `state` = 'Cancelling' THEN 'Pending'
WHEN `state` = 'Cancelled' THEN 'Failed'
ELSE `state`
END
WHERE `state` IN ('Cancelling', 'Cancelled');
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE `message_recipients`
MODIFY COLUMN `state` enum(
'Pending',
'Processed',
'Sent',
'Delivered',
'Failed'
) NOT NULL DEFAULT 'Pending';
-- +goose StatementEnd
-- +goose StatementBegin
UPDATE `message_states`
SET `state` = CASE
WHEN `state` = 'Cancelling' THEN 'Pending'
WHEN `state` = 'Cancelled' THEN 'Failed'
ELSE `state`
END
WHERE `state` IN ('Cancelling', 'Cancelled');
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE `message_states`
MODIFY COLUMN `state` enum(
'Pending',
'Processed',
'Sent',
'Delivered',
'Failed'
) NOT NULL;
-- +goose StatementEnd
Comment thread
capcom6 marked this conversation as resolved.
6 changes: 6 additions & 0 deletions internal/sms-gateway/modules/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ func NewMessagesExportRequestedEvent(
)
}

func NewMessageCancelledEvent(messageID string) Event {
return NewEvent(smsgateway.PushMessageCancelled, map[string]string{
"messageId": messageID,
})
}

func NewSettingsUpdatedEvent() Event {
return NewEvent(smsgateway.PushSettingsUpdated, nil)
}
10 changes: 10 additions & 0 deletions internal/sms-gateway/modules/messages/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package messages
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -70,3 +71,12 @@ func (c *stateCache) Get(ctx context.Context, userID, id string) (*MessageState,

return message, nil
}

func (c *stateCache) Delete(ctx context.Context, userID, id string) error {
err := c.storage.Delete(ctx, userID+":"+id)
if err == nil || errors.Is(err, cacheImpl.ErrKeyNotFound) {
return nil
}

return fmt.Errorf("failed to delete message from cache: %w", err)
}
Comment thread
capcom6 marked this conversation as resolved.
1 change: 1 addition & 0 deletions internal/sms-gateway/modules/messages/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func messageToDomain(input messageModel) (Message, error) {
ScheduleAt: input.ScheduleAt,
Priority: smsgateway.MessagePriority(input.Priority),
},
State: input.State,
CreatedAt: input.CreatedAt,
}, nil
}
Expand Down
1 change: 1 addition & 0 deletions internal/sms-gateway/modules/messages/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type MessageInput struct {
type Message struct {
MessageInput

State ProcessingState
CreatedAt time.Time
}

Expand Down
1 change: 1 addition & 0 deletions internal/sms-gateway/modules/messages/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var (
ErrMessageNotFound = errors.New("message not found")
ErrMultipleMessagesFound = errors.New("multiple messages found")
ErrNoContent = errors.New("no text or data content")
ErrMessageNotPending = errors.New("message is not pending")

ErrQueueLimitExceeded = errors.New("queue limits exceeded")
)
Expand Down
18 changes: 10 additions & 8 deletions internal/sms-gateway/modules/messages/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ type ProcessingState string
type MessageType string

const (
ProcessingStatePending ProcessingState = "Pending"
ProcessingStateProcessed ProcessingState = "Processed"
ProcessingStateSent ProcessingState = "Sent"
ProcessingStateDelivered ProcessingState = "Delivered"
ProcessingStateFailed ProcessingState = "Failed"
ProcessingStatePending ProcessingState = "Pending"
ProcessingStateCancelling ProcessingState = "Cancelling"
ProcessingStateCancelled ProcessingState = "Cancelled"
ProcessingStateProcessed ProcessingState = "Processed"
ProcessingStateSent ProcessingState = "Sent"
ProcessingStateDelivered ProcessingState = "Delivered"
ProcessingStateFailed ProcessingState = "Failed"

MessageTypeText MessageType = "Text"
MessageTypeData MessageType = "Data"
Expand All @@ -34,7 +36,7 @@ type messageModel struct {
ExtID string `gorm:"not null;type:varchar(36);uniqueIndex:unq_messages_id_device,priority:1"`
Type MessageType `gorm:"not null;type:enum('Text','Data');default:Text"`
Content string `gorm:"not null;type:text"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending;index:idx_messages_device_state"`
State ProcessingState `gorm:"not null;type:enum('Pending','Cancelling','Cancelled','Processed','Sent','Delivered','Failed');default:Pending;index:idx_messages_device_state"`
ValidUntil *time.Time `gorm:"type:datetime"`
ScheduleAt *time.Time `gorm:"type:datetime"`
SimNumber *uint8 `gorm:"type:tinyint(1) unsigned"`
Expand Down Expand Up @@ -197,7 +199,7 @@ type messageRecipientModel struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
MessageID uint64 `gorm:"uniqueIndex:unq_message_recipients_message_id_phone_number,priority:1;type:BIGINT UNSIGNED"`
PhoneNumber string `gorm:"uniqueIndex:unq_message_recipients_message_id_phone_number,priority:2;type:varchar(128)"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending"`
State ProcessingState `gorm:"not null;type:enum('Pending','Cancelling','Cancelled','Processed','Sent','Delivered','Failed');default:Pending"`
Error *string `gorm:"type:varchar(256)"`
}

Expand Down Expand Up @@ -226,7 +228,7 @@ func (m *messageRecipientModel) toDomain() smsgateway.RecipientState {
type messageStateModel struct {
ID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED;autoIncrement"`
MessageID uint64 `gorm:"not null;type:BIGINT UNSIGNED;uniqueIndex:unq_message_states_message_id_state,priority:1"`
State ProcessingState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');uniqueIndex:unq_message_states_message_id_state,priority:2"`
State ProcessingState `gorm:"not null;type:enum('Pending','Cancelling','Cancelled','Processed','Sent','Delivered','Failed');uniqueIndex:unq_message_states_message_id_state,priority:2"`
UpdatedAt time.Time `gorm:"<-:create;not null;autoupdatetime:false"`
}

Expand Down
Loading
Loading