forked from rapidpro/mailroom
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rapidpro#336 from Ilhasoft/rocketchat-handler
Rocketchat handler
- Loading branch information
Showing
3 changed files
with
307 additions
and
0 deletions.
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
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 | ||
} |
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,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) | ||
} |