Skip to content

Commit

Permalink
Split twilio messages at 1600 chars
Browse files Browse the repository at this point in the history
  • Loading branch information
nicpottier committed Sep 15, 2017
1 parent cbf978b commit cb3ee75
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 53 deletions.
24 changes: 24 additions & 0 deletions handlers/base.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handlers

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -189,3 +190,26 @@ func DecodePossibleBase64(original string) string {

return decoded
}

// SplitMsg splits the passed in string into segments that are at most max length
func SplitMsg(text string, max int) []string {
// smaller than our max, just return it
if len(text) <= max {
return []string{text}
}

parts := make([]string, 0, 2)
part := bytes.Buffer{}
for _, r := range text {
part.WriteRune(r)
if part.Len() == max || (part.Len() > max-6 && r == ' ') {
parts = append(parts, strings.TrimSpace(part.String()))
part.Reset()
}
}
if part.Len() > 0 {
parts = append(parts, strings.TrimSpace(part.String()))
}

return parts
}
9 changes: 9 additions & 0 deletions handlers/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ func TestDecodePossibleBase64(t *testing.T) {
assert.Contains(DecodePossibleBase64("Tm93IGlzDQp0aGUgdGltZQ0KZm9yIGFsbCBnb29kDQpwZW9wbGUgdG8NCnJlc2lzdC4NCg0KSG93IGFib3V0IGhhaWt1cz8NCkkgZmluZCB0aGVtIHRvIGJlIGZyaWVuZGx5Lg0KcmVmcmlnZXJhdG9yDQoNCjAxMjM0NTY3ODkNCiFAIyQlXiYqKCkgW117fS09Xys7JzoiLC4vPD4/fFx+YA0KQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eg=="), "I find them to be friendly")
assert.Contains(DecodePossibleBase64(test6), "I received your letter today")
}

func TestSplitMsg(t *testing.T) {
assert := assert.New(t)
assert.Equal([]string{""}, SplitMsg("", 160))
assert.Equal([]string{"Simple message"}, SplitMsg("Simple message", 160))
assert.Equal([]string{"This is a message", "longer than 10"}, SplitMsg("This is a message longer than 10", 20))
assert.Equal([]string{" "}, SplitMsg(" ", 20))
assert.Equal([]string{"This is a message", "longer than 10"}, SplitMsg("This is a message longer than 10", 20))
}
114 changes: 61 additions & 53 deletions handlers/twilio/twilio.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const configSendURL = "send_url"

const twSignatureHeader = "X-Twilio-Signature"

var maxMsgLength = 1600
var sendURL = "https://api.twilio.com/2010-04-01/Accounts"

// error code twilio returns when a contact has sent "stop"
Expand Down Expand Up @@ -185,69 +186,76 @@ func (h *handler) SendMsg(msg courier.Msg) (courier.MsgStatus, error) {
return nil, fmt.Errorf("missing account auth token for twilio channel")
}

// build our request
form := url.Values{
"To": []string{msg.URN().Path()},
"Body": []string{msg.Text()},
"StatusCallback": []string{callbackURL},
}
status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored)
parts := handlers.SplitMsg(msg.Text(), maxMsgLength)
for i, part := range parts {
// build our request
form := url.Values{
"To": []string{msg.URN().Path()},
"Body": []string{part},
"StatusCallback": []string{callbackURL},
}

// add any media URL
if len(msg.Attachments()) > 0 {
_, mediaURL := courier.SplitAttachment(msg.Attachments()[0])
form["MediaUrl"] = []string{mediaURL}
}
// add any media URL to the first part
if len(msg.Attachments()) > 0 && i == 0 {
_, mediaURL := courier.SplitAttachment(msg.Attachments()[0])
form["MediaUrl"] = []string{mediaURL}
}

// set our from, either as a messaging service or from our address
serviceSID := msg.Channel().StringConfigForKey(configMessagingServiceSID, "")
if serviceSID != "" {
form["MessagingServiceSID"] = []string{serviceSID}
} else {
form["From"] = []string{msg.Channel().Address()}
}
// set our from, either as a messaging service or from our address
serviceSID := msg.Channel().StringConfigForKey(configMessagingServiceSID, "")
if serviceSID != "" {
form["MessagingServiceSID"] = []string{serviceSID}
} else {
form["From"] = []string{msg.Channel().Address()}
}

baseSendURL := msg.Channel().StringConfigForKey(configSendURL, sendURL)
sendURL, err := utils.AddURLPath(baseSendURL, accountSID, "Messages.json")
if err != nil {
return nil, err
}
baseSendURL := msg.Channel().StringConfigForKey(configSendURL, sendURL)
sendURL, err := utils.AddURLPath(baseSendURL, accountSID, "Messages.json")
if err != nil {
return nil, err
}

req, err := http.NewRequest(http.MethodPost, sendURL, strings.NewReader(form.Encode()))
req.SetBasicAuth(accountSID, accountToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
rr, err := utils.MakeHTTPRequest(req)
req, err := http.NewRequest(http.MethodPost, sendURL, strings.NewReader(form.Encode()))
req.SetBasicAuth(accountSID, accountToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
rr, err := utils.MakeHTTPRequest(req)

// record our status and log
status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored)
log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), rr).WithError("Message Send Error", err)
status.AddLog(log)
// record our status and log
log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), rr).WithError("Message Send Error", err)
status.AddLog(log)

// fail if we received an error
if err != nil {
return status, nil
}
// fail if we received an error
if err != nil {
return status, nil
}

// was this request successful?
errorCode, _ := jsonparser.GetInt([]byte(rr.Body), "error_code")
if errorCode != 0 {
if errorCode == errorStopped {
status.SetStatus(courier.MsgFailed)
h.Backend().StopMsgContact(msg)
// was this request successful?
errorCode, _ := jsonparser.GetInt([]byte(rr.Body), "error_code")
if errorCode != 0 {
if errorCode == errorStopped {
status.SetStatus(courier.MsgFailed)
h.Backend().StopMsgContact(msg)
}
log.WithError("Message Send Error", errors.Errorf("received error code from twilio '%d'", errorCode))
return status, nil
}
log.WithError("Message Send Error", errors.Errorf("received error code from twilio '%d'", errorCode))
return status, nil
}

// grab the external id
externalID, err := jsonparser.GetString([]byte(rr.Body), "sid")
if err != nil {
log.WithError("Message Send Error", errors.Errorf("unable to get sid from body"))
return status, nil
}
// grab the external id
externalID, err := jsonparser.GetString([]byte(rr.Body), "sid")
if err != nil {
log.WithError("Message Send Error", errors.Errorf("unable to get sid from body"))
return status, nil
}

status.SetStatus(courier.MsgWired)
status.SetExternalID(externalID)
status.SetStatus(courier.MsgWired)

// only save the first external id
if i == 0 {
status.SetExternalID(externalID)
}
}

return status, nil
}
Expand Down
10 changes: 10 additions & 0 deletions handlers/twilio/twilio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ var defaultSendTestCases = []ChannelSendTestCase{
Path: "/Account/accountSID/Messages.json",
Headers: map[string]string{"Authorization": "Basic YWNjb3VudFNJRDphdXRoVG9rZW4="},
SendPrep: setSendURL},
{Label: "Long Send",
Text: "This is a longer message than 160 characters and will cause us to split it into two separate parts, isn't that right but it is even longer than before I say, I need to keep adding more things to make it work",
URN: "tel:+250788383383",
Status: "W", ExternalID: "1002",
ResponseBody: `{ "sid": "1002" }`, ResponseStatus: 200,
PostParams: map[string]string{"Body": "I need to keep adding more things to make it work", "To": "+250788383383"},
Path: "/Account/accountSID/Messages.json",
Headers: map[string]string{"Authorization": "Basic YWNjb3VudFNJRDphdXRoVG9rZW4="},
SendPrep: setSendURL},
{Label: "Error Sending",
Text: "Error Message", URN: "tel:+250788383383",
Status: "E",
Expand Down Expand Up @@ -126,6 +135,7 @@ var defaultSendTestCases = []ChannelSendTestCase{
}

func TestSending(t *testing.T) {
maxMsgLength = 160
var defaultChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "T", "2020", "US",
map[string]interface{}{
configAccountSID: "accountSID",
Expand Down

0 comments on commit cb3ee75

Please sign in to comment.