diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..71883d86 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.idea/ +node_modules/ +npm-debug.log diff --git a/.gitignore b/.gitignore index c2658d7d..9303c347 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +npm-debug.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d9e9dc57 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:6 + +MAINTAINER xVir + +RUN mkdir -p /usr/app/src + +WORKDIR /usr/app +COPY . /usr/app + +EXPOSE 5000 + +RUN npm install +CMD ["npm", "start"] diff --git a/README.md b/README.md index dbac19b1..646cc47c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,39 @@ # api-ai-facebook -Facebook bot sources for api.ai integration +[![](https://images.microbadger.com/badges/image/xvir/api-ai-facebook.svg)](https://microbadger.com/images/xvir/api-ai-facebook "Get your own image badge on microbadger.com") -[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) \ No newline at end of file +Facebook bot sources for Api.ai integration + +## Deploy with Heroku +Follow [these instructions](https://docs.api.ai/docs/facebook-integration#hosting-fb-messenger-bot-with-heroku). +Then, +[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) + +## Deploy with Docker + +```bash +docker run -it --name fb_bot \ + -p :5000 \ + -e APIAI_ACCESS_TOKEN="API.AI client access token" \ + -e FB_PAGE_ACCESS_TOKEN="Facebook Page Access Token" \ + -e FB_VERIFY_TOKEN="Facebook Verify Token" \ + -e APIAI_LANG="en" \ + xvir/api-ai-facebook +``` + +## Note about languages: +When you deploy the app manually to Heroku, the APIAI_LANG not filled with a value. +You need to provide language parameter according to your agent settings in the form of two-letters code. + + * "en" + * "ru" + * "de" + * "pt" + * "pt-BR" + * "es" + * "fr" + * "it" + * "ja" + * "ko" + * "zh-CN" + * "zh-HK" + * "zh-TW" diff --git a/app.json b/app.json index 7ed8972c..deeabc2e 100644 --- a/app.json +++ b/app.json @@ -1,9 +1,9 @@ { "name": "API.AI Facebook integration", "description": "Api.ai Facebook integration allows you to create Facebook bots with natural language understanding based on Api.ai technology.", - "repository": "https://github.com/xVir/api-ai-slack-bot", + "repository": "https://github.com/api-ai/api-ai-facebook", "logo": "http://xvir.github.io/img/apiai.png", - "keywords": ["api.ai", "slack", "natural language"], + "keywords": ["api.ai", "facebook", "natural language"], "env": { "APIAI_ACCESS_TOKEN": { "description": "Client access token for Api.ai", @@ -16,9 +16,14 @@ "FB_PAGE_ACCESS_TOKEN": { "description": "Page Access Token", "value": "" + }, + "APIAI_LANG": { + "description": "Agent language", + "value": "", + "required": false } }, "engines": { - "node": "5.9.0" + "node": "5.10.1" } } diff --git a/package.json b/package.json index f137871a..9561e891 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "api-ai-facebook", "version": "1.0.0", - "description": "API.AI Slack Bot", + "description": "API.AI Facebook Bot", "main": "src/app.js", "scripts": { "start": "node src/app.js", @@ -10,11 +10,17 @@ "author": "Danil Skachkov (https://api.ai)", "license": "ISC", "dependencies": { - "apiai": "^1.0.8", - "body-parser": "^1.15.0", - "express": "4.13.3", + "apiai": "2.0.5", + "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.71.0" + "request": "^2.73.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/api-ai/api-ai-facebook" } } diff --git a/src/app.js b/src/app.js index bc8a70e8..a6c5b9ec 100644 --- a/src/app.js +++ b/src/app.js @@ -5,20 +5,23 @@ const express = require('express'); const bodyParser = require('body-parser'); const uuid = require('node-uuid'); const request = require('request'); +const JSONbig = require('json-bigint'); +const async = require('async'); const REST_PORT = (process.env.PORT || 5000); 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 apiAiService = apiai(APIAI_ACCESS_TOKEN, "deprecated", {}); +const apiAiService = apiai(APIAI_ACCESS_TOKEN, {language: APIAI_LANG, requestSource: "fb"}); const sessionIds = new Map(); function processEvent(event) { - var sender = event.sender.id; + var sender = event.sender.id.toString(); - if (event.message && event.message.text) { - var text = event.message.text; + 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)) { @@ -35,18 +38,42 @@ function processEvent(event) { apiaiRequest.on('response', (response) => { if (isDefined(response.result)) { let responseText = response.result.fulfillment.speech; - let responseData = response.result.data; + let responseData = response.result.fulfillment.data; let action = response.result.action; - if (isDefined(responseData)) { - try { - let responseObject = JSON.parse(responseData); - sendFBMessage(sender, responseObject.facebook); - } catch (err) { - sendFBMessage(sender, err.message); + 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}); + } + } 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); + } + } catch (err) { + sendFBMessage(sender, {text: err.message}); + } + }); } } else if (isDefined(responseText)) { - sendFBMessage(sender, {text: 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); + }); } } @@ -57,7 +84,43 @@ function processEvent(event) { } } -function sendFBMessage(sender, messageData) { +function splitResponse(str) { + if (str.length <= 320) { + return [str]; + } + + return chunkString(str, 300); +} + +function chunkString(s, len) { + var curr = len, prev = 0; + + var output = []; + + while (s[curr]) { + if (s[curr++] == ' ') { + output.push(s.substring(prev, curr)); + prev = curr; + curr += len; + } + 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) + } + } + 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}, @@ -66,15 +129,56 @@ function sendFBMessage(sender, messageData) { recipient: {id: sender}, message: messageData } - }, function (error, response, body) { + }, (error, response, body) => { if (error) { console.log('Error sending message: ', error); } else if (response.body.error) { console.log('Error: ', response.body.error); } + + if (callback) { + callback(); + } }); } +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); + } + if (callback) { + callback(); + } + }); + }, 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); + } + }); +} + function isDefined(obj) { if (typeof obj == 'undefined') { return false; @@ -88,28 +192,40 @@ function isDefined(obj) { } const app = express(); -app.use(bodyParser.json()); -app.all('*', function (req, res, next) { - // res.header("Access-Control-Allow-Origin", '*'); - // res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, content-type, accept"); - next(); -}); -app.get('/webhook/', function (req, res) { +app.use(bodyParser.text({type: 'application/json'})); + +app.get('/webhook/', (req, res) => { if (req.query['hub.verify_token'] == FB_VERIFY_TOKEN) { res.send(req.query['hub.challenge']); + + setTimeout(() => { + doSubscribeRequest(); + }, 3000); } else { res.send('Error, wrong validation token'); } }); -app.post('/webhook/', function (req, res) { - try{ - var messaging_events = req.body.entry[0].messaging; - for (var i = 0; i < messaging_events.length; i++) { - var event = req.body.entry[0].messaging[i]; - processEvent(event); +app.post('/webhook/', (req, res) => { + try { + var data = JSONbig.parse(req.body); + + if (data.entry) { + let entries = data.entry; + entries.forEach((entry) => { + 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); + } + }); + } + }); } + return res.status(200).json({ status: "ok" }); @@ -122,6 +238,8 @@ app.post('/webhook/', function (req, res) { }); -app.listen(REST_PORT, function () { +app.listen(REST_PORT, () => { console.log('Rest service ready on port ' + REST_PORT); -}); \ No newline at end of file +}); + +doSubscribeRequest();