claudia-bot-builder-fb
Version:
Create chat-bots for various platforms and deploy to AWS Lambda quickly
1,072 lines (814 loc) • 25.9 kB
JavaScript
'use strict';
const isUrl = require('../is-url');
const breakText = require('../breaktext');
const messageTags = ['COMMUNITY_ALERT', 'CONFIRMED_EVENT_REMINDER',
'NON_PROMOTIONAL_SUBSCRIPTION', 'PAIRING_UPDATE', 'APPLICATION_UPDATE',
'ACCOUNT_UPDATE', 'PAYMENT_UPDATE', 'PERSONAL_FINANCE_UPDATE',
'SHIPPING_UPDATE', 'RESERVATION_UPDATE','ISSUE_RESOLUTION',
'APPOINTMENT_UPDATE', 'GAME_EVENT', 'TRANSPORTATION_UPDATE',
'FEATURE_FUNCTIONALITY_UPDATE', 'TICKET_UPDATE'];
function isNumber(number) {
return !isNaN(parseFloat(number)) && isFinite(number);
}
class FacebookTemplate {
constructor() {
this.template = {};
this.template.messaging_type = 'RESPONSE';
}
setNotificationType(type) {
if (type !== 'REGULAR' && type !== 'SILENT_PUSH' && type !== 'NO_PUSH')
throw new Error('Notification type must be one of REGULAR, SILENT_PUSH, or NO_PUSH');
this.template.notification_type = type;
return this;
}
setMessagingType(type) {
if (type !== 'RESPONSE' && type !== 'UPDATE' && type !== 'MESSAGE_TAG') {
type = 'RESPONSE';
}
this.template.messaging_type = type;
return this;
}
setMessageTag(tag) {
if (messageTags.indexOf(tag) === -1)
throw new Error(`Message tag must be one of the following: ${JSON.stringify(messageTags, null, 2)}`);
this.template.message_tag = tag;
return this;
}
addQuickReply(text, payload, imageUrl) {
if (!text || !payload)
throw new Error('Both text and payload are required for a quick reply');
if (payload.length > 1000)
throw new Error('Payload can not be more than 1000 characters long');
if (imageUrl && !isUrl(imageUrl))
throw new Error('Image has a bad url');
if (text.length > 20)
text = breakText(text, 20)[0];
let quickReply = {
content_type: 'text',
title: text,
payload: payload
};
if (imageUrl) quickReply.image_url = imageUrl;
return this.addQuickReplyItem(quickReply);
}
addQuickReplyLocation() {
let quickReply = {
content_type: 'location'
};
return this.addQuickReplyItem(quickReply);
}
addQuickReplyUserEmail() {
let quickReply = {
content_type: 'user_email'
};
return this.addQuickReplyItem(quickReply);
}
addQuickReplyItem(quickReply) {
if (!quickReply)
throw new TypeError('"quickReply" is null or not defined');
if (!this.template.quick_replies)
this.template.quick_replies = [];
if (this.template.quick_replies.length === 11)
throw new Error('There can not be more than 11 quick replies');
this.template.quick_replies.push(quickReply);
return this;
}
get() {
return this.template;
}
}
class Text extends FacebookTemplate {
constructor(text) {
super();
if (!text)
throw new Error('Text is required for text template');
this.template = {
text: text
};
}
}
class Attachment extends FacebookTemplate {
constructor(url, type) {
super();
if (!url || !isUrl(url))
throw new Error('Attachment template requires a valid URL as a first parameter');
this.template = {
attachment: {
type: type || 'file',
payload: {
url: url
}
}
};
}
}
class Image extends FacebookTemplate {
constructor(url) {
super();
if (!url || !isUrl(url))
throw new Error('Image template requires a valid URL as a first parameter');
this.template = {
attachment: {
type: 'image',
payload: {
url: url
}
}
};
}
}
class Audio extends FacebookTemplate {
constructor(url) {
super();
if (!url || !isUrl(url))
throw new Error('Audio template requires a valid URL as a first parameter');
this.template = {
attachment: {
type: 'audio',
payload: {
url: url
}
}
};
}
}
class Video extends FacebookTemplate {
constructor(url) {
super();
if (!url || !isUrl(url))
throw new Error('Video template requires a valid URL as a first parameter');
this.template = {
attachment: {
type: 'video',
payload: {
url: url
}
}
};
}
}
class File extends FacebookTemplate {
constructor(url) {
super();
if (!url || !isUrl(url))
throw new Error('File attachment template requires a valid URL as a first parameter');
this.template = {
attachment: {
type: 'file',
payload: {
url: url
}
}
};
}
}
class Generic extends FacebookTemplate {
constructor() {
super();
this.bubbles = [];
this.template = {
attachment: {
type: 'template',
payload: {
template_type: 'generic',
elements: []
}
}
};
}
useSquareImages() {
this.template.attachment.payload.image_aspect_ratio = 'square';
return this;
}
getLastBubble() {
if (!this.bubbles || !this.bubbles.length)
throw new Error('Add at least one bubble first!');
return this.bubbles[this.bubbles.length - 1];
}
addBubble(title, subtitle) {
if (this.bubbles.length === 10)
throw new Error('10 bubbles are maximum for Generic template');
if (!title)
throw new Error('Bubble title cannot be empty');
if (title.length > 80)
throw new Error('Bubble title cannot be longer than 80 characters');
if (subtitle && subtitle.length > 80)
throw new Error('Bubble subtitle cannot be longer than 80 characters');
let bubble = {
title: title
};
if (subtitle)
bubble['subtitle'] = subtitle;
this.bubbles.push(bubble);
return this;
}
addUrl(url) {
if (!url)
throw new Error('URL is required for addUrl method');
if (!isUrl(url))
throw new Error('URL needs to be valid for addUrl method');
this.getLastBubble()['item_url'] = url;
return this;
}
addImage(url) {
if (!url)
throw new Error('Image URL is required for addImage method');
if (!isUrl(url))
throw new Error('Image URL needs to be valid for addImage method');
this.getLastBubble()['image_url'] = url;
return this;
}
addDefaultAction(url) {
const bubble = this.getLastBubble();
if (bubble.default_action)
throw new Error('Bubble already has default action');
if (!url)
throw new Error('Bubble default action URL is required');
if (!isUrl(url))
throw new Error('Bubble default action URL must be valid URL');
bubble.default_action = {
type: 'web_url',
url: url
};
return this;
}
addButtonByType(title, value, type, options) {
if (!title)
throw new Error('Button title cannot be empty');
const bubble = this.getLastBubble();
bubble.buttons = bubble.buttons || [];
if (bubble.buttons.length === 3)
throw new Error('3 buttons are already added and that\'s the maximum');
if (!title)
throw new Error('Button title cannot be empty');
const button = {
title: title,
type: type || 'postback'
};
if (type === 'web_url') {
button.url = value;
} else if (type === 'account_link') {
delete button.title;
button.url = value;
} else if (type === 'phone_number') {
button.payload = value;
} else if (type === 'payment') {
button.payload = value;
button.payment_summary = options.paymentSummary;
} else if (type === 'element_share' || type === 'account_unlink') {
delete button.title;
if (type === 'element_share' && options && typeof options.shareContent)
button.share_contents = options.shareContent;
} else {
button.type = 'postback';
button.payload = value;
}
bubble.buttons.push(button);
return this;
}
addButton(title, value) {
// Keeping this to prevent breaking change
if (!title)
throw new Error('Button title cannot be empty');
if (!value)
throw new Error('Button value is required');
if (isUrl(value)) {
return this.addButtonByType(title, value, 'web_url');
} else {
return this.addButtonByType(title, value, 'postback');
}
}
addCallButton(title, phoneNumber) {
if (!/^\+[0-9]{4,20}$/.test(phoneNumber))
throw new Error('Call button value needs to be a valid phone number in following format: +1234567...');
return this.addButtonByType(title, phoneNumber, 'phone_number');
}
addShareButton(shareContent) {
return this.addButtonByType('Share', null, 'element_share', {
shareContent: shareContent || null
});
}
addBuyButton(title, value, paymentSummary) {
if (!value)
throw new Error('Button value is required');
if (typeof paymentSummary !== 'object')
throw new Error('Payment summary is required for buy button');
return this.addButtonByType(title, value, 'payment', {
paymentSummary: paymentSummary
});
}
addLoginButton(url) {
if (!isUrl(url))
throw new Error('Valid URL is required for Login button');
return this.addButtonByType('Login', url, 'account_link');
}
addLogoutButton() {
return this.addButtonByType('Logout', null, 'account_unlink');
}
get() {
if (!this.bubbles || !this.bubbles.length)
throw new Error('Add at least one bubble first!');
this.template.attachment.payload.elements = this.bubbles;
return this.template;
}
}
class Button extends FacebookTemplate {
constructor(text) {
super();
if (!text)
throw new Error('Button template text cannot be empty');
if (text.length > 640)
throw new Error('Button template text cannot be longer than 640 characters');
this.template = {
attachment: {
type: 'template',
payload: {
template_type: 'button',
text: text,
buttons: []
}
}
};
}
addButtonByType(title, value, type, options) {
if (!title)
throw new Error('Button title cannot be empty');
if (this.template.attachment.payload.buttons.length === 3)
throw new Error('3 buttons are already added and that\'s the maximum');
const button = {
title: title,
type: type || 'postback'
};
if (type === 'web_url') {
button.url = value;
} else if (type === 'account_link') {
delete button.title;
button.url = value;
} else if (type === 'phone_number') {
button.payload = value;
} else if (type === 'payment') {
button.payload = value;
button.payment_summary = options.paymentSummary;
} else if (type === 'element_share' || type === 'account_unlink') {
delete button.title;
if (type === 'element_share' && options && typeof options.shareContent)
button.share_contents = options.shareContent;
} else {
button.type = 'postback';
button.payload = value;
}
this.template.attachment.payload.buttons.push(button);
return this;
}
addButton(title, value) {
// Keeping this to prevent breaking change
if (!title)
throw new Error('Button title cannot be empty');
if (!value)
throw new Error('Button value is required');
if (isUrl(value)) {
return this.addButtonByType(title, value, 'web_url');
} else {
return this.addButtonByType(title, value, 'postback');
}
}
addCallButton(title, phoneNumber) {
if (!/^\+[0-9]{4,20}$/.test(phoneNumber))
throw new Error('Call button value needs to be a valid phone number in following format: +1234567...');
return this.addButtonByType(title, phoneNumber, 'phone_number');
}
addShareButton(shareContent) {
return this.addButtonByType('Share', null, 'element_share', {
shareContent: shareContent || null
});
}
addBuyButton(title, value, paymentSummary) {
if (!value)
throw new Error('Button value is required');
if (typeof paymentSummary !== 'object')
throw new Error('Payment summary is required for buy button');
return this.addButtonByType(title, value, 'payment', {
paymentSummary: paymentSummary
});
}
addLoginButton(url) {
if (!isUrl(url))
throw new Error('Valid URL is required for Login button');
return this.addButtonByType('Login', url, 'account_link');
}
addLogoutButton() {
return this.addButtonByType('Logout', null, 'account_unlink');
}
get() {
if (this.template.attachment.payload.buttons.length === 0)
throw new Error('Add at least one button first!');
return this.template;
}
}
class Receipt extends FacebookTemplate {
constructor(name, orderNumber, currency, paymentMethod) {
super();
if (!name)
throw new Error('Recipient\'s name cannot be empty');
if (!orderNumber)
throw new Error('Order number cannot be empty');
if (!currency)
throw new Error('Currency cannot be empty');
if (!paymentMethod)
throw new Error('Payment method cannot be empty');
this.template = {
attachment: {
type: 'template',
payload: {
template_type: 'receipt',
recipient_name: name,
order_number: orderNumber,
currency: currency,
payment_method: paymentMethod,
elements: [],
summary: {}
}
}
};
}
addTimestamp(timestamp) {
if (!timestamp)
throw new Error('Timestamp is required for addTimestamp method');
if (!(timestamp instanceof Date))
throw new Error('Timestamp needs to be a valid Date object');
this.template.attachment.payload.timestamp = timestamp.getTime();
return this;
}
addOrderUrl(url) {
if (!url)
throw new Error('Url is required for addOrderUrl method');
if (!isUrl(url))
throw new Error('Url needs to be valid for addOrderUrl method');
this.template.attachment.payload.order_url = url;
return this;
}
getLastItem() {
if (!this.template.attachment.payload.elements || !this.template.attachment.payload.elements.length)
throw new Error('Add at least one order item first!');
return this.template.attachment.payload.elements[this.template.attachment.payload.elements.length - 1];
}
addItem(title) {
if (!title)
throw new Error('Item title is required');
this.template.attachment.payload.elements.push({
title: title
});
return this;
}
addSubtitle(subtitle) {
if (!subtitle)
throw new Error('Subtitle is required for addSubtitle method');
let item = this.getLastItem();
item.subtitle = subtitle;
return this;
}
addQuantity(quantity) {
if (!quantity)
throw new Error('Quantity is required for addQuantity method');
if (!isNumber(quantity))
throw new Error('Quantity needs to be a number');
let item = this.getLastItem();
item.quantity = quantity;
return this;
}
addPrice(price) {
if (!price)
throw new Error('Price is required for addPrice method');
if (!isNumber(price))
throw new Error('Price needs to be a number');
let item = this.getLastItem();
item.price = price;
return this;
}
addCurrency(currency) {
if (!currency)
throw new Error('Currency is required for addCurrency method');
let item = this.getLastItem();
item.currency = currency;
return this;
}
addImage(image) {
if (!image)
throw new Error('Absolute url is required for addImage method');
if (!isUrl(image))
throw new Error('Valid absolute url is required for addImage method');
let item = this.getLastItem();
item.image_url = image;
return this;
}
addShippingAddress(street1, street2, city, zip, state, country) {
if (!street1)
throw new Error('Street is required for addShippingAddress');
if (!city)
throw new Error('City is required for addShippingAddress method');
if (!zip)
throw new Error('Zip code is required for addShippingAddress method');
if (!state)
throw new Error('State is required for addShippingAddress method');
if (!country)
throw new Error('Country is required for addShippingAddress method');
this.template.attachment.payload.address = {
street_1: street1,
street_2: street2 || '',
city: city,
postal_code: zip,
state: state,
country: country
};
return this;
}
addAdjustment(name, amount) {
if (!amount || !isNumber(amount))
throw new Error('Adjustment amount must be a number');
let adjustment = {};
if (name)
adjustment.name = name;
if (amount)
adjustment.amount = amount;
if (name || amount) {
this.template.attachment.payload.adjustments = this.template.attachment.payload.adjustments || [];
this.template.attachment.payload.adjustments.push(adjustment);
}
return this;
}
addSubtotal(subtotal) {
if (!subtotal)
throw new Error('Subtotal is required for addSubtotal method');
if (!isNumber(subtotal))
throw new Error('Subtotal must be a number');
this.template.attachment.payload.summary.subtotal = subtotal;
return this;
}
addShippingCost(shippingCost) {
if (!shippingCost)
throw new Error('shippingCost is required for addShippingCost method');
if (!isNumber(shippingCost))
throw new Error('Shipping cost must be a number');
this.template.attachment.payload.summary.shipping_cost = shippingCost;
return this;
}
addTax(tax) {
if (!tax)
throw new Error('Total tax amount is required for addSubtotal method');
if (!isNumber(tax))
throw new Error('Total tax amount must be a number');
this.template.attachment.payload.summary.total_tax = tax;
return this;
}
addTotal(total) {
if (!total)
throw new Error('Total amount is required for addSubtotal method');
if (!isNumber(total))
throw new Error('Total amount must be a number');
this.template.attachment.payload.summary.total_cost = total;
return this;
}
get() {
if (!this.template.attachment.payload.elements.length)
throw new Error('At least one element/item is required');
if (!this.template.attachment.payload.summary.total_cost)
throw new Error('Total amount is required');
return this.template;
}
}
class List extends FacebookTemplate {
constructor(topElementStyle) {
super();
this.bubbles = [];
this.template = {
attachment: {
type: 'template',
payload: {
template_type: 'list',
top_element_style: topElementStyle ? topElementStyle : 'large',
elements: [],
buttons: []
}
}
};
}
getFirstBubble() {
if (!this.bubbles || !this.bubbles.length)
throw new Error('Add at least one bubble first!');
return this.bubbles[0];
}
getLastBubble() {
if (!this.bubbles || !this.bubbles.length)
throw new Error('Add at least one bubble first!');
return this.bubbles[this.bubbles.length - 1];
}
addBubble(title, subtitle) {
if (this.bubbles.length === 4)
throw new Error('4 bubbles are maximum for List template');
if (!title)
throw new Error('Bubble title cannot be empty');
if (title.length > 80)
throw new Error('Bubble title cannot be longer than 80 characters');
if (subtitle && subtitle.length > 80)
throw new Error('Bubble subtitle cannot be longer than 80 characters');
let bubble = {
title: title
};
if (subtitle)
bubble['subtitle'] = subtitle;
this.bubbles.push(bubble);
return this;
}
addImage(url) {
if (!url)
throw new Error('Image URL is required for addImage method');
if (!isUrl(url))
throw new Error('Image URL needs to be valid for addImage method');
this.getLastBubble()['image_url'] = url;
return this;
}
addDefaultAction(url) {
const bubble = this.getLastBubble();
if (bubble.default_action)
throw new Error('Bubble already has default action');
if (!url)
throw new Error('Bubble default action URL is required');
if (!isUrl(url))
throw new Error('Bubble default action URL must be valid URL');
bubble.default_action = {
type: 'web_url',
url: url
};
return this;
}
addButton(title, value, type) {
const bubble = this.getLastBubble();
bubble.buttons = bubble.buttons || [];
if (bubble.buttons.length === 1)
throw new Error('One button is already added and that\'s the maximum');
if (!title)
throw new Error('Button title cannot be empty');
if (!value)
throw new Error('Button value is required');
const button = {
title: title
};
if (isUrl(value)) {
button.type = 'web_url';
button.url = value;
} else {
button.type = 'postback';
button.payload = value;
}
if (type) {
button.type = type;
}
bubble.buttons.push(button);
return this;
}
addShareButton() {
const bubble = this.getLastBubble();
bubble.buttons = bubble.buttons || [];
if (bubble.buttons.length === 1)
throw new Error('One button is already added and that\'s the maximum');
const button = {
type: 'element_share'
};
bubble.buttons.push(button);
return this;
}
addListButton(title, value, type) {
if (this.template.attachment.payload.buttons.length === 1)
throw new Error('One List button is already added and that\'s the maximum');
if (!title)
throw new Error('List button title cannot be empty');
if (!value)
throw new Error('List button value is required');
const button = {
title: title
};
if (isUrl(value)) {
button.type = 'web_url';
button.url = value;
} else {
button.type = 'postback';
button.payload = value;
}
if (type) {
button.type = type;
}
this.template.attachment.payload.buttons.push(button);
return this;
}
get() {
if (!this.bubbles || !this.bubbles.length || this.bubbles.length < 2)
throw new Error('2 bubbles are minimum for List template!');
if (this.template.attachment.payload.top_element_style === 'large' && !this.getFirstBubble()['image_url'])
throw new Error('You need to add image to the first bubble because you use `large` top element style');
this.template.attachment.payload.elements = this.bubbles;
return this.template;
}
}
class ChatAction {
constructor(action) {
const AVAILABLE_TYPES = ['typing_on', 'typing_off', 'mark_seen'];
if (AVAILABLE_TYPES.indexOf(action) < 0)
throw new Error('Valid action is required for Facebook ChatAction template. Available actions are: typing_on, typing_off and mark_seen.');
this.template = {
sender_action: action
};
}
get() {
return this.template;
}
}
class Pause {
constructor(milliseconds) {
this.template = {
claudiaPause: milliseconds || 500
};
}
get() {
return this.template;
}
}
/* Deprecated methods */
class text extends Text {
constructor(text) {
super(text);
console.log('Deprecation notice: please use .Text instead of .text method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class attachment extends Attachment {
constructor(url, type) {
super(url, type);
console.log('Deprecation notice: please use .Attachment instead of .attachment method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class image extends Image {
constructor(url) {
super(url);
console.log('Deprecation notice: please use .Image instead of .image method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class audio extends Audio {
constructor(url) {
super(url);
console.log('Deprecation notice: please use .Audio instead of .audio method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class video extends Video {
constructor(url) {
super(url);
console.log('Deprecation notice: please use .Video instead of .video method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class file extends File {
constructor(url) {
super(url);
console.log('Deprecation notice: please use .File instead of .file method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class generic extends Generic {
constructor() {
super();
console.log('Deprecation notice: please use .Generic instead of .generic method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class button extends Button {
constructor(text) {
super(text);
console.log('Deprecation notice: please use .Button instead of .button method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
class receipt extends Receipt {
constructor(name, orderNumber, currency, paymentMethod) {
super(name, orderNumber, currency, paymentMethod);
console.log('Deprecation notice: please use .Receipt instead of .receipt method, lower case method names will be removed in next major version of Claudia bot builder');
}
}
module.exports = {
BaseTemplate: FacebookTemplate,
Text: Text,
Attachment: Attachment,
Image: Image,
Audio: Audio,
Video: Video,
File: File,
Generic: Generic,
Button: Button,
Receipt: Receipt,
List: List,
ChatAction: ChatAction,
Pause: Pause,
// Deprecated methods
text: text,
attachment: attachment,
image: image,
audio: audio,
video: video,
file: file,
generic: generic,
button: button,
receipt: receipt
};