diff --git a/smsgateway/client.go b/smsgateway/client.go index 758c16a..9e0d42a 100644 --- a/smsgateway/client.go +++ b/smsgateway/client.go @@ -135,7 +135,7 @@ func (c *Client) ExportInbox(ctx context.Context, req MessagesExportRequest) err return nil } -// ListInboxMessages retrieves incoming messages with filtering and pagination. +// ListInboxMessages retrieves inbox messages with filtering and pagination. // Returns the messages, total count (from X-Total-Count header), and error. func (c *Client) ListInboxMessages(ctx context.Context, opts ListInboxOptions) ([]IncomingMessage, int, error) { path := "/inbox?" + opts.ToURLValues().Encode() diff --git a/smsgateway/client_test.go b/smsgateway/client_test.go index 6edb720..ce8f26b 100644 --- a/smsgateway/client_test.go +++ b/smsgateway/client_test.go @@ -1140,11 +1140,15 @@ func TestClient_ListInboxMessages(t *testing.T) { client := newClient(server.URL) msgs, total, err := client.ListInboxMessages(context.Background(), smsgateway.ListInboxOptions{ - Type: &msgType, - Limit: &limit, - Offset: &offset, - From: &from, - To: &to, + Type: &msgType, + PaginationOptions: smsgateway.PaginationOptions{ + Limit: &limit, + Offset: &offset, + }, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + To: &to, + }, DeviceID: &deviceID, }) if err != nil { @@ -1311,12 +1315,16 @@ func TestClient_ListMessages(t *testing.T) { client := newClient(server.URL) msgs, total, err := client.ListMessages(context.Background(), smsgateway.ListMessagesOptions{ - From: &from, - To: &to, - State: &state, - DeviceID: &deviceID, - Limit: &limit, - Offset: &offset, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + To: &to, + }, + State: &state, + DeviceID: &deviceID, + PaginationOptions: smsgateway.PaginationOptions{ + Limit: &limit, + Offset: &offset, + }, IncludeContent: &includeContent, }) if err != nil { diff --git a/smsgateway/domain_auth.go b/smsgateway/domain_auth.go index 597d305..c08bbf6 100644 --- a/smsgateway/domain_auth.go +++ b/smsgateway/domain_auth.go @@ -8,6 +8,7 @@ const ( ScopeInboxList JWTScope = "inbox:list" ScopeInboxRefresh JWTScope = "inbox:refresh" + ScopeInboxRead JWTScope = "inbox:read" ScopeLogsRead JWTScope = "logs:read" diff --git a/smsgateway/dto_inbox.go b/smsgateway/dto_inbox.go index 06ce770..35d63ab 100644 --- a/smsgateway/dto_inbox.go +++ b/smsgateway/dto_inbox.go @@ -4,7 +4,7 @@ import ( "time" ) -// IncomingMessageType represents the type of incoming message. +// IncomingMessageType represents the type of inbox message. type IncomingMessageType string const ( @@ -14,21 +14,33 @@ const ( IncomingMessageTypeMmsDownloaded IncomingMessageType = "MMS_DOWNLOADED" // Downloaded MMS message ) -// IncomingMessage represents an incoming message received by the device. +// IncomingMessage represents an inbox message received by the device. // -// ID is the incoming message ID. -// Type is the type of the incoming message (SMS, DATA_SMS, MMS, MMS_DOWNLOADED). -// Sender is the incoming sender phone number. +// ID is the inbox message ID. +// Type is the type of the inbox message (SMS, DATA_SMS, MMS, MMS_DOWNLOADED). +// Sender is the inbox sender phone number. // Recipient is the recipient phone number on the device. // SimNumber is the SIM slot number. // ContentPreview is the message body preview or metadata. +// IsEncrypted indicates whether the message is encrypted. +// Attachments is the list of MMS attachments. // CreatedAt is the message received timestamp. type IncomingMessage struct { - ID string `json:"id" example:"PyDmBQZZXYmyxMwED8Fzy" validate:"required"` // Incoming message ID - Type IncomingMessageType `json:"type" example:"SMS" validate:"required"` // Message type - Sender string `json:"sender" example:"+79990001234" validate:"required"` // Incoming sender phone number - Recipient *string `json:"recipient,omitempty" example:"+79990001234" validate:"optional"` // Recipient phone number on the device - SimNumber *uint8 `json:"simNumber,omitempty" example:"1" validate:"optional"` // SIM slot number - ContentPreview string `json:"contentPreview" example:"Hello World!" validate:"required"` // Message body preview or metadata - CreatedAt time.Time `json:"createdAt" example:"2020-01-01T00:00:00Z" validate:"required" format:"date-time"` // Message received timestamp + ID string `json:"id" example:"PyDmBQZZXYmyxMwED8Fzy" validate:"required"` // Inbox message ID + Type IncomingMessageType `json:"type" example:"SMS" validate:"required"` // Message type + Sender string `json:"sender" example:"+79990001234" validate:"required"` // Inbox sender phone number + Recipient *string `json:"recipient,omitempty" example:"+79990001234" validate:"optional"` // Recipient phone number on the device + SimNumber *uint8 `json:"simNumber,omitempty" example:"1" validate:"optional"` // SIM slot number + ContentPreview string `json:"contentPreview" example:"Hello World!" validate:"required"` // Message body preview or metadata + IsEncrypted bool `json:"isEncrypted" example:"true"` // Whether the message is encrypted + Attachments []InboxAttachment `json:"attachments,omitempty"` // MMS attachments + CreatedAt time.Time `json:"createdAt" example:"2020-01-01T00:00:00Z" validate:"required" format:"date-time"` // Message received timestamp +} + +// InboxAttachment represents an MMS attachment for an inbox message. +type InboxAttachment struct { + PartID int64 `json:"partId" example:"1"` // Attachment part ID + Name string `json:"name" example:"photo.jpg"` // Attachment file name + Size int64 `json:"size" example:"102400"` // Attachment file size + ContentType string `json:"contentType" example:"image/jpeg"` // Attachment MIME type } diff --git a/smsgateway/requests_3rdparty.go b/smsgateway/requests_3rdparty.go index c1ac4e7..1b8d619 100644 --- a/smsgateway/requests_3rdparty.go +++ b/smsgateway/requests_3rdparty.go @@ -2,12 +2,45 @@ package smsgateway import ( "fmt" + "maps" "math" "net/url" "strconv" "time" ) +type PaginationOptions struct { + Limit *int `query:"limit" validate:"omitempty,min=1,max=100" default:"50"` + Offset *int `query:"offset" validate:"omitempty,min=0"` +} + +func (o PaginationOptions) ToURLValues() url.Values { + values := url.Values{} + if o.Limit != nil { + values.Set("limit", strconv.Itoa(*o.Limit)) + } + if o.Offset != nil { + values.Set("offset", strconv.Itoa(*o.Offset)) + } + return values +} + +type DatePeriodOptions struct { + From *time.Time `query:"from"` + To *time.Time `query:"to"` +} + +func (o DatePeriodOptions) ToURLValues() url.Values { + values := url.Values{} + if o.From != nil { + values.Set("from", o.From.Format(time.RFC3339)) + } + if o.To != nil { + values.Set("to", o.To.Format(time.RFC3339)) + } + return values +} + type SendOption func(*SendOptions) type SendOptions struct { @@ -68,49 +101,14 @@ func WithDeviceActiveWithin(hours uint) SendOption { } } -// ListInboxOptions holds optional filters for listing inbox messages. -type ListInboxOptions struct { - Type *IncomingMessageType - Limit *int - Offset *int - From *time.Time - To *time.Time - DeviceID *string -} - -// ToURLValues returns the ListInboxOptions as URL query parameters. -func (o ListInboxOptions) ToURLValues() url.Values { - values := url.Values{} - if o.Type != nil { - values.Set("type", string(*o.Type)) - } - if o.Limit != nil { - values.Set("limit", strconv.Itoa(*o.Limit)) - } - if o.Offset != nil { - values.Set("offset", strconv.Itoa(*o.Offset)) - } - if o.From != nil { - values.Set("from", o.From.Format(time.RFC3339)) - } - if o.To != nil { - values.Set("to", o.To.Format(time.RFC3339)) - } - if o.DeviceID != nil { - values.Set("deviceId", *o.DeviceID) - } - return values -} - // ListMessagesOptions holds optional filters and sorting for listing messages. // Sorting follows the JSON:API specification (sort parameter). type ListMessagesOptions struct { - From *time.Time `query:"from"` - To *time.Time `query:"to"` + DatePeriodOptions + PaginationOptions + 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"` IncludeContent *bool `query:"includeContent"` Sort *MessagesSortOrder `query:"sort" validate:"omitempty,oneof=created_at -created_at"` } @@ -127,30 +125,22 @@ func (o ListMessagesOptions) Validate() error { // ToURLValues returns the ListMessagesOptions as URL query parameters. func (o ListMessagesOptions) ToURLValues() url.Values { values := url.Values{} - if o.From != nil { - values.Set("from", o.From.Format(time.RFC3339)) - } - if o.To != nil { - values.Set("to", o.To.Format(time.RFC3339)) - } if o.State != nil { values.Set("state", *o.State) } if o.DeviceID != nil { values.Set("deviceId", *o.DeviceID) } - if o.Limit != nil { - values.Set("limit", strconv.Itoa(*o.Limit)) - } - if o.Offset != nil { - values.Set("offset", strconv.Itoa(*o.Offset)) - } if o.IncludeContent != nil { values.Set("includeContent", strconv.FormatBool(*o.IncludeContent)) } if o.Sort != nil { values.Set("sort", string(*o.Sort)) } + + maps.Copy(values, o.DatePeriodOptions.ToURLValues()) + maps.Copy(values, o.PaginationOptions.ToURLValues()) + return values } diff --git a/smsgateway/requests_3rdparty_test.go b/smsgateway/requests_3rdparty_test.go index 2548f33..f73d9ec 100644 --- a/smsgateway/requests_3rdparty_test.go +++ b/smsgateway/requests_3rdparty_test.go @@ -190,7 +190,9 @@ func TestListInboxOptions_ToURLValues(t *testing.T) { { name: "Limit set", options: smsgateway.ListInboxOptions{ - Limit: ptr(10), + PaginationOptions: smsgateway.PaginationOptions{ + Limit: ptr(10), + }, }, expected: url.Values{ "limit": {"10"}, @@ -199,7 +201,9 @@ func TestListInboxOptions_ToURLValues(t *testing.T) { { name: "Offset set", options: smsgateway.ListInboxOptions{ - Offset: ptr(20), + PaginationOptions: smsgateway.PaginationOptions{ + Offset: ptr(20), + }, }, expected: url.Values{ "offset": {"20"}, @@ -208,7 +212,9 @@ func TestListInboxOptions_ToURLValues(t *testing.T) { { name: "From set", options: smsgateway.ListInboxOptions{ - From: &from, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + }, }, expected: url.Values{ "from": {from.Format(time.RFC3339)}, @@ -217,7 +223,9 @@ func TestListInboxOptions_ToURLValues(t *testing.T) { { name: "To set", options: smsgateway.ListInboxOptions{ - To: &to, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + To: &to, + }, }, expected: url.Values{ "to": {to.Format(time.RFC3339)}, @@ -235,11 +243,15 @@ func TestListInboxOptions_ToURLValues(t *testing.T) { { name: "All fields set", options: smsgateway.ListInboxOptions{ - Type: ptr(smsgateway.IncomingMessageTypeSMS), - Limit: ptr(50), - Offset: ptr(10), - From: &from, - To: &to, + Type: ptr(smsgateway.IncomingMessageTypeSMS), + PaginationOptions: smsgateway.PaginationOptions{ + Limit: ptr(50), + Offset: ptr(10), + }, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + To: &to, + }, DeviceID: &deviceID, }, expected: url.Values{ @@ -254,7 +266,9 @@ func TestListInboxOptions_ToURLValues(t *testing.T) { { name: "Limit zero", options: smsgateway.ListInboxOptions{ - Limit: ptr(0), + PaginationOptions: smsgateway.PaginationOptions{ + Limit: ptr(0), + }, }, expected: url.Values{ "limit": {"0"}, @@ -263,7 +277,9 @@ func TestListInboxOptions_ToURLValues(t *testing.T) { { name: "Offset zero", options: smsgateway.ListInboxOptions{ - Offset: ptr(0), + PaginationOptions: smsgateway.PaginationOptions{ + Offset: ptr(0), + }, }, expected: url.Values{ "offset": {"0"}, @@ -308,38 +324,48 @@ func TestListMessagesOptions_Validate(t *testing.T) { { name: "From before To", options: smsgateway.ListMessagesOptions{ - From: &from, - To: &to, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + To: &to, + }, }, wantErr: false, }, { name: "From after To", options: smsgateway.ListMessagesOptions{ - From: &to, - To: &from, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &to, + To: &from, + }, }, wantErr: true, }, { name: "From equal to To", options: smsgateway.ListMessagesOptions{ - From: &from, - To: &from, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + To: &from, + }, }, wantErr: false, }, { name: "Only From set", options: smsgateway.ListMessagesOptions{ - From: &from, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + }, }, wantErr: false, }, { name: "Only To set", options: smsgateway.ListMessagesOptions{ - To: &to, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + To: &to, + }, }, wantErr: false, }, @@ -384,7 +410,9 @@ func TestListMessagesOptions_ToURLValues(t *testing.T) { { name: "From set", options: smsgateway.ListMessagesOptions{ - From: &from, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + }, }, expected: url.Values{ "from": {from.Format(time.RFC3339)}, @@ -393,7 +421,9 @@ func TestListMessagesOptions_ToURLValues(t *testing.T) { { name: "To set", options: smsgateway.ListMessagesOptions{ - To: &to, + DatePeriodOptions: smsgateway.DatePeriodOptions{ + To: &to, + }, }, expected: url.Values{ "to": {to.Format(time.RFC3339)}, @@ -420,7 +450,9 @@ func TestListMessagesOptions_ToURLValues(t *testing.T) { { name: "Limit set", options: smsgateway.ListMessagesOptions{ - Limit: ptr(25), + PaginationOptions: smsgateway.PaginationOptions{ + Limit: ptr(25), + }, }, expected: url.Values{ "limit": {"25"}, @@ -429,7 +461,9 @@ func TestListMessagesOptions_ToURLValues(t *testing.T) { { name: "Offset set", options: smsgateway.ListMessagesOptions{ - Offset: ptr(5), + PaginationOptions: smsgateway.PaginationOptions{ + Offset: ptr(5), + }, }, expected: url.Values{ "offset": {"5"}, @@ -474,12 +508,16 @@ func TestListMessagesOptions_ToURLValues(t *testing.T) { { name: "All fields set", options: smsgateway.ListMessagesOptions{ - From: &from, - To: &to, - State: &state, - DeviceID: &deviceID, - Limit: ptr(100), - Offset: ptr(0), + DatePeriodOptions: smsgateway.DatePeriodOptions{ + From: &from, + To: &to, + }, + State: &state, + DeviceID: &deviceID, + PaginationOptions: smsgateway.PaginationOptions{ + Limit: ptr(100), + Offset: ptr(0), + }, IncludeContent: ptr(true), Sort: ptr(smsgateway.CreatedAtDescending), }, diff --git a/smsgateway/requests_inbox.go b/smsgateway/requests_inbox.go index bf582a6..e1b21ec 100644 --- a/smsgateway/requests_inbox.go +++ b/smsgateway/requests_inbox.go @@ -1,6 +1,35 @@ package smsgateway -import "time" +import ( + "maps" + "net/url" + "time" +) + +// ListInboxOptions holds optional filters for listing inbox messages. +type ListInboxOptions struct { + DatePeriodOptions + PaginationOptions + + Type *IncomingMessageType `query:"type" validate:"omitempty,oneof=SMS DATA_SMS MMS MMS_DOWNLOADED"` + DeviceID *string `query:"deviceId" validate:"omitempty,len=21"` +} + +// ToURLValues returns the ListInboxOptions as URL query parameters. +func (o ListInboxOptions) ToURLValues() url.Values { + values := url.Values{} + if o.Type != nil { + values.Set("type", string(*o.Type)) + } + if o.DeviceID != nil { + values.Set("deviceId", *o.DeviceID) + } + + maps.Copy(values, o.DatePeriodOptions.ToURLValues()) + maps.Copy(values, o.PaginationOptions.ToURLValues()) + + return values +} // WebhookDelivery represents the delivery mode for webhooks. type WebhookDelivery string diff --git a/smsgateway/requests_mobile.go b/smsgateway/requests_mobile.go index 8646d87..76ac64f 100644 --- a/smsgateway/requests_mobile.go +++ b/smsgateway/requests_mobile.go @@ -41,3 +41,28 @@ type MobilePatchMessageItem struct { // MobilePatchMessageRequest represents a request to patch messages. type MobilePatchMessageRequest []MobilePatchMessageItem + +// MobilePostInboxRequestItemAttachment represents an attachment in an inbox message upload. +type MobilePostInboxRequestItemAttachment struct { + PartID int64 `json:"partId" validate:"required" example:"1"` + ContentType string `json:"contentType" validate:"required,max=128" example:"image/jpeg"` + Name string `json:"name" validate:"required,max=512" example:"part001"` + Size *int64 `json:"size,omitempty" example:"12345"` + Data []byte `json:"data" validate:"required"` +} + +// MobilePostInboxRequestItem represents a single inbox message upload. +type MobilePostInboxRequestItem struct { + ID string `json:"id" validate:"required,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` + Type IncomingMessageType `json:"type" validate:"required" example:"SMS"` + Sender string `json:"sender" validate:"required,max=512" example:"+79990001234"` + Recipient *string `json:"recipient,omitempty" validate:"omitempty,max=512" example:"+79990001234"` + SimNumber *uint8 `json:"simNumber,omitempty" example:"1"` + Content string `json:"content" validate:"required"` + IsEncrypted bool `json:"isEncrypted" validate:"required" example:"true"` + CreatedAt time.Time `json:"createdAt" validate:"required" format:"date-time"` + Attachments []MobilePostInboxRequestItemAttachment `json:"attachments,omitempty" validate:"omitempty,dive"` +} + +// MobilePostInboxRequest represents a batch of inbox messages from a device. +type MobilePostInboxRequest []MobilePostInboxRequestItem