Skip to content

Commit

Permalink
Merge pull request rapidpro#336 from Ilhasoft/rocketchat-handler
Browse files Browse the repository at this point in the history
Rocketchat handler
  • Loading branch information
rowanseymour authored Nov 2, 2020
2 parents e5c9ea0 + 3e14abb commit 98db720
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/courier/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
_ "github.com/nyaruka/courier/handlers/playmobile"
_ "github.com/nyaruka/courier/handlers/plivo"
_ "github.com/nyaruka/courier/handlers/redrabbit"
_ "github.com/nyaruka/courier/handlers/rocketchat"
_ "github.com/nyaruka/courier/handlers/shaqodoon"
_ "github.com/nyaruka/courier/handlers/smscentral"
_ "github.com/nyaruka/courier/handlers/start"
Expand Down
158 changes: 158 additions & 0 deletions handlers/rocketchat/rocketchat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package rocketchat

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/buger/jsonparser"
"github.com/nyaruka/courier"
"github.com/nyaruka/courier/handlers"
"github.com/nyaruka/courier/utils"
"github.com/nyaruka/gocommon/urns"
"net/http"
)

const (
configBaseURL = "base_url"
configSecret = "secret"
configBotUsername = "bot_username"
configAdminAuthToken = "admin_auth_token"
configAdminUserID = "admin_user_id"
)

func init() {
courier.RegisterHandler(newHandler())
}

type handler struct {
handlers.BaseHandler
}

func newHandler() courier.ChannelHandler {
return &handler{handlers.NewBaseHandler(courier.ChannelType("RC"), "RocketChat")}
}

// 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", h.receiveMessage)
return nil
}

type Attachment struct {
Type string `json:"type"`
URL string `json:"url"`
}

type moPayload struct {
User struct {
URN string `json:"urn" validate:"required"`
Username string `json:"username"`
FullName string `json:"full_name"`
} `json:"user" validate:"required"`
Text string `json:"text"`
Attachments []Attachment `json:"attachments"`
}

// receiveMessage is our HTTP handler function for incoming messages
func (h *handler) receiveMessage(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) {
// check authorization
secret := channel.StringConfigForKey(configSecret, "")
if fmt.Sprintf("Token %s", secret) != r.Header.Get("Authorization") {
return nil, courier.WriteAndLogUnauthorized(ctx, w, r, channel, fmt.Errorf("invalid Authorization header"))
}

payload := &moPayload{}
err := handlers.DecodeAndValidateJSON(payload, r)
if err != nil {
return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err)
}

// check content empty
if payload.Text == "" && len(payload.Attachments) == 0 {
return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, errors.New("no text or attachment"))
}

urn, err := urns.NewURNFromParts(urns.RocketChatScheme, payload.User.URN, "", payload.User.Username)
if err != nil {
return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err)
}

msg := h.Backend().NewIncomingMsg(channel, urn, payload.Text).WithContactName(payload.User.FullName)
for _, attachment := range payload.Attachments {
msg.WithAttachment(attachment.URL)
}

return handlers.WriteMsgsAndResponse(ctx, h, []courier.Msg{msg}, w, r)
}

// BuildDownloadMediaRequest download media for message attachment with RC auth_token/user_id set
func (h *handler) BuildDownloadMediaRequest(ctx context.Context, b courier.Backend, channel courier.Channel, attachmentURL string) (*http.Request, error) {
adminAuthToken := channel.StringConfigForKey(configAdminAuthToken, "")
adminUserID := channel.StringConfigForKey(configAdminUserID, "")
if adminAuthToken == "" || adminUserID == "" {
return nil, fmt.Errorf("missing token for RC channel")
}

req, _ := http.NewRequest(http.MethodGet, attachmentURL, nil)
req.Header.Set("X-Auth-Token", adminAuthToken)
req.Header.Set("X-User-Id", adminUserID)
return req, nil
}

type mtPayload struct {
UserURN string `json:"user"`
BotUsername string `json:"bot"`
Text string `json:"text,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
}

func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) {
baseURL := msg.Channel().StringConfigForKey(configBaseURL, "")
secret := msg.Channel().StringConfigForKey(configSecret, "")
botUsername := msg.Channel().StringConfigForKey(configBotUsername, "")

// the status that will be written for this message
status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored)

payload := &mtPayload{
UserURN: msg.URN().Path(),
BotUsername: botUsername,
Text: msg.Text(),
}
for _, attachment := range msg.Attachments() {
mimeType, url := handlers.SplitAttachment(attachment)
payload.Attachments = append(payload.Attachments, Attachment{mimeType, url})
}

body, err := json.Marshal(payload)
if err != nil {
return status, err
}

req, err := http.NewRequest(http.MethodPost, baseURL+"/message", bytes.NewReader(body))
if err != nil {
return status, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Token %s", secret))

res, err := utils.MakeHTTPRequest(req)

log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), res).WithError("Message Send Error", err)
status.AddLog(log)

if err != nil {
return status, err
}

msgID, err := jsonparser.GetString(res.Body, "id")
if err == nil {
status.SetExternalID(msgID)
}

status.SetStatus(courier.MsgSent)
return status, nil
}
148 changes: 148 additions & 0 deletions handlers/rocketchat/rocketchat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package rocketchat

import (
"github.com/nyaruka/courier"
. "github.com/nyaruka/courier/handlers"
"net/http/httptest"
"testing"
)

const (
channelUUID = "8eb23e93-5ecb-45ba-b726-3b064e0c568c"
receiveURL = "/c/rc/" + channelUUID + "/receive"
)

var testChannels = []courier.Channel{
courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "RC", "1234", "",
map[string]interface{}{
configBaseURL: "https://my.rocket.chat/api/apps/public/684202ed-1461-4983-9ea7-fde74b15026c",
configSecret: "123456789",
configBotUsername: "rocket.cat",
},
),
}

const emptyMsg = `{
"user": {
"urn": "direct:john.doe",
"username": "john.doe",
"full_name": "John Doe"
}
}`

const helloMsg = `{
"user": {
"urn": "direct:john.doe",
"username": "john.doe",
"full_name": "John Doe"
},
"text": "Hello World"
}`

const attachmentMsg = `{
"user": {
"urn": "livechat:onrMgdKbpX9Qqtvoi",
"full_name": "John Doe"
},
"attachments": [{"type": "image/jpg", "url": "https://link.to/image.jpg"}]
}`

var testCases = []ChannelHandleTestCase{
{
Label: "Receive Hello Msg",
URL: receiveURL,
Headers: map[string]string{
"Authorization": "Token 123456789",
},
Data: helloMsg,
URN: Sp("rocketchat:direct:john.doe#john.doe"),
Text: Sp("Hello World"),
Status: 200,
Response: "Accepted",
},
{
Label: "Receive Attachment Msg",
URL: receiveURL,
Headers: map[string]string{
"Authorization": "Token 123456789",
},
Data: attachmentMsg,
URN: Sp("rocketchat:livechat:onrMgdKbpX9Qqtvoi"),
Attachment: Sp("https://link.to/image.jpg"),
Status: 200,
Response: "Accepted",
},
{
Label: "Don't Receive Empty Msg",
URL: receiveURL,
Headers: map[string]string{
"Authorization": "Token 123456789",
},
Data: emptyMsg,
Status: 400,
Response: "no text or attachment",
},
{
Label: "Invalid Authorization",
URL: receiveURL,
Headers: map[string]string{
"Authorization": "123456789",
},
Data: emptyMsg,
Status: 401,
Response: "invalid Authorization header",
},
}

func TestHandler(t *testing.T) {
RunChannelTestCases(t, testChannels, newHandler(), testCases)
}

func BenchmarkHandler(b *testing.B) {
RunChannelBenchmarks(b, testChannels, newHandler(), testCases)
}

func setSendURL(s *httptest.Server, h courier.ChannelHandler, c courier.Channel, m courier.Msg) {
c.(*courier.MockChannel).SetConfig(configBaseURL, s.URL)
}

var sendTestCases = []ChannelSendTestCase{
{
Label: "Plain Send",
Text: "Simple Message",
URN: "rocketchat:direct:john.doe#john.doe",
Status: "S",
RequestBody: `{"user":"direct:john.doe","bot":"rocket.cat","text":"Simple Message"}`,
ResponseStatus: 201,
ResponseBody: `{"id":"iNKE8a6k6cjbqWhWd"}`,
ExternalID: "iNKE8a6k6cjbqWhWd",
SendPrep: setSendURL,
},
{
Label: "Send Attachment",
URN: "rocketchat:livechat:onrMgdKbpX9Qqtvoi",
Attachments: []string{"application/pdf:https://link.to/attachment.pdf"},
Status: "S",
RequestBody: `{"user":"livechat:onrMgdKbpX9Qqtvoi","bot":"rocket.cat","attachments":[{"type":"application/pdf","url":"https://link.to/attachment.pdf"}]}`,
ResponseStatus: 201,
ResponseBody: `{"id":"iNKE8a6k6cjbqWhWd"}`,
ExternalID: "iNKE8a6k6cjbqWhWd",
SendPrep: setSendURL,
},
{
Label: "Send Text And Attachment",
URN: "rocketchat:direct:john.doe",
Text: "Simple Message",
Attachments: []string{"application/pdf:https://link.to/attachment.pdf"},
Status: "S",
RequestBody: `{"user":"direct:john.doe","bot":"rocket.cat","text":"Simple Message","attachments":[{"type":"application/pdf","url":"https://link.to/attachment.pdf"}]}`,
ResponseStatus: 201,
ResponseBody: `{"id":"iNKE8a6k6cjbqWhWd"}`,
ExternalID: "iNKE8a6k6cjbqWhWd",
SendPrep: setSendURL,
},
}

func TestSending(t *testing.T) {
RunChannelSendTestCases(t, testChannels[0], newHandler(), sendTestCases, nil)
}

0 comments on commit 98db720

Please sign in to comment.