@sava.team/broid-messenger
Version:
Convert Facebook Messenger messages into Activity Streams 2 with Broid Integration
327 lines (326 loc) • 13.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const broid_schemas_1 = require("@sava.team/broid-schemas");
const broid_utils_1 = require("@sava.team/broid-utils");
const Promise = require("bluebird");
const events_1 = require("events");
const express_1 = require("express");
const R = require("ramda");
const rp = require("request-promise");
const rxjs_1 = require("rxjs");
const uuid = require("uuid");
const helpers_1 = require("./helpers");
const Parser_1 = require("./Parser");
const WebHookServer_1 = require("./WebHookServer");
class Adapter {
constructor(obj) {
this.serviceID = (obj && obj.serviceID) || uuid.v4();
this.logLevel = (obj && obj.logLevel) || 'info';
this.token = (obj && obj.token) || null;
this.tokenSecret = (obj && obj.tokenSecret) || null;
this.consumerSecret = (obj && obj.consumerSecret) || null;
this.storeUsers = new Map();
this.connections = new Map();
this.parser = new Parser_1.Parser(this.serviceName(), this.serviceID, this.logLevel);
this.logger = new broid_utils_1.Logger('adapter', this.logLevel);
this.router = this.setupRouter();
this.emitter = new events_1.EventEmitter();
this.versionAPI = 'v10.0';
if (obj.http) {
this.webhookServer = new WebHookServer_1.WebHookServer(obj.http, this.router, this.logLevel);
}
}
users() {
return Promise.resolve(this.storeUsers);
}
channels() {
return Promise.reject(new Error('Not supported'));
}
serviceId() {
return this.serviceID;
}
serviceName() {
return 'messenger';
}
getRouter() {
if (this.webhookServer) {
return null;
}
return this.router;
}
connect() {
if (this.connected) {
return rxjs_1.Observable.of({ type: 'connected', serviceID: this.serviceId() });
}
if (!this.token || !this.tokenSecret) {
return rxjs_1.Observable.throw(new Error('Credentials should exist.'));
}
if (this.webhookServer) {
this.webhookServer.listen();
}
this.connected = true;
return rxjs_1.Observable.of({ type: 'connected', serviceID: this.serviceId() });
}
addConnection(pageId, accessToken = '', additionalData = {}) {
this.connections.set(pageId, Object.assign({ accessToken }, additionalData));
}
getConnection(pageId) {
return this.connections.size && this.connections.get(pageId) || null;
}
removeConnection(pageId) {
this.connections.delete(pageId);
}
getConnections() {
return this.connections;
}
disconnect() {
this.connected = false;
return Promise.resolve(null);
}
listen() {
return rxjs_1.Observable.fromEvent(this.emitter, 'message')
.switchMap(value => {
return rxjs_1.Observable.of(value)
.mergeMap((event) => this.parser.normalize(event))
.mergeMap((messages) => {
if (!messages || R.isEmpty(messages)) {
return rxjs_1.Observable.empty();
}
return rxjs_1.Observable.from(messages);
})
.mergeMap((message) => {
const pageId = message.pageId || null;
const connection = pageId && this.getConnection(pageId);
return this.user(message.author, 'first_name,last_name', true, connection && connection.accessToken || null)
.then(author => R.assoc('authorInformation', author, message));
})
.mergeMap(normalized => this.parser.parse(normalized))
.mergeMap(parsed => this.parser.validate(parsed))
.mergeMap(validated => {
if (!validated) {
return rxjs_1.Observable.empty();
}
return Promise.resolve(validated);
})
.catch(err => {
this.logger.error('Caught Error, continuing', err);
return rxjs_1.Observable.of(err);
});
})
.mergeMap(value => {
if (value instanceof Error) {
return rxjs_1.Observable.empty();
}
return Promise.resolve(value);
});
}
send(data, pageId = null) {
this.logger.debug('sending', { message: data });
return broid_schemas_1.default(data, 'send').then(() => {
const toID = R.path(['to', 'id'], data) || R.path(['to', 'name'], data);
const dataType = R.path(['object', 'type'], data);
let messageData = {
recipient: { id: toID }
};
if (dataType === 'Collection') {
const items = R.filter((item) => item.type === 'Image', R.path(['object', 'items'], data));
const elements = R.map(helpers_1.createElement, items);
messageData = R.assoc('message', {
attachment: {
payload: {
elements,
template_type: 'generic'
},
type: 'template'
}
}, messageData);
}
else if (['Note', 'Image', 'Video', 'Audio', 'Document'].indexOf(dataType) > -1) {
messageData = R.assoc('message', {
attachment: {},
text: ''
}, messageData);
let content = R.path(['object', 'content'], data);
let name = R.path(['object', 'name'], data) || content;
const attachments = R.path(['object', 'attachment'], data) || [];
const buttons = R.filter((attachment) => attachment.type === 'Button' || attachment.type === 'Link', attachments);
const fButtons = helpers_1.createButtons(buttons);
if (['Image', 'Audio', 'Video', 'Document'].indexOf(dataType) > -1) {
if (dataType === 'Video' && R.isEmpty(fButtons)) {
messageData.message.text = broid_utils_1.concat([name || '', content || '', R.path(['object', 'url'], data)]);
}
else {
if (dataType === 'Image') {
name = content = 'ᅠ';
}
messageData.message.attachment = helpers_1.createCard(name, content, fButtons, R.path(['object', 'url'], data), dataType === 'Document' ? 'File' : dataType);
}
}
else if (dataType === 'Note') {
const quickReplies = helpers_1.createQuickReplies(buttons);
if (!R.isEmpty(quickReplies)) {
messageData.message.quick_replies = quickReplies;
messageData.message.text = content;
}
else if (!R.isEmpty(fButtons)) {
messageData.message.attachment = helpers_1.createTextWithButtons(name, content, fButtons);
}
else {
messageData.message.text = content;
}
}
}
else if (dataType === 'Activity') {
const content = R.path(['object', 'content'], data);
if (content === 'typing/on') {
messageData.sender_action = 'typing_on';
}
else if (content === 'typing/off') {
messageData.sender_action = 'typing_off';
}
else if (content === 'mark/seen') {
messageData.sender_action = 'mark_seen';
}
}
if (R.isEmpty(R.path(['message', 'attachment'], messageData))) {
delete messageData.message.attachment;
}
if (!R.isEmpty(messageData)) {
this.logger.debug('Message build', { message: messageData });
const connection = pageId && this.getConnection(pageId);
return rp({
json: messageData,
method: 'POST',
qs: { access_token: connection && connection.accessToken || this.token },
uri: `https://graph.facebook.com/${this.versionAPI}/me/messages`
}).then(() => ({ type: 'sent', serviceID: this.serviceId() }));
}
return Promise.reject(new Error('Only Note, Image, Video, Audio and Document are supported.'));
});
}
user(id, fields = 'first_name,last_name', cache = true, accessToken = null) {
const key = `${id}${fields}`;
if (cache) {
const data = this.storeUsers.get(key);
if (data) {
return Promise.resolve(data);
}
}
const params = {
json: true,
method: 'GET',
qs: { access_token: accessToken || this.token, fields },
uri: `https://graph.facebook.com/${this.versionAPI}/${id}`
};
return rp(params)
.catch(err => {
if (err.message && err.message.includes('nonexisting field')) {
params.qs.fields = 'name';
return rp(params);
}
throw err;
})
.then((data) => {
data.id = data.id || id;
if (!data.first_name && data.name) {
data.first_name = data.name;
data.last_name = '';
}
this.storeUsers.set(key, data);
return data;
});
}
setupGetStarted(message = '/start') {
if (message.length) {
this.logger.debug('setupGetStarted', { message });
return rp({
json: { get_started: { payload: message } },
method: 'POST',
qs: { access_token: this.token },
uri: `https://graph.facebook.com/${this.versionAPI}/me/messenger_profile`
}).then(() => ({ type: 'setupGetStarted', serviceID: this.serviceId() }));
}
return Promise.reject(new Error('The postback message cannot be empty.'));
}
getLongTokenUser(appId = '', appSecretKey = '') {
if (appId.length && appSecretKey.length) {
this.logger.debug('getLongTokenUser', { appId, appSecretKey });
return rp({
method: 'GET',
qs: {
fb_exchange_token: this.token,
client_id: appId,
client_secret: appSecretKey,
grant_type: 'fb_exchange_token'
},
uri: `https://graph.facebook.com/${this.versionAPI}/oauth/access_token`
}).then(response => JSON.parse(response))
.then(response => ({
type: 'getLongTokenUser',
serviceID: this.serviceId(),
response
}));
}
return Promise.reject(new Error('The app id or app secret key cannot be empty.'));
}
getLongTokenPage(userId = 0) {
if (userId) {
this.logger.debug('getLongTokenPage', { userId });
return rp({
method: 'GET',
qs: { access_token: this.token },
uri: `https://graph.facebook.com/${this.versionAPI}/${userId}/accounts`
})
.then(response => JSON.parse(response))
.then(({ data }) => ({
type: 'getLongTokenPage',
serviceID: this.serviceId(),
response: data
}));
}
return Promise.reject(new Error('The user id cannot be empty.'));
}
subscribeApp(pageID, fields = 'messages') {
if (pageID && fields.length) {
this.logger.debug('subscribeApp', { pageID, fields });
return rp({
method: 'POST',
qs: { access_token: this.token, subscribed_fields: fields },
uri: `https://graph.facebook.com/${this.versionAPI}/${pageID}/subscribed_apps`
}).then(() => ({ type: 'subscribeApp', serviceID: this.serviceId() }));
}
return Promise.reject(new Error('Page ID or subscribe fields cannot be empty.'));
}
setupRouter() {
const router = express_1.Router();
router.get('/', (req, res) => {
if (req.query['hub.mode'] === 'subscribe') {
if (req.query['hub.verify_token'] === this.tokenSecret) {
res.send(req.query['hub.challenge']);
}
else {
res.send('OK');
}
}
});
router.post('/', (req, res) => {
let verify = true;
if (this.consumerSecret) {
verify = helpers_1.isXHubSignatureValid(req, this.consumerSecret);
}
if (verify) {
const event = {
request: req,
response: res
};
this.emitter.emit('message', event);
res.sendStatus(200);
return;
}
this.logger.error('Failed signature validation. Make sure the consumerSecret is match.');
res.sendStatus(403);
});
return router;
}
}
exports.Adapter = Adapter;