Skip to content

Instantly share code, notes, and snippets.

@mark-stephenson-
Created December 21, 2016 14:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mark-stephenson-/77d09c1b794036b1d0178315ee9f3878 to your computer and use it in GitHub Desktop.
Save mark-stephenson-/77d09c1b794036b1d0178315ee9f3878 to your computer and use it in GitHub Desktop.
Node WIT/FB Index
'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