intelligo
Version:
AI Chatbot Framework for Node.js
476 lines (437 loc) • 13.7 kB
text/typescript
import * as express from 'express';
import { EventEmitter } from 'eventemitter3';
import { json as _json } from 'body-parser';
import { createHmac } from 'crypto';
import * as request from 'request';
import * as neuro from 'neuro.js';
export class MessengerBot extends EventEmitter {
PAGE_ACCESS_TOKEN: any;
VALIDATION_TOKEN: any;
APP_SECRET: any;
FB_URL: any;
app: any;
webhook: any;
intelligoClassifier: any;
/**
* @param params
* @constructor
*/
constructor(params: any) {
super();
if (!params || (params && (!params.PAGE_ACCESS_TOKEN || !params.VALIDATION_TOKEN || !params.APP_SECRET))) {
throw new Error('You need to specify an PAGE_ACCESS_TOKEN, VALIDATION_TOKEN and APP_SECRET');
}
this.PAGE_ACCESS_TOKEN = params.PAGE_ACCESS_TOKEN;
this.VALIDATION_TOKEN = params.VALIDATION_TOKEN;
this.APP_SECRET = params.APP_SECRET;
this.FB_URL = params.FB_URL || 'https://graph.facebook.com/v3.1/';
this.app = params.app || express();
this.webhook = params.webhook || '/webhook';
this.app.use(_json({ verify: this.verifyRequestSignature.bind(this) }));
this.intelligoClassifier;
}
learn(data: any) {
// Repeat multiple levels
const TextClassifier = neuro.classifiers.multilabel.BinaryRelevance.bind(0, {
binaryClassifierType: neuro.classifiers.Winnow.bind(0, { retrain_count: 10 }),
});
const WordExtractor = (input: any, features: any) => {
return input.split(' ').map((word: string | number) => {
return (features[word] = 1);
});
};
this.intelligoClassifier = new neuro.classifiers.EnhancedClassifier({
classifierType: TextClassifier,
featureExtractor: WordExtractor,
});
this.intelligoClassifier.trainBatch(data);
}
answer(question: any) {
const result = this.intelligoClassifier.classify(question);
return result;
}
initWebhook() {
/*
* Use your own validation token. Check that the token used in the Webhook
* setup is the same token used here.
*/
this.app.get(this.webhook, (req: any, res: any) => {
if (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === this.VALIDATION_TOKEN) {
console.log('Validating webhook');
res.status(200).send(req.query['hub.challenge']);
} else {
console.error('Failed validation. Make sure the validation tokens match.');
res.sendStatus(403);
}
});
/*
* All callbacks for Messenger are POST-ed. They will be sent to the same
* webhook. Be sure to subscribe your app to your page to receive callbacks
* for your page.
* https://developers.facebook.com/docs/messenger-platform/product-overview/setup#subscribe_app
*/
this.app.post(this.webhook, (req: any, res: any) => {
var data = req.body;
if (data.object === 'page') {
for (const pageEntry of data.entry) {
for (const messagingEvent of pageEntry.messaging) {
this.handleEvent(messagingEvent);
}
}
res.sendStatus(200);
}
});
}
// Iterate over each messaging event
handleEvent(event: any) {
if (event.optin) {
let optin = event.optin.ref;
this.emit('optin', event.sender.id, event, optin);
} else if (typeof event.message === 'string') {
this.emit('message', event);
} else if (event.message && !event.message.is_echo) {
this.emit('message', event);
} else if (event.message && event.message.attachment) {
this.emit('attachment', event.sender.id, event.message.attachment, event.message.url, event.message.quickReplies);
} else if (event.delivery) {
let mids = event.delivery.mids;
this.emit('delivery', event.sender.id, event, mids);
} else if (event.read) {
let recipient = event.recipient.id;
this.emit('read', event.sender.id, recipient, event.read);
} else if (event.postback || (event.message && !event.message.is_echo && event.message.quick_reply)) {
let postback = (event.postback && event.postback.payload) || event.message.quick_reply.payload;
let ref = event.postback && event.postback.referral && event.postback.referral.ref;
this.emit('postback', event.sender.id, postback);
} else if (event.referral) {
let ref = event.referral.ref;
this.emit('referral', event.sender.id, event, ref);
} else if (event.account_linking) {
let link = event.account_linking;
this.emit('account_link', event.sender.id, event, link);
} else {
console.error('Invalid format for message.');
}
}
/*
* 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
*
*/
verifyRequestSignature(req: any, res: any, buf: any) {
const 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 {
const elements = signature.split('='),
method = elements[0],
signatureHash = elements[1];
const expectedHash = createHmac('sha1', this.APP_SECRET)
.update(buf)
.digest('hex');
if (signatureHash != expectedHash) {
throw new Error("Couldn't validate the request signature.");
}
}
}
addGreeting(text: string) {
request(
{
url: `${this.FB_URL}me/thread_settings`,
qs: { access_token: this.PAGE_ACCESS_TOKEN },
method: 'POST',
json: {
setting_type: 'greeting',
greeting: {
text: text,
},
},
},
function(error, response, body) {
if (error) {
console.log('Error sending message: ', error);
} else if (response.body.error) {
console.log('Error: ', response.body.error);
}
},
);
}
addGetStartedButton() {
request(
{
url: `${this.FB_URL}me/messenger_profile`,
qs: { access_token: this.PAGE_ACCESS_TOKEN },
method: 'POST',
json: {
get_started: {
payload: 'GET_STARTED_PAYLOAD',
},
},
},
function(error, response, body) {
if (error) {
console.log('Error sending messages: ', error);
} else if (response.body.error) {
console.log('Error: ', response.body.error);
}
},
);
}
addPersistentMenu(persistent_menu: any) {
request(
{
url: `${this.FB_URL}me/messenger_profile`,
qs: { access_token: this.PAGE_ACCESS_TOKEN },
method: 'POST',
json: {
persistent_menu: persistent_menu,
},
},
function(error, response, body) {
if (error) {
console.log('Error sending message: ', error);
} else if (response.body.error) {
console.log('Error: ', response.body.error);
}
},
);
}
removePersistentMenu() {
request(
{
url: `${this.FB_URL}me/thread_settings`,
qs: { access_token: this.PAGE_ACCESS_TOKEN },
method: 'POST',
json: {
setting_type: 'call_to_actions',
thread_state: 'existing_thread',
call_to_actions: [],
},
},
function(error, response, body) {
console.log(response);
if (error) {
console.log('Error sending messages: ', error);
} else if (response.body.error) {
console.log('Error: ', response.body.error);
}
},
);
}
/**
* @param {Recipient|Object} recipientId Recipient object or ID.
* @param {String} messageText
*/
sendTextMessage(recipientId: any, messageText: string) {
this.callSendAPI({
recipient: {
id: recipientId,
},
message: {
text: messageText,
},
});
}
/**
* @param {Recipient|String} recipientId
* @param {String} type Must be 'image', 'audio', 'video' or 'file'.
* @param {String} url URL of the attachment.
*/
sendAttachment(recipientId: any, type: any, url: any) {
this.callSendAPI({
recipient: {
id: recipientId,
},
message: {
attachment: {
type: type,
payload: {
url: url,
},
},
},
});
}
/**
* @param {Recipient|String} recipientId
* @param {String} url URL of the attachment.
*/
sendFileMessage(recipientId: any, url: any) {
this.sendAttachment(recipientId, 'file', url);
}
/**
* @param {Recipient|String} recipientId
* @param {String} url URL of the attachment.
*/
sendImageMessage(recipientId: any, url: any) {
this.sendAttachment(recipientId, 'image', url);
}
/**
* @param {Recipient|String} recipientId
* @param {String} url URL of the attachment.
*/
sendVideoMessage(recipientId: any, url: any) {
this.sendAttachment(recipientId, 'video', url);
}
/**
* @param {Recipient|String} recipientId
* @param {String} url URL of the attachment.
*/
sendAudioMessage(recipientId: any, url: any) {
this.sendAttachment(recipientId, 'audio', url);
}
/**
* @param {Recipient|String} recipientId
* @param {Array.<Element>} elements
*/
sendGenericMessage(recipientId: any, elements: any) {
var messageData = {
recipient: {
id: recipientId,
},
message: {
attachment: {
type: 'template',
payload: {
template_type: 'generic',
elements: elements,
},
},
},
};
this.callSendAPI(messageData);
}
sendButtonMessage(recipientId: any, text: any, buttons: any) {
var messageData = {
recipient: {
id: recipientId,
},
message: {
attachment: {
type: 'template',
payload: {
template_type: 'button',
text: text,
buttons: buttons,
},
},
},
};
this.callSendAPI(messageData);
}
callSendAPI(messageData: any) {
request(
{
uri: `${this.FB_URL}me/messages`,
qs: { access_token: this.PAGE_ACCESS_TOKEN },
method: 'POST',
json: messageData,
},
function(error, response, body) {
if (!error && response.statusCode == 200) {
const recipientId = body.recipient_id,
messageId = body.message_id;
if (messageId) {
console.log('Successfully sent message with id %s to recipient %s', messageId, recipientId);
} else {
console.log('Successfully called Send API for recipient %s', recipientId);
}
} else {
console.error('Failed calling Send API', response.statusCode, response.statusMessage, body.error);
}
},
);
}
/**
* @param {Recipient|String} recipientId
* @param greetings
* @param text
*/
sendWelcome(recipientId: any, greetings: any, text: any) {
// this (aka "the context") is a special keyword inside each function and its value only depends on how the function was called,
// not how/when/where it was defined. It is not affected by lexical scopes, like other variables
const self = this;
request(
{
url: `${this.FB_URL}` + recipientId + `?access_token=` + this.PAGE_ACCESS_TOKEN,
},
function(error, response, body) {
if (error || response.statusCode != 200) return;
const fbProfileBody = JSON.parse(body),
userName = fbProfileBody['first_name'],
randomGreeting = greetings[self.getRandomNumber(0, greetings.length - 1)],
welcomeMsg = `${randomGreeting} ${userName} ${text}`;
self.sendTextMessage(recipientId, welcomeMsg);
},
);
}
/*
* Postback Event
*
* This event is called when a postback is tapped on a Structured Message.
* https://developers.facebook.com/docs/messenger-platform/webhook-reference/postback-received
*
*/
receivedPostback(event: any) {
const senderID = event.sender.id,
recipientID = event.recipient.id,
timeOfPostback = event.timestamp,
payload = event.postback.payload;
console.log(
"Received postback for user %d and page %d with payload '%s' " + 'at %d',
senderID,
recipientID,
payload,
timeOfPostback,
);
this.sendTextMessage(senderID, 'Postback called');
}
/*
* Send a read receipt to indicate the message has been read
*
*/
sendReadReceipt(recipientId: any) {
this.callSendAPI({
recipient: {
id: recipientId,
},
sender_action: 'mark_seen',
});
}
/**
* @param {Recipient|Object} recipientId Recipient object or ID.
*/
sendTypingOn(recipientId: any) {
this.callSendAPI({
recipient: {
id: recipientId,
},
sender_action: 'typing_on',
});
}
/**
* @param {Recipient|Object} recipientId Recipient object or ID.
*/
sendTypingOff(recipientId: any) {
this.callSendAPI({
recipient: {
id: recipientId,
},
sender_action: 'typing_off',
});
}
getRandomNumber(minimum: number, maxmimum: number) {
return Math.floor(Math.exp(Math.random() * Math.log(maxmimum - minimum + 1))) + minimum;
}
randomIntFromInterval(min: number, max: number) {
return this.getRandomNumber(min, max);
}
textMatches(message: string, matchString: string) {
return message.toLowerCase().indexOf(matchString) != -1;
}
}