diff --git a/openmessaging_channel.go b/openmessaging_channel.go index 4a55057..2ffac02 100644 --- a/openmessaging_channel.go +++ b/openmessaging_channel.go @@ -15,7 +15,7 @@ type OpenMessageChannel struct { Type string `json:"type"` // Private, Public MessageID string `json:"messageId,omitempty"` Time time.Time `json:"-"` - To *OpenMessageTo `json:"to"` + To *OpenMessageTo `json:"to,omitempty"` From *OpenMessageFrom `json:"from"` Metadata *OpenMessageChannelMetadata `json:"metadata,omitempty"` } diff --git a/openmessaging_integration.go b/openmessaging_integration.go index 80a7011..7872c3d 100644 --- a/openmessaging_integration.go +++ b/openmessaging_integration.go @@ -297,6 +297,31 @@ func (integration *OpenMessagingIntegration) SendInboundReceipt(context context. return result.ID, err } +// SendInboundEvent sends an event from the middleware to GENESYS Cloud +// +// See https://developer.genesys.cloud/commdigital/digital/openmessaging/inboundEventMessages +func (integration *OpenMessagingIntegration) SendInboundEvents(context context.Context, from *OpenMessageFrom, attributes map[string]string, metadata map[string]string, events ...OpenMessageEvent) (id string, err error) { + if integration.ID == uuid.Nil { + return "", errors.ArgumentMissing.With("ID") + } + result := OpenMessageEvents{} + err = integration.client.Post( + integration.logger.ToContext(context), + NewURI("/conversations/messages/%s/inbound/open/event", integration.ID), + &OpenMessageEvents{ + Channel: NewOpenMessageChannel( + "", + &OpenMessageTo{ID: integration.ID.String()}, + from, + ).WithAttributes(attributes), + Events: events, + Metadata: metadata, + }, + &result, + ) + return result.ID, err +} + // SendOutboundMessage sends a message from GENESYS Cloud to the middleware // // The message can be only text as it is sent bia the AgentLess Message API. diff --git a/openmessaging_message_event.go b/openmessaging_message_event.go new file mode 100644 index 0000000..6c50a32 --- /dev/null +++ b/openmessaging_message_event.go @@ -0,0 +1,31 @@ +package gcloudcx + +import ( + "strings" + + "github.com/gildas/go-core" + "github.com/gildas/go-errors" +) + +type OpenMessageEvent interface { + core.TypeCarrier +} + +var openMessageEventRegistry = core.TypeRegistry{} + +func UnmarshalOpenMessageEvent(payload []byte) (OpenMessageEvent, error) { + message, err := openMessageEventRegistry.UnmarshalJSON(payload, "eventType") + if err == nil { + return message.(OpenMessageEvent), nil + } + if strings.HasPrefix(err.Error(), "Missing JSON Property") { + return nil, errors.JSONUnmarshalError.Wrap(errors.ArgumentMissing.With("type")) + } + if strings.HasPrefix(err.Error(), "Unsupported Type") { + return nil, errors.JSONUnmarshalError.Wrap(errors.InvalidType.With(strings.TrimSuffix(strings.TrimPrefix(err.Error(), `Unsupported Type "`), `"`))) + } + if errors.Is(err, errors.JSONUnmarshalError) { + return nil, err + } + return nil, errors.JSONUnmarshalError.Wrap(err) +} diff --git a/openmessaging_message_event_typing.go b/openmessaging_message_event_typing.go new file mode 100644 index 0000000..a3961d2 --- /dev/null +++ b/openmessaging_message_event_typing.go @@ -0,0 +1,92 @@ +package gcloudcx + +import ( + "encoding/json" + "time" + + "github.com/gildas/go-errors" +) + +// OpenMessageTypingEvent is a typing event sent or received by the Open Messaging API +type OpenMessageTypingEvent struct { + IsTyping bool `json:"-"` + Duration time.Duration `json:"-"` +} + +func init() { + openMessageEventRegistry.Add(OpenMessageTypingEvent{}) +} + +// GetType returns the type of this event +// +// implements core.TypeCarrier +func (event OpenMessageTypingEvent) GetType() string { + return "Typing" +} + +// MarshalJSON marshals this into JSON +// +// implements json.Marshaler +func (event OpenMessageTypingEvent) MarshalJSON() (data []byte, err error) { + if !event.IsTyping || event.Duration > 0 { + type TypingInfo struct { + Type string `json:"type"` + Duration int `json:"duration"` + } + newTypingInfo := func(isTyping bool, duration time.Duration) TypingInfo { + if !isTyping { + return TypingInfo{ + Type: "On", + Duration: int(duration.Milliseconds()), + } + } + return TypingInfo{ + Type: "Off", + Duration: int(duration.Milliseconds()), + } + } + data, err = json.Marshal(struct { + Type string `json:"eventType"` + Typing TypingInfo `json:"typing"` + }{ + Type: event.GetType(), + Typing: newTypingInfo(event.IsTyping, event.Duration), + }) + } else { + data, err = json.Marshal(struct { + Type string `json:"eventType"` + }{ + Type: event.GetType(), + }) + } + return data, errors.JSONMarshalError.Wrap(err) +} + +// UnmarshalJSON unmarshals JSON into this +// +// implements json.Unmarshaler +func (event *OpenMessageTypingEvent) UnmarshalJSON(payload []byte) (err error) { + type surrogate OpenMessageTypingEvent + var inner struct { + surrogate + Type string `json:"eventType"` + Typing *struct { + Type string `json:"type"` + Duration int `json:"duration"` + } `json:"typing"` + } + if err = json.Unmarshal(payload, &inner); errors.Is(err, errors.JSONUnmarshalError) { + return err + } else if err != nil { + return errors.JSONUnmarshalError.Wrap(err) + } + *event = OpenMessageTypingEvent(inner.surrogate) + + if inner.Typing != nil { + event.IsTyping = inner.Typing.Type == "On" + event.Duration = time.Duration(inner.Typing.Duration) * time.Millisecond + } else { + event.IsTyping = true + } + return nil +} diff --git a/openmessaging_message_events.go b/openmessaging_message_events.go new file mode 100644 index 0000000..3ad6c3d --- /dev/null +++ b/openmessaging_message_events.go @@ -0,0 +1,81 @@ +package gcloudcx + +import ( + "encoding/json" + + "github.com/gildas/go-errors" +) + +// OpenMessageText is a text message sent or received by the Open Messaging API +// +// See https://developer.genesys.cloud/commdigital/digital/openmessaging/inboundEventMessages +type OpenMessageEvents struct { + ID string `json:"id,omitempty"` // Can be anything + Channel *OpenMessageChannel `json:"channel"` + Direction string `json:"direction,omitempty"` // Can be "Inbound" or "Outbound" + Events []OpenMessageEvent `json:"events"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// init initializes this type +func init() { + openMessageRegistry.Add(OpenMessageEvents{}) +} + +// GetType returns the type of this event +// +// implements core.TypeCarrier +func (message OpenMessageEvents) GetType() string { + return "Event" +} + +// GetID gets the identifier of this +// +// implements OpenMessage +func (message OpenMessageEvents) GetID() string { + return message.ID +} + +// MarshalJSON marshals this into JSON +// +// implements json.Marshaler +func (message OpenMessageEvents) MarshalJSON() ([]byte, error) { + type surrogate OpenMessageEvents + + data, err := json.Marshal(struct { + surrogate + Type string `json:"type"` + }{ + surrogate: surrogate(message), + Type: message.GetType(), + }) + return data, errors.JSONMarshalError.Wrap(err) +} + +// UnmarshalJSON unmarshals JSON into this +// +// implements json.Unmarshaler +func (message *OpenMessageEvents) UnmarshalJSON(payload []byte) (err error) { + type surrogate OpenMessageEvents + var inner struct { + surrogate + Type string `json:"type"` + Events []json.RawMessage `json:"events"` + } + if err = json.Unmarshal(payload, &inner); errors.Is(err, errors.JSONUnmarshalError) { + return err + } else if err != nil { + return errors.JSONUnmarshalError.Wrap(err) + } + *message = OpenMessageEvents(inner.surrogate) + + message.Events = make([]OpenMessageEvent, 0, len(inner.Events)) + for _, raw := range inner.Events { + event, err := UnmarshalOpenMessageEvent(raw) + if err != nil { + return err + } + message.Events = append(message.Events, event) + } + return +} diff --git a/openmessaging_test.go b/openmessaging_test.go index 0691d7b..f739ecc 100644 --- a/openmessaging_test.go +++ b/openmessaging_test.go @@ -412,3 +412,48 @@ func (suite *OpenMessagingSuite) TestCanStringifyIntegration() { integration.Name = "" suite.Assert().Equal(id.String(), integration.String()) } + +func (suite *OpenMessagingSuite) TestCanMarshalTypingEvent() { + channel := gcloudcx.NewOpenMessageChannel( + "", + nil, + &gcloudcx.OpenMessageFrom{ + ID: "abcdef12345", + Type: "Email", + Firstname: "Bob", + Lastname: "Minion", + Nickname: "Bobby", + }, + ) + channel.Time = time.Date(2021, 4, 9, 4, 43, 33, 0, time.UTC) + event := gcloudcx.OpenMessageEvents{ + Channel: channel, + Events: []gcloudcx.OpenMessageEvent{ + gcloudcx.OpenMessageTypingEvent{IsTyping: true}, + }, + } + payload, err := json.Marshal(event) + suite.Require().NoErrorf(err, "Failed to marshal OpenMessageEvents. %s", err) + expected := suite.LoadTestData("openmessaging-event-typing.json") + suite.Require().JSONEq(string(expected), string(payload)) +} + +func (suite *OpenMessagingSuite) TestCanUnmarshalTypingEvent() { + payload := suite.LoadTestData("inbound-openmessaging-event-typing.json") + message, err := gcloudcx.UnmarshalOpenMessage(payload) + suite.Require().NoError(err, "Failed to unmarshal OpenMessage") + suite.Require().NotNil(message, "Unmarshaled message should not be nil") + + actual, ok := message.(*gcloudcx.OpenMessageEvents) + suite.Require().True(ok, "Unmarshaled message should be of type OpenMessageEvents, but was %T", message) + suite.Require().NotNil(actual, "Unmarshaled message should not be nil") + suite.Assert().Equal("6ffd815bca1570e46251fcc71c103837", actual.ID) + suite.Assert().Equal(uuid.MustParse("1af69355-f1b0-477e-8ed9-66baff370209"), actual.Channel.ID) + suite.Assert().Equal("Outbound", actual.Direction) + suite.Require().Len(actual.Events, 1, "Unmarshaled message should have 1 event") + + messageEvent, ok := actual.Events[0].(*gcloudcx.OpenMessageTypingEvent) + suite.Require().True(ok, "Unmarshaled message event should be of type *OpenMessageTypingEvent, but was %T", actual.Events[0]) + suite.Assert().True(messageEvent.IsTyping) + suite.Assert().Equal(5*time.Second, messageEvent.Duration) +} diff --git a/testdata/inbound-openmessaging-event-typing.json b/testdata/inbound-openmessaging-event-typing.json new file mode 100644 index 0000000..2149574 --- /dev/null +++ b/testdata/inbound-openmessaging-event-typing.json @@ -0,0 +1,29 @@ +{ + "id": "6ffd815bca1570e46251fcc71c103837", + "channel": { + "id": "1af69355-f1b0-477e-8ed9-66baff370209", + "platform": "Open", + "type": "Private", + "to": { + "id": "abcdef12345" + }, + "from": { + "nickname": "TEST-GO-PURECLOUD", + "id": "1af69355-f1b0-477e-8ed9-66baff370209", + "idType": "Opaque" + }, + "time": "2023-09-19T08:25:16.925Z", + "messageId": "6ffd815bca1570e46251fcc71c103837" + }, + "type": "Event", + "events": [ + { + "eventType": "Typing", + "typing": { + "type": "On", + "duration": 5000 + } + } + ], + "direction": "Outbound" +} diff --git a/testdata/openmessaging-event-typing.json b/testdata/openmessaging-event-typing.json new file mode 100644 index 0000000..134d881 --- /dev/null +++ b/testdata/openmessaging-event-typing.json @@ -0,0 +1,18 @@ +{ + "channel": { + "platform": "Open", + "type": "Private", + "from": { + "id": "abcdef12345", + "idType": "Email", + "firstName": "Bob", + "lastName": "Minion", + "nickname": "Bobby" + }, + "time": "2021-04-09T04:43:33Z" + }, + "type": "Event", + "events": [ + { "eventType": "Typing" } + ] +} diff --git a/version.go b/version.go index 89d0762..de8481d 100644 --- a/version.go +++ b/version.go @@ -4,7 +4,7 @@ package gcloudcx var commit string // VERSION is the version of this application -var VERSION = "0.8.7" + commit +var VERSION = "0.8.8" + commit // APP is the name of the application const APP string = "GCloudCX Client"