-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
366 additions
and
1 deletion.
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 |
---|---|---|
@@ -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 | ||
|
||
} |
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,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) | ||
} |