Created
December 21, 2016 14:43
-
-
Save mark-stephenson-/77d09c1b794036b1d0178315ee9f3878 to your computer and use it in GitHub Desktop.
Node WIT/FB Index
This file contains hidden or 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
'use strict'; | |
const bodyParser = require('body-parser'); | |
const crypto = require('crypto'); | |
const express = require('express'); | |
const fetch = require('node-fetch'); | |
const request = require('request'); | |
const config = require('./config'); | |
const LevelUp = require('./lib/levelup').LevelUp; | |
let Wit = null; | |
let log = null; | |
try { | |
// if running from repo | |
Wit = require('../').Wit; | |
log = require('../').log; | |
} catch (e) { | |
Wit = require('node-wit').Wit; | |
log = require('node-wit').log; | |
} | |
// Webserver parameter | |
const PORT = config.PORT || 8445; | |
// Wit.ai parameters | |
const WIT_TOKEN = config.WIT_TOKEN; | |
// Messenger API parameters | |
const FB_PAGE_TOKEN = config.FB_PAGE_TOKEN; | |
if (!FB_PAGE_TOKEN) { throw new Error('missing FB_PAGE_TOKEN'); } | |
const FB_APP_SECRET = config.FB_APP_SECRET; | |
if (!FB_APP_SECRET) { throw new Error('missing FB_APP_SECRET'); } | |
let FB_VERIFY_TOKEN = config.FB_VERIFY_TOKEN; | |
// ---------------------------------------------------------------------------- | |
// Messenger API specific code | |
const fbMessage = (id, text) => { | |
const body = JSON.stringify({ | |
recipient: { id }, | |
message: { text }, | |
}); | |
const qs = 'access_token=' + encodeURIComponent(FB_PAGE_TOKEN); | |
return fetch('https://graph.facebook.com/me/messages?' + qs, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body, | |
}) | |
.then(rsp => rsp.json()) | |
.then(json => { | |
if (json.error && json.error.message) { | |
throw new Error(json.error.message); | |
} | |
return json; | |
}); | |
}; | |
const fbActivateMessage = (id, desc) => { | |
const body = JSON.stringify({ | |
recipient: { id }, | |
message: { | |
attachment: { | |
type: "template", | |
payload: { | |
template_type: "button", | |
text: desc, | |
buttons: [ | |
{ | |
"type":"postback", | |
"title":"Activate Now", | |
"payload":"TurnOn" | |
} | |
] | |
} | |
} | |
} | |
}); | |
const qs = 'access_token=' + encodeURIComponent(FB_PAGE_TOKEN); | |
return fetch('https://graph.facebook.com/me/messages?' + qs, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body, | |
}) | |
.then(rsp => rsp.json()) | |
.then(json => { | |
if (json.error && json.error.message) { | |
throw new Error(json.error.message); | |
} | |
return json; | |
}); | |
}; | |
const setupMenu = () => { | |
const body = JSON.stringify({ | |
setting_type : "call_to_actions", | |
thread_state : "existing_thread", | |
call_to_actions:[ | |
{ | |
type:"postback", | |
title:"About", | |
payload:"About" | |
}, | |
{ | |
type:"postback", | |
title:"Turn ON", | |
payload:"TurnOn" | |
}, | |
{ | |
type:"postback", | |
title:"Turn OFF", | |
payload:"TurnOff" | |
} | |
] | |
}); | |
const qs = 'access_token=' + encodeURIComponent(FB_PAGE_TOKEN); | |
return fetch('https://graph.facebook.com/v2.6/me/thread_settings?' + qs, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body | |
}) | |
.then(rsp => rsp.json()) | |
.then(json => { | |
if (json.error && json.error.message) { | |
throw new Error(json.error.message); | |
} | |
return json; | |
}); | |
}; | |
// ---------------------------------------------------------------------------- | |
// Wit.ai bot specific code | |
// This will contain all user sessions. | |
// Each session has an entry: | |
// sessionId -> {fbid: facebookUserId, context: sessionState} | |
const sessions = {}; | |
const findOrCreateSession = (fbid) => { | |
let sessionId; | |
// Let's see if we already have a session for the user fbid | |
Object.keys(sessions).forEach(k => { | |
if (sessions[k].fbid === fbid) { | |
// Yep, got it! | |
sessionId = k; | |
} | |
}); | |
if (!sessionId) { | |
// No session found for user fbid, let's create a new one | |
sessionId = new Date().toISOString(); | |
sessions[sessionId] = {fbid: fbid, context: {}}; | |
LevelUp.init_user(fbid); | |
} | |
return sessionId; | |
}; | |
// Our bot actions | |
const actions = { | |
send({sessionId}, {text}) { | |
// Our bot has something to say! | |
// Let's retrieve the Facebook user whose session belongs to | |
const recipientId = sessions[sessionId].fbid; | |
if (recipientId) { | |
// Yay, we found our recipient! | |
// Let's forward our bot response to her. | |
// We return a promise to let our bot know when we're done sending | |
return fbMessage(recipientId, text) | |
.then(() => null) | |
.catch((err) => { | |
console.error( | |
'Oops! An error occurred while forwarding the response to', | |
recipientId, | |
':', | |
err.stack || err | |
); | |
}); | |
} else { | |
console.error('Oops! Couldn\'t find user for session:', sessionId); | |
// Giving the wheel back to our bot | |
return Promise.resolve(); | |
} | |
}, | |
// Custom Actions | |
get_activation_state({sessionId, context, entities}) { | |
return new Promise(function(resolve, reject) { | |
LevelUp.get_activation_state(sessions[sessionId].fbid) | |
.then(function(user_schedule){ | |
if(user_schedule.schedule_on){ | |
context.schedule_active= true; | |
delete context.schedule_inactive; | |
}else { | |
delete context.schedule_active; | |
context.schedule_inactive = true; | |
} | |
return resolve(context); | |
} | |
); | |
}); | |
}, | |
activate_schedule({sessionId, context, entities}) { | |
return new Promise(function(resolve, reject) { | |
LevelUp.set_activation_state(sessions[sessionId].fbid, true) | |
.then(user_schedule => { | |
if(user_schedule.schedule.schedule_on){ | |
context.schedule_active= true; | |
delete context.schedule_inactive; | |
}else { | |
delete context.schedule_active; | |
context.schedule_inactive = true; | |
} | |
return resolve(context); | |
}); | |
}); | |
}, | |
deactivate_schedule({sessionId, context, entities}) { | |
return new Promise(function(resolve, reject) { | |
LevelUp.set_activation_state(sessions[sessionId].fbid, false) | |
.then(user_schedule => { | |
if(!user_schedule.schedule.schedule_on){ | |
delete context.schedule_active; | |
delete context.schedule_inactive; | |
} | |
return resolve(context); | |
}); | |
}); | |
}, | |
show_activate({sessionId, context, entities}) { | |
return new Promise(function(resolve, reject) { | |
const recipientId = sessions[sessionId].fbid; | |
if (recipientId) { | |
return fbActivateMessage(recipientId, 'You can cancel notifications using the menu. Get started by clicking ACTIVATE below.') | |
.then(() => null) | |
.catch((err) => { | |
console.error( | |
'Oops! An error occurred while forwarding the response to', | |
recipientId, | |
':', | |
err.stack || err | |
); | |
}); | |
} else { | |
console.error('Oops! Couldn\'t find user for session:', sessionId); | |
return Promise.resolve(); | |
} | |
return resolve(context); | |
}); | |
} | |
}; | |
// Setting up our bot | |
const wit = new Wit({ | |
accessToken: WIT_TOKEN, | |
actions, | |
logger: new log.Logger(log.INFO) | |
}); | |
// Starting our webserver and putting it all together | |
const app = express(); | |
app.use(({method, url}, rsp, next) => { | |
rsp.on('finish', () => { | |
console.log(`${rsp.statusCode} ${method} ${url}`); | |
}); | |
next(); | |
}); | |
app.use(bodyParser.json({ verify: verifyRequestSignature })); | |
// Webhook setup | |
app.get('/webhook', (req, res) => { | |
if (req.query['hub.mode'] === 'subscribe' && | |
req.query['hub.verify_token'] === FB_VERIFY_TOKEN) { | |
res.send(req.query['hub.challenge']); | |
} else { | |
res.sendStatus(400); | |
} | |
}); | |
// Message handler | |
app.post('/webhook', (req, res) => { | |
const data = req.body; | |
if (data.object === 'page') { | |
data.entry.forEach(entry => { | |
entry.messaging.forEach(event => { | |
if (event.message && !event.message.is_echo && !event.postback) { | |
// Yay! We got a new message! | |
// We retrieve the Facebook user ID of the sender | |
const sender = event.sender.id; | |
// We retrieve the user's current session, or create one if it doesn't exist | |
// This is needed for our bot to figure out the conversation history | |
const sessionId = findOrCreateSession(sender); | |
// We retrieve the message content | |
const {text, attachments} = event.message; | |
if (attachments) { | |
// We received an attachment | |
// Let's reply with an automatic message | |
fbMessage(sender, 'Sorry I can only process text messages for now.') | |
.catch(console.error); | |
} else if (text) { | |
// We received a text message | |
// Let's forward the message to the Wit.ai Bot Engine | |
// This will run all actions until our bot has nothing left to do | |
wit.runActions( | |
sessionId, // the user's current session | |
text, // the user's message | |
sessions[sessionId].context // the user's current session state | |
).then((context) => { | |
// Our bot did everything it has to do. | |
// Now it's waiting for further messages to proceed. | |
console.log('Waiting for next user messages'); | |
// Based on the session state, you might want to reset the session. | |
// This depends heavily on the business logic of your bot. | |
// Example: | |
// if (context['done']) { | |
// delete sessions[sessionId]; | |
// } | |
// Updating the user's current session state | |
sessions[sessionId].context = context; | |
}) | |
.catch((err) => { | |
console.error('Oops! Got an error from Wit: ', err.stack || err); | |
}); | |
} | |
}else if (event.postback) { | |
const sender = event.sender.id; | |
const sessionId = findOrCreateSession(sender); | |
var text = event.postback.payload; | |
console.log(text); | |
//Send the message to Wit to handle | |
wit.runActions( | |
sessionId, // the user's current session | |
text, // the user's message | |
sessions[sessionId].context // the user's current session state | |
).then((context) => { | |
console.log('Waiting for next user messages'); | |
sessions[sessionId].context = context; | |
}) | |
.catch((err) => { | |
console.error('Oops! Got an error from Wit: ', err.stack || err); | |
}); | |
}else { | |
console.log('received unknown event', JSON.stringify(event)); | |
} | |
}); | |
}); | |
} | |
res.sendStatus(200); | |
}); | |
/* | |
* Verify that the callback came from Facebook. Using the App Secret from | |
* the App Dashboard, we can verify the signature that is sent with each | |
* callback in the x-hub-signature field, located in the header. | |
* | |
* https://developers.facebook.com/docs/graph-api/webhooks#setup | |
* | |
*/ | |
function verifyRequestSignature(req, res, buf) { | |
var signature = req.headers["x-hub-signature"]; | |
if (!signature) { | |
// For testing, let's log an error. In production, you should throw an | |
// error. | |
console.error("Couldn't validate the signature."); | |
} else { | |
var elements = signature.split('='); | |
var method = elements[0]; | |
var signatureHash = elements[1]; | |
var expectedHash = crypto.createHmac('sha1', FB_APP_SECRET) | |
.update(buf) | |
.digest('hex'); | |
if (signatureHash != expectedHash) { | |
throw new Error("Couldn't validate the request signature."); | |
} | |
} | |
} | |
setupMenu(); | |
app.listen(PORT); | |
console.log('Listening on :' + PORT + '...'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment