Skip to content

Commit fd48d63

Browse files
authored
Merge pull request #49 from messagebird/add-conversations
Add conversations
2 parents 12d131c + 2804691 commit fd48d63

27 files changed

+1129
-8
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
language: go
2-
32
go:
3+
- 1.7
44
- 1.8
5+
- 1.9
56
- stable
67
- master
7-
88
matrix:
99
allow_failures:
1010
- go: master

contact/contact.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ type Contact struct {
3232
TotalCount int
3333
HRef string
3434
}
35-
CreatedDatetime time.Time
36-
UpdatedDatetime time.Time
35+
CreatedDatetime *time.Time
36+
UpdatedDatetime *time.Time
3737
}
3838

3939
type ContactList struct {

conversation/api.go

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package conversation
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/url"
7+
"strconv"
8+
"time"
9+
10+
messagebird "github.com/messagebird/go-rest-api"
11+
)
12+
13+
const (
14+
// apiRoot is the absolute URL of the Converstations API. All paths are
15+
// relative to apiRoot (e.g.
16+
// https://conversations.messagebird.com/v1/webhooks).
17+
apiRoot = "https://conversations.messagebird.com/v1"
18+
19+
// path is the path for the Conversation resource, relative to apiRoot.
20+
path = "conversations"
21+
22+
// messagesPath is the path for the Message resource, relative to apiRoot
23+
// and path.
24+
messagesPath = "messages"
25+
26+
// webhooksPath is the path for the Webhook resource, relative to apiRoot.
27+
webhooksPath = "webhooks"
28+
)
29+
30+
type ConversationList struct {
31+
Offset int
32+
Limit int
33+
Count int
34+
TotalCount int
35+
Items []*Conversation
36+
}
37+
38+
type Conversation struct {
39+
ID string
40+
ContactID string
41+
Contact *Contact
42+
LastUsedChannelID string
43+
Channels []*Channel
44+
Messages *MessagesCount
45+
Status ConversationStatus
46+
CreatedDatetime time.Time
47+
UpdatedDatetime *time.Time
48+
LastReceivedDatetime *time.Time
49+
}
50+
51+
type Contact struct {
52+
ID string
53+
Href string
54+
MSISDN string
55+
FirstName string
56+
LastName string
57+
CustomDetails map[string]interface{}
58+
CreatedAt *time.Time
59+
UpdatedAt *time.Time
60+
}
61+
62+
type Channel struct {
63+
ID string
64+
Name string
65+
PlatformID string
66+
Status string
67+
CreatedDatetime *time.Time
68+
UpdatedDatetime *time.Time
69+
}
70+
71+
type MessagesCount struct {
72+
HRef string
73+
TotalCount int
74+
}
75+
76+
// ConversationStatus indicates what state a Conversation is in.
77+
type ConversationStatus string
78+
79+
const (
80+
// ConversationStatusActive is returned when the Conversation is active.
81+
// Only one active conversation can ever exist for a given contact.
82+
ConversationStatusActive ConversationStatus = "active"
83+
84+
// ConversationStatusArchived is returned when the Conversation is
85+
// archived. When this is the case, a new Conversation is created when a
86+
// message is received from a contact.
87+
ConversationStatusArchived ConversationStatus = "archived"
88+
)
89+
90+
type MessageList struct {
91+
Offset int
92+
Limit int
93+
Count int
94+
TotalCount int
95+
Items []*Message
96+
}
97+
98+
type Message struct {
99+
ID string
100+
ConversationID string
101+
ChannelID string
102+
Direction MessageDirection
103+
Status MessageStatus
104+
Type MessageType
105+
Content MessageContent
106+
CreatedDatetime *time.Time
107+
UpdatedDatetime *time.Time
108+
}
109+
110+
type MessageDirection string
111+
112+
const (
113+
// MessageDirectionReceived indicates an inbound message received from the customer.
114+
MessageDirectionReceived MessageDirection = "received"
115+
116+
// MessageDirectionSent indicates an outbound message sent from the API.
117+
MessageDirectionSent MessageDirection = "sent"
118+
)
119+
120+
// MessageStatus is a field set by the API. It indicates what the state of the
121+
// message is, e.g. whether it has been successfully delivered or read.
122+
type MessageStatus string
123+
124+
const (
125+
MessageStatusDeleted MessageStatus = "deleted"
126+
MessageStatusDelivered MessageStatus = "delivered"
127+
MessageStatusFailed MessageStatus = "failed"
128+
MessageStatusPending MessageStatus = "pending"
129+
MessageStatusRead MessageStatus = "read"
130+
MessageStatusReceived MessageStatus = "received"
131+
MessageStatusSent MessageStatus = "sent"
132+
MessageStatusUnsupported MessageStatus = "unsupported"
133+
MessageStatusRejected MessageStatus = "rejected"
134+
)
135+
136+
// MessageType indicates what kind of content a Message has, e.g. audio or
137+
// text.
138+
type MessageType string
139+
140+
const (
141+
MessageTypeAudio MessageType = "audio"
142+
MessageTypeFile MessageType = "file"
143+
MessageTypeHSM MessageType = "hsm"
144+
MessageTypeImage MessageType = "image"
145+
MessageTypeLocation MessageType = "location"
146+
MessageTypeText MessageType = "text"
147+
MessageTypeVideo MessageType = "video"
148+
)
149+
150+
// MessageContent holds a message's actual content. Only one field can be set
151+
// per request.
152+
type MessageContent struct {
153+
Audio *Audio `json:"audio,omitempty"`
154+
File *File `json:"file,omitempty"`
155+
Image *Image `json:"image,omitempty"`
156+
Location *Location `json:"location,omitempty"`
157+
Video *Video `json:"video,omitempty"`
158+
Text string `json:"text,omitempty"`
159+
160+
// HSM is a highly structured message for WhatsApp. Its definition lives in
161+
// hsm.go.
162+
HSM *HSM `json:"hsm,omitempty"`
163+
}
164+
165+
type Media struct {
166+
URL string `json:"url"`
167+
}
168+
169+
type Audio Media
170+
type File Media
171+
type Image Media
172+
type Video Media
173+
174+
type Location struct {
175+
Latitude float32 `json:"latitude"`
176+
Longitude float32 `json:"longitude"`
177+
}
178+
179+
type WebhookList struct {
180+
Offset int
181+
Limit int
182+
Count int
183+
TotalCount int
184+
Items []*Webhook
185+
}
186+
187+
type Webhook struct {
188+
ID string
189+
ChannelID string
190+
Events []WebhookEvent
191+
URL string
192+
CreatedDatetime *time.Time
193+
UpdatedDatetime *time.Time
194+
}
195+
196+
type WebhookEvent string
197+
198+
const (
199+
WebhookEventConversationCreated WebhookEvent = "conversation.created"
200+
WebhookEventConversationUpdated WebhookEvent = "conversation.updated"
201+
WebhookEventMessageCreated WebhookEvent = "message.created"
202+
WebhookEventMessageUpdated WebhookEvent = "message.updated"
203+
)
204+
205+
// request does the exact same thing as Client.Request. It does, however,
206+
// prefix the path with the Conversation API's root. This ensures the client
207+
// doesn't "handle" this for us: by default, it uses the REST API.
208+
func request(c *messagebird.Client, v interface{}, method, path string, data interface{}) error {
209+
return c.Request(v, method, fmt.Sprintf("%s/%s", apiRoot, path), data)
210+
}
211+
212+
// paginationQuery builds the query string for paginated endpoints.
213+
func paginationQuery(options *ListOptions) string {
214+
query := url.Values{}
215+
query.Set("limit", strconv.Itoa(options.Limit))
216+
query.Set("offset", strconv.Itoa(options.Offset))
217+
218+
return query.Encode()
219+
}
220+
221+
// UnmarshalJSON is used to unmarshal the MSISDN to a string rather than an
222+
// int64. The API returns integers, but this client always uses strings.
223+
// Exposing a json.Number doesn't seem nice.
224+
func (c *Contact) UnmarshalJSON(data []byte) error {
225+
target := struct {
226+
ID string
227+
Href string
228+
MSISDN json.Number
229+
FirstName string
230+
LastName string
231+
CustomDetails map[string]interface{}
232+
CreatedAt *time.Time
233+
UpdatedAt *time.Time
234+
}{}
235+
236+
if err := json.Unmarshal(data, &target); err != nil {
237+
return err
238+
}
239+
240+
// In many cases, the CustomDetails will contain the user ID. As
241+
// CustomDetails has interface{} values, these are unmarshalled as floats.
242+
// Convert them to int64.
243+
// Map key is not a typo: API returns userId and not userID.
244+
if val, ok := target.CustomDetails["userId"]; ok {
245+
var userID float64
246+
if userID, ok = val.(float64); ok {
247+
target.CustomDetails["userId"] = int64(userID)
248+
}
249+
}
250+
251+
*c = Contact{
252+
target.ID,
253+
target.Href,
254+
target.MSISDN.String(),
255+
target.FirstName,
256+
target.LastName,
257+
target.CustomDetails,
258+
target.CreatedAt,
259+
target.UpdatedAt,
260+
}
261+
262+
return nil
263+
}

conversation/conversation.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package conversation
2+
3+
import (
4+
"net/http"
5+
6+
messagebird "github.com/messagebird/go-rest-api"
7+
)
8+
9+
// ListOptions can be used to set pagination options in List().
10+
type ListOptions struct {
11+
Limit, Offset int
12+
}
13+
14+
// StartRequest contains the request data for the Start endpoint.
15+
type StartRequest struct {
16+
ChannelID string `json:"channelId"`
17+
Content *MessageContent `json:"content"`
18+
To string `json:"to"`
19+
Type MessageType `json:"type"`
20+
}
21+
22+
// UpdateRequest contains the request data for the Update endpoint.
23+
type UpdateRequest struct {
24+
Status ConversationStatus `json:"status"`
25+
}
26+
27+
// DefaultListOptions provide a reasonable default for paginated endpoints.
28+
var DefaultListOptions = &ListOptions{10, 0}
29+
30+
// List gets a collection of Conversations. Pagination can be set in options.
31+
func List(c *messagebird.Client, options *ListOptions) (*ConversationList, error) {
32+
query := paginationQuery(options)
33+
34+
convList := &ConversationList{}
35+
if err := request(c, convList, http.MethodGet, path+"?"+query, nil); err != nil {
36+
return nil, err
37+
}
38+
39+
return convList, nil
40+
}
41+
42+
// Read fetches a single Conversation based on its ID.
43+
func Read(c *messagebird.Client, id string) (*Conversation, error) {
44+
conv := &Conversation{}
45+
if err := request(c, conv, http.MethodGet, path+"/"+id, nil); err != nil {
46+
return nil, err
47+
}
48+
49+
return conv, nil
50+
}
51+
52+
// Start creates a conversation by sending an initial message. If an active
53+
// conversation exists for the recipient, it is resumed.
54+
func Start(c *messagebird.Client, req *StartRequest) (*Conversation, error) {
55+
conv := &Conversation{}
56+
if err := request(c, conv, http.MethodPost, path+"/start", req); err != nil {
57+
return nil, err
58+
}
59+
60+
return conv, nil
61+
}
62+
63+
// Update changes the conversation's status, so this can be used to (un)archive
64+
// conversations.
65+
func Update(c *messagebird.Client, id string, req *UpdateRequest) (*Conversation, error) {
66+
conv := &Conversation{}
67+
if err := request(c, conv, http.MethodPatch, path+"/"+id, req); err != nil {
68+
return nil, err
69+
}
70+
71+
return conv, nil
72+
}

0 commit comments

Comments
 (0)