-
Notifications
You must be signed in to change notification settings - Fork 71
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
Changes from 3 commits
e263e31
f1d2cf3
475ec55
82b1a92
bc4a016
6442182
6fc0499
17f9b77
065ed29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,119 @@ | ||
package smscentral | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return status, errors.Errorf("Got non-200 response [%d] from API") | ||
} | ||
|
||
status.SetStatus(courier.MsgWired) | ||
|
||
return status, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe see if you can refactor this a bit with There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add tests for smscentral.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the file