Skip to content

Commit

Permalink
Merge pull request rapidpro#245 from nyaruka/twiml-whatsapp
Browse files Browse the repository at this point in the history
add whatsapp scheme to twiml channels
  • Loading branch information
nicpottier authored Jun 14, 2019
2 parents 8964860 + 9e3b491 commit 55f86d1
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 3 deletions.
5 changes: 5 additions & 0 deletions backends/rapidpro/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ func (c *DBChannel) Address() string { return c.Address_.String }
// Country returns the country code for this channel if any
func (c *DBChannel) Country() string { return c.Country_.String }

// IsScheme returns whether this channel serves only the passed in scheme
func (c *DBChannel) IsScheme(scheme string) bool {
return len(c.Schemes_) == 1 && c.Schemes_[0] == scheme
}

// ConfigForKey returns the config value for the passed in key, or defaultValue if it isn't found
func (c *DBChannel) ConfigForKey(key string, defaultValue interface{}) interface{} {
// no value, return our default value
Expand Down
3 changes: 3 additions & 0 deletions channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ type Channel interface {
Country() string
Address() string

// is this channel for the passed in scheme (and only that scheme)
IsScheme(string) bool

// CallbackDomain returns the domain that should be used for any callbacks the channel registers
CallbackDomain(fallbackDomain string) string

Expand Down
18 changes: 17 additions & 1 deletion handlers/twiml/twiml.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,17 @@ func (h *handler) receiveMessage(ctx context.Context, channel courier.Channel, w
}

// create our URN
urn, err := urns.NewTelURNForCountry(form.From, form.FromCountry)
var urn urns.URN
if channel.IsScheme(urns.WhatsAppScheme) {
// Twilio Whatsapp from is in the form: whatsapp:+12211414154
parts := strings.Split(form.From, ":")

// trim off left +, official whatsapp IDs dont have that
urn, err = urns.NewWhatsAppURN(strings.TrimLeft(parts[1], "+"))
} else {
urn, err = urns.NewTelURNForCountry(form.From, form.FromCountry)
}

if err != nil {
return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err)
}
Expand Down Expand Up @@ -216,6 +226,12 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat
form["From"] = []string{channel.Address()}
}

// for whatsapp channels, we have to prepend whatsapp to the To and From
if channel.IsScheme(urns.WhatsAppScheme) {
form["To"][0] = fmt.Sprintf("%s:+%s", urns.WhatsAppScheme, form["To"][0])
form["From"][0] = fmt.Sprintf("%s:%s", urns.WhatsAppScheme, form["From"][0])
}

// build our URL
baseURL := h.baseURL(channel)
if baseURL == "" {
Expand Down
40 changes: 39 additions & 1 deletion handlers/twiml/twiml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/nyaruka/courier"
. "github.com/nyaruka/courier/handlers"
"github.com/nyaruka/gocommon/urns"
)

var testChannels = []courier.Channel{
Expand Down Expand Up @@ -58,6 +59,8 @@ var (

tmsStatusExtra = "SmsStatus=sent&MessageStatus=sent&To=2021&MessagingServiceSid=MGdb23ec0f89ee2632e46e91d8128f5e2b&MessageSid=SM0b6e2697aae04182a9f5b5c7a8994c7f&AccountSid=acctid&From=%2B14133881111&ApiVersion=2010-04-01"
tmsReceiveExtra = "ToCountry=US&ToState=&SmsMessageSid=SMbbf29aeb9d380ce2a1c0ae4635ff9dab&NumMedia=0&ToCity=&FromZip=27609&SmsSid=SMbbf29aeb9d380ce2a1c0ae4635ff9dab&FromState=NC&SmsStatus=received&FromCity=RALEIGH&Body=John+Cruz&FromCountry=US&To=384387&ToZip=&NumSegments=1&MessageSid=SMbbf29aeb9d380ce2a1c0ae4635ff9dab&AccountSid=acctid&From=%2B14133881111&ApiVersion=2010-04-01"

waReceiveValid = "ToCountry=US&ToState=District+Of+Columbia&SmsMessageSid=SMe287d7109a5a925f182f0e07fe5b223b&NumMedia=0&ToCity=&FromZip=01022&SmsSid=SMe287d7109a5a925f182f0e07fe5b223b&FromState=MA&SmsStatus=received&FromCity=CHICOPEE&Body=Msg&FromCountry=US&To=whatsapp:%2B12028831111&ToZip=&NumSegments=1&MessageSid=SMe287d7109a5a925f182f0e07fe5b223b&AccountSid=acctid&From=whatsapp:%2B14133881111&ApiVersion=2010-04-01"
)

var testCases = []ChannelHandleTestCase{
Expand Down Expand Up @@ -177,6 +180,12 @@ var swTestCases = []ChannelHandleTestCase{
{Label: "Status ID Invalid", URL: swStatusInvalidIDURL, Data: statusValid, Status: 200, Response: `"status":"D"`, ExternalID: Sp("SMe287d7109a5a925f182f0e07fe5b223b")},
}

var waTestCases = []ChannelHandleTestCase{
{Label: "Receive Valid", URL: receiveURL, Data: waReceiveValid, Status: 200, Response: "<Response/>",
Text: Sp("Msg"), URN: Sp("whatsapp:14133881111"), ExternalID: Sp("SMe287d7109a5a925f182f0e07fe5b223b"),
PrepRequest: addValidSignature},
}

func addValidSignature(r *http.Request) {
r.ParseForm()
sig, _ := twCalculateSignature(fmt.Sprintf("%s://%s%s", r.URL.Scheme, r.Host, r.URL.RequestURI()), r.PostForm, "6789")
Expand All @@ -199,6 +208,15 @@ func TestHandler(t *testing.T) {
RunChannelTestCases(t, tmsTestChannels, newTWIMLHandler("TMS", "Twilio Messaging Service", true), tmsTestCases)
RunChannelTestCases(t, twTestChannels, newTWIMLHandler("TW", "TwiML API", true), twTestCases)
RunChannelTestCases(t, swTestChannels, newTWIMLHandler("SW", "SignalWire", false), swTestCases)

waChannel := courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "SW", "+12065551212", "US",
map[string]interface{}{
configAccountSID: "accountSID",
courier.ConfigAuthToken: "6789",
},
)
waChannel.SetScheme(urns.WhatsAppScheme)
RunChannelTestCases(t, []courier.Channel{waChannel}, newTWIMLHandler("T", "TwilioWhatsApp", true), waTestCases)
}

func BenchmarkHandler(b *testing.B) {
Expand Down Expand Up @@ -420,6 +438,16 @@ var swSendTestCases = []ChannelSendTestCase{
SendPrep: setSendURL},
}

var waSendTestCases = []ChannelSendTestCase{
{Label: "Plain Send",
Text: "Simple Message ☺", URN: "whatsapp:250788383383",
Status: "W", ExternalID: "1002",
ResponseBody: `{ "sid": "1002" }`, ResponseStatus: 200,
PostParams: map[string]string{"Body": "Simple Message ☺", "To": "whatsapp:+250788383383", "From": "whatsapp:+12065551212", "StatusCallback": "https://localhost/c/t/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/status?id=10&action=callback"},
Headers: map[string]string{"Authorization": "Basic YWNjb3VudFNJRDphdXRoVG9rZW4="},
SendPrep: setSendURL},
}

func TestSending(t *testing.T) {
maxMsgLength = 160
var defaultChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "T", "2020", "US",
Expand All @@ -441,7 +469,7 @@ func TestSending(t *testing.T) {
})

var swChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "SW", "2020", "US",
map[string]interface{}{
map[string]interface{}{
configAccountSID: "accountSID",
courier.ConfigAuthToken: "authToken",
configSendURL: "BASE_URL",
Expand All @@ -451,4 +479,14 @@ func TestSending(t *testing.T) {
RunChannelSendTestCases(t, tmsDefaultChannel, newTWIMLHandler("TMS", "Twilio Messaging Service", true), tmsDefaultSendTestCases, nil)
RunChannelSendTestCases(t, twDefaultChannel, newTWIMLHandler("TW", "TwiML", true), twDefaultSendTestCases, nil)
RunChannelSendTestCases(t, swChannel, newTWIMLHandler("SW", "SignalWire", false), swSendTestCases, nil)

waChannel := courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "SW", "+12065551212", "US",
map[string]interface{}{
configAccountSID: "accountSID",
courier.ConfigAuthToken: "authToken",
},
)
waChannel.SetScheme(urns.WhatsAppScheme)

RunChannelSendTestCases(t, waChannel, newTWIMLHandler("T", "Twilio Whatsapp", true), waSendTestCases, nil)
}
10 changes: 9 additions & 1 deletion test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,17 @@ func (c *MockChannel) Name() string { return fmt.Sprintf("Channel: %s", c.uuid.S
// ChannelType returns the type of this channel
func (c *MockChannel) ChannelType() ChannelType { return c.channelType }

// SetScheme sets the scheme for this channel
func (c *MockChannel) SetScheme(scheme string) { c.schemes = []string{scheme} }

// Schemes returns the schemes for this channel
func (c *MockChannel) Schemes() []string { return c.schemes }

// IsScheme returns whether the passed in scheme is the scheme for this channel
func (c *MockChannel) IsScheme(scheme string) bool {
return len(c.schemes) == 1 && c.schemes[0] == scheme
}

// Address returns the address of this channel
func (c *MockChannel) Address() string { return c.address }

Expand Down Expand Up @@ -457,7 +465,7 @@ func (c *MockChannel) OrgConfigForKey(key string, defaultValue interface{}) inte
}

// NewMockChannel creates a new mock channel for the passed in type, address, country and config
func NewMockChannel(uuid string, channelType string, address string, country string, config map[string]interface{}) Channel {
func NewMockChannel(uuid string, channelType string, address string, country string, config map[string]interface{}) *MockChannel {
cUUID, _ := NewChannelUUID(uuid)

channel := &MockChannel{
Expand Down

0 comments on commit 55f86d1

Please sign in to comment.