diff --git a/.dockerignore b/.dockerignore index 71883d86..2d7dda8c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ .idea/ node_modules/ +coverage/ npm-debug.log diff --git a/.gitignore b/.gitignore index 9303c347..c3c859c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ +coverage/ npm-debug.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3ab6aecd..d9e9dc57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:5.10 +FROM node:6 MAINTAINER xVir diff --git a/app.json b/app.json index deeabc2e..765299c7 100644 --- a/app.json +++ b/app.json @@ -24,6 +24,6 @@ } }, "engines": { - "node": "5.10.1" + "node": "6.9.5" } } diff --git a/package.json b/package.json index 9561e891..9cbf9cf5 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,32 @@ { "name": "api-ai-facebook", - "version": "1.0.0", + "version": "1.1.0", "description": "API.AI Facebook Bot", "main": "src/app.js", "scripts": { "start": "node src/app.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "mocha", + "coverage": "istanbul cover _mocha" }, "author": "Danil Skachkov (https://api.ai)", "license": "ISC", "dependencies": { - "apiai": "2.0.5", + "apiai": "^3.0.3", "async": "^2.0.0", "body-parser": "^1.15.2", "express": "^4.13.3", "html-entities": "^1.2.0", "json-bigint": "^0.2.0", - "node-uuid": "^1.4.7", - "request": "^2.73.0" + "request": "^2.73.0", + "uuid": "^3.0.1" }, "repository": { "type": "git", "url": "https://github.com/api-ai/api-ai-facebook" + }, + "devDependencies": { + "rewire": "^2.5.2", + "should": "^11.2.0", + "supertest": "^3.0.0" } } diff --git a/src/app.js b/src/app.js index a6c5b9ec..82050440 100644 --- a/src/app.js +++ b/src/app.js @@ -3,7 +3,7 @@ const apiai = require('apiai'); const express = require('express'); const bodyParser = require('body-parser'); -const uuid = require('node-uuid'); +const uuid = require('uuid'); const request = require('request'); const JSONbig = require('json-bigint'); const async = require('async'); @@ -13,194 +13,486 @@ const APIAI_ACCESS_TOKEN = process.env.APIAI_ACCESS_TOKEN; const APIAI_LANG = process.env.APIAI_LANG || 'en'; const FB_VERIFY_TOKEN = process.env.FB_VERIFY_TOKEN; const FB_PAGE_ACCESS_TOKEN = process.env.FB_PAGE_ACCESS_TOKEN; +const FB_TEXT_LIMIT = 640; -const apiAiService = apiai(APIAI_ACCESS_TOKEN, {language: APIAI_LANG, requestSource: "fb"}); -const sessionIds = new Map(); +const FACEBOOK_LOCATION = "FACEBOOK_LOCATION"; +const FACEBOOK_WELCOME = "FACEBOOK_WELCOME"; -function processEvent(event) { - var sender = event.sender.id.toString(); +class FacebookBot { + constructor() { + this.apiAiService = apiai(APIAI_ACCESS_TOKEN, {language: APIAI_LANG, requestSource: "fb"}); + this.sessionIds = new Map(); + this.messagesDelay = 200; + } - if ((event.message && event.message.text) || (event.postback && event.postback.payload)) { - var text = event.message ? event.message.text : event.postback.payload; - // Handle a text message from this sender - if (!sessionIds.has(sender)) { - sessionIds.set(sender, uuid.v1()); + doDataResponse(sender, facebookResponseData) { + if (!Array.isArray(facebookResponseData)) { + console.log('Response as formatted message'); + this.sendFBMessage(sender, facebookResponseData) + .catch(err => console.error(err)); + } else { + async.eachSeries(facebookResponseData, (facebookMessage, callback) => { + if (facebookMessage.sender_action) { + console.log('Response as sender action'); + this.sendFBSenderAction(sender, facebookMessage.sender_action) + .then(() => callback()) + .catch(err => callback(err)); + } + else { + console.log('Response as formatted message'); + this.sendFBMessage(sender, facebookMessage) + .then(() => callback()) + .catch(err => callback(err)); + } + }, (err) => { + if (err) { + console.error(err); + } else { + console.log('Data response completed'); + } + }); } + } - console.log("Text", text); + doRichContentResponse(sender, messages) { + let facebookMessages = []; // array with result messages - let apiaiRequest = apiAiService.textRequest(text, - { - sessionId: sessionIds.get(sender) - }); + for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) { + let message = messages[messageIndex]; - apiaiRequest.on('response', (response) => { - if (isDefined(response.result)) { - let responseText = response.result.fulfillment.speech; - let responseData = response.result.fulfillment.data; - let action = response.result.action; - - if (isDefined(responseData) && isDefined(responseData.facebook)) { - if (!Array.isArray(responseData.facebook)) { - try { - console.log('Response as formatted message'); - sendFBMessage(sender, responseData.facebook); - } catch (err) { - sendFBMessage(sender, {text: err.message}); + switch (message.type) { + //message.type 0 means text message + case 0: + // speech: ["hi"] + // we have to get value from fulfillment.speech, because of here is raw speech + if (message.speech) { + + let splittedText = this.splitResponse(message.speech); + + splittedText.forEach(s => { + facebookMessages.push({text: s}); + }); + } + + break; + //message.type 1 means card message + case 1: { + let carousel = [message]; + + for (messageIndex++; messageIndex < messages.length; messageIndex++) { + if (messages[messageIndex].type == 1) { + carousel.push(messages[messageIndex]); + } else { + messageIndex--; + break; } - } else { - responseData.facebook.forEach((facebookMessage) => { - try { - if (facebookMessage.sender_action) { - console.log('Response as sender action'); - sendFBSenderAction(sender, facebookMessage.sender_action); - } - else { - console.log('Response as formatted message'); - sendFBMessage(sender, facebookMessage); + } + + let facebookMessage = {}; + carousel.forEach((c) => { + // buttons: [ {text: "hi", postback: "postback"} ], imageUrl: "", title: "", subtitle: "" + + let card = {}; + + card.title = c.title; + card.image_url = c.imageUrl; + if (this.isDefined(c.subtitle)) { + card.subtitle = c.subtitle; + } + //If button is involved in. + if (c.buttons.length > 0) { + let buttons = []; + for (let buttonIndex = 0; buttonIndex < c.buttons.length; buttonIndex++) { + let button = c.buttons[buttonIndex]; + + if (button.text) { + let postback = button.postback; + if (!postback) { + postback = button.text; + } + + let buttonDescription = { + title: button.text + }; + + if (postback.startsWith("http")) { + buttonDescription.type = "web_url"; + buttonDescription.url = postback; + } else { + buttonDescription.type = "postback"; + buttonDescription.payload = postback; + } + + buttons.push(buttonDescription); } - } catch (err) { - sendFBMessage(sender, {text: err.message}); } + + if (buttons.length > 0) { + card.buttons = buttons; + } + } + + if (!facebookMessage.attachment) { + facebookMessage.attachment = {type: "template"}; + } + + if (!facebookMessage.attachment.payload) { + facebookMessage.attachment.payload = {template_type: "generic", elements: []}; + } + + facebookMessage.attachment.payload.elements.push(card); + }); + + facebookMessages.push(facebookMessage); + } + + break; + //message.type 2 means quick replies message + case 2: { + if (message.replies && message.replies.length > 0) { + let facebookMessage = {}; + + facebookMessage.text = message.title ? message.title : 'Choose an item'; + facebookMessage.quick_replies = []; + + message.replies.forEach((r) => { + facebookMessage.quick_replies.push({ + content_type: "text", + title: r, + payload: r + }); }); + + facebookMessages.push(facebookMessage); } - } else if (isDefined(responseText)) { - console.log('Response as text message'); - // facebook API limit for text length is 320, - // so we must split message if needed - var splittedText = splitResponse(responseText); - - async.eachSeries(splittedText, (textPart, callback) => { - sendFBMessage(sender, {text: textPart}, callback); - }); } + break; + //message.type 3 means image message + case 3: + + if (message.imageUrl) { + let facebookMessage = {}; + + // "imageUrl": "http://example.com/image.jpg" + facebookMessage.attachment = {type: "image"}; + facebookMessage.attachment.payload = {url: message.imageUrl}; + + facebookMessages.push(facebookMessage); + } + + break; + //message.type 4 means custom payload message + case 4: + if (message.payload && message.payload.facebook) { + facebookMessages.push(message.payload.facebook); + } + break; + + default: + break; } - }); + } - apiaiRequest.on('error', (error) => console.error(error)); - apiaiRequest.end(); - } -} + return new Promise((resolve, reject) => { + async.eachSeries(facebookMessages, (msg, callback) => { + this.sendFBSenderAction(sender, "typing_on") + .then(() => this.sleep(this.messagesDelay)) + .then(() => this.sendFBMessage(sender, msg)) + .then(() => callback()) + .catch(callback); + }, + (err) => { + if (err) { + console.error(err); + reject(err); + } else { + console.log('Messages sent'); + resolve(); + } + }); + }); -function splitResponse(str) { - if (str.length <= 320) { - return [str]; } - return chunkString(str, 300); -} + doTextResponse(sender, responseText) { + console.log('Response as text message'); + // facebook API limit for text length is 640, + // so we must split message if needed + let splittedText = this.splitResponse(responseText); -function chunkString(s, len) { - var curr = len, prev = 0; + async.eachSeries(splittedText, (textPart, callback) => { + this.sendFBMessage(sender, {text: textPart}) + .then(() => callback()) + .catch(err => callback(err)); + }); + } - var output = []; + //which webhook event + getEventText(event) { + if (event.message) { + if (event.message.quick_reply && event.message.quick_reply.payload) { + return event.message.quick_reply.payload; + } - while (s[curr]) { - if (s[curr++] == ' ') { - output.push(s.substring(prev, curr)); - prev = curr; - curr += len; + if (event.message.text) { + return event.message.text; + } } - else { - var currReverse = curr; - do { - if (s.substring(currReverse - 1, currReverse) == ' ') { - output.push(s.substring(prev, currReverse)); - prev = currReverse; - curr = currReverse + len; - break; - } - currReverse--; - } while (currReverse > prev) + + if (event.postback && event.postback.payload) { + return event.postback.payload; } + + return null; + } - output.push(s.substr(prev)); - return output; -} -function sendFBMessage(sender, messageData, callback) { - request({ - url: 'https://graph.facebook.com/v2.6/me/messages', - qs: {access_token: FB_PAGE_ACCESS_TOKEN}, - method: 'POST', - json: { - recipient: {id: sender}, - message: messageData + getFacebookEvent(event) { + if (event.postback && event.postback.payload) { + + let payload = event.postback.payload; + + switch (payload) { + case FACEBOOK_WELCOME: + return {name: FACEBOOK_WELCOME}; + + case FACEBOOK_LOCATION: + return {name: FACEBOOK_LOCATION, data: event.postback.data} + } } - }, (error, response, body) => { - if (error) { - console.log('Error sending message: ', error); - } else if (response.body.error) { - console.log('Error: ', response.body.error); + + return null; + } + + processFacebookEvent(event) { + const sender = event.sender.id.toString(); + const eventObject = this.getFacebookEvent(event); + + if (eventObject) { + + // Handle a text message from this sender + if (!this.sessionIds.has(sender)) { + this.sessionIds.set(sender, uuid.v4()); + } + + let apiaiRequest = this.apiAiService.eventRequest(eventObject, + { + sessionId: this.sessionIds.get(sender), + originalRequest: { + data: event, + source: "facebook" + } + }); + this.doApiAiRequest(apiaiRequest, sender); } + } + + processMessageEvent(event) { + const sender = event.sender.id.toString(); + const text = this.getEventText(event); - if (callback) { - callback(); + if (text) { + + // Handle a text message from this sender + if (!this.sessionIds.has(sender)) { + this.sessionIds.set(sender, uuid.v4()); + } + + console.log("Text", text); + //send user's text to api.ai service + let apiaiRequest = this.apiAiService.textRequest(text, + { + sessionId: this.sessionIds.get(sender), + originalRequest: { + data: event, + source: "facebook" + } + }); + + this.doApiAiRequest(apiaiRequest, sender); } - }); -} + } + + doApiAiRequest(apiaiRequest, sender) { + apiaiRequest.on('response', (response) => { + if (this.isDefined(response.result) && this.isDefined(response.result.fulfillment)) { + let responseText = response.result.fulfillment.speech; + let responseData = response.result.fulfillment.data; + let responseMessages = response.result.fulfillment.messages; + + if (this.isDefined(responseData) && this.isDefined(responseData.facebook)) { + let facebookResponseData = responseData.facebook; + this.doDataResponse(sender, facebookResponseData); + } else if (this.isDefined(responseMessages) && responseMessages.length > 0) { + this.doRichContentResponse(sender, responseMessages); + } + else if (this.isDefined(responseText)) { + this.doTextResponse(sender, responseText); + } -function sendFBSenderAction(sender, action, callback) { - setTimeout(() => { - request({ - url: 'https://graph.facebook.com/v2.6/me/messages', - qs: {access_token: FB_PAGE_ACCESS_TOKEN}, - method: 'POST', - json: { - recipient: {id: sender}, - sender_action: action } - }, (error, response, body) => { - if (error) { - console.log('Error sending action: ', error); - } else if (response.body.error) { - console.log('Error: ', response.body.error); + }); + + apiaiRequest.on('error', (error) => console.error(error)); + apiaiRequest.end(); + } + + splitResponse(str) { + if (str.length <= FB_TEXT_LIMIT) { + return [str]; + } + + return this.chunkString(str, FB_TEXT_LIMIT); + } + + chunkString(s, len) { + let curr = len, prev = 0; + + let output = []; + + while (s[curr]) { + if (s[curr++] == ' ') { + output.push(s.substring(prev, curr)); + prev = curr; + curr += len; } - if (callback) { - callback(); + else { + let currReverse = curr; + do { + if (s.substring(currReverse - 1, currReverse) == ' ') { + output.push(s.substring(prev, currReverse)); + prev = currReverse; + curr = currReverse + len; + break; + } + currReverse--; + } while (currReverse > prev) } + } + output.push(s.substr(prev)); + return output; + } + + sendFBMessage(sender, messageData) { + return new Promise((resolve, reject) => { + request({ + url: 'https://graph.facebook.com/v2.6/me/messages', + qs: {access_token: FB_PAGE_ACCESS_TOKEN}, + method: 'POST', + json: { + recipient: {id: sender}, + message: messageData + } + }, (error, response) => { + if (error) { + console.log('Error sending message: ', error); + reject(error); + } else if (response.body.error) { + console.log('Error: ', response.body.error); + reject(new Error(response.body.error)); + } + + resolve(); + }); }); - }, 1000); -} + } -function doSubscribeRequest() { - request({ - method: 'POST', - uri: "https://graph.facebook.com/v2.6/me/subscribed_apps?access_token=" + FB_PAGE_ACCESS_TOKEN - }, - (error, response, body) => { - if (error) { - console.error('Error while subscription: ', error); - } else { - console.log('Subscription result: ', response.body); - } + sendFBSenderAction(sender, action) { + return new Promise((resolve, reject) => { + request({ + url: 'https://graph.facebook.com/v2.6/me/messages', + qs: {access_token: FB_PAGE_ACCESS_TOKEN}, + method: 'POST', + json: { + recipient: {id: sender}, + sender_action: action + } + }, (error, response) => { + if (error) { + console.error('Error sending action: ', error); + reject(error); + } else if (response.body.error) { + console.error('Error: ', response.body.error); + reject(new Error(response.body.error)); + } + + resolve(); + }); }); -} + } -function isDefined(obj) { - if (typeof obj == 'undefined') { - return false; + doSubscribeRequest() { + request({ + method: 'POST', + uri: `https://graph.facebook.com/v2.6/me/subscribed_apps?access_token=${FB_PAGE_ACCESS_TOKEN}` + }, + (error, response, body) => { + if (error) { + console.error('Error while subscription: ', error); + } else { + console.log('Subscription result: ', response.body); + } + }); } - if (!obj) { - return false; + configureGetStartedEvent() { + request({ + method: 'POST', + uri: `https://graph.facebook.com/v2.6/me/thread_settings?access_token=${FB_PAGE_ACCESS_TOKEN}`, + json: { + setting_type: "call_to_actions", + thread_state: "new_thread", + call_to_actions: [ + { + payload: FACEBOOK_WELCOME + } + ] + } + }, + (error, response, body) => { + if (error) { + console.error('Error while subscription', error); + } else { + console.log('Subscription result', response.body); + } + }); + } + + isDefined(obj) { + if (typeof obj == 'undefined') { + return false; + } + + if (!obj) { + return false; + } + + return obj != null; + } + + sleep(delay) { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), delay); + }); } - return obj != null; } + +let facebookBot = new FacebookBot(); + const app = express(); app.use(bodyParser.text({type: 'application/json'})); app.get('/webhook/', (req, res) => { - if (req.query['hub.verify_token'] == FB_VERIFY_TOKEN) { + if (req.query['hub.verify_token'] === FB_VERIFY_TOKEN) { res.send(req.query['hub.challenge']); setTimeout(() => { - doSubscribeRequest(); + facebookBot.doSubscribeRequest(); }, 3000); } else { res.send('Error, wrong validation token'); @@ -209,7 +501,7 @@ app.get('/webhook/', (req, res) => { app.post('/webhook/', (req, res) => { try { - var data = JSONbig.parse(req.body); + const data = JSONbig.parse(req.body); if (data.entry) { let entries = data.entry; @@ -217,9 +509,36 @@ app.post('/webhook/', (req, res) => { let messaging_events = entry.messaging; if (messaging_events) { messaging_events.forEach((event) => { - if (event.message && !event.message.is_echo || - event.postback && event.postback.payload) { - processEvent(event); + if (event.message && !event.message.is_echo) { + + if (event.message.attachments) { + let locations = event.message.attachments.filter(a => a.type === "location"); + + // delete all locations from original message + event.message.attachments = event.message.attachments.filter(a => a.type !== "location"); + + if (locations.length > 0) { + locations.forEach(l => { + let locationEvent = { + sender: event.sender, + postback: { + payload: "FACEBOOK_LOCATION", + data: l.payload.coordinates + } + }; + + facebookBot.processFacebookEvent(locationEvent); + }); + } + } + + facebookBot.processMessageEvent(event); + } else if (event.postback && event.postback.payload) { + if (event.postback.payload === "FACEBOOK_WELCOME") { + facebookBot.processFacebookEvent(event); + } else { + facebookBot.processMessageEvent(event); + } } }); } @@ -242,4 +561,4 @@ app.listen(REST_PORT, () => { console.log('Rest service ready on port ' + REST_PORT); }); -doSubscribeRequest(); +facebookBot.doSubscribeRequest(); diff --git a/test/app_test.js b/test/app_test.js new file mode 100644 index 00000000..0b299483 --- /dev/null +++ b/test/app_test.js @@ -0,0 +1,337 @@ +'use strict'; + +const APIAI_ACCESS_TOKEN = 'api_ai_access_token'; +const FB_VERIFY_TOKEN = 'fb_verify_token'; +const FB_PAGE_ACCESS_TOKEN = 'fb_access_token'; +const APIAI_LANG = 'en'; + +process.env['APIAI_ACCESS_TOKEN'] = APIAI_ACCESS_TOKEN; +process.env['FB_VERIFY_TOKEN'] = FB_VERIFY_TOKEN; +process.env['FB_PAGE_ACCESS_TOKEN'] = FB_PAGE_ACCESS_TOKEN; +process.env['APIAI_LANG'] = APIAI_LANG; + +const supertest = require('supertest'); +const should = require('should'); +const rewire = require('rewire'); +const app = rewire('../src/app'); + +const server = supertest.agent('http://localhost:5000'); + +describe('app', () => { + + beforeEach((done) => { + done(); + }); + + it('should pass verification', function (done) { + + const challengeValue = 'secret'; + const verifyToken = FB_VERIFY_TOKEN; + + server + .get(`/webhook?hub.verify_token=${verifyToken}&hub.challenge=${challengeValue}`) + .set('Accept', 'text/plain') + .expect(200, challengeValue) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it('should fail verification if wrong params', function (done) { + + const challengeValue = 'secret'; + const verifyToken = 'wrongtoken'; + + server + .get(`/webhook?hub.verify_token=${verifyToken}&hub.challenge=${challengeValue}`) + .set('Accept', 'text/plain') + .expect(200, "Error, wrong validation token") + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it("/webhook should response", function (done) { + + let fbBot = app.__get__('facebookBot'); + fbBot.processEvent = ()=>{}; + + // https://developers.facebook.com/docs/messenger-platform/webhook-reference#common_format + server + .post('/webhook') + .set('Accept', 'application/json') + .send({ + entry: [ + { + messaging: [ + { + sender: { + id: "99102094378730843167656799" + }, + message: { + text: "hello" + } + } + ] + } + ] + }) + .expect(200, {status: "ok"}) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it("/webhook should response on postback", function (done) { + + let fbBot = app.__get__('facebookBot'); + fbBot.processEvent = ()=>{}; + + server + .post('/webhook') + .set('Accept', 'application/json') + .send({ + entry: [ + { + messaging: [ + { + sender: { + id: "99102094378730843167656799" + }, + postback: { + payload: "hello" + } + } + ] + } + ] + }) + .expect(200, {status: "ok"}) + .end((err, res) => { + if (err) return done(err); + done(); + }); + }); + + it("doRichContentResponse should process all rich content types", (done) => { + let textMessage = {type: 0, speech: 'sample speech'}; + let wrongTextMessage = {type: 0}; + + let fbTextMessage = {text: 'sample speech'}; + + let cardMessage1 = { + "title": "Kitten", + "subtitle": "Cat", + "imageUrl": "https://example.com/cat.jpg", + "buttons": [ + { + "text": "Buy", + "postback": "buy" + } + ], + "type": 1 + }; + + let cardMessage2 = { + "title": "Gnome", + "imageUrl": "https://example.com/gnome.png", + "buttons": [ + { + "text": "Info", + "postback": "info" + }, + { + "text": "Gnome info", + "postback": "https://example.com/gnome" + } + ], + "type": 1 + }; + + let fbCardsMessage = { + attachment: { + type: "template", + payload: { + template_type: "generic", + elements: [ + { + title: 'Kitten', + image_url: 'https://example.com/cat.jpg', + subtitle: 'Cat', + buttons: [{type: 'postback', title: 'Buy', payload: 'buy'}] + }, + { + title: 'Gnome', + image_url: 'https://example.com/gnome.png', + buttons: [ + {type: 'postback', title: 'Info', payload: 'info'}, + {type: 'web_url', title: 'Gnome info', url: 'https://example.com/gnome'} + ] + } + ] + } + } + }; + + let repliesMessage = {type: 2, title: 'Replies title', replies: ['first', 'second']}; + let fbRepliesMessage = {text: 'Replies title', + quick_replies: [ + { + content_type: 'text', + title: 'first', + payload: 'first' + }, + { + content_type: 'text', + title: 'second', + payload: 'second' + }] + }; + + let repliesMessage2 = {type: 2, replies: ['without title']}; + let fbRepliesMessage2 = { + text: 'Choose an item', + quick_replies: [ + { + content_type: 'text', + title: 'without title', + payload: 'without title' + } + ] + }; + + let wrongRepliesMessage = {type: 2}; + + let imageMessage = {type: 3, imageUrl: 'https://example.com/1.png'}; + let fbImageMessage = {attachment: {type: 'image', payload: {url: 'https://example.com/1.png'}}}; + + let wrongImageMessage = {type: 3}; + + let payloadMessage = {type:4, payload: {facebook: {text: 'some facebook payload'}}}; + let fbPayloadMessage = {text: 'some facebook payload'}; + + let wrongPayloadMessage = {type: 4}; + + let unknownType = {type: 10, someData: {}}; + + let cardMessage3 = { + "title": "Kitten", + "imageUrl": "https://example.com/cat.jpg", + "buttons": [ + { + "text": "More Info" + } + ], + "type": 1 + }; + let fbCardMessage2 = { + attachment: { + type: "template", + payload: { + template_type: "generic", + elements: [ + { + title: 'Kitten', + image_url: 'https://example.com/cat.jpg', + buttons: [{type: 'postback', title: 'More Info', payload: 'More Info'}] + } + ] + } + } + }; + + let expectedMessages = [fbTextMessage, fbCardsMessage, fbRepliesMessage, fbRepliesMessage2, fbImageMessage, fbPayloadMessage, fbCardMessage2]; + let counter = 0; + let resultMessages = []; + + let fbBot = app.__get__('facebookBot'); + + fbBot.messagesDelay = 10; + + fbBot.sendFBSenderAction = (sender, action) => { + return Promise.resolve(); + }; + + fbBot.sendFBMessage = (sender, messageData) => { + counter += 1; + resultMessages.push(messageData); + return Promise.resolve(); + }; + + + fbBot.doRichContentResponse('senderId', [ + textMessage, + wrongTextMessage, + cardMessage1, + cardMessage2, + repliesMessage, + repliesMessage2, + wrongRepliesMessage, + unknownType, + imageMessage, + wrongImageMessage, + payloadMessage, + wrongPayloadMessage, + cardMessage3 + ]) + .then(() => { + resultMessages.should.deepEqual(expectedMessages); + done(); + }) + .catch(done); + }); + + it("chunkString should split string to parts", () => { + + let fbBot = app.__get__('facebookBot'); + let chunkString = fbBot.chunkString; + + let stringToSplit = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; + let expected = ['Lorem ipsum ', 'dolor sit amet, ', 'consectetur ', 'adipiscing ', 'elit.']; + + let result = chunkString(stringToSplit, 15); + result.forEach(s => s.length.should.lessThan(17)); + + result.should.deepEqual(expected); + }); + + it("getEventText should get right text", () => { + + const fbBot = app.__get__('facebookBot'); + const getEventText = fbBot.getEventText; + + should(getEventText({})).be.equal(null); + should(getEventText({message: {}})).be.equal(null); + + const expectedText = 'expectedText'; + + const simpleMessage = { + "message": { + "text": expectedText, + } + }; + getEventText(simpleMessage).should.be.equal(expectedText); + + const postbackMessage = { + "postback": { + "payload": expectedText + } + }; + getEventText(postbackMessage).should.be.equal(expectedText); + + const quickReplyMessage = { + "message": { + "text": "Red", + "quick_reply": { + "payload": expectedText + } + } + }; + getEventText(quickReplyMessage).should.be.equal(expectedText); + + }); + +}); \ No newline at end of file