-
Notifications
You must be signed in to change notification settings - Fork 67
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
5 changed files
with
276 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
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
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
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,146 @@ | ||
package mtarget | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"github.com/buger/jsonparser" | ||
"github.com/nyaruka/courier" | ||
"github.com/nyaruka/courier/handlers" | ||
"github.com/nyaruka/courier/utils" | ||
"github.com/nyaruka/gocommon/urns" | ||
) | ||
|
||
var sendURL = "https://api-public.mtarget.fr/sms.json" | ||
var maxLength = 765 | ||
var statuses = map[string]courier.MsgStatusValue{ | ||
"0": courier.MsgWired, | ||
"1": courier.MsgWired, | ||
"2": courier.MsgSent, | ||
"3": courier.MsgDelivered, | ||
"4": courier.MsgFailed, | ||
"6": courier.MsgFailed, | ||
} | ||
|
||
func init() { | ||
courier.RegisterHandler(newHandler()) | ||
} | ||
|
||
type handler struct { | ||
handlers.BaseHandler | ||
} | ||
|
||
func newHandler() courier.ChannelHandler { | ||
return &handler{handlers.NewBaseHandler(courier.ChannelType("MT"), "Mtarget")} | ||
} | ||
|
||
// Initialize is called by the engine once everything is loaded | ||
func (h *handler) Initialize(s courier.Server) error { | ||
h.SetServer(s) | ||
|
||
err := s.AddHandlerRoute(h, http.MethodPost, "receive", h.receiveMsg) | ||
if err != nil { | ||
return nil | ||
} | ||
|
||
statusHandler := handlers.NewExternalIDQueryStatusHandler(h.BaseHandler, statuses, "MsgId", "Status") | ||
return s.AddHandlerRoute(h, http.MethodPost, "status", statusHandler) | ||
} | ||
|
||
// ReceiveMsg handles both MO messages and Stop commands | ||
func (h *handler) receiveMsg(ctx context.Context, c courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) { | ||
text := r.URL.Query().Get("Content") | ||
from := r.URL.Query().Get("Msisdn") | ||
keyword := r.URL.Query().Get("Keyword") | ||
|
||
if from == "" { | ||
return nil, courier.WriteAndLogRequestError(ctx, w, r, c, fmt.Errorf("missing required field 'Msisdn'")) | ||
} | ||
|
||
// create our URN | ||
urn := urns.NewTelURNForCountry(from, c.Country()) | ||
|
||
// if this a stop command, shortcut stopping that contact | ||
if keyword == "Stop" { | ||
stop := h.Backend().NewChannelEvent(c, courier.StopContact, urn) | ||
err := h.Backend().WriteChannelEvent(ctx, stop) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return []courier.Event{stop}, courier.WriteChannelEventSuccess(ctx, w, r, stop) | ||
} | ||
|
||
// otherwise, create our incoming message and write that | ||
msg := h.Backend().NewIncomingMsg(c, urn, text).WithReceivedOn(time.Now().UTC()) | ||
err := h.Backend().WriteMsg(ctx, msg) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return []courier.Event{msg}, courier.WriteMsgSuccess(ctx, w, r, []courier.Msg{msg}) | ||
} | ||
|
||
// SendMsg sends the passed in message, returning any error | ||
func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { | ||
username := msg.Channel().StringConfigForKey(courier.ConfigUsername, "") | ||
if username == "" { | ||
return nil, fmt.Errorf("no username set for MT channel") | ||
} | ||
|
||
password := msg.Channel().StringConfigForKey(courier.ConfigPassword, "") | ||
if password == "" { | ||
return nil, fmt.Errorf("no password set for MT channel") | ||
} | ||
|
||
// send our message | ||
status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored) | ||
for _, part := range handlers.SplitMsg(handlers.GetTextAndAttachments(msg), maxLength) { | ||
// build our request | ||
params := url.Values{ | ||
"username": []string{username}, | ||
"password": []string{password}, | ||
"msisdn": []string{msg.URN().Path()}, | ||
"msg": []string{part}, | ||
} | ||
|
||
msgURL, _ := url.Parse(sendURL) | ||
msgURL.RawQuery = params.Encode() | ||
req, err := http.NewRequest(http.MethodGet, msgURL.String(), nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
rr, err := utils.MakeHTTPRequest(req) | ||
log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), rr).WithError("Message Send Error", err) | ||
status.AddLog(log) | ||
if err != nil { | ||
break | ||
} | ||
|
||
// parse our response for our status code and ticket (external id) | ||
// { | ||
// "results": [{ | ||
// "msisdn": "+447xxxxxxxx", | ||
// "smscount": "1", | ||
// "code": "0", | ||
// "reason": "ACCEPTED", | ||
// "ticket": "760eeaa0-5034-11e7-bb92-00000a0a643a" | ||
// }] | ||
// } | ||
code, _ := jsonparser.GetString(rr.Body, "results", "[0]", "code") | ||
externalID, _ := jsonparser.GetString(rr.Body, "results", "[0]", "ticket") | ||
if code == "0" && externalID != "" { | ||
// all went well, set ourselves to wired | ||
status.SetStatus(courier.MsgWired) | ||
status.SetExternalID(externalID) | ||
} else { | ||
status.SetStatus(courier.MsgFailed) | ||
log.WithError("Message Send Error", fmt.Errorf("Error status code, failing permanently")) | ||
break | ||
} | ||
} | ||
|
||
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,97 @@ | ||
package mtarget | ||
|
||
import ( | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/nyaruka/courier" | ||
. "github.com/nyaruka/courier/handlers" | ||
) | ||
|
||
var ( | ||
receiveValidMessage = "/c/mt/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive?Msisdn=+923161909799&Content=hello+world&Keyword=Default" | ||
receiveStop = "/c/mt/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive?Msisdn=+923161909799&Content=Stop&Keyword=Stop" | ||
receiveMissingFrom = "/c/mt/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive?Content=hello&Keyword=Default" | ||
|
||
statusDelivered = "/c/mt/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/status?MsgId=12a7ee90-50ce-11e7-80ae-00000a0a643c&Status=3" | ||
statusFailed = "/c/mt/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/status?MsgId=12a7ee90-50ce-11e7-80ae-00000a0a643c&Status=4" | ||
statusMissingID = "/c/mt/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/status?Status=4" | ||
) | ||
|
||
var testChannels = []courier.Channel{ | ||
courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "MT", "2020", "FR", nil), | ||
} | ||
|
||
var handleTestCases = []ChannelHandleTestCase{ | ||
{Label: "Receive Valid Message", URL: receiveValidMessage, Data: " ", Status: 200, Response: "Accepted", | ||
Text: Sp("hello world"), URN: Sp("tel:+923161909799")}, | ||
{Label: "Receive Stop", URL: receiveStop, Data: " ", Status: 200, Response: "Accepted", | ||
URN: Sp("tel:+923161909799"), ChannelEvent: Sp("stop_contact")}, | ||
{Label: "Receive Missing From", URL: receiveMissingFrom, Data: " ", Status: 400, Response: "missing required field 'Msisdn'"}, | ||
|
||
{Label: "Status Delivered", URL: statusDelivered, Data: " ", Status: 200, Response: "Accepted", | ||
ExternalID: Sp("12a7ee90-50ce-11e7-80ae-00000a0a643c"), MsgStatus: Sp("D")}, | ||
{Label: "Status Failed", URL: statusFailed, Data: " ", Status: 200, Response: "Accepted", | ||
ExternalID: Sp("12a7ee90-50ce-11e7-80ae-00000a0a643c"), MsgStatus: Sp("F")}, | ||
{Label: "Status Missing ID", URL: statusMissingID, Data: " ", Status: 400, Response: "missing required field 'MsgId'"}, | ||
} | ||
|
||
func TestHandler(t *testing.T) { | ||
RunChannelTestCases(t, testChannels, newHandler(), handleTestCases) | ||
} | ||
|
||
func BenchmarkHandler(b *testing.B) { | ||
RunChannelBenchmarks(b, testChannels, newHandler(), handleTestCases) | ||
} | ||
|
||
// setSendURL takes care of setting the send_url to our test server host | ||
func setSendURL(s *httptest.Server, h courier.ChannelHandler, c courier.Channel, m courier.Msg) { | ||
sendURL = s.URL | ||
} | ||
|
||
var defaultSendTestCases = []ChannelSendTestCase{ | ||
{Label: "Plain Send", | ||
Text: "Simple Message", URN: "tel:+250788383383", | ||
Status: "W", | ||
ResponseBody: `{"results":[{"code": "0", "ticket": "externalID"}]}`, ResponseStatus: 200, | ||
URLParams: map[string]string{ | ||
"msisdn": "+250788383383", | ||
"msg": "Simple Message", | ||
"username": "Username", | ||
"password": "Password", | ||
}, | ||
SendPrep: setSendURL}, | ||
{Label: "Unicode Send", | ||
Text: "☺", URN: "tel:+250788383383", | ||
Status: "W", | ||
ResponseBody: `{"results":[{"code": "0", "ticket": "externalID"}]}`, ResponseStatus: 200, | ||
URLParams: map[string]string{"msg": "☺"}, | ||
SendPrep: setSendURL}, | ||
{Label: "Send Attachment", | ||
Text: "My pic!", URN: "tel:+250788383383", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, | ||
Status: "W", | ||
ResponseBody: `{"results":[{"code": "0", "ticket": "externalID"}]}`, ResponseStatus: 200, | ||
URLParams: map[string]string{"msg": "My pic!\nhttps://foo.bar/image.jpg"}, | ||
SendPrep: setSendURL}, | ||
{Label: "Error Sending", | ||
Text: "Error Sending", URN: "tel:+250788383383", | ||
Status: "E", | ||
ResponseBody: `{"results":[{"code": "3", "ticket": "null"}]}`, ResponseStatus: 403, | ||
SendPrep: setSendURL}, | ||
{Label: "Error Response", | ||
Text: "Error Sending", URN: "tel:+250788383383", | ||
Status: "F", | ||
ResponseBody: `{"results":[{"code": "3", "ticket": "null"}]}`, ResponseStatus: 200, | ||
SendPrep: setSendURL}, | ||
} | ||
|
||
func TestSending(t *testing.T) { | ||
var defaultChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "MT", "2020", "FR", | ||
map[string]interface{}{ | ||
"password": "Password", | ||
"username": "Username", | ||
}, | ||
) | ||
|
||
RunChannelSendTestCases(t, defaultChannel, newHandler(), defaultSendTestCases) | ||
} |