Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add smart reply to remove boilerplate #53

Merged
merged 15 commits into from
Apr 15, 2017
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ We emit a variety of events. Attach listeners like:
// messenger.on(eventName, ({dataItem1, dataItem2}) => {});
const { Messenger, Text, Image } = require('launch-vehicle-fbm');
const messenger = new Messenger();
messenger.on('text', ({senderId, text}) => {
messenger.on('text', ({reply, text}) => {
if (text.includes('corgis')) {
messenger.send(senderId, new Text('aRf aRf!'))
.then(() => messenger.send(senderId, new Image('https://i.imgur.com/izwcQLS.jpg')));
reply(new Text('aRf aRf!'))
.then(() => reply(new Image('https://i.imgur.com/izwcQLS.jpg')));
}
});
messenger.start();
Expand All @@ -52,52 +52,62 @@ messenger.start();
The event name and what's in the `data` for each event handler:

* `message` Any kind of message event. This is sent in addition to the events for specific message types.
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `message` Direct access to `event.message`
* `text` Text message
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `source` One of `quickReply`, `postback`, `text`
* `text` Message content, `event.message.text` for text events, `payload` for `postback` and `quickReply` events
* `text.greeting` (optional, defaults to enabled) Text messages that match common greetings
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `firstName` Trimmed first name from the user's public Facebook profile
* `surName` Trimmed first name from the user's public Facebook profile
* `fullName` Concatenating of `firstName` and `surName` with a single, separating space
* `text.help` (optional, defaults to enabled) Text messages that match requests for assistance
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `message.image` Image (both attached and from user's camera)
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `url` Direct access to `event.message.attachments[0].payload.url` for the url of the image
* `message.sticker` Sticker
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `message.thumbsup` User clicked the "thumbsup"/"like" button
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `message.text` For conversation, use the `text` event
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `text` Message content, `event.message.text` for text events
* `message.quickReply` For conversation, use the `text` event, this is for the raw message sent via a quick reply button
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `session` [A Session object](#the-session-object) you can mutate
* `source` One of `quickReply`, `postback`, `text`
* `payload` Quick reply content, `event.quick_reply.payload`
* `postback` For conversation, use the `text` event, this is for the raw message sent via a postback
* `reply: Function` Reply back to the user with the arguments
* `event` The raw event
* `senderId` The ID of the sender
* `payload` Direct access to `event.postback.payload`
Expand All @@ -117,11 +127,17 @@ messages too. You'll need to examine `event.message.is_echo` in your handlers.

### Sending responses to the user

Send responses back to the user like:
You're given a `reply` in event emitters (see above):

messenger.send(senderId, responseObject, [pageId])
reply(responseObject)

But this syntax will be deprecated for a simpler version in the future.
The original syntax will also work:

messenger.send(senderId, responseObject)

or if you have multiple Pages, you can send responses like:

messenger.pageSend(pageId, senderId, responseObject)

Some factories for generating `responseObject` are available at the top level and
are also available in a `responses` object if you need a namespace:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"scripts": {
"lint": "eslint --ignore-path .gitignore .",
"pretest": "npm run lint",
"tdd": "NODE_ENV=test env $(cat example.env | xargs) mocha -R dot --recursive --watch",
"test": "NODE_ENV=test env $(cat example.env | xargs) istanbul cover _mocha -- --recursive"
"tdd": "NODE_ENV=test env $(cat test.env | xargs) mocha -R dot --recursive --watch",
"test": "NODE_ENV=test env $(cat test.env | xargs) istanbul cover _mocha -- --recursive"
},
"engines": {
"node": ">6.0"
Expand Down
92 changes: 57 additions & 35 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ const DEFAULT_HELP_REGEX = /^help\b/i;

/*:: type Session = {count: number, profile: ?Object} */

class Response {
/*:: _messenger: Messenger */
constructor(messenger/*: Messenger */, options/*: Object */) {
Object.assign(this, options);
['senderId', 'session'].forEach((required) => {
// $FlowFixMe
if (!this[required]) {
throw new Error(`Incomplete Response, missing ${required}: ${JSON.stringify(options)}`);
}
});
this._messenger = messenger;
// $FlowFixMe
this.reply = this.reply.bind(this);
}

reply(response) {
// $FlowFixMe
return this._messenger.pageSend(this.session._pageId, this.senderId, response);
}
}

class Messenger extends EventEmitter {
/*:: app: Object */
/*:: cache: Object */
Expand Down Expand Up @@ -61,7 +82,15 @@ class Messenger extends EventEmitter {
this.cache = new Cacheman('sessions', {ttl: SESSION_TIMEOUT_MS / 1000});
}

this.pages = pages;
if (pages && Object.keys(pages).length) {
this.pages = pages;
} else if (config.has('messenger.pageAccessToken') && config.has('facebook.pageId')) {
this.pages = {};
this.pages[config.get('facebook.pageId')] = config.get('messenger.pageAccessToken');
} else {
this.pages = {};
debug("MISSING options.pages; you won't be able to reply or get profile information");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my favorite line in this PR. It shows how those two config variables are actually optional depending on what your bot is doing. While the SDK doesn't exactly gracefully degrade, we could make it.

}

this.app = express();
this.app.engine('handlebars', exphbs({defaultLayout: 'main'}));
Expand Down Expand Up @@ -181,6 +210,7 @@ class Messenger extends EventEmitter {
.then((session) => this.saveSession(session));
}

// TODO flesh these out later
doLogin(senderId/*: number */, pageId/*: string */) {
// Open question: is building the event object worth it for the 'emit'?
const event = {
Expand Down Expand Up @@ -208,21 +238,14 @@ class Messenger extends EventEmitter {
}
}
};
this.send(senderId, messageData);
this.pageSend(pageId, senderId, messageData);
}

getPublicProfile(senderId/*: number */, pageId/*: string|void */)/*: Promise<Object> */ {
let pageAccessToken;
if (!pageId) {
// This will be deprecated in the future in favor of finding the token from `this.pages`
pageAccessToken = config.get('messenger.pageAccessToken');
} else {
pageAccessToken = this.pages[pageId];
// eslint-disable-next-line eqeqeq
if (!pageAccessToken && pageId != config.get('facebook.pageId')) {
throw new Error(`Tried accessing a profile for page ${pageId} but the page config is missing`);
}
pageAccessToken = config.get('messenger.pageAccessToken');
// TODO make `pageId` required, then simplify. `getPublicProfile` is only internal right now
const pageAccessToken = this.pages[pageId || config.get('facebook.pageId')];
if (!pageAccessToken) {
throw new Error(`Missing page config for: ${pageId || ''}`);
}
const options = {
json: true,
Expand All @@ -246,14 +269,15 @@ class Messenger extends EventEmitter {
const senderId = event.sender.id;
// The 'ref' is the data passed through the 'Send to Messenger' call
const optinRef = event.optin.ref;
this.emit('auth', {event, senderId, session, optinRef});
this.emit('auth', new Response(this, {event, senderId, session, optinRef}));
debug('onAuth for user:%d with param: %j', senderId, optinRef);
}

/*
This is not an event triggered by Messenger, it is the post-back from the
static Facebook login page that is made to look similar to an 'event'
*/
// TODO flesh these out later
onLink(event) {
const senderId = event.sender.id;
const fbData = event.facebook;
Expand All @@ -266,7 +290,7 @@ class Messenger extends EventEmitter {
const senderId = event.sender.id;
const {message} = event;

this.emit('message', {event, senderId, session, message});
this.emit('message', new Response(this, {event, senderId, session, message}));
debug('onMessage from user:%d with message: %j', senderId, message);

const {
Expand All @@ -281,28 +305,28 @@ class Messenger extends EventEmitter {
const surName = session.profile && session.profile.last_name.trim() || '';
const fullName = `${firstName} ${surName}`;

this.emit('text.greeting', {event, senderId, session, firstName, surName, fullName});
this.emit('text.greeting', new Response(this, {event, senderId, session, firstName, surName, fullName}));
return;
}

if (this.help.test(text)) {
this.emit('text.help', {event, senderId, session});
this.emit('text.help', new Response(this, {event, senderId, session}));
return;
}

if (quickReply) {
debug('message.quickReply payload: "%s"', quickReply.payload);

this.emit('text', {event, senderId, session, source: 'quickReply', text: quickReply.payload});
this.emit('message.quickReply', {event, senderId, session, payload: quickReply.payload});
this.emit('text', new Response(this, {event, senderId, session, source: 'quickReply', text: quickReply.payload}));
this.emit('message.quickReply', new Response(this, {event, senderId, session, payload: quickReply.payload}));
return;
}

if (text) {
debug('text user:%d text: "%s" count: %s seq: %s',
senderId, text, session.count, message.seq);
this.emit('text', {event, senderId, session, source: 'text', text: text.toLowerCase().trim()});
this.emit('message.text', {event, senderId, session, text});
this.emit('text', new Response(this, {event, senderId, session, source: 'text', text: text.toLowerCase().trim()}));
this.emit('message.text', new Response(this, {event, senderId, session, text}));
return;
}

Expand All @@ -326,7 +350,7 @@ class Messenger extends EventEmitter {
// - message.video
// https://developers.facebook.com/docs/messenger-platform/webhook-reference/message

this.emit(`message.${type}`, {event, senderId, session, attachment, url: attachment.payload.url});
this.emit(`message.${type}`, new Response(this, {event, senderId, session, attachment, url: attachment.payload.url}));
return;
}
}
Expand All @@ -340,8 +364,8 @@ class Messenger extends EventEmitter {

debug("onPostback for user:%d with payload '%s'", senderId, payload);

this.emit('text', {event, senderId, session, source: 'postback', text: payload});
this.emit('postback', {event, senderId, session, payload});
this.emit('text', new Response(this, {event, senderId, session, source: 'postback', text: payload}));
this.emit('postback', new Response(this, {event, senderId, session, payload}));
}

// HELPERS
Expand All @@ -355,17 +379,14 @@ class Messenger extends EventEmitter {
return this.cache.set(session._key, session);
}

send(recipientId/*: string|number */, messageData/*: Object */, pageId/*: string|void */)/* Promise<Object> */ {
let pageAccessToken;
if (!pageId) {
// This will be deprecated in the future in favor of finding the token from `this.pages`
pageAccessToken = config.get('messenger.pageAccessToken');
} else {
pageAccessToken = this.pages[pageId];
// eslint-disable-next-line eqeqeq
if (!pageAccessToken && pageId != config.get('facebook.pageId')) {
throw new Error(`Tried accessing a profile for page ${pageId} but the page config is missing`);
}
send(recipientId/*: number */, messageData/*: Object */) {
return this.pageSend(config.get('facebook.pageId'), recipientId, messageData);
}

pageSend(pageId/*: string|number */, recipientId/*: string|number */, messageData/*: Object */)/* Promise<Object> */ {
let pageAccessToken = this.pages[pageId];
if (!pageAccessToken) {
throw new Error(`Missing page config for: ${pageId}`);
}
const options = {
uri: 'https://graph.facebook.com/v2.8/me/messages',
Expand Down Expand Up @@ -410,3 +431,4 @@ class Messenger extends EventEmitter {

exports.SESSION_TIMEOUT_MS = SESSION_TIMEOUT_MS;
exports.Messenger = Messenger;
exports.Response = Response;
10 changes: 10 additions & 0 deletions test.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FACEBOOK_APP_ID=1234567890123456
FACEBOOK_PAGE_ID=1029384756
MESSENGER_APP_SECRET=0123456789abcdef0123456789abcdef
MESSENGER_VALIDATION_TOKEN=validate_me
MESSENGER_PAGE_ACCESS_TOKEN=ThatsAReallyLongStringYouGotThere
SERVER_URL=https://localhost/
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T123456/B33Z/Pajamas
SLACK_CHANNEL=channel
LOG_FILE=/var/log/my-bot/chat.log
ALLOW_CONFIG_MUTATIONS=Y
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALLOW_CONFIG_MUTATIONS is the new config I needed. This will let you do PORT=-1 in #54 without potentially confusing users.

Loading