UNPKG

@jaybirdgroup/data-hub-connector-facebook

Version:

Connector to interact with Facebook API in score of Data Hub.

543 lines (497 loc) 19.3 kB
/** * Facebook bot re-posts latest post from random competitor Facebook page. * Create a Facebook app and permanent Page Access Token. [See details]{@link https://stackoverflow.com/questions/17197970/facebook-permanent-page-access-token?answertab=votes#tab-top}. * * @module fb-bot */ 'use strict'; const Async = require('async'); const FB = require('fb'); const _merge = require('lodash/merge'); const _sample = require('lodash/sample'); const _get = require('lodash/get'); /** * Class representing a Facebook bot re-posts latest post from random competitor Facebook page */ class Facebook { /** * Init class and apply defaults. * @param {Object} config - Bot configurations and Facebook app keys. */ constructor(config) { // default params this.defaults = { clientId: '', clientSecret: '', grantType: 'client_credentials', destination: null }; // TODO: check for missing parameters this.config = _merge({}, this.defaults, config); } /** * Method to be called after post is published or error occurred. * @callback publishPostCallback * @param {Array} errs - Array with JS Errors objects or empty. * @param {Array} publishedPosts - Array with results or empty. */ /** * Publish post to one or several destination Facebook pages * @param {Object} data - Post data to be sent * @param {string} data.url - Post url * @param {?string} data.caption - Photo caption if post type is photo * @param {?string} data.title - Video title if post type is video * @param {?string} data.description - Video description if post type is video * @param {string} type - Post type. Possible values: photo, video * @param {publishPostCallback} next - [Callback function]{@link module:fb-bot~publishPostCallback}. */ publishPost(data, type, next) { let errs = null; const publishedPosts = []; Async.each( this.config.destination, (destination, callback) => { switch (type) { case 'photo': this.publishPhoto( data, destination.pageId, destination.permanentPageToken, (err, res) => { if (err) { if (!errs) { errs = []; } errs.push(err); } else { publishedPosts.push(res); } callback(); } ); break; case 'video': this.publishVideo( data, destination.pageId, destination.permanentPageToken, (err, res) => { if (err) { if (!errs) { errs = []; } errs.push(err); } else { publishedPosts.push(res); } callback(); } ); break; default: callback(); break; } }, () => { next(errs, publishedPosts); } ); } /** * Method to be called after post is published or error occurred. * @callback nextCallback * @param {?Error} err - JS Error object or null. * @param {?Object} info - A result or undefined. */ /** * Publish photo file to the destination Facebook page. * @param {Object} options - Options set. * @param {string} options.url - Photo url. * @param {string} options.caption - Photo caption. * @param {string} destinationPageId - Destination Facebook Page ID * @param {string} permanentPageToken - Destination Facebook Page Token * @param {nextCallback} next - [Callback function]{@link module:fb-bot~nextCallback}. */ publishPhoto( { url, caption }, destinationPageId, permanentPageToken, next ) { // Details https://developers.facebook.com/docs/graph-api/reference/photo/ FB.api( `${destinationPageId}/photos`, 'post', { caption, url, access_token: permanentPageToken }, res => { const err = this._error(res); if (err) { return next(err); } next(null, res); } ); } /** * Publish video file to the destination Facebook page. * @param {Object} options - Options set. * @param {string} options.url - Video url. * @param {string} options.title - Video title. * @param {string} options.description - Video description. * @param {string} destinationPageId - Destination Facebook Page ID * @param {string} permanentPageToken - Destination Facebook Page Token * @param {nextCallback} next - [Callback function]{@link module:fb-bot~nextCallback}. */ publishVideo( { url, title, description }, destinationPageId, permanentPageToken, next ) { // Details https://developers.facebook.com/docs/graph-api/reference/video/ FB.api( `${destinationPageId}/videos`, 'post', { title, description, file_url: url, access_token: permanentPageToken }, res => { const err = this._error(res); if (err) { return next(err); } next(null, res); } ); } /** * Get latest post by page id. * @param {string} source - Object containing . * @param {nextCallback} next - [Callback function]{@link module:fb-bot~nextCallback}. */ getLatestPost(fbPageId, accessToken, next) { this.getPosts(fbPageId, accessToken, (err, res) => { if (err) { return next(err); } // take first element with a message and proper type const latestPost = _get(res, 'data', []).find( p => p.message && ['video', 'photo', 'link'].indexOf(p.type) !== -1 && this._checkPostDate(p.created_time) ); next(null, latestPost); }); } /** * Get all latest posts from a Facebook page. * @param {string} pageId - A Facebook page id or slug. * @param {nextCallback} next - [Callback function]{@link module:fb-bot~nextCallback}. */ getPosts(pageId, accessToken, next) { // get posts from FB page FB.api( `${pageId}/posts`, { // which fields to include // see all available fields https://developers.facebook.com/docs/graph-api/reference/v2.10/post fields: [ 'id', 'full_picture', 'created_time', 'message', 'caption', 'description', 'is_published', 'name', 'source', 'type', 'likes.limit(0).summary(1)', 'place' ], access_token: accessToken }, res => { const err = this._error(res); if (err) { return next(err); } // data: [ // { // "full_picture": "https://scontent.xx.fbcdn.net/v/t15.0-10/p720x720/15132116_10153853343265870_7390284410148356096_n.jpg?oh=c1885b23554ca1e5a6acad595a2c5227&oe=5A6BBFA5", // "created_time": "2017-10-26T00:30:01+0000", // "message": "Free up oven space with this easy CROCK POT PUMPKIN POKE CAKE. See full recipe: http://www.coupons.com/thegoodstuff/crock-pot-pumpkin-poke-cake/", // "id": "97406985869_10154732670855870", // "name": "Crock Pot Pumpkin Poke Cake", // "source": "https://video.xx.fbcdn.net/v/t42.1790-2/15196179_288571798205863_12521852597436416_n.mp4?efg=eyJybHIiOjU4NSwicmxhIjo1MTIsInZlbmNvZGVfdGFnIjoic3ZlX3NkIn0%3D&rl=585&vabr=325&oh=bcda00bbe33122dce9405da410f037f2&oe=59F44E1F", // "type": "video" // }, // { // "full_picture": "https://scontent.xx.fbcdn.net/v/t1.0-9/s720x720/22730494_10154746691840870_1283630152385365387_n.jpg?oh=8796893b899764a768984b53f4896a08&oe=5A73CA4D", // "created_time": "2017-10-24T23:59:54+0000", // "message": "Join us tomorrow on Facebook Live as we talk about Halloween party ideas and more healthy treats! Click \"follow\" at the top of the page to get an alert when we go live at 11 am PT. Sneak peek: http://www.coupons.com/thegoodstuff/halloween-party-ideas-target/", // "id": "97406985869_10154746694140870", // "type": "photo" // }, // { // "full_picture": "https://scontent.xx.fbcdn.net/v/t39.2147-6/22509415_10154746607335870_7573488031160598528_n.jpg?oh=c55d1b318781ae4efe3254e9d0d2aec7&oe=5A7AA183", // "created_time": "2017-10-24T23:10:45+0000", // "message": "Save on pants that suit your style with our exclusive Bonobos coupon code. Save 30% off select pants until 10/26.", // "id": "97406985869_10154746611145870", // "name": "Bonobos | 30% Off Select Pants", // "type": "link" // }, // ... // ] next(null, res); } ); } /** * Generate facebook login url * @param {string} returnUrl - Return url to add to facebook login url * @param {string} scope - A comma separated list of Permissions to request from the person using your app * @return {string} The url to login with facebook */ getLoginUrl(returnUrl, scope) { return FB.getLoginUrl({ client_id: this.config.clientId, scope, redirect_uri: returnUrl }); } /** * Method to be called after getting short lived token or error occurred. * @callback getAccessTokenCallback * @param {?Error} err - JS Error object or null. * @param {?string} info - A short lived token or undefined. */ /** * Exchange code for access token * @param {string} code - The code from facebook login * @param {string} redirectUrl - The redirect url used for facebook login * @param {getAccessTokenCallback} next - [Callback function]{@link module:fb-bot~getAccessTokenCallback}. * @return {Promise} */ getAccessToken(code, redirectUrl, next) { return new Promise((resolve, reject) => { FB.api( 'oauth/access_token', { client_id: this.config.clientId, client_secret: this.config.clientSecret, redirect_uri: redirectUrl, code: code }, res => { const err = this._error(res); if (err) { if (next) { next(err); } return reject(err); } if (next) { next(null, res.access_token); } resolve(res.access_token); } ); }); } /** * Method to be called after getting long lived access token or error occurred. * @callback getLongLivedAccessTokenCallback * @param {?Error} err - JS Error object or null. * @param {?string} info - A long lived token or undefined. */ /** * Get long lived access token * @param {string} accessToken - The short lived access token * @param {getLongLivedAccessTokenCallback} next - [Callback function]{@link module:fb-bot~getLongLivedAccessTokenCallback}. * @return {Promise} */ getLongLivedAccessToken(accessToken, next) { return new Promise((resolve, reject) => { FB.api( 'oauth/access_token', { client_id: this.config.clientId, client_secret: this.config.clientSecret, grant_type: 'fb_exchange_token', fb_exchange_token: accessToken }, res => { const err = this._error(res); if (err) { if (next) { next(err); } return reject(err); } if (next) { next(null, res.access_token); } resolve(res.access_token); } ); }); } /** * Method to be called after getting facebook user information or error occurred. * @callback getUserCallback * @param {?Error} err - JS Error object or null. * @param {?Object} user - A facebook user information or undefined. * @param {string} user.id - A user id * @param {string} user.name - A user name */ /** * Get facebook user information * @param {string} accessToken - The long lived access token * @param {getUserCallback} next - [Callback function]{@link module:fb-bot~getUserCallback}. * @return {Promise} */ getUser(accessToken, next) { return new Promise((resolve, reject) => { FB.api('me', { access_token: accessToken }, res => { const err = this._error(res); if (err) { if (next) { next(err); } return reject(err); } if (next) { next(null, res); } resolve(res); }); }); } /** * Method to be called after getting Facebook Pages or error occurred. * @callback getUserAccountsCallback * @param {?Error} err - JS Error object or null. * @param {?Array} pages - An array with pages or undefined. */ /** * Get Facebook Pages this person administers/is an admin for * @param {string} userId - The user id of the person * @param {string} accessToken - The long lived access token * @param {getUserAccountsCallback} next - [Callback function]{@link module:fb-bot~getUserAccountsCallback}. * @return {Promise} */ getUserAccounts(userId, accessToken, next) { return new Promise((resolve, reject) => { FB.api(`${userId}/accounts`, { access_token: accessToken }, res => { const err = this._error(res); if (err) { if (next) { next(err); } return reject(err); } if (next) { next(null, res); } resolve(res); }); }); } /** * Remove all links from the string. * @param {string} str - An input string. * @returns {string} - A cleaned string. */ _removeLinks(str) { return str.replace( /(http(s)?(:\/\/))?(www\.)?[a-zA-Z0-9-_\.]+(\.[a-zA-Z0-9]{2,})([-a-zA-Z0-9:%_\+.~#?&//=]*)/g, '' ); } /** * Remove not allowed unicode characters. * @param {string} str - An input string. * @returns {string} - A cleaned string. */ _removeUnicode(str) { // strip out unicode characters from rss feed // the feed should be passing validation on // http://validator.w3.org/feed/check.cgi // Reference: https://stackoverflow.com/questions/10430562/how-do-you-remove-unicode-characters-in-javascript return str.replace(/[^(\x20-\x7F)\x0A]/g, ''); } /** * Check for a Facebook error. * @param {Object} res - A response from a Facebook API. * @returns {?Error} - An Error object or null. */ _error(res) { if (!res || res.error) { let errorMessage = 'unknown error occurred'; if (res.error && typeof res.error === 'string') { errorMessage = res.error; } else if (res.error) { errorMessage = JSON.stringify(res.error); } return new Error(errorMessage); } return null; } /** * Method to be called after email is sent or error. * @callback tokenCallback * @param {?Error} err - JS Error object or null. * @param {?string} accessToken - A token or undefined. */ /** * Get access token using oAuth https://www.npmjs.com/package/fb#oauth-requests. * @param {Object} res - A response from a Facebook API. * @param {tokenCallback} next - [Callback function]{@link module:fb-bot~tokenCallback}. */ _getAccessToken(next) { FB.api( 'oauth/access_token', { client_id: this.config.clientId, client_secret: this.config.clientSecret, grant_type: this.config.grantType }, res => { const err = this._error(res); if (err) { return next(err); } next(null, res.access_token); } ); } /** * Check is facebook post created today * * @param {string} date - The facebook post created date * @return {boolean} * @private */ _checkPostDate(date) { // Create date from input value const inputDate = new Date(date); // Get today's date const todayDate = new Date(); // call setHours to take the time out of the comparison return ( inputDate.setHours(0, 0, 0, 0) === todayDate.setHours(0, 0, 0, 0) ); } } // export class to have the extend ability module.exports = Facebook;