diff --git a/.pubnub.yml b/.pubnub.yml index 1159a0c2..216f6223 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,6 +1,11 @@ --- -version: v7.2.1 +version: v7.3.0 changelog: + - date: 2024-12-10 + version: v7.3.0 + changes: + - type: feature + text: "Add `CustomMessageType` to messages and files." - date: 2023-11-27 version: v7.2.1 changes: @@ -745,7 +750,7 @@ sdks: distribution-type: package distribution-repository: GitHub package-name: Go - location: https://github.com/pubnub/go/releases/tag/v7.2.1 + location: https://github.com/pubnub/go/releases/tag/v7.3.0 requires: - name: "Go" diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ef2b7a..209c4bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v7.3.0 +December 10 2024 + +#### Added +- Add `CustomMessageType` to messages and files. + ## v7.2.1 November 27 2023 diff --git a/files_send_file.go b/files_send_file.go index 8f08cc75..4251dc5e 100644 --- a/files_send_file.go +++ b/files_send_file.go @@ -94,9 +94,18 @@ func (b *sendFileBuilder) QueryParam(queryParam map[string]string) *sendFileBuil // Transport sets the Transport for the sendFile request. func (b *sendFileBuilder) Transport(tr http.RoundTripper) *sendFileBuilder { b.opts.Transport = tr + return b } +// CustomMessageType sets the User-specified message type string - limited by 3-50 case-sensitive alphanumeric characters +// with only `-` and `_` special characters allowed. +func (b *sendFileBuilder) CustomMessageType(messageType string) *sendFileBuilder { + b.opts.CustomMessageType = messageType + + return b +} + // Execute runs the sendFile request. func (b *sendFileBuilder) Execute() (*PNSendFileResponse, StatusResponse, error) { rawJSON, status, err := executeRequest(b.opts) @@ -110,19 +119,24 @@ func (b *sendFileBuilder) Execute() (*PNSendFileResponse, StatusResponse, error) type sendFileOpts struct { endpointOpts - Channel string - Name string - Message string - File *os.File - CipherKey string - TTL int - Meta interface{} - ShouldStore bool - QueryParam map[string]string + Channel string + Name string + Message string + File *os.File + CipherKey string + TTL int + Meta interface{} + ShouldStore bool + QueryParam map[string]string + CustomMessageType string Transport http.RoundTripper } +func (o *sendFileOpts) isCustomMessageTypeCorrect() bool { + return isCustomMessageTypeValid(o.CustomMessageType) +} + func (o *sendFileOpts) validate() error { if o.config().SubscribeKey == "" { return newValidationError(o, StrMissingSubKey) @@ -135,6 +149,11 @@ func (o *sendFileOpts) validate() error { if o.Name == "" { return newValidationError(o, StrMissingFileName) } + + if !o.isCustomMessageTypeCorrect() { + return newValidationError(o, StrInvalidCustomMessageType) + } + return nil } @@ -149,6 +168,10 @@ func (o *sendFileOpts) buildQuery() (*url.Values, error) { SetQueryParam(q, o.QueryParam) + if len(o.CustomMessageType) > 0 { + q.Set("custom_message_type", o.CustomMessageType) + } + return q, nil } diff --git a/files_send_file_test.go b/files_send_file_test.go index fd074479..e624198e 100644 --- a/files_send_file_test.go +++ b/files_send_file_test.go @@ -29,6 +29,7 @@ func AssertSendFile(t *testing.T, checkQueryParam, testContext bool) { channel := "chan" o.Channel(channel) o.QueryParam(queryParam) + o.CustomMessageType("custom") path, err := o.opts.buildPath() assert.Nil(err) @@ -45,6 +46,7 @@ func AssertSendFile(t *testing.T, checkQueryParam, testContext bool) { u, _ := o.opts.buildQuery() assert.Equal("v1", u.Get("q1")) assert.Equal("v2", u.Get("q2")) + assert.Equal("custom", u.Get("custom_message_type")) } } @@ -66,3 +68,15 @@ func TestSendFileResponseValueError(t *testing.T) { _, _, err := newPNSendFileResponse(jsonBytes, opts, StatusResponse{}) assert.Equal("pubnub/parsing: Error unmarshalling response: {s}", err.Error()) } + +func TestSendFileCustomMessageTypeValidation(t *testing.T) { + assert := assert.New(t) + pn := NewPubNub(NewDemoConfig()) + opts := newSendFileOpts(pn, pn.ctx) + opts.CustomMessageType = "custom-message_type" + assert.True(opts.isCustomMessageTypeCorrect()) + opts.CustomMessageType = "a" + assert.False(opts.isCustomMessageTypeCorrect()) + opts.CustomMessageType = "!@#$%^&*(" + assert.False(opts.isCustomMessageTypeCorrect()) +} diff --git a/history_request.go b/history_request.go index 46111c17..bf54499b 100644 --- a/history_request.go +++ b/history_request.go @@ -91,9 +91,17 @@ func (b *historyBuilder) QueryParam(queryParam map[string]string) *historyBuilde // Transport sets the Transport for the History request. func (b *historyBuilder) Transport(tr http.RoundTripper) *historyBuilder { b.opts.Transport = tr + return b } +// includeCustomMessageType tells server to send the custom message type with each history item +func (b *historyBuilder) includeCustomMessageType(i bool) *historyBuilder { + b.opts.includeCustomMessageType = i + + return b +} + // Execute runs the History request. func (b *historyBuilder) Execute() (*HistoryResponse, StatusResponse, error) { rawJSON, status, err := executeRequest(b.opts) @@ -127,6 +135,8 @@ type historyOpts struct { setStart bool setEnd bool + includeCustomMessageType bool + Transport http.RoundTripper } @@ -165,6 +175,10 @@ func (o *historyOpts) buildQuery() (*url.Values, error) { q.Set("count", "100") } + if o.includeCustomMessageType { + q.Set("include_custom_message_type", "true") + } + q.Set("reverse", strconv.FormatBool(o.Reverse)) q.Set("include_token", strconv.FormatBool(o.IncludeTimetoken)) q.Set("include_meta", strconv.FormatBool(o.WithMeta)) diff --git a/history_request_test.go b/history_request_test.go index 927fbe21..449e0c2e 100644 --- a/history_request_test.go +++ b/history_request_test.go @@ -27,6 +27,7 @@ func (pn *PubNub) initHistoryOpts() *historyOpts { opts.Count = 3 opts.IncludeTimetoken = true opts.pubnub = pn + opts.includeCustomMessageType = false return opts } @@ -132,6 +133,7 @@ func TestHistoryRequestAllParams(t *testing.T) { opts.Reverse = false opts.Count = 3 opts.IncludeTimetoken = true + opts.includeCustomMessageType = true path, err := opts.buildPath() assert.Nil(err) @@ -151,6 +153,7 @@ func TestHistoryRequestAllParams(t *testing.T) { expected.Set("reverse", "false") expected.Set("count", "3") expected.Set("include_token", "true") + expected.Set("include_custom_message_type", "true") h.AssertQueriesEqual(t, expected, query, []string{"pnsdk", "uuid", "include_meta"}, []string{}) } diff --git a/listener_manager.go b/listener_manager.go index c4f279ab..ba773bb6 100644 --- a/listener_manager.go +++ b/listener_manager.go @@ -270,6 +270,7 @@ type PNMessage struct { Subscription string Publisher string Timetoken int64 + CustomMessageType string Error error } diff --git a/publish_request.go b/publish_request.go index c625f3e5..b0bf4ec8 100644 --- a/publish_request.go +++ b/publish_request.go @@ -34,9 +34,11 @@ type publishOpts struct { DoNotReplicate bool QueryParam map[string]string + CustomMessageType string + Transport http.RoundTripper - // nil hacks + // nil hacks setTTL bool setShouldStore bool } @@ -168,6 +170,14 @@ func (b *publishBuilder) QueryParam(queryParam map[string]string) *publishBuilde return b } +// CustomMessageType sets the User-specified message type string - limited by 3-50 case-sensitive alphanumeric characters +// with only `-` and `_` special characters allowed. +func (b *publishBuilder) CustomMessageType(messageType string) *publishBuilder { + b.opts.CustomMessageType = messageType + + return b +} + // Execute runs the Publish request. func (b *publishBuilder) Execute() (*PublishResponse, StatusResponse, error) { rawJSON, status, err := executeRequest(b.opts) @@ -178,6 +188,10 @@ func (b *publishBuilder) Execute() (*PublishResponse, StatusResponse, error) { return newPublishResponse(rawJSON, status) } +func (o *publishOpts) isCustomMessageTypeCorrect() bool { + return isCustomMessageTypeValid(o.CustomMessageType) +} + func (o *publishOpts) validate() error { if o.config().PublishKey == "" { return newValidationError(o, StrMissingPubKey) @@ -195,6 +209,10 @@ func (o *publishOpts) validate() error { return newValidationError(o, StrMissingMessage) } + if !o.isCustomMessageTypeCorrect() { + return newValidationError(o, StrInvalidCustomMessageType) + } + return nil } @@ -324,6 +342,10 @@ func (o *publishOpts) buildQuery() (*url.Values, error) { o.pubnub.Config.Log.Println("seqn:", seqn) q.Set("seqn", seqn) + if len(o.CustomMessageType) > 0 { + q.Set("custom_message_type", o.CustomMessageType) + } + SetQueryParam(q, o.QueryParam) if o.DoNotReplicate == true { diff --git a/publish_request_test.go b/publish_request_test.go index b14e2e0d..c4cebc6c 100644 --- a/publish_request_test.go +++ b/publish_request_test.go @@ -35,6 +35,7 @@ func AssertSuccessPublishGet(t *testing.T, expectedString string, message interf assert.Equal(10, o.opts.TTL) assert.Equal(true, o.opts.ShouldStore) assert.Equal(true, o.opts.DoNotReplicate) + assert.Equal("", o.opts.CustomMessageType) } func AssertSuccessPublishGetContext(t *testing.T, expectedString string, message interface{}) { @@ -48,6 +49,7 @@ func AssertSuccessPublishGetContext(t *testing.T, expectedString string, message o.TTL(10) o.ShouldStore(true) o.DoNotReplicate(true) + o.CustomMessageType("custom") path, err := o.opts.buildPath() assert.Nil(err) @@ -63,6 +65,7 @@ func AssertSuccessPublishGetContext(t *testing.T, expectedString string, message assert.Equal(10, o.opts.TTL) assert.Equal(true, o.opts.ShouldStore) assert.Equal(true, o.opts.DoNotReplicate) + assert.Equal("custom", o.opts.CustomMessageType) } func AssertSuccessPublishGet2(t *testing.T, expectedString string, message interface{}) { @@ -77,6 +80,7 @@ func AssertSuccessPublishGet2(t *testing.T, expectedString string, message inter o.TTL(10) o.ShouldStore(false) o.DoNotReplicate(true) + o.CustomMessageType("custom") path, err := o.opts.buildPath() assert.Nil(err) @@ -96,6 +100,7 @@ func AssertSuccessPublishGet2(t *testing.T, expectedString string, message inter expected.Set("pnsdk", Version) expected.Set("norep", "true") expected.Set("store", "0") + expected.Set("custom_message_type", "custom") h.AssertQueriesEqual(t, expected, query, []string{"seqn", "pnsdk", "uuid", "store"}, []string{}) @@ -529,3 +534,15 @@ func TestPublishValidateSubscribeKey(t *testing.T) { assert.Equal("pubnub/validation: pubnub: Publish: Missing Subscribe Key", opts.validate().Error()) } + +func TestPublishValidateCustomMessageType(t *testing.T) { + assert := assert.New(t) + pn := NewPubNub(NewDemoConfig()) + opts := newPublishOpts(pn, pn.ctx) + opts.CustomMessageType = "custom-message_type" + assert.True(opts.isCustomMessageTypeCorrect()) + opts.CustomMessageType = "a" + assert.False(opts.isCustomMessageTypeCorrect()) + opts.CustomMessageType = "!@#$%^&*(" + assert.False(opts.isCustomMessageTypeCorrect()) +} diff --git a/pubnub.go b/pubnub.go index 93c2a585..324e16e9 100644 --- a/pubnub.go +++ b/pubnub.go @@ -15,7 +15,7 @@ import ( // Default constants const ( // Version :the version of the SDK - Version = "7.2.1" + Version = "7.3.0" // MaxSequence for publish messages MaxSequence = 65535 ) @@ -55,6 +55,8 @@ const ( StrMissingFileName = "Missing File Name" // StrMissingToken shows `Missing PAMv3 token` message StrMissingToken = "Missing PAMv3 token" + // StrInvalidCustomMessageType shows `Invalid CustomMessageType` message + StrInvalidCustomMessageType = "Invalid CustomMessageType: size different than 3-50 or contains invalid characters" ) // PubNub No server connection will be established when you create a new PubNub object. diff --git a/signal_request.go b/signal_request.go index 4e7dde1e..732ffa53 100644 --- a/signal_request.go +++ b/signal_request.go @@ -69,6 +69,14 @@ func (b *signalBuilder) QueryParam(queryParam map[string]string) *signalBuilder return b } +// CustomMessageType sets the User-specified message type string - limited by 3-50 case-sensitive alphanumeric characters +// with only `-` and `_` special characters allowed. +func (b *signalBuilder) CustomMessageType(messageType string) *signalBuilder { + b.opts.CustomMessageType = messageType + + return b +} + // Execute runs the Signal request. func (b *signalBuilder) Execute() (*SignalResponse, StatusResponse, error) { rawJSON, status, err := executeRequest(b.opts) @@ -86,6 +94,11 @@ type signalOpts struct { UsePost bool QueryParam map[string]string Transport http.RoundTripper + CustomMessageType string +} + +func (o *signalOpts) isCustomMessageTypeCorrect() bool { + return isCustomMessageTypeValid(o.CustomMessageType) } func (o *signalOpts) validate() error { @@ -97,6 +110,10 @@ func (o *signalOpts) validate() error { return newValidationError(o, StrMissingPubKey) } + if !o.isCustomMessageTypeCorrect() { + return newValidationError(o, StrInvalidCustomMessageType) + } + return nil } @@ -130,6 +147,10 @@ func (o *signalOpts) buildQuery() (*url.Values, error) { SetQueryParam(q, o.QueryParam) + if len(o.CustomMessageType) > 0 { + q.Set("custom_message_type", o.CustomMessageType) + } + return q, nil } diff --git a/signal_request_test.go b/signal_request_test.go index faea4ea6..a01f8c03 100644 --- a/signal_request_test.go +++ b/signal_request_test.go @@ -26,6 +26,7 @@ func AssertSuccessSignalGet(t *testing.T, channel string, checkQueryParam bool) opts.Channel = channel opts.Message = msgMap opts.QueryParam = queryParam + opts.CustomMessageType = "custom" path, err := opts.buildPath() assert.Nil(err) @@ -43,8 +44,8 @@ func AssertSuccessSignalGet(t *testing.T, channel string, checkQueryParam bool) u, _ := opts.buildQuery() assert.Equal("v1", u.Get("q1")) assert.Equal("v2", u.Get("q2")) + assert.Equal("custom", u.Get("custom_message_type")) } - } func TestSignalPath(t *testing.T) { @@ -144,3 +145,15 @@ func TestSignalResponseValuePass(t *testing.T) { _, _, err := newSignalResponse(jsonBytes, opts, StatusResponse{}) assert.Nil(err) } + +func TestSignalCustomMessageTypeValidation(t *testing.T) { + assert := assert.New(t) + pn := NewPubNub(NewDemoConfig()) + opts := newSignalOpts(pn, pn.ctx) + opts.CustomMessageType = "custom-message_type" + assert.True(opts.isCustomMessageTypeCorrect()) + opts.CustomMessageType = "a" + assert.False(opts.isCustomMessageTypeCorrect()) + opts.CustomMessageType = "!@#$%^&*(" + assert.False(opts.isCustomMessageTypeCorrect()) +} diff --git a/subscription_manager.go b/subscription_manager.go index cbbe4913..fe20926d 100644 --- a/subscription_manager.go +++ b/subscription_manager.go @@ -481,6 +481,7 @@ type subscribeMessage struct { UserMetadata interface{} `json:"u"` MessageType PNMessageType `json:"e"` SequenceNumber int `json:"s"` + CustomMessageType string `json:"ctm"` PublishMetaData publishMetadata `json:"p"` } @@ -638,7 +639,7 @@ func processNonPresencePayload(m *SubscriptionManager, payload subscribeMessage, switch payload.MessageType { case PNMessageTypeSignal: - pnMessageResult := createPNMessageResult(payload.Payload, actualCh, subscribedCh, channel, subscriptionMatch, payload.IssuingClientID, payload.UserMetadata, timetoken, /*no error*/nil) + pnMessageResult := createPNMessageResult(payload.Payload, actualCh, subscribedCh, channel, subscriptionMatch, payload.IssuingClientID, payload.UserMetadata, timetoken, payload.CustomMessageType, /*no error*/nil) m.pubnub.Config.Log.Println("announceSignal,", pnMessageResult) m.listenerManager.announceSignal(pnMessageResult) case PNMessageTypeObjects: @@ -693,7 +694,7 @@ func processNonPresencePayload(m *SubscriptionManager, payload subscribeMessage, m.listenerManager.announceStatus(pnStatus) } - pnMessageResult := createPNMessageResult(messagePayload, actualCh, subscribedCh, channel, subscriptionMatch, payload.IssuingClientID, payload.UserMetadata, timetoken, err) + pnMessageResult := createPNMessageResult(messagePayload, actualCh, subscribedCh, channel, subscriptionMatch, payload.IssuingClientID, payload.UserMetadata, timetoken, payload.CustomMessageType, err) m.pubnub.Config.Log.Println("announceMessage,", pnMessageResult) m.listenerManager.announceMessage(pnMessageResult) } @@ -934,7 +935,7 @@ func createPNObjectsResult(objPayload interface{}, m *SubscriptionManager, actua return pnUUIDEvent, pnChannelEvent, pnMembershipEvent, eventType } -func createPNMessageResult(messagePayload interface{}, actualCh, subscribedCh, channel, subscriptionMatch, issuingClientID string, userMetadata interface{}, timetoken int64, error error) *PNMessage { +func createPNMessageResult(messagePayload interface{}, actualCh, subscribedCh, channel, subscriptionMatch, issuingClientID string, userMetadata interface{}, timetoken int64, CustomMessageType string, error error) *PNMessage { pnMessageResult := &PNMessage{ Message: messagePayload, @@ -945,6 +946,7 @@ func createPNMessageResult(messagePayload interface{}, actualCh, subscribedCh, c Timetoken: timetoken, Publisher: issuingClientID, UserMetadata: userMetadata, + CustomMessageType: CustomMessageType, Error: error, } diff --git a/subscription_manager_test.go b/subscription_manager_test.go index 47f8243f..f5c384fe 100644 --- a/subscription_manager_test.go +++ b/subscription_manager_test.go @@ -571,6 +571,43 @@ func TestProcessSubscribePayloadCipherErr(t *testing.T) { //pn.Destroy() } +func TestProcessSubscribePayloadWithCustomMessageType(t *testing.T) { + assert := assert.New(t) + done := make(chan bool) + pn := NewPubNub(NewDemoConfig()) + + listener := NewListener() + + go func() { + for { + select { + case result := <-listener.Message: + assert.Equal("test", result.Message) + assert.Equal("custom", result.CustomMessageType) + assert.Nil(result.Error) + done <- true + break + case <-listener.Status: + case <-listener.Presence: + break + } + } + }() + + pn.AddListener(listener) + + sm := &subscribeMessage{ + Shard: "1", + SubscriptionMatch: "channel", + Channel: "channel", + Payload: "test", + CustomMessageType: "custom", + } + + processSubscribePayload(pn.subscriptionManager, *sm) + <-done +} + func TestDecryptionProcessOnEncryptedMessage(t *testing.T) { assert := assert.New(t) pn := NewPubNub(NewDemoConfig()) diff --git a/utils.go b/utils.go index 01aeeadd..2062a20f 100644 --- a/utils.go +++ b/utils.go @@ -118,3 +118,21 @@ func decryptString(cryptoModule crypto.CryptoModule, message string) (retVal int val, e := cryptoModule.Decrypt(value) return fmt.Sprintf("%s", string(val)), e } + +func isCustomMessageTypeValid(customMessageType string) bool { + if len(customMessageType) == 0 { + return true + } + + if len(customMessageType) < 3 || len(customMessageType) > 50 { + return false + } + + for _, c := range customMessageType { + if !('a' <= c && 'z' >= c) && !('A' <= c && 'Z' >= c) && c != '-' && c != '_' { + return false + } + } + + return true +}