├── .gitignore ├── init ├── redis.js ├── index.js └── db.js ├── scripts ├── add_greeting_text.sh ├── setup_getting_started.sh └── add_persistant_menu.sh ├── config ├── config.js └── index.js ├── platformHelpers.js ├── handlers ├── message.js ├── postback.js ├── quickreply.js └── attachment.js ├── actions └── sample.js ├── witHelpers.js ├── package.json ├── schemas └── user.js ├── app.js ├── LICENSE ├── services ├── user.js └── redis.js ├── wit.js ├── sessionStore.js ├── graphAPI.js ├── routes.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | .idea 11 | .tmp 12 | pids 13 | logs 14 | thumbs.db 15 | dump.rdb 16 | 17 | .DS_Store 18 | npm-debug.log 19 | node_modules 20 | -------------------------------------------------------------------------------- /init/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('cbp:init:redis'); 4 | const redis = require('../services/redis'); 5 | 6 | exports.init = function (config) { 7 | debug('initializing redis'); 8 | 9 | redis.init(config); 10 | }; -------------------------------------------------------------------------------- /scripts/add_greeting_text.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PAGE_ACCESS_TOKEN=$(node -e "console.log(require('./config').fbPageToken);") 3 | 4 | curl -X POST -H "Content-Type: application/json" -d '{ 5 | "setting_type":"greeting", 6 | "greeting":{ 7 | "text":"Welcome to my awesome bot!" 8 | } 9 | }' "https://graph.facebook.com/v2.6/me/thread_settings?access_token=$PAGE_ACCESS_TOKEN" 10 | -------------------------------------------------------------------------------- /scripts/setup_getting_started.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PAGE_ACCESS_TOKEN=$(node -e "console.log(require('./config').fbPageToken);") 3 | 4 | curl -X POST -H "Content-Type: application/json" -d '{ 5 | "setting_type" : "call_to_actions", 6 | "thread_state" : "new_thread", 7 | "call_to_actions":[ 8 | { 9 | "payload":"getstarted" 10 | } 11 | ] 12 | }' "https://graph.facebook.com/v2.6/me/thread_settings?access_token=$PAGE_ACCESS_TOKEN" -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | shared: { 5 | }, 6 | 7 | development: { 8 | redis: { 9 | port: 6379, 10 | host: 'localhost', 11 | pass: '', 12 | db: 1 13 | }, 14 | db: 'mongodb://localhost/chatbotdb', 15 | 16 | fbPageToken: '', 17 | fbPageID: '', 18 | fbWebhookVerifyToken: '', 19 | witToken: '' 20 | }, 21 | 22 | production: { 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /init/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const debug = require('debug')('cbp:init'); 6 | 7 | module.exports = function (config) { 8 | var initPath = __dirname; 9 | var init = fs.readdirSync(initPath); 10 | init.forEach(function (js) { 11 | if (js === 'index.js') { 12 | return; 13 | } 14 | 15 | debug('initializing ' + js); 16 | 17 | require(path.join(initPath, js)).init(config, true); 18 | }); 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /platformHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const _ = require('lodash'); 3 | 4 | exports.generateQuickReplies = function (text, replies) { 5 | return { 6 | "text": text, 7 | "quick_replies": _.map(Object.keys(replies), key => { 8 | return { 9 | "content_type":"text", 10 | "title": replies[key], 11 | "payload": key 12 | } 13 | }) 14 | } 15 | } 16 | 17 | exports.generateSendLocation = function (text) { 18 | return { 19 | text: text, 20 | quick_replies: [{content_type: 'location'}] 21 | } 22 | } -------------------------------------------------------------------------------- /handlers/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sessionStore = require('../sessionStore'); 4 | const wit = require('../wit'); 5 | 6 | module.exports = function handleTextMessage (sessionId, context, msg) { 7 | context.message = msg; 8 | //console.log(context) 9 | wit.runActions(sessionId, msg, context, (error, context) => { 10 | if (error) { 11 | console.log('Oops! Got an error from Wit:', error); 12 | return; 13 | } 14 | 15 | console.log('Waiting for futher messages.'); 16 | 17 | if (context['done']) { 18 | sessionStore.destroy(sessionId); 19 | } 20 | }); 21 | }; 22 | 23 | 24 | -------------------------------------------------------------------------------- /actions/sample.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GraphAPI = require('../graphAPI'); 4 | const sessionStore = require('../sessionStore'); 5 | const debug = require('debug')('cbp:actions:sample'); 6 | 7 | 8 | module.exports = function({sessionId, context, text, entities}) { 9 | 10 | return sessionStore.get(sessionId) 11 | .then(session => { 12 | const recipientId = session.fbid; 13 | debug(`Session ${sessionId} received ${text}`); 14 | debug(`The current context is ${JSON.stringify(context)}`); 15 | debug(`Wit extracted ${JSON.stringify(entities)}`); 16 | 17 | return GraphAPI.sendPlainMessage(recipientId, 'you got this'); 18 | }) 19 | .then(function() { 20 | return context; 21 | }); 22 | } -------------------------------------------------------------------------------- /scripts/add_persistant_menu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PAGE_ACCESS_TOKEN=$(node -e "console.log(require('./config').fbPageToken);") 3 | 4 | curl -X POST -H "Content-Type: application/json" -d '{ 5 | "setting_type" : "call_to_actions", 6 | "thread_state" : "existing_thread", 7 | "call_to_actions":[ 8 | { 9 | "type":"postback", 10 | "title":"Demo features", 11 | "payload":"showSamples" 12 | }, 13 | { 14 | "type":"postback", 15 | "title":"Menu Item 2", 16 | "payload":"item2payload" 17 | }, 18 | { 19 | type: "web_url", 20 | url: "http://gph.is/XJSmL6", 21 | title: "Some url" 22 | } 23 | ] 24 | }' "https://graph.facebook.com/v2.6/me/thread_settings?access_token=$PAGE_ACCESS_TOKEN" 25 | -------------------------------------------------------------------------------- /witHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | class Helpers { 5 | getEntityValues(entities, name) { 6 | const val = entities && entities[name] && 7 | Array.isArray(entities[name]) && 8 | entities[name].length > 0 && 9 | entities[name].map((e) => {return e.value;}); 10 | if (!val || !val.length) { 11 | return null; 12 | } 13 | 14 | return _.uniq(val); 15 | } 16 | 17 | getFirstEntityValue(entities, entity) { 18 | 19 | const val = entities && entities[entity] && 20 | Array.isArray(entities[entity]) && 21 | entities[entity].length > 0 && 22 | entities[entity][0].value; 23 | 24 | if (!val) { 25 | return null; 26 | } 27 | return typeof val === 'object' ? val.value : val; 28 | } 29 | } 30 | 31 | 32 | module.exports = new Helpers(); -------------------------------------------------------------------------------- /handlers/postback.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GraphAPI = require('../graphAPI'); 4 | const platformHelpers = require('../platformHelpers'); 5 | 6 | 7 | module.exports = function handlePostback(sender, sessionId, context, payload) { 8 | let payloadTokens = payload.split(':'); 9 | const action = payloadTokens[0]; 10 | const data = payloadTokens[1]; 11 | 12 | switch(action) { 13 | case 'showSamples': 14 | return sendSamplesQuickReplies(sender); 15 | break; 16 | case 'somepostback': 17 | break; 18 | case 'getstarted': 19 | break; 20 | 21 | } 22 | }; 23 | 24 | 25 | function sendSamplesQuickReplies(sender) { 26 | const replies = { 27 | 'sampleLocation': 'Send location', 28 | 'sampleList': 'List', 29 | 'sampleGenericCards': 'Generic cards' 30 | }; 31 | let data = platformHelpers.generateQuickReplies('Explore Messenger Platform features', replies); 32 | return GraphAPI.sendTemplateMessage(sender, data); 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messenger-bot-boilerplate", 3 | "description": "A boilerplate for building Facebook Messenger Bot powered by Wit.ai", 4 | "author": "Simply Technologies", 5 | "version": "0.1.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/SimplyTechnologies/messenger-bot-wit-boilerplate.git" 9 | }, 10 | "cacheDirectories": [ 11 | "node_modules" 12 | ], 13 | "scripts": { 14 | "start": "node app" 15 | }, 16 | "dependencies": { 17 | "body-parser": "^1.15.1", 18 | "cookie-parser": "^1.4.3", 19 | "debug": "^2.6.0", 20 | "express": "^4.14.0", 21 | "lodash": "^4.17.4", 22 | "mongoose": "^4.8.1", 23 | "mongoose-q": "^0.1.0", 24 | "morgan": "^1.7.0", 25 | "node-uuid": "^1.4.7", 26 | "node-wit": "^4.2.0", 27 | "q": "^1.4.1", 28 | "redis": "^2.6.5", 29 | "request-promise": "^3.0.0", 30 | "wit": "^1.3.4" 31 | }, 32 | "engines": { 33 | "node": "^6.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /schemas/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose-q')(require('mongoose')); 4 | const Schema = mongoose.Schema; 5 | 6 | const UserSchema = new Schema({ 7 | 8 | recipientId: {type: String, unique: true}, 9 | 10 | firstName: String, 11 | lastName: String, 12 | profilePic: String, 13 | locale: String, 14 | timezone: Number, 15 | gender: String, 16 | 17 | lastLocation: { 18 | title: String, 19 | lat: Number, 20 | lon: Number, 21 | when: Date 22 | }, 23 | 24 | locations: [ 25 | { 26 | title: String, 27 | lat: Number, 28 | lon: Number, 29 | when: Date 30 | } 31 | ], 32 | 33 | lastActivity: {type: Date, default: Date.now, index: true}, 34 | 35 | created: {type: Date, default: Date.now, index: true}, 36 | updated: {type: Date, default: Date.now} 37 | }); 38 | 39 | 40 | 41 | mongoose.model('User', UserSchema); 42 | 43 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('cbp:app'); 4 | const express = require('express'); 5 | const logger = require('morgan'); 6 | const cookieParser = require('cookie-parser'); 7 | const bodyParser = require('body-parser'); 8 | 9 | debug('loading configuration'); 10 | const config = require('./config'); 11 | require('./init')(config); 12 | 13 | const app = express(); 14 | 15 | app.enable('trust proxy'); 16 | app.set('port', process.env.PORT || 3000); 17 | 18 | if (app.get('env') !== 'testing') { 19 | app.use(logger('dev')); 20 | } 21 | 22 | app.use(bodyParser.json({limit: '10mb'})); 23 | app.use(bodyParser.urlencoded({ extended: true })); 24 | app.use(cookieParser()); 25 | 26 | 27 | //Bot routes 28 | const botRoutes = require('./routes'); 29 | 30 | app.get('/bot', botRoutes.get); 31 | app.post('/bot', botRoutes.receive); 32 | 33 | 34 | const server = app.listen(app.get('port'), function () { 35 | console.log('express server listening on port ' + server.address().port); 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /handlers/quickreply.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GraphAPI = require('../graphAPI'); 4 | const platformHelpers = require('../platformHelpers'); 5 | 6 | 7 | module.exports = function handleQuickReply(sender, sessionId, context, payload) { 8 | let payloadTokens = payload.split(':'); 9 | const action = payloadTokens[0]; 10 | const data = payloadTokens[1]; 11 | 12 | switch(action) { 13 | 14 | case 'sampleLocation': 15 | return sendShareLocationSample(sender); 16 | break; 17 | case 'sampleList': 18 | return sendListSample(sender); 19 | break; 20 | case 'sampleGenericCards': 21 | return sendGenericCardsSample(sender); 22 | break; 23 | } 24 | } 25 | 26 | 27 | function sendShareLocationSample(sender) { 28 | let data = platformHelpers.generateSendLocation('Please share your location'); 29 | return GraphAPI.sendTemplateMessage(sender, data); 30 | } 31 | 32 | 33 | function sendListSample(sender) { 34 | return GraphAPI.sendPlainMessage(sender, 'coming soon'); 35 | } 36 | 37 | 38 | function sendGenericCardsSample(sender) { 39 | return GraphAPI.sendPlainMessage(sender, 'coming soon'); 40 | } -------------------------------------------------------------------------------- /handlers/attachment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sessionStore = require('../sessionStore'); 4 | const GraphAPI = require('../graphAPI'); 5 | const userService = require('../services/user'); 6 | 7 | const _ = require('lodash'); 8 | const Q = require('q'); 9 | 10 | module.exports = function handler(sender, sessionId, context, atts) { 11 | 12 | let promises = _.map(atts, att => { 13 | switch(att.type){ 14 | case 'location': 15 | return processLocationData(sender, att); 16 | default: 17 | return GraphAPI.sendPlainMessage(sender,'Wow, cool pic! I\'ll keep this one ;)'); 18 | } 19 | }); 20 | 21 | return Q.all(promises) 22 | .then(function() { 23 | return sessionStore.save(sessionId, context); 24 | }); 25 | }; 26 | 27 | 28 | function processLocationData(sender, attachment) { 29 | let location = { 30 | title: attachment.title, 31 | lat: attachment.payload.coordinates.lat, 32 | lon: attachment.payload.coordinates.long 33 | }; 34 | 35 | return userService.updateUserLocation(sender, location) 36 | .then(user => { 37 | return GraphAPI.sendPlainMessage(sender, 'Hey, thanks for sharing your location') 38 | }); 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Simply Technologies LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /services/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('cbp:services:user'); 4 | 5 | var mongoose = require('mongoose-q')(require('mongoose')); 6 | var User = mongoose.model('User'); 7 | 8 | var Q = require('q'); 9 | 10 | exports.getByRecipientId = function (id) { 11 | return User.findOneQ({recipientId: id}); 12 | }; 13 | 14 | exports.getById = function (id) { 15 | return User.findOneQ({_id: id}); 16 | }; 17 | 18 | 19 | exports.getOrCreateUserByRecipientId = function(id, data) { 20 | return exports.getByRecipientId(id) 21 | .then( user => { 22 | if (!user) { 23 | user = new User({ 24 | recipientId: id, 25 | firstName: data.first_name, 26 | lastName: data.last_name, 27 | gender: data.gender, 28 | locale: data.locale, 29 | timezone: data.timezone, 30 | profilePic: data.profile_pic 31 | }); 32 | return user.saveQ(); 33 | } 34 | return user; 35 | }); 36 | }; 37 | 38 | 39 | exports.updateUserLocation = function(recipientId, location) { 40 | let loc = { 41 | title: location.title, 42 | lat: location.lat, 43 | lon: location.lon, 44 | when: new Date() 45 | } 46 | return User.updateQ({recipientId: recipientId}, {$set: {lastLocation: loc}, $push: {locations: loc}}); 47 | }; 48 | 49 | exports.logActivity = function(id) { 50 | return User.updateQ({_id: id}, {$set: {lastActivity: new Date()}}); 51 | }; 52 | 53 | 54 | -------------------------------------------------------------------------------- /wit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('cbp:wit'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const Q = require('q'); 7 | 8 | const Wit = require('node-wit').Wit; 9 | 10 | const GraphAPI = require('./graphAPI'); 11 | const sessionStore = require('./sessionStore'); 12 | const config = require('./config'); 13 | 14 | 15 | // bot actions 16 | const actions = { 17 | send(request, response) { 18 | 19 | debug('saying', response); 20 | 21 | let recipientId; 22 | return sessionStore.get(request.sessionId) 23 | .then(session => { 24 | recipientId = session.fbid; 25 | return GraphAPI.sendTemplateMessage(recipientId, response); 26 | }) 27 | .catch((err) => { 28 | console.log('Oops! An error occurred while forwarding the response to', recipientId, ':', err ); 29 | }); 30 | } 31 | 32 | }; 33 | 34 | 35 | 36 | (function initCustomActions() { 37 | var actionsPath = path.join(__dirname, 'actions'); 38 | var actionsFile = fs.readdirSync(actionsPath); 39 | 40 | actionsFile.forEach(function (js) { 41 | const actionName = path.basename(js,'.js'); 42 | debug(`Initializing wit action [${actionName}]`); 43 | actions[actionName] = require(path.join(actionsPath, js)); 44 | }); 45 | })(); 46 | 47 | 48 | 49 | 50 | 51 | // Setting up our bot 52 | module.exports = new Wit({accessToken: config.witToken, actions: actions}); -------------------------------------------------------------------------------- /init/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Initializes the database instance. 4 | * @author Alexander Adamyan 5 | */ 6 | 7 | const fs = require('fs'); 8 | const mongoose = require('mongoose'); 9 | mongoose.Promise = require('q').Promise; 10 | const debug = require('debug')('cbp:init:db'); 11 | const path = require('path'); 12 | 13 | var dbInitialized = false; 14 | 15 | /** 16 | * Initialize Database connection 17 | * @param {Object} config current environment configuration 18 | * @param forceNoDebug 19 | */ 20 | exports.init = function (config, forceNoDebug) { 21 | //Preventing the module to be initialize more than one time 22 | if (dbInitialized) { 23 | return; 24 | } 25 | dbInitialized = true; 26 | 27 | //Connecting to the database 28 | debug('initializing database connection'); 29 | mongoose.connect(config.db); 30 | 31 | //Set debug mode for dev environment 32 | var env = process.env.NODE_ENV || 'development'; 33 | if (env === 'development' && !forceNoDebug) { 34 | mongoose.set('debug', true); 35 | } 36 | 37 | //Init model schemas 38 | debug('initializing model schemas'); 39 | var schemasPath = path.join(__dirname, '../schemas'); 40 | var schemaFiles = fs.readdirSync(schemasPath); 41 | 42 | schemaFiles.forEach(function (file) { 43 | require(schemasPath + '/' + file); 44 | debug('model schema initialized: %s', file); 45 | }); 46 | }; -------------------------------------------------------------------------------- /sessionStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const redis = require('./services/redis'); 4 | const uuid = require('node-uuid'); 5 | const SESSION_WINDOW = 60 * 20; 6 | 7 | class SessionStore { 8 | constructor() { 9 | this.sessions = {}; 10 | this._redisPrefix = "cs"; 11 | } 12 | 13 | get(id) { 14 | return redis.getKey(id) 15 | .then(data => { 16 | return redis.setExpire(id, SESSION_WINDOW) 17 | .then(() => { 18 | return JSON.parse(data); 19 | }); 20 | }); 21 | } 22 | 23 | save(id, context) { 24 | return this.get(id) 25 | .then(session => { 26 | session.context = context; 27 | return redis.setKey(id, JSON.stringify(session)) 28 | .then(() => { 29 | return redis.setExpire(id, SESSION_WINDOW); 30 | }) 31 | .then(() => { 32 | return context; 33 | }); 34 | }); 35 | } 36 | saveSession(id, session) { 37 | return redis.setKey(id, JSON.stringify(session)) 38 | .then(() => { 39 | return redis.setExpire(id, SESSION_WINDOW); 40 | }) 41 | .then(() => { 42 | return session.context; 43 | }); 44 | } 45 | 46 | findOrCreate(fbid) { 47 | let newSession = false; 48 | return redis.findFirstKey(this._redisPrefix + '*' + fbid) 49 | .then(key => { 50 | if (key) { 51 | return key; 52 | } 53 | newSession = true; 54 | key = this._redisPrefix + uuid.v1() + fbid; 55 | 56 | return redis.setKey(key, JSON.stringify({fbid: fbid, context: {}})) 57 | .then(() => { 58 | return key; 59 | }); 60 | }) 61 | .then(key => { 62 | return redis.setExpire(key, SESSION_WINDOW) 63 | .then(() => { 64 | return redis.getKey(key) 65 | }) 66 | .then(data => { 67 | return JSON.parse(data); 68 | }) 69 | .then(session => { 70 | return {sessionId: key, newSession: newSession, session: session}; 71 | }); 72 | }); 73 | } 74 | 75 | destroy(id) { 76 | return redis.deleteHash(id); 77 | } 78 | } 79 | 80 | module.exports = new SessionStore(); 81 | -------------------------------------------------------------------------------- /graphAPI.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const request = require('request-promise'); 4 | const config = require('./config') 5 | const FB_PAGE_TOKEN = config.fbPageToken; 6 | const Q = require('q'); 7 | const _ = require('lodash'); 8 | 9 | class GraphAPI { 10 | constructor() { 11 | this.api = request.defaults({ 12 | uri: 'https://graph.facebook.com/v2.6/me/messages', 13 | method: 'POST', 14 | json: true, 15 | qs: { access_token: FB_PAGE_TOKEN }, 16 | headers: {'Content-Type': 'application/json'}, 17 | }); 18 | } 19 | 20 | sendTemplateMessage(recipientId, data) { 21 | const opts = { 22 | form: { 23 | recipient: { 24 | id: recipientId, 25 | }, 26 | message: data, 27 | } 28 | }; 29 | return this.api(opts); 30 | } 31 | 32 | sendPlainMessage(recipientId, msg) { 33 | return this.sendTemplateMessage(recipientId, {text: msg}); 34 | } 35 | 36 | sendBulkMessages(recipientId, messages) { 37 | return messages.reduce((p, message) => { 38 | return p.then(() => { 39 | return this.sendTypingOn(recipientId) 40 | .then(() => { 41 | const delay = message.text && message.text.length * 20; 42 | return Q.delay(delay || 500) 43 | }) 44 | .then(() => { 45 | if (_.isString(message)) { 46 | return this.sendPlainMessage(recipientId, message); 47 | } else { 48 | return this.sendTemplateMessage(recipientId, message); 49 | } 50 | }); 51 | }); 52 | }, Q()); 53 | } 54 | 55 | sendTypingOn(recipientId) { 56 | return this._sendTyping(recipientId, 'typing_on'); 57 | } 58 | 59 | sendTypingOff(recipientId) { 60 | return this._sendTyping(recipientId, 'typing_off'); 61 | } 62 | 63 | _sendTyping(recipientId, action) { 64 | const opts = { 65 | form: { 66 | recipient: { 67 | id: recipientId, 68 | }, 69 | sender_action: action 70 | } 71 | }; 72 | return this.api(opts); 73 | } 74 | 75 | getUserProfile(recipientId) { 76 | return request({ 77 | method:'GET', 78 | url: 'https://graph.facebook.com/v2.6/' + recipientId, 79 | json: true, 80 | qs: { 81 | fields: 'first_name,last_name,locale,timezone,gender', 82 | access_token: FB_PAGE_TOKEN 83 | } 84 | }) 85 | } 86 | } 87 | 88 | 89 | module.exports = new GraphAPI(); -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * The Module Dynamically loads the configurations for 5 | * the heroku deployed project. This way of managing the configuration 6 | * is done because of the heroku suggestion for 7 | * Multiple Environments for the App article. 8 | */ 9 | 10 | const url = require('url'); 11 | 12 | 13 | /** 14 | * Returns the Redis config object for the staging, 15 | * testing and production servers 16 | * @returns {{port: *, host: (*|string), pass: *}} 17 | * @private 18 | */ 19 | function redisConfig() { 20 | if (!process.env.REDISCLOUD_URL || process.env.REDISCLOUD_URL === 'undefined') { 21 | return null; 22 | } 23 | var redisURL = url.parse(process.env.REDISCLOUD_URL); 24 | return { 25 | port: redisURL.port, 26 | host: redisURL.hostname, 27 | pass: redisURL.auth.split(':')[1] 28 | }; 29 | } 30 | 31 | /** 32 | * Returns the mongo db config for the staging, 33 | * testing and production servers 34 | * @returns {*} 35 | * @private 36 | */ 37 | function mongoConfig() { 38 | return process.env.MONGOHQ_URL !== 'undefined' && process.env.MONGOHQ_URL || 39 | process.env.MONGOLAB_URI !== 'undefined' && process.env.MONGOLAB_URI; 40 | } 41 | 42 | 43 | function mergeSharedConfigs(shared, config) { 44 | for (var key in shared) { 45 | config[key] = config[key] || shared[key]; 46 | } 47 | 48 | return config 49 | } 50 | 51 | 52 | /** 53 | * Creates a config object dynamically for the application. 54 | * @returns {*} 55 | * @private 56 | */ 57 | function createConfig() { 58 | const env = process.env.NODE_ENV || 'development'; 59 | var config = require('./config'); 60 | 61 | config = mergeSharedConfigs(config.shared, config[env]); 62 | 63 | config.fbPageToken = process.env.FB_VERIFY_TOKEN || config.fbPageToken; 64 | config.fbPageID = process.env.FB_PAGE_ID || config.fbPageID; 65 | config.fbWebhookVerifyToken = process.env.FB_WEBHOOK_VERIFY_TOKEN || config.fbWebhookVerifyToken; 66 | config.witToken = process.env.WIT_TOKEN || config.witToken; 67 | 68 | config.redis = redisConfig() || config.redis; 69 | config.db = mongoConfig() || config.db; 70 | 71 | return config; 72 | } 73 | 74 | module.exports = createConfig(); -------------------------------------------------------------------------------- /services/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Function for working with Redis DB 5 | * @author Alexander A. 6 | */ 7 | 8 | const Q = require('q'); 9 | const redis = require('redis'); 10 | const debug = require('debug')('cbp:lib:redis'); 11 | const error = require('debug')('cbp:lib:redis:error'); 12 | error.log = console.error.bind(console); 13 | 14 | var redisIsReady = false; 15 | 16 | 17 | // Redis client object 18 | var client; 19 | 20 | var methods = ['eval', 'del', 'hdel', 'expire', 'get', 'set', 'del','keys']; 21 | function createClientQ(client) { 22 | methods.forEach(function (method) { 23 | client[method + 'Q'] = Q.nbind(client[method], client); 24 | }); 25 | 26 | return client; 27 | } 28 | 29 | function createClient(config, returnBuffers) { 30 | debug('connecting to %s:%s', config.host, config.port); 31 | var client; 32 | 33 | client = redis.createClient(config.port, config.host, {return_buffers: returnBuffers}); 34 | client = createClientQ(client); 35 | 36 | if (config.pass) { 37 | client.auth(config.pass); 38 | } 39 | 40 | if (config.db) { 41 | client.select(config.db); 42 | } 43 | 44 | client.on('error', function (err) { 45 | error(err); 46 | }); 47 | 48 | client.on('connect', function() { 49 | redisIsReady = true; 50 | }); 51 | 52 | return client; 53 | } 54 | 55 | exports.redisIsReady = function () { 56 | return redisIsReady; 57 | }; 58 | 59 | /** 60 | * Initializing Redis 61 | * @param {Object} config configuration with host, port, pass 62 | */ 63 | exports.init = function initRedis(config) { 64 | client = createClient(config.redis); 65 | }; 66 | 67 | 68 | /** 69 | * Deletes hash from Redis 70 | * @param {String} hashName hash that will be deleted 71 | */ 72 | exports.deleteHash = function (hashName) { 73 | return client.delQ(hashName); 74 | }; 75 | 76 | /** 77 | * Deletes key from hash 78 | * @param {String} hashName hash where key is located 79 | * @param {String} key key name of entry that will be deleted 80 | */ 81 | exports.deleteHashKey = function (hashName, key) { 82 | return client.hdelQ(hashName, key); 83 | }; 84 | 85 | /** 86 | * Setting expire for key after which it will be deleted 87 | * @description http://redis.io/commands/expire 88 | * @param {String} keyName name of the key 89 | * @param {Number} expireTime expire time in seconds 90 | */ 91 | exports.setExpire = function (keyName, expireTime) { 92 | return client.expireQ(keyName, expireTime); 93 | }; 94 | 95 | exports.setKey = function (keyName, value) { 96 | return client.setQ(keyName, value); 97 | }; 98 | 99 | exports.getKey = function (keyName) { 100 | return client.getQ(keyName); 101 | }; 102 | 103 | exports.findFirstKey = function(pattern) { 104 | return client.keysQ(pattern) 105 | .then(function(keys) { 106 | return keys && keys.length && keys[0]; 107 | }); 108 | } 109 | 110 | /** 111 | * Returns redis client 112 | * @returns {*} 113 | */ 114 | exports.getClient = function() { 115 | return client; 116 | }; 117 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Q = require('q'); 4 | const _ = require('lodash'); 5 | 6 | const GraphAPI = require('./graphAPI'); 7 | const sessionStore = require('./sessionStore'); 8 | 9 | const userService = require('./services/user'); 10 | const config = require('./config'); 11 | 12 | const FB_VERIFY_TOKEN = config.fbWebhookVerifyToken; 13 | const FB_PAGE_ID = config.fbPageID; 14 | 15 | 16 | 17 | function extractMessagingObjects(body) { 18 | var messages = []; 19 | 20 | for (var i = 0; i < body.entry.length; i++) { 21 | var eventEntry = body.entry[i]; 22 | if (eventEntry.id.toString() === FB_PAGE_ID){ 23 | var recievedMessages = _.filter(eventEntry.messaging, function(msg) { 24 | return !!(msg.message || msg.postback); 25 | }) 26 | messages = messages.concat(recievedMessages); 27 | } 28 | } 29 | 30 | return messages; 31 | } 32 | 33 | 34 | //Main routes 35 | exports.get = function(req, res, next) { 36 | if (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === FB_VERIFY_TOKEN) { 37 | res.send(req.query['hub.challenge']); 38 | } else { 39 | res.sendStatus(400); 40 | } 41 | } 42 | 43 | 44 | exports.receive = function(req, res, next) { 45 | //respond as soon as possible 46 | res.status(200).end(); 47 | 48 | const messages = extractMessagingObjects(req.body); 49 | if (!messages.length) { 50 | return; 51 | } 52 | 53 | var processPromises = _.map(messages, (messaging) => { 54 | return processMessage(messaging); 55 | }); 56 | 57 | Q.all(processPromises) 58 | .then(function(){ 59 | console.log('all messages processed'); 60 | }) 61 | .catch(function(err) { 62 | console.error('error handling messages', err); 63 | console.error(err.stack); 64 | }); 65 | } 66 | 67 | 68 | //user data handlers 69 | const handleAttachment = require('./handlers/attachment'); 70 | const handleQuickReply = require('./handlers/quickreply'); 71 | const handleTextMessage = require('./handlers/message'); 72 | const handlePostback = require('./handlers/postback'); 73 | 74 | 75 | function processMessage(messaging) { 76 | 77 | const sender = messaging.sender.id; 78 | 79 | let sessionId; 80 | let session; 81 | let newSession; 82 | 83 | return sessionStore.findOrCreate(sender) 84 | .then(data => { 85 | sessionId = data.sessionId; 86 | session = data.session; 87 | newSession = data.newSession; 88 | 89 | if (!session.context.userData) { 90 | return GraphAPI.getUserProfile(sender) 91 | .then(user => { 92 | user.recipientId = sender; 93 | session.context.userData = user; 94 | return data; 95 | }); 96 | } 97 | return data; 98 | }) 99 | .then(data => { 100 | 101 | if (!session.context.userData._id) { 102 | return userService.getOrCreateUserByRecipientId(sender, session.context.userData) 103 | .then(user => { 104 | session.context.userData._id = user._id.toString(); 105 | return sessionStore.saveSession(sessionId, session); 106 | }); 107 | } 108 | }) 109 | .then(() => { 110 | return userService.logActivity(session.context.userData._id); 111 | }) 112 | .then(() => { 113 | //console.log(session.context); 114 | return GraphAPI.sendTypingOn(sender); 115 | }) 116 | .then(() => { 117 | 118 | const atts = messaging.message && messaging.message.attachments; 119 | if (atts) { 120 | return handleAttachment(sender, sessionId, session.context, atts); 121 | } 122 | 123 | const quickReply = messaging.message && messaging.message.quick_reply; 124 | if (quickReply) { 125 | return handleQuickReply(sender, sessionId, session.context, quickReply.payload); 126 | } 127 | 128 | const msg = messaging.message && messaging.message.text; 129 | if (msg) { 130 | return handleTextMessage(sessionId, session.context, msg); 131 | } 132 | 133 | const payload = messaging.postback && messaging.postback.payload; 134 | if (payload) { 135 | return handlePostback(sender, sessionId, session.context, payload); 136 | } 137 | }) 138 | .catch(err => { 139 | console.log(err.stack); 140 | }); 141 | } 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Facebook Messenger Chatbot Boilerplate 2 | Boilerplate project for developing Facebook Messenger Bot powered by Wit.ai, MongoDB and Redis as a session store. 3 | This boilerplate will allow you to bootstrap your chatbot within 8 minutes, all you need is wit.ai application setup, Facebook page, Facebook app and running MongoDB and Redis. 4 | 5 | 6 | ## External dependencies 7 | 8 | ### Wit.ai 9 | 10 | The AI engine used in this boilerplate is wit.ai, please make sure to create a new app in the [wit.ai](https://wit.ai/) dashboard. 11 | 12 | 13 | ### MongoDB 14 | 15 | This boilerplate is using MongoDB for storing user's data and for tracking their last activity with the bot 16 | Please install MongoDB v3 or greater and setup connection in the `config/config.js` 17 | 18 | By default the in the development environment `chatbotdb` database will be created 19 | 20 | ### Session store using Redis 21 | 22 | Redis server is required for managing sessions and keeping the context with each user. 23 | Please install Redis and setup connection in the `config/config.js` 24 | 25 | 26 | ## Setup 27 | 28 | `npm install` 29 | 30 | ## Configuration 31 | 32 | Set environment variables or write values directly in the `config/config.js` 33 | 34 | `FB_PAGE_TOKEN` - Access token generated for the Facebook application to allow making Graph API requests to the page 35 | 36 | `WIT_TOKEN` - WIT server side token 37 | 38 | `FB_WEBHOOK_VERIFY_TOKEN` - a verify token that should be used for Webhooks setup, this is something you need to generate by yourself 39 | 40 | `FB_PAGE_ID` - ID of the Facebook page that will be used as a bot. 41 | 42 | 43 | ## Run 44 | 45 | In order to run and test chatbot it is required to have server running with secured connection. For that you can use **ngrok** 46 | 47 | ###### install ngrok and run 48 | 49 | `./ngrok http 3000` 50 | 51 | ###### run the server 52 | `npm start` 53 | 54 | to enable debug mode 55 | `DEBUG=* npm start` or `DEBUG=cbp* npm start` for app level log messages only 56 | 57 | ###### update Facebook application's webhook 58 | `https://xxxxxxxx.ngrok.io/bot` 59 | enter the `FB_VERIFY_TOKEN` value and submit 60 | 61 | 62 | 63 | ## Project structure 64 | ``` 65 | actions/ 66 | - define custom wit.ai actions here, the file name should be the name of the action, all actions from this folder are automatically registered as wit.ai actions. 67 | 68 | config/ 69 | - configuration files for access keys, tokens, connection strings 70 | 71 | handlers/ 72 | - messenger event handlers for text messages, attachments, postbacks and quick replies 73 | 74 | init/ 75 | - init scripts for mongodb and redis 76 | 77 | schemas/ 78 | - mongodb/mongoose schemas 79 | 80 | scripts/ 81 | - shell scripts for messenger greeting text, persistant menu and getting started button setup 82 | 83 | services/ 84 | - custom services 85 | 86 | app.js 87 | - application entry point 88 | 89 | graphAPI.js 90 | - Facebook Graph API helper 91 | 92 | platformHelpers.js 93 | - Helpers for generating message templates, e.g send location, quick replies and others 94 | 95 | routes.js 96 | - express routes setup and message handler delegation 97 | 98 | sessionStore.js 99 | - Redis based session store 100 | 101 | wit.js 102 | - wit.ai setup with actions loading 103 | 104 | witHelpers.js 105 | - helpers for extracting entities from wit.ai response 106 | 107 | 108 | ``` 109 | 110 | ## The Roadmap 111 | 112 | - [x] Redis session store 113 | - [x] Automatic actions binding 114 | - [x] User info tracking in the database 115 | - [x] Saving last and previous geo locations shared by user 116 | - [x] Demo of send location, quick replies 117 | - [x] Shell scripts for adding greeting message, persistent menu and getting started button 118 | - [ ] Demo of generic template 119 | - [ ] Demo of list template 120 | - [ ] Unit tests 121 | 122 | --- 123 | 124 | [ChatBotKitchen](https://www.chatbotkitchen.com) project 125 | 126 | *made with love ❤️ in Armenia by [Simply Technologies](http://www.simplytechnologies.net)* 127 | --------------------------------------------------------------------------------