-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
305 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,210 @@ | ||
package shaqdoon | ||
package shaqodoon | ||
|
||
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" | ||
) | ||
|
||
/* | ||
POST /api/v1/shaqodoon/received/uuid/ | ||
from=252634101111&text=Msg | ||
*/ | ||
|
||
func init() { | ||
courier.RegisterHandler(NewHandler()) | ||
} | ||
|
||
type handler struct { | ||
handlers.BaseHandler | ||
} | ||
|
||
// NewHandler returns a new External handler | ||
func NewHandler() courier.ChannelHandler { | ||
return &handler{handlers.NewBaseHandler(courier.ChannelType("SQ"), "External")} | ||
} | ||
|
||
// 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) | ||
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) { | ||
shaqodoonMessage := &shaqodoonMessage{} | ||
handlers.DecodeAndValidateQueryParams(shaqodoonMessage, r) | ||
|
||
// if this is a post, also try to parse the form body | ||
if r.Method == http.MethodPost { | ||
handlers.DecodeAndValidateForm(shaqodoonMessage, r) | ||
} | ||
|
||
// validate whether our required fields are present | ||
err := handlers.Validate(shaqodoonMessage) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// must have one of from or sender set, error if neither | ||
sender := shaqodoonMessage.Sender | ||
if sender == "" { | ||
sender = shaqodoonMessage.From | ||
} | ||
if sender == "" { | ||
return nil, errors.New("must have one of 'sender' or 'from' set") | ||
} | ||
|
||
// if we have a date, parse it | ||
dateString := shaqodoonMessage.Date | ||
if dateString == "" { | ||
dateString = shaqodoonMessage.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, shaqodoonMessage.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 shaqodoonMessage 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) { | ||
sendURL := msg.Channel().StringConfigForKey(courier.ConfigSendURL, "") | ||
if sendURL == "" { | ||
return nil, fmt.Errorf("no send url set for SQ channel") | ||
} | ||
|
||
username := msg.Channel().StringConfigForKey(courier.ConfigUsername, "") | ||
if username == "" { | ||
return nil, fmt.Errorf("no username set for SQ channel") | ||
} | ||
|
||
password := msg.Channel().StringConfigForKey(courier.ConfigPassword, "") | ||
if password == "" { | ||
return nil, fmt.Errorf("no password set for SQ channel") | ||
} | ||
|
||
// build our request | ||
form := url.Values{ | ||
"from": []string{strings.TrimPrefix(msg.Channel().Address(), "+")}, | ||
"msg": []string{courier.GetTextAndAttachments(msg)}, | ||
"to": []string{strings.TrimPrefix(msg.URN().Path(), "+")}, | ||
"username": []string{username}, | ||
"password": []string{password}, | ||
} | ||
|
||
encodedForm := form.Encode() | ||
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) | ||
|
||
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 | ||
} | ||
|
||
status.SetStatus(courier.MsgWired) | ||
return status, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package shaqodoon | ||
|
||
import ( | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
|
||
"github.com/nyaruka/courier" | ||
. "github.com/nyaruka/courier/handlers" | ||
) | ||
|
||
var ( | ||
receiveValidMessage = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive/?sender=%2B2349067554729&text=Join" | ||
receiveValidMessageFrom = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive/?from=%2B2349067554729&text=Join" | ||
receiveValidMessageWithDate = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive/?sender=%2B2349067554729&text=Join&date=2017-06-23T12:30:00.500Z" | ||
receiveValidMessageWithTime = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive/?sender=%2B2349067554729&text=Join&time=2017-06-23T12:30:00Z" | ||
receiveNoParams = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive/" | ||
receiveNoSender = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive/?text=Join" | ||
receiveInvalidDate = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive/?sender=%2B2349067554729&text=Join&time=20170623T123000Z" | ||
failedNoParams = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/failed/" | ||
failedValid = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/failed/?id=12345" | ||
sentValid = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/sent/?id=12345" | ||
invalidStatus = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/wired/" | ||
deliveredValid = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/delivered/?id=12345" | ||
deliveredValidPost = "/c/sq/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/delivered/" | ||
) | ||
|
||
var testChannels = []courier.Channel{ | ||
courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "SQ", "2020", "US", nil), | ||
} | ||
|
||
var handleTestCases = []ChannelHandleTestCase{ | ||
{Label: "Receive Valid Message", URL: receiveValidMessage, Data: "empty", Status: 200, Response: "Accepted", | ||
Text: Sp("Join"), URN: Sp("tel:+2349067554729")}, | ||
{Label: "Receive Valid From", URL: receiveValidMessageFrom, Data: "empty", Status: 200, Response: "Accepted", | ||
Text: Sp("Join"), URN: Sp("tel:+2349067554729")}, | ||
{Label: "Receive Valid Message With Date", URL: receiveValidMessageWithDate, Data: "empty", Status: 200, Response: "Accepted", | ||
Text: Sp("Join"), URN: Sp("tel:+2349067554729"), Date: Tp(time.Date(2017, 6, 23, 12, 30, 0, int(500*time.Millisecond), time.UTC))}, | ||
{Label: "Receive Valid Message With Time", URL: receiveValidMessageWithTime, Data: "empty", Status: 200, Response: "Accepted", | ||
Text: Sp("Join"), URN: Sp("tel:+2349067554729"), Date: Tp(time.Date(2017, 6, 23, 12, 30, 0, 0, time.UTC))}, | ||
{Label: "Receive No Params", URL: receiveNoParams, Data: "empty", Status: 400, Response: "field 'text' required"}, | ||
{Label: "Receive No Sender", URL: receiveNoSender, Data: "empty", Status: 400, Response: "must have one of 'sender' or 'from' set"}, | ||
{Label: "Receive Invalid Date", URL: receiveInvalidDate, Data: "empty", Status: 400, Response: "invalid date format, must be RFC 3339"}, | ||
{Label: "Failed No Params", URL: failedNoParams, Status: 400, Response: "field 'id' required"}, | ||
{Label: "Failed Valid", URL: failedValid, Status: 200, Response: `{"status":"F"}`}, | ||
{Label: "Invalid Status", URL: invalidStatus, Status: 404, Response: `page not found`}, | ||
{Label: "Sent Valid", URL: sentValid, Status: 200, Response: `{"status":"S"}`}, | ||
{Label: "Delivered Valid", URL: deliveredValid, Status: 200, Response: `{"status":"D"}`}, | ||
{Label: "Delivered Valid Post", URL: deliveredValidPost, Data: "id=12345", Status: 200, Response: `{"status":"D"}`}, | ||
} | ||
|
||
func TestHandler(t *testing.T) { | ||
RunChannelTestCases(t, testChannels, NewHandler(), handleTestCases) | ||
} | ||
|
||
func BenchmarkHandler(b *testing.B) { | ||
RunChannelBenchmarks(b, testChannels, NewHandler(), handleTestCases) | ||
} | ||
|
||
func setSendURL(server *httptest.Server, channel courier.Channel, msg courier.Msg) { | ||
channel.(*courier.MockChannel).SetConfig(courier.ConfigSendURL, server.URL) | ||
} | ||
|
||
var getSendTestCases = []ChannelSendTestCase{ | ||
{Label: "Plain Send", | ||
Text: "Simple Message", URN: "tel:+250788383383", | ||
Status: "W", | ||
ResponseBody: "0: Accepted for delivery", ResponseStatus: 200, | ||
URLParams: map[string]string{"msg": "Simple Message", "to": "250788383383", "from": "2020"}, | ||
SendPrep: setSendURL}, | ||
{Label: "Unicode Send", | ||
Text: "☺", URN: "tel:+250788383383", | ||
Status: "W", | ||
ResponseBody: "0: Accepted for delivery", ResponseStatus: 200, | ||
URLParams: map[string]string{"msg": "☺", "to": "250788383383", "from": "2020"}, | ||
SendPrep: setSendURL}, | ||
{Label: "Error Sending", | ||
Text: "Error Message", URN: "tel:+250788383383", | ||
Status: "E", | ||
ResponseBody: "1: Unknown channel", ResponseStatus: 401, | ||
Error: "received non 200 status: 401", | ||
URLParams: map[string]string{"msg": `Error Message`, "to": "250788383383"}, | ||
SendPrep: setSendURL}, | ||
{Label: "Send Attachment", | ||
Text: "My pic!", URN: "tel:+250788383383", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, | ||
Status: "W", | ||
ResponseBody: `0: Accepted for delivery`, ResponseStatus: 200, | ||
URLParams: map[string]string{"msg": "My pic!\nhttps://foo.bar/image.jpg", "to": "250788383383", "from": "2020"}, | ||
SendPrep: setSendURL}, | ||
} | ||
|
||
func TestSending(t *testing.T) { | ||
var getChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "SQ", "2020", "US", | ||
map[string]interface{}{ | ||
courier.ConfigSendURL: "SendURL", | ||
courier.ConfigPassword: "Password", | ||
courier.ConfigUsername: "Username"}) | ||
|
||
RunChannelSendTestCases(t, getChannel, NewHandler(), getSendTestCases) | ||
} |