Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
34 changes: 34 additions & 0 deletions classes/Dispatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Dispatch {
this.templates = reqDir('../routes/messages/Templates/');
this.db = reqDir('../models/db/');
this.helpers = reqDir('../routes/messages/helpers');
this.state = {};
}

async execute(Event, override) {
Expand Down Expand Up @@ -85,6 +86,38 @@ class Dispatch {
return this.import('helpers', ...args);
}

setState(Event) {
// Save a single Event object to be pulled later by the app
// ie: the user is going through onboarding, save the Event
// that triggered the onboarding

// NOTE: Does NOT save across server crashes/restarts at this time
console.log('SAVING STATE');
this.state[Event.user_id] = { ...Event };
}

getState(Event) {
console.log('GETTING STATE');
const state = this.state[Event.user_id];
// Can either simply run the method or set Event to the return
Event.insertPreviousState(state);
return Event;
}

clearState(Event) {
delete this.state[Event.user_id];
}

hasOpenState(Event) {
// Returns true is state for user is empty
return !this.state[Event.user_id];
}

hasState(Event) {
// Returns truthy if user has data in state
return this.state[Event.user_id];
}

async sendTemplate(name, ...args) {
// If you want to immediately run a template instead of importing it then running it,
// run this with any arguments the template wants, everything separated by a comma.
Expand All @@ -98,6 +131,7 @@ class Dispatch {

redirectTo(Event, command) {
Event.validatedCommand = command;
Event.type = 'redirect';
this.execute(Event, true);
}

Expand Down
59 changes: 32 additions & 27 deletions classes/WebhookEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,31 +89,28 @@ module.exports = class WebhookEvent {
return this.bookCount > 1;
}

overrideOnUserMessage(command) {
if (this.type === 'message' && !this.isOverridden) {
this.setOverride(command);
isPostback() {
return this.type === 'postback';
}

overrideOnUserMessage(command = 'get_started') {
if (this.type === 'message') {
return this.setOverride(command);
}
}

async overrideIfNotOnboarded() {
// User has completed onboarding
if (this.hasOwnProperty('prefersLongSummaries')) {
// Event.prefersLongSummaries will exist on the postback
// from request_summary_preference (in English: the user sent the final
// piece needed to complete onboarding and should progress)
const { prefersLongSummaries } = this;
const updatedUser = await Users.edit(
{ id: this.user_id },
{ prefersLongSummaries }
);
this.user = updatedUser;
overrideOnMissingProperty(prop, command = 'get_started') {
if (!this[prop]) {
return this.setOverride(command);
}
}

async overrideIfNotOnboarded() {
if (!this.isOverridden) {
const { user_id } = this;
const userCategories = await UserCategories.retrieve({ user_id });
if (userCategories.length < 3) {
return this.setOverride('pick_category');
return this.setOverride('request_categories');
} else if (!this.user.email) {
return this.setOverride('request_email');
} else if (this.user.prefersLongSummaries === null) {
Expand Down Expand Up @@ -166,9 +163,10 @@ module.exports = class WebhookEvent {
setOverride(command) {
// The first override should receive priority
if (!this.isOverridden && !this.override) {
console.log('Overriding to: ', command);
this.type = 'redirect';
this.isOverridden = true;
this.override = command;
return this.override;
}
}

Expand Down Expand Up @@ -216,11 +214,13 @@ module.exports = class WebhookEvent {
this.response = response;
}

setUser(u) {
setUser(u, addTimeUser) {
const { id, ...user } = u;
this.user = user;
this.user_id = id;
this.addTimedMessage();
if (addTimeUser) {
this.addTimedMessage();
}
}

setValidatedCommand(command) {
Expand Down Expand Up @@ -264,6 +264,12 @@ module.exports = class WebhookEvent {
return allCategories.filter(c => userCategoryIDs.indexOf(c.id) === -1);
}

insertPreviousState(state) {
for (let property in state) {
this[property] = state[property];
}
}

async addTimedMessage() {
const { user_id, page_id } = this;
if (!user_id || !page_id) {
Expand Down Expand Up @@ -310,7 +316,6 @@ module.exports = class WebhookEvent {
}

async _parseUserAction(entry) {
console.log('Received user action');
const { Event } = entry;
const message = entry.messaging[0];

Expand All @@ -326,7 +331,7 @@ module.exports = class WebhookEvent {
Event.isNewUser = true;
}

this.setUser(user);
this.setUser(user, true);

// So many commands currently rely on how many books are available
// for the page, we've added the bookCount as a default
Expand Down Expand Up @@ -354,14 +359,14 @@ module.exports = class WebhookEvent {

isReferral
? this.setEventData({
type: 'referral',
...parsed_data,
...this._handleReferral(message.postback.referral.ref),
type: 'referral'
...this._handleReferral(message.postback.referral.ref)
})
: this.setEventData({
type: 'postback',
...parsed_data,
...JSON.parse(payload),
type: 'postback'
...JSON.parse(payload)
});
} else if (message.referral) {
// This block will fire under one condition:
Expand All @@ -371,9 +376,9 @@ module.exports = class WebhookEvent {
// http://m.me/109461977131004?ref=command=start_book,book_id=1

this.setEventData({
type: 'referral',
...parsed_data,
...this._handleReferral(message.referral.ref),
type: 'referral'
...this._handleReferral(message.referral.ref)
});
} else if (message && message.message) {
// Fires whenever the user types something at the bot
Expand Down
20 changes: 9 additions & 11 deletions jobs/timedMessages.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const TimedMessages = require('../models/db/timedMessages.js');
const Users = require('models/db/users.js');
const Pages = require('models/db/pages.js');
const Message = require('classes/WebhookEvent.js');
const MessageQueue = require('classes/MessageQueue.js');
const GenericTemplate = require('routes/messages/Templates/Generic.js');

module.exports = setInterval(cycleMessages, 1000 * 60 * 30);
Expand All @@ -16,7 +16,7 @@ async function cycleMessages() {
const user = await Users.retrieve({ id: m.user_id }).first();
// This gets us around having to add a page_id column to the timed_messages
// table but likely won't work long term if books lose the page_id column
const page = await Pages.retrieve({ id: m.page_id }).first();
const { access_token } = await Pages.retrieve({ id: m.page_id }).first();
const psid = user.facebook_id;

const response = GenericTemplate([
Expand All @@ -34,16 +34,14 @@ async function cycleMessages() {
}
]);

const timed = new Message(
{
sender: psid,
page,
command: 'timed_message'
},
[response]
);
const timed = new MessageQueue({
sender_id: psid,
access_token,
validatedCommand: 'timed_message'
});

await timed.respond();
timed.add(response);
timed.send();
await TimedMessages.remove(m.id);
console.log('Sent timed response');
}
Expand Down
24 changes: 10 additions & 14 deletions routes/messages/Templates/Book.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,19 @@ module.exports = async (Event, books) => {

if (Event.isMultiBookPage()) {
const usersLibrary = await UserLibraries.retrieve({ user_id });
const isInLibrary = usersLibrary.find(
const isAdding = !usersLibrary.find(
lib => lib.id === parseInt(book_id, 10)
);

if (awaitEvent.isUserOnboarded()) {
// Temporary measure. Don't let the user save to their
// library if they aren't onboarded
buttons.push({
type: 'postback',
title: isInLibrary ? 'Remove from Library' : 'Add to Library',
payload: JSON.stringify({
command: 'toggle_in_library',
book_id,
isAdding: !isInLibrary
})
});
}
buttons.push({
type: 'postback',
title: isAdding ? 'Add to Library' : 'Remove from Library',
payload: JSON.stringify({
command: 'toggle_in_library',
book_id,
isAdding
})
});
}

buttons.push({
Expand Down
40 changes: 0 additions & 40 deletions routes/messages/commands/pick_category.js

This file was deleted.

34 changes: 34 additions & 0 deletions routes/messages/commands/request_categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const UserCategories = require('models/db/userCategories.js');

module.exports = async function(Event) {
if (this.hasState(Event)) {
this.getState(Event);
}

const { user_id } = Event;

const userCategories = await UserCategories.retrieve({ user_id });

// Only show categories the user has not saved
const categories = await Event.getNewCategoriesForUser();

const quick_replies = categories.map(c => ({
title: c.name,
payload: JSON.stringify({
command: 'save_category',
category_id: c.id,
redirect: Event.validatedCommand
})
}));

const intro =
Event.validatedCommand === 'browse' ? 'To get started, please ' : 'Please ';

const text = !userCategories.length
? intro + 'select 3 categories'
: userCategories.length === 1
? '2 more to go...'
: 'Last one!';

return this.sendTemplate('QuickReply', text, quick_replies);
};
13 changes: 0 additions & 13 deletions routes/messages/commands/request_email.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
module.exports = async Event => {
// const textResponses = {
// pick_category:
// "Great, I have what I need to make some suggestions! Though first, I'd like to make an account for you so I can remember your preferences across platforms. What email address can I attach to your account?",
// get_started:
// 'I have some suggestions ready to go for you, just respond with a valid email and I can get them right to you!',
// default:
// "We use an email address to identify you across platforms. Type a valid email address at any time and I'll save it to your account so you can proceed!"
// };

// const text = textResponses[Event.command]
// ? textResponses[Event.command]
// : textResponses.default;

return [
{
text: "And what's your email address?"
Expand Down
11 changes: 8 additions & 3 deletions routes/messages/commands/request_summary_preference.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
// Part of onboarding, requires the user to specify if they want long or short summaries.
// The value is saved in the users table under "prefersLongSummaries" inside of the "browse" command
module.exports = async function() {

module.exports = async function(Event) {
// If Event was previously saved to onboard, restore the Event
if (this.hasState(Event)) {
this.getState(Event);
}
const text = 'Would you like to read 2-minute or 10-15 minute summaries?';
const buttons = [
{
title: '2-minute',
payload: JSON.stringify({
command: 'browse',
command: 'save_summary_preferences',
prefersLongSummaries: false
})
},
{
title: '10-minute',
payload: JSON.stringify({
command: 'browse',
command: 'save_summary_preferences',
prefersLongSummaries: true
})
}
Expand Down
Loading