Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for YO! and SMS Central channels #31

Merged
merged 9 commits into from
Aug 15, 2017
2 changes: 1 addition & 1 deletion handlers/external/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type handler struct {
handlers.BaseHandler
}

// NewHandler returns a new Externla handler
// NewHandler returns a new External handler
func NewHandler() courier.ChannelHandler {
return &handler{handlers.NewBaseHandler(courier.ChannelType("EX"), "External")}
}
Expand Down
113 changes: 113 additions & 0 deletions handlers/smscentral/smscentral.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,119 @@
package smscentral
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests for smscentral.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the file


import (
"fmt"
"net/http"
"net/url"
"strings"

"github.com/nyaruka/courier"
"github.com/nyaruka/courier/handlers"
"github.com/nyaruka/courier/utils"
"github.com/pkg/errors"
)

/*
POST /handlers/smscentral/receive/uuid/
mobile=9779811781111&message=Msg
*/

var sendURL = "http://smail.smscentral.com.np/bp/ApiSms.php"

func init() {
courier.RegisterHandler(NewHandler())
}

type handler struct {
handlers.BaseHandler
}

// NewHandler returns a new Yo! handler
func NewHandler() courier.ChannelHandler {
return &handler{handlers.NewBaseHandler(courier.ChannelType("SC"), "SMS Central")}
}

// Initialize is called by the engine once everything is loaded
func (h *handler) Initialize(s courier.Server) error {
h.SetServer(s)
s.AddReceiveMsgRoute(h, "POST", "receive", h.ReceiveMessage)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be checking error status and returning them if non-nil

s.AddReceiveMsgRoute(h, "GET", "receive", h.ReceiveMessage)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's figure out which SMS Central uses and only support that one.


return nil
}

type smsCentralMessage struct {
Message string `validate:"required" name:"message"`
Mobile string `validate:"required" name:"mobile"`
}

// ReceiveMessage is our HTTP handler function for incoming messages
func (h *handler) ReceiveMessage(channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Msg, error) {
smsCentralMessage := &smsCentralMessage{}
handlers.DecodeAndValidateQueryParams(smsCentralMessage, r)

// if this is a post, also try to parse the form body
if r.Method == http.MethodPost {
handlers.DecodeAndValidateForm(smsCentralMessage, r)
}

// validate whether our required fields are present
err := handlers.Validate(smsCentralMessage)
if err != nil {
return nil, err
}

// create our URN
urn := courier.NewTelURNForChannel(smsCentralMessage.Mobile, channel)

// build our msg
msg := h.Backend().NewIncomingMsg(channel, urn, smsCentralMessage.Message)

// and finally queue our message
err = h.Backend().WriteMsg(msg)
if err != nil {
return nil, err
}

return []courier.Msg{msg}, courier.WriteReceiveSuccess(w, r, msg)

}

// SendMsg sends the passed in message, returning any error
func (h *handler) SendMsg(msg courier.Msg) (courier.MsgStatus, error) {
username := msg.Channel().StringConfigForKey(courier.ConfigUsername, "")
if username == "" {
return nil, fmt.Errorf("no username set for KN channel")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong error msg.

}

password := msg.Channel().StringConfigForKey(courier.ConfigPassword, "")
if password == "" {
return nil, fmt.Errorf("no password set for KN channel")
}

// build our request
form := url.Values{
"user": []string{username},
"pass": []string{password},
"mobile": []string{strings.TrimPrefix(msg.URN().Path(), "+")},
"content": []string{courier.GetTextAndAttachments(msg)},
}

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

// record our status and log
status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored)
status.AddLog(courier.NewChannelLogFromRR(msg.Channel(), msg.ID(), rr))
if err != nil {
return status, err
}

if rr.StatusCode != 200 && rr.StatusCode != 201 && rr.StatusCode != 202 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rr.StatusCode / 100 != 2?

return status, errors.Errorf("Got non-200 response [%d] from API")
}

status.SetStatus(courier.MsgWired)

return status, nil
}
250 changes: 250 additions & 0 deletions handlers/yo/yo.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,253 @@ package yo
/*
GET /handlers/yo/received/uuid?account=12345&dest=8500&message=Msg&sender=256778021111
*/

import (
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/nyaruka/courier"
"github.com/nyaruka/courier/handlers"
"github.com/nyaruka/courier/utils"
"github.com/pkg/errors"
)

var sendURL1 = "http://smgw1.yo.co.ug:9100/sendsms"
var sendURL2 = "http://41.220.12.201:9100/sendsms"
var sendURL3 = "http://164.40.148.210:9100/sendsms"

func init() {
courier.RegisterHandler(NewHandler())
}

type handler struct {
handlers.BaseHandler
}

// NewHandler returns a new Yo! handler
func NewHandler() courier.ChannelHandler {
return &handler{handlers.NewBaseHandler(courier.ChannelType("YO"), "YO!")}
}

// Initialize is called by the engine once everything is loaded
func (h *handler) Initialize(s courier.Server) error {
h.SetServer(s)
s.AddReceiveMsgRoute(h, "POST", "receive", h.ReceiveMessage)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check error status, lets only register what these guys currently use. (look at our logs to figure out)

s.AddReceiveMsgRoute(h, "GET", "receive", h.ReceiveMessage)

sentHandler := h.buildStatusHandler("sent")
s.AddUpdateStatusRoute(h, "GET", "sent", sentHandler)
s.AddUpdateStatusRoute(h, "POST", "sent", sentHandler)

deliveredHandler := h.buildStatusHandler("delivered")
s.AddUpdateStatusRoute(h, "GET", "delivered", deliveredHandler)
s.AddUpdateStatusRoute(h, "POST", "delivered", deliveredHandler)

failedHandler := h.buildStatusHandler("failed")
s.AddUpdateStatusRoute(h, "GET", "failed", failedHandler)
s.AddUpdateStatusRoute(h, "POST", "failed", failedHandler)

return nil
}

// ReceiveMessage is our HTTP handler function for incoming messages
func (h *handler) ReceiveMessage(channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Msg, error) {
yoMessage := &yoMessage{}
handlers.DecodeAndValidateQueryParams(yoMessage, r)

// if this is a post, also try to parse the form body
if r.Method == http.MethodPost {
handlers.DecodeAndValidateForm(yoMessage, r)
}

// validate whether our required fields are present
err := handlers.Validate(yoMessage)
if err != nil {
return nil, err
}

// must have one of from or sender set, error if neither
sender := yoMessage.Sender
if sender == "" {
sender = yoMessage.From
}
if sender == "" {
return nil, errors.New("must have one of 'sender' or 'from' set")
}

// if we have a date, parse it
dateString := yoMessage.Date
if dateString == "" {
dateString = yoMessage.Time
}

date := time.Now()
if dateString != "" {
date, err = time.Parse(time.RFC3339Nano, dateString)
if err != nil {
return nil, errors.New("invalid date format, must be RFC 3339")
}
}

// create our URN
urn, err := courier.NewURNFromParts(channel.Scheme(), sender)
if err != nil {
return nil, err
}

// build our msg
msg := h.Backend().NewIncomingMsg(channel, urn, yoMessage.Text).WithReceivedOn(date)

// and write it
err = h.Backend().WriteMsg(msg)
if err != nil {
return nil, err
}

return []courier.Msg{msg}, courier.WriteReceiveSuccess(w, r, msg)
}

type yoMessage struct {
From string `name:"from"`
Sender string `name:"sender"`
Text string `validate:"required" name:"text"`
Date string `name:"date"`
Time string `name:"time"`
}

// buildStatusHandler deals with building a handler that takes what status is received in the URL
func (h *handler) buildStatusHandler(status string) courier.ChannelUpdateStatusFunc {
return func(channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.MsgStatus, error) {
return h.StatusMessage(status, channel, w, r)
}
}

// StatusMessage is our HTTP handler function for status updates
func (h *handler) StatusMessage(statusString string, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.MsgStatus, error) {
statusForm := &statusForm{}
handlers.DecodeAndValidateQueryParams(statusForm, r)

// if this is a post, also try to parse the form body
if r.Method == http.MethodPost {
handlers.DecodeAndValidateForm(statusForm, r)
}

// validate whether our required fields are present
err := handlers.Validate(statusForm)
if err != nil {
return nil, err
}

// get our id
msgStatus, found := statusMappings[strings.ToLower(statusString)]
if !found {
return nil, fmt.Errorf("unknown status '%s', must be one failed, sent or delivered", statusString)
}

// write our status
status := h.Backend().NewMsgStatusForID(channel, courier.NewMsgID(statusForm.ID), msgStatus)
err = h.Backend().WriteMsgStatus(status)
if err != nil {
return nil, err
}

return []courier.MsgStatus{status}, courier.WriteStatusSuccess(w, r, status)
}

type statusForm struct {
ID int64 `name:"id" validate:"required"`
}

var statusMappings = map[string]courier.MsgStatusValue{
"failed": courier.MsgFailed,
"sent": courier.MsgSent,
"delivered": courier.MsgDelivered,
}

// SendMsg sends the passed in message, returning any error
func (h *handler) SendMsg(msg courier.Msg) (courier.MsgStatus, error) {

username := msg.Channel().StringConfigForKey(courier.ConfigUsername, "")
if username == "" {
return nil, fmt.Errorf("no username set for YO channel")
}

password := msg.Channel().StringConfigForKey(courier.ConfigPassword, "")
if password == "" {
return nil, fmt.Errorf("no password set for YO channel")
}

// build our request
form := url.Values{
"origin": []string{strings.TrimPrefix(msg.Channel().Address(), "+")},
"sms_content": []string{courier.GetTextAndAttachments(msg)},
"destinations": []string{strings.TrimPrefix(msg.URN().Path(), "+")},
"ybsacctno": []string{username},
"password": []string{password},
}

var status courier.MsgStatus
encodedForm := form.Encode()
sendURLs := []string{sendURL1, sendURL2, sendURL3}

for _, sendURL := range sendURLs {
failed := false
sendURL := fmt.Sprintf("%s?%s", sendURL, encodedForm)

req, err := http.NewRequest(http.MethodGet, sendURL, nil)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr, err := utils.MakeHTTPRequest(req)

if err != nil {
failed = true
}
// record our status and log
status = h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored)
status.AddLog(courier.NewChannelLogFromRR(msg.Channel(), msg.ID(), rr))
if err != nil {
return status, err
}

responseQS, err := url.ParseQuery(string(rr.Body))

if err != nil {
failed = true
}

if failed == false && rr.StatusCode != 200 && rr.StatusCode != 201 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!failed

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe see if you can refactor this a bit with continues instead of checking failed everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By using continue we won't be able to stop blacklisted contacts

failed = true
}

ybsAutocreateStatus, ok := responseQS["ybs_autocreate_status"]
if !ok {
ybsAutocreateStatus = []string{""}
}

if failed == false && ybsAutocreateStatus[0] != "OK" {
failed = true
}

ybsAutocreateMessage, ok := responseQS["ybs_autocreate_message"]

if !ok {
ybsAutocreateMessage = []string{""}
}

if failed == true && strings.Contains(ybsAutocreateMessage[0], "BLACKLISTED") {
status.SetStatus(courier.MsgFailed)
h.Backend().StopMsgContact(msg)
return status, nil
}

if failed == false {
status.SetStatus(courier.MsgWired)
return status, nil
}

}

return status, errors.Errorf("Received error from Yo! API")
}
Loading