diff --git a/cmd/courier/main.go b/cmd/courier/main.go index b8085d6c2..c8a7ed09c 100644 --- a/cmd/courier/main.go +++ b/cmd/courier/main.go @@ -63,6 +63,7 @@ import ( _ "github.com/nyaruka/courier/handlers/start" _ "github.com/nyaruka/courier/handlers/telegram" _ "github.com/nyaruka/courier/handlers/telesom" + _ "github.com/nyaruka/courier/handlers/tembachat" _ "github.com/nyaruka/courier/handlers/thinq" _ "github.com/nyaruka/courier/handlers/twiml" _ "github.com/nyaruka/courier/handlers/twitter" diff --git a/handlers/tembachat/handler.go b/handlers/tembachat/handler.go new file mode 100644 index 000000000..1c3626b02 --- /dev/null +++ b/handlers/tembachat/handler.go @@ -0,0 +1,83 @@ +package tembachat + +import ( + "bytes" + "context" + "net/http" + + "github.com/nyaruka/courier" + "github.com/nyaruka/courier/handlers" + "github.com/nyaruka/gocommon/jsonx" + "github.com/nyaruka/gocommon/urns" +) + +var ( + defaultSendURL = "http://chatserver:8070/send" +) + +func init() { + courier.RegisterHandler(newHandler()) +} + +type handler struct { + handlers.BaseHandler +} + +func newHandler() courier.ChannelHandler { + return &handler{handlers.NewBaseHandler(courier.ChannelType("TWC"), "Temba Chat")} +} + +// Initialize is called by the engine once everything is loaded +func (h *handler) Initialize(s courier.Server) error { + h.SetServer(s) + s.AddHandlerRoute(h, http.MethodPost, "receive", courier.ChannelLogTypeMsgReceive, handlers.JSONPayload(h, h.receiveMessage)) + return nil +} + +type receivePayload struct { + Type string `json:"type" validate:"required"` + Message struct { + Identifier string `json:"identifier" validate:"required"` + Text string `json:"text" validate:"required"` + } `json:"message"` +} + +// receiveMessage is our HTTP handler function for incoming messages +func (h *handler) receiveMessage(ctx context.Context, c courier.Channel, w http.ResponseWriter, r *http.Request, payload *receivePayload, clog *courier.ChannelLog) ([]courier.Event, error) { + if payload.Type == "message" { + urn, err := urns.NewWebChatURN(payload.Message.Identifier) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, c, w, r, err) + } + + msg := h.Backend().NewIncomingMsg(c, urn, payload.Message.Text, "", clog) + return handlers.WriteMsgsAndResponse(ctx, h, []courier.MsgIn{msg}, w, r, clog) + } + return nil, handlers.WriteAndLogRequestIgnored(ctx, h, c, w, r, "") +} + +type sendPayload struct { + Identifier string `json:"identifier"` + Text string `json:"text"` + Origin string `json:"origin"` +} + +func (h *handler) Send(ctx context.Context, msg courier.MsgOut, clog *courier.ChannelLog) (courier.StatusUpdate, error) { + sendURL := msg.Channel().StringConfigForKey(courier.ConfigSendURL, defaultSendURL) + + payload := &sendPayload{ + Identifier: msg.URN().Path(), + Text: msg.Text(), + Origin: string(msg.Origin()), + } + req, _ := http.NewRequest("POST", sendURL, bytes.NewReader(jsonx.MustMarshal(payload))) + + status := h.Backend().NewStatusUpdate(msg.Channel(), msg.ID(), courier.MsgStatusWired, clog) + + resp, _, err := h.RequestHTTP(req, clog) + if err != nil || resp.StatusCode/100 != 2 { + status.SetStatus(courier.MsgStatusErrored) + } + + return status, nil +} diff --git a/handlers/tembachat/handler_test.go b/handlers/tembachat/handler_test.go new file mode 100644 index 000000000..f5b4905d3 --- /dev/null +++ b/handlers/tembachat/handler_test.go @@ -0,0 +1,77 @@ +package tembachat + +import ( + "net/http/httptest" + "testing" + + "github.com/nyaruka/courier" + . "github.com/nyaruka/courier/handlers" + "github.com/nyaruka/courier/test" +) + +var testChannels = []courier.Channel{ + test.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "TWC", "", "", nil), +} + +var handleTestCases = []IncomingTestCase{ + { + Label: "Receive Valid Message", + URL: "/c/twc/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive", + Data: `{"type": "message", "message": {"identifier": "65vbbDAQCdPdEWlEhDGy4utO", "text": "Join"}}`, + ExpectedRespStatus: 200, + ExpectedBodyContains: "Accepted", + ExpectedMsgText: Sp("Join"), + ExpectedURN: "webchat:65vbbDAQCdPdEWlEhDGy4utO", + }, + { + Label: "Invalid URN", + URL: "/c/twc/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive", + Data: `{"type": "message", "message": {"identifier": "xxxxx", "text": "Join"}}`, + ExpectedRespStatus: 400, + ExpectedBodyContains: "invalid webchat id: xxxxx", + }, + { + Label: "Missing fields", + URL: "/c/twc/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive", + Data: `{"foo": "message"}`, + ExpectedRespStatus: 400, + ExpectedBodyContains: "Field validation for 'Type' failed on the 'required' tag", + }, +} + +func TestIncoming(t *testing.T) { + RunIncomingTestCases(t, testChannels, newHandler(), handleTestCases) +} + +func setSendURL(s *httptest.Server, h courier.ChannelHandler, c courier.Channel, m courier.MsgOut) { + defaultSendURL = s.URL +} + +var defaultSendTestCases = []OutgoingTestCase{ + { + Label: "Plain Send", + MsgText: "Simple message ☺", + MsgURN: "webchat:65vbbDAQCdPdEWlEhDGy4utO", + MockResponseBody: `{"status": "queued"}`, + MockResponseStatus: 200, + ExpectedRequestBody: `{"identifier":"65vbbDAQCdPdEWlEhDGy4utO","text":"Simple message ☺","origin":"flow"}`, + ExpectedMsgStatus: "W", + SendPrep: setSendURL, + }, + { + Label: "Error Sending", + MsgText: "Error message", + MsgURN: "webchat:65vbbDAQCdPdEWlEhDGy4utO", + MockResponseBody: `{"error": "boom"}`, + MockResponseStatus: 400, + ExpectedRequestBody: `{"identifier":"65vbbDAQCdPdEWlEhDGy4utO","text":"Error message","origin":"flow"}`, + ExpectedMsgStatus: "E", + SendPrep: setSendURL, + }, +} + +func TestOutgoing(t *testing.T) { + var defaultChannel = test.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "TWC", "", "", nil) + + RunOutgoingTestCases(t, defaultChannel, newHandler(), defaultSendTestCases, nil, nil) +}