From b6dc55230a3f40913804d0e46c2461a3baaf86a8 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 10 Jan 2024 12:10:44 -0500 Subject: [PATCH 1/3] WIP --- cmd/courier/main.go | 1 + go.mod | 2 +- go.sum | 2 + handlers/tembachat/handler.go | 78 +++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 handlers/tembachat/handler.go 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/go.mod b/go.mod index 84f0c2ad0..aefea2dba 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 github.com/nyaruka/ezconf v0.2.1 - github.com/nyaruka/gocommon v1.42.7 + github.com/nyaruka/gocommon v1.42.8-0.20240109225831-33bd5cd84012 github.com/nyaruka/null/v3 v3.0.0 github.com/nyaruka/redisx v0.5.0 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index 7f26cddc6..5d7cf8854 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/nyaruka/ezconf v0.2.1 h1:TDXWoqjqYya1uhou1mAJZg7rgFYL98EB0Tb3+BWtUh0= github.com/nyaruka/ezconf v0.2.1/go.mod h1:ey182kYkw2MIi4XiWe1FR/mzI33WCmTWuceDYYxgnQw= github.com/nyaruka/gocommon v1.42.7 h1:4U7Ta1LIHVc/uv8sfqmmV5oRiFU8TcJM9a7QjxVoaeA= github.com/nyaruka/gocommon v1.42.7/go.mod h1:DMj0TJPT2zi6eoXrBSsJTGBxSAUkpBk+UzcMyAbq5DA= +github.com/nyaruka/gocommon v1.42.8-0.20240109225831-33bd5cd84012 h1:Tv3MVkpLEYIj7o+i0VmVja8grrlwspI+CtpDi7mqxoI= +github.com/nyaruka/gocommon v1.42.8-0.20240109225831-33bd5cd84012/go.mod h1:gZUIZARrFlC2PUfcumSAgcMzmaWiPp6TnYqM18KIpBU= github.com/nyaruka/librato v1.1.1 h1:0nTYtJLl3Sn7lX3CuHsLf+nXy1k/tGV0OjVxLy3Et4s= github.com/nyaruka/librato v1.1.1/go.mod h1:fme1Fu1PT2qvkaBZyw8WW+SrnFe2qeeCWpvqmAaKAKE= github.com/nyaruka/null/v2 v2.0.3 h1:rdmMRQyVzrOF3Jff/gpU/7BDR9mQX0lcLl4yImsA3kw= diff --git a/handlers/tembachat/handler.go b/handlers/tembachat/handler.go new file mode 100644 index 000000000..0ddd62248 --- /dev/null +++ b/handlers/tembachat/handler.go @@ -0,0 +1,78 @@ +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" +) + +const ( + defaultSendURL = "http://localhost: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"` + Message struct { + Identifier string `json:"identifier"` + Text string `json:"text"` + } `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, _ := urns.NewWebChatURN(payload.Message.Identifier) + 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"` +} + +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(), + } + 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 +} From f21e870d993b715e0b31ecaf6d6f7ad0050ea24b Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 10 Jan 2024 15:26:06 -0500 Subject: [PATCH 2/3] Use latest gocommon --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index aefea2dba..bafd9c2b8 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 github.com/nyaruka/ezconf v0.2.1 - github.com/nyaruka/gocommon v1.42.8-0.20240109225831-33bd5cd84012 + github.com/nyaruka/gocommon v1.50.0 github.com/nyaruka/null/v3 v3.0.0 github.com/nyaruka/redisx v0.5.0 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index 5d7cf8854..046290b2a 100644 --- a/go.sum +++ b/go.sum @@ -68,10 +68,8 @@ github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nyaruka/ezconf v0.2.1 h1:TDXWoqjqYya1uhou1mAJZg7rgFYL98EB0Tb3+BWtUh0= github.com/nyaruka/ezconf v0.2.1/go.mod h1:ey182kYkw2MIi4XiWe1FR/mzI33WCmTWuceDYYxgnQw= -github.com/nyaruka/gocommon v1.42.7 h1:4U7Ta1LIHVc/uv8sfqmmV5oRiFU8TcJM9a7QjxVoaeA= -github.com/nyaruka/gocommon v1.42.7/go.mod h1:DMj0TJPT2zi6eoXrBSsJTGBxSAUkpBk+UzcMyAbq5DA= -github.com/nyaruka/gocommon v1.42.8-0.20240109225831-33bd5cd84012 h1:Tv3MVkpLEYIj7o+i0VmVja8grrlwspI+CtpDi7mqxoI= -github.com/nyaruka/gocommon v1.42.8-0.20240109225831-33bd5cd84012/go.mod h1:gZUIZARrFlC2PUfcumSAgcMzmaWiPp6TnYqM18KIpBU= +github.com/nyaruka/gocommon v1.50.0 h1:pvEyNq4k42iMHsnfAgTjgSP9mga88DnNRmMtH/mDJlU= +github.com/nyaruka/gocommon v1.50.0/go.mod h1:gZUIZARrFlC2PUfcumSAgcMzmaWiPp6TnYqM18KIpBU= github.com/nyaruka/librato v1.1.1 h1:0nTYtJLl3Sn7lX3CuHsLf+nXy1k/tGV0OjVxLy3Et4s= github.com/nyaruka/librato v1.1.1/go.mod h1:fme1Fu1PT2qvkaBZyw8WW+SrnFe2qeeCWpvqmAaKAKE= github.com/nyaruka/null/v2 v2.0.3 h1:rdmMRQyVzrOF3Jff/gpU/7BDR9mQX0lcLl4yImsA3kw= From 7375941d803fca6cfdcc6e7d239d2df0b91d9a2c Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 11 Jan 2024 09:27:23 -0500 Subject: [PATCH 3/3] Add tests --- handlers/tembachat/handler.go | 17 ++++--- handlers/tembachat/handler_test.go | 77 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 handlers/tembachat/handler_test.go diff --git a/handlers/tembachat/handler.go b/handlers/tembachat/handler.go index bc502a52e..1c3626b02 100644 --- a/handlers/tembachat/handler.go +++ b/handlers/tembachat/handler.go @@ -11,7 +11,7 @@ import ( "github.com/nyaruka/gocommon/urns" ) -const ( +var ( defaultSendURL = "http://chatserver:8070/send" ) @@ -35,19 +35,22 @@ func (h *handler) Initialize(s courier.Server) error { } type receivePayload struct { - Type string `json:"type"` + Type string `json:"type" validate:"required"` Message struct { - Identifier string `json:"identifier"` - Text string `json:"text"` + 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, _ := urns.NewWebChatURN(payload.Message.Identifier) - msg := h.Backend().NewIncomingMsg(c, urn, payload.Message.Text, "", clog) + 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, "") @@ -56,6 +59,7 @@ func (h *handler) receiveMessage(ctx context.Context, c courier.Channel, w http. 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) { @@ -64,6 +68,7 @@ func (h *handler) Send(ctx context.Context, msg courier.MsgOut, clog *courier.Ch payload := &sendPayload{ Identifier: msg.URN().Path(), Text: msg.Text(), + Origin: string(msg.Origin()), } req, _ := http.NewRequest("POST", sendURL, bytes.NewReader(jsonx.MustMarshal(payload))) 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) +}