Skip to content

Commit

Permalink
Merge 7f58581 into f68e4f2
Browse files Browse the repository at this point in the history
  • Loading branch information
norkans7 committed Feb 15, 2018
2 parents f68e4f2 + 7f58581 commit 8468a29
Show file tree
Hide file tree
Showing 3 changed files with 366 additions and 1 deletion.
1 change: 1 addition & 0 deletions cmd/courier/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
_ "github.com/nyaruka/courier/handlers/jasmin"
_ "github.com/nyaruka/courier/handlers/jiochat"
_ "github.com/nyaruka/courier/handlers/kannel"
_ "github.com/nyaruka/courier/handlers/line"
_ "github.com/nyaruka/courier/handlers/m3tech"
_ "github.com/nyaruka/courier/handlers/mtarget"
_ "github.com/nyaruka/courier/handlers/nexmo"
Expand Down
180 changes: 179 additions & 1 deletion handlers/line/line.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,181 @@
package line

/* no logs */
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/nyaruka/courier/utils"
"github.com/nyaruka/gocommon/urns"
"net/http"
"time"

"github.com/nyaruka/courier"
"github.com/nyaruka/courier/handlers"
)

var sendURL = "https://api.line.me/v2/bot/message/push"
var maxMsgLength = 2000

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

type handler struct {
handlers.BaseHandler
}

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

// Initialize is called by the engine once everything is loaded
func (h *handler) Initialize(s courier.Server) error {
h.SetServer(s)
return s.AddHandlerRoute(h, http.MethodPost, "receive", h.ReceiveMessage)
}

// {
// "events": [
// {
// "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
// "type": "message",
// "timestamp": 1462629479859,
// "source": {
// "type": "user",
// "userId": "U4af4980629..."
// },
// "message": {
// "id": "325708",
// "type": "text",
// "text": "Hello, world"
// }
// },
// {
// "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
// "type": "follow",
// "timestamp": 1462629479859,
// "source": {
// "type": "user",
// "userId": "U4af4980629..."
// }
// }
// ]
// }
type moMsg struct {
Events []struct {
Type string `json:"type"`
Timestamp int64 `json:"timestamp"`
Source struct {
Type string `json:"type"`
UserID string `json:"userId"`
} `json:"source"`
Message struct {
ID string `json:"id"`
Type string `json:"type"`
Text string `json:"text"`
} `json:"message"`
} `json:"events"`
}

// 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) {
lineRequest := &moMsg{}
err := handlers.DecodeAndValidateJSON(lineRequest, r)
if err != nil {
return nil, courier.WriteAndLogRequestError(ctx, w, r, channel, err)
}

msgs := []courier.Msg{}
events := []courier.Event{}

for _, lineEvent := range lineRequest.Events {
if (lineEvent.Source.Type == "" && lineEvent.Source.UserID == "") || (lineEvent.Message.Type == "" && lineEvent.Message.ID == "" && lineEvent.Message.Text == "") || lineEvent.Message.Type != "text" {

continue
}

// create our date from the timestamp (they give us millis, arg is nanos)
date := time.Unix(0, lineEvent.Timestamp*1000000).UTC()

urn := urns.NewURNFromParts(urns.LineScheme, lineEvent.Source.UserID, "")

msg := h.Backend().NewIncomingMsg(channel, urn, lineEvent.Message.Text).WithReceivedOn(date)

// and write it
err = h.Backend().WriteMsg(ctx, msg)
if err != nil {
return nil, err
}
msgs = append(msgs, msg)
events = append(events, msg)
}

if len(msgs) == 0 {
if len(lineRequest.Events) > 0 {
return nil, courier.WriteAndLogRequestIgnored(ctx, w, r, channel, "ignoring request, no message")
}
return nil, courier.WriteAndLogRequestError(ctx, w, r, channel, fmt.Errorf("missing message, source or type in the event"))

}

return events, courier.WriteMsgSuccess(ctx, w, r, msgs)

}

type mtMsg struct {
Type string `json:"type"`
Text string `json:"text"`
}

type mtEnvelop struct {
To string `json:"to"`
Messages []mtMsg `json:"messages"`
}

// SendMsg sends the passed in message, returning any error
func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) {
authToken := msg.Channel().StringConfigForKey(courier.ConfigAuthToken, "")
if authToken == "" {
return nil, fmt.Errorf("no auth token set for LN channel: %s", msg.Channel().UUID())
}

status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored)
parts := handlers.SplitMsg(handlers.GetTextAndAttachments(msg), maxMsgLength)
for _, part := range parts {
lineEnvelop := mtEnvelop{
To: msg.URN().Path(),
Messages: []mtMsg{
mtMsg{
Type: "text",
Text: part,
},
},
}

requestBody := &bytes.Buffer{}
json.NewEncoder(requestBody).Encode(lineEnvelop)

// build our request
req, err := http.NewRequest(http.MethodPost, sendURL, requestBody)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken))
if err != nil {
return nil, err
}

rr, err := utils.MakeHTTPRequest(req)
// record our status and log
log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), rr).WithError("Message Send Error", err)
status.AddLog(log)

if err != nil {
return status, err
}
status.SetStatus(courier.MsgWired)
}

return status, nil

}
186 changes: 186 additions & 0 deletions handlers/line/line_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package line

import (
"net/http/httptest"
"testing"
"time"

"github.com/nyaruka/courier"
. "github.com/nyaruka/courier/handlers"
)

var (
receiveURL = "/c/ln/8eb23e93-5ecb-45ba-b726-3b064e0c56ab/receive"
)

var receiveValidMessage = `
{
"events": [{
"replyToken": "abcdefghij",
"type": "message",
"timestamp": 1459991487970,
"source": {
"type": "user",
"userId": "uabcdefghij"
},
"message": {
"id": "100001",
"type": "text",
"text": "Hello, world"
}
}, {
"replyToken": "abcdefghijklm",
"type": "message",
"timestamp": 1459991487970,
"source": {
"type": "user",
"userId": "uabcdefghij"
},
"message": {
"id": "100002",
"type": "sticker",
"packageId": "1",
"stickerId": "1"
}
}]
}`

var receiveValidMessageLast = `
{
"events": [{
"replyToken": "abcdefghijklm",
"type": "message",
"timestamp": 1459991487970,
"source": {
"type": "user",
"userId": "uabcdefghij"
},
"message": {
"id": "100002",
"type": "sticker",
"packageId": "1",
"stickerId": "1"
}
}, {
"replyToken": "abcdefghij",
"type": "message",
"timestamp": 1459991487970,
"source": {
"type": "user",
"userId": "uabcdefghij"
},
"message": {
"id": "100001",
"type": "text",
"text": "Last event"
}
}]
}`

var missingMessage = `{
"events": [{
"replyToken": "abcdefghij",
"type": "message",
"timestamp": 1451617200000,
"source": {
"type": "user",
"userId": "uabcdefghij"
}
}]
}`

var noEvent = `{
"events": []
}`

var testChannels = []courier.Channel{
courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "LN", "2020", "US", nil),
}

var handleTestCases = []ChannelHandleTestCase{
{Label: "Receive Valid Message", URL: receiveURL, Data: receiveValidMessage, Status: 200, Response: "Accepted",
Text: Sp("Hello, world"), URN: Sp("line:uabcdefghij"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC))},
{Label: "Receive Valid Message", URL: receiveURL, Data: receiveValidMessageLast, Status: 200, Response: "Accepted",
Text: Sp("Last event"), URN: Sp("line:uabcdefghij"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC))},
{Label: "Missing message", URL: receiveURL, Data: missingMessage, Status: 200, Response: "ignoring request, no message"},
{Label: "No event request", URL: receiveURL, Data: noEvent, Status: 400, Response: "missing message, source or type in the event"},
}

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: "line:uabcdefghij",
Status: "W",
ResponseBody: `{}`, ResponseStatus: 200,
Headers: map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer AccessToken",
},
RequestBody: `{"to":"uabcdefghij","messages":[{"type":"text","text":"Simple Message"}]}`,
SendPrep: setSendURL},
{Label: "Unicode Send",
Text: "Simple Message ☺", URN: "line:uabcdefghij",
Status: "W",
ResponseBody: `{}`, ResponseStatus: 200,
Headers: map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer AccessToken",
},
RequestBody: `{"to":"uabcdefghij","messages":[{"type":"text","text":"Simple Message ☺"}]}`,
SendPrep: setSendURL},
{Label: "Long Send",
Text: "This is a longer message than 160 characters and will cause us to split it into two separate parts, isn't that right but it is even longer than before I say, I need to keep adding more things to make it work",
URN: "line:uabcdefghij",
Status: "W",
ResponseBody: `{}`, ResponseStatus: 200,
Headers: map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer AccessToken",
},
RequestBody: `{"to":"uabcdefghij","messages":[{"type":"text","text":"I need to keep adding more things to make it work"}]}`,
SendPrep: setSendURL},
{Label: "Send Attachment",
Text: "My pic!", URN: "line:uabcdefghij", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"},
Status: "W",
ResponseBody: `{}`, ResponseStatus: 200,
Headers: map[string]string{
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer AccessToken",
},
RequestBody: `{"to":"uabcdefghij","messages":[{"type":"text","text":"My pic!\nhttps://foo.bar/image.jpg"}]}`,
SendPrep: setSendURL},
{Label: "Error Sending",
Text: "Error Sending", URN: "line:uabcdefghij",
Status: "E",
ResponseBody: `{"message": "Error"}`, ResponseStatus: 403,
RequestBody: `{"to":"uabcdefghij","messages":[{"type":"text","text":"Error Sending"}]}`,
Error: "received non 200 status: 403",
SendPrep: setSendURL},
}

func TestSending(t *testing.T) {
maxMsgLength = 160
var defaultChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "LN", "2020", "US",
map[string]interface{}{
"auth_token": "AccessToken",
},
)

RunChannelSendTestCases(t, defaultChannel, newHandler(), defaultSendTestCases, nil)
}

0 comments on commit 8468a29

Please sign in to comment.