/** * Module Dependencies */ var request = require('request') , qs = require('qs') , url = require('url') , crypto = require('crypto') , noop = function(){}; // Using `extend` from https://github.com/Raynos/xtend function extend(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] , keys = Object.keys(source); for (var j = 0; j < keys.length; j++) { var name = keys[j]; target[name] = source[name]; } } return target; } /** * @private */ var accessToken = null , appSecret = null , graphUrl = 'https://graph.facebook.com' , graphVersion = '2.9' // default to the oldest version , oauthDialogUrl = "https://www.facebook.com/v2.0/dialog/oauth?" // oldest version for auth , oauthDialogUrlMobile = "https://m.facebook.com/v2.0/dialog/oauth?" // oldest version for auth , requestOptions = {}; /** * Library version */ exports.version = '1.3.0'; /** * Graph Stream * * @param {String} method * @param {String} url * @param {object/function} - postData * - object to be used for post * - assumed to be a callback function if callback is undefined * @param {function/undefined} - callback function */ function Graph(method, url, postData, callback) { if (typeof callback === 'undefined') { callback = postData; postData = {}; } url = this.prepareUrl(url); this.callback = callback || noop; this.postData = postData; this.options = extend({}, requestOptions); this.options.encoding = this.options.encoding || 'utf-8'; // these particular set of options should be immutable this.options.method = method; this.options.uri = url; this.options.followRedirect = false; this.request = this[method.toLowerCase()](); return this; } /** * "Prepares" given url string * - adds protocol and host prefix if none is given * @param {string} url string */ Graph.prototype.prepareUrl = function(url) { url = this.cleanUrl(url); if (url.substr(0,4) !== 'http') { url = graphUrl + '/v' + graphVersion + url; } return url; }; /** * "Cleans" given url string * - adds lading slash * - adds access token if we have one * - adds appsecret_proof if we have an accessToken and appSecret * @param {string} url string */ Graph.prototype.cleanUrl = function(url) { url = url.trim(); // prep access token in url for appsecret proofing var regex = /access_token=([^&]*)/; var results = regex.exec(url); var sessionAccessToken = results ? results[1] : accessToken; // add leading slash if (url.charAt(0) !== '/' && url.substr(0,4) !== 'http') url = '/' + url; // add access token to url if (accessToken && url.indexOf('access_token=') === -1) { url += ~url.indexOf('?') ? '&' : '?'; url += "access_token=" + accessToken; } // add appsecret_proof to the url if (sessionAccessToken && appSecret && url.indexOf('appsecret_proof') === -1) { var hmac = crypto.createHmac('sha256', appSecret); hmac.update(sessionAccessToken); url += ~url.indexOf('?') ? '&' : '?'; url += "appsecret_proof=" + hmac.digest('hex'); } return url; }; /** * Gets called on response.end * @param {String|Object} body */ Graph.prototype.end = function (body) { var json = typeof body === 'string' ? null : body , err = null; if (!json) { try { // this accounts for `real` json strings if (~body.indexOf('{') && ~body.indexOf('}')) { json = JSON.parse(body); } else { // this accounts for responses that are plain strings // access token responses have format of "accessToken=....&..." // but facebook has random responses that just return "true" // so we'll convert those to { data: true } if (!~body.indexOf('=')) body = 'data=' + body; if (body.charAt(0) !== '?') body = '?' + body; json = url.parse(body, true).query; } } catch (e) { err = { message: 'Error parsing json' , exception: e }; } } if (!err && (json && json.error)) err = json.error; this.callback(err, json); }; /** * https.get request wrapper */ Graph.prototype.get = function () { var self = this; return request.get(this.options, function(err, res, body) { if (err) { self.callback({ message: 'Error processing https request' , exception: err }, null); return; } if (~res.headers['content-type'].indexOf('image')) { body = { image: true , location: res.headers.location }; } self.end(body); }).on('error', function(err) { self.callback({ message: 'Error processing https request' , exception: err }, null); }); }; /** * https.post request wrapper */ Graph.prototype.post = function() { var self = this , postData = qs.stringify(this.postData); this.options.body = postData; return request(this.options, function (err, res, body) { if (err) { self.callback({ message: 'Error processing https request' , exception: err }, null); return; } self.end(body); }) .on('error', function(err) { self.callback({ message: 'Error processing https request' , exception: err }, null); }); }; /** * Accepts an url an returns facebook * json data to the callback provided * * if the response is an image * ( FB redirects profile image requests directly to the image ) * We'll send back json containing {image: true, location: imageLocation } * * Ex: * * Passing params directly in the url * * graph.get("zuck?fields=picture", callback) * * OR * * var params = { fields: picture }; * graph.get("zuck", params, callback); * * GraphApi calls that redirect directly to an image * will return a json response with relavant fields * * graph.get("/zuck/picture", callback); * * { * image: true, * location: "http://profile.ak.fbcdn.net/hprofile-ak-snc4/157340_4_3955636_q.jpg" * } * * * @param {object} params * @param {string} url * @param {function} callback */ exports.get = function(url, params, callback) { if (typeof params === 'function') { callback = params; params = null; } if (typeof url !== 'string') { return callback({ message: 'Graph api url must be a string' }, null); } if (params) { url += ~url.indexOf('?') ? '&' : '?'; url += qs.stringify(params); } return new Graph('GET', url, callback); }; /** * Publish to the facebook graph * access token will be needed for posts * Ex: * * var wallPost = { message: "heyooo budday" }; * graph.post(friendID + "/feed", wallPost, callback); * * @param {string} url * @param {object} postData * @param {function} callback */ exports.post = function (url, postData, callback) { if (typeof url !== 'string') { return callback({ message: 'Graph api url must be a string' }, null); } if (typeof postData === 'function') { callback = postData; postData = url.indexOf('access_token') !== -1 ? {} : {access_token: accessToken}; } return new Graph('POST', url, postData, callback); }; /** * Deletes an object from the graph api * by sending a "DELETE", which is really * a post call, along with a method=delete param * * @param {string} url * @param {object} postData (optional) * @param {function} callback */ exports.del = function (url, postData, callback) { if (!url.match(/[?|&]method=delete/i)) { url += ~url.indexOf('?') ? '&' : '?'; url += 'method=delete'; } if (typeof postData === 'function') { callback = postData; postData = url.indexOf('access_token') !== -1 ? {} : {access_token: accessToken}; } return this.post(url, postData, callback); }; /** * Perform a search on the graph api * * @param {object} options (search options) * @param {function} callback */ exports.search = function (options, callback) { options = options || {}; var url = '/search?' + qs.stringify(options); return this.get(url, callback); }; /** * Perform a batch query on the graph api * * @param {Array} reqs An array containing queries * @param {[Object]} additionalData Additional data to send, e.g. attachments or the `include_headers` parameter. * @param {Function} callback * * @see https://developers.facebook.com/docs/graph-api/making-multiple-requests */ exports.batch = function (reqs, additionalData, callback) { if (!(reqs instanceof Array)) { return callback({ message: 'Graph api batch requests must be an array' }, null); } if (typeof additionalData === 'function') { callback = additionalData; additionalData = {}; } return new Graph('POST', '', extend({}, { access_token: accessToken, batch: JSON.stringify(reqs) }, additionalData), callback); }; /** * Perform a fql query or mutliquery * multiqueries are done by sending in * an object : * * var query = { * name: "SELECT name FROM user WHERE uid = me()" * , permissions: "SELECT " + FBConfig.scope + " FROM permissions WHERE uid = me()" * }; * * @param {string/object} query * @param {object} params * @param {function} callback */ exports.fql = function (query, params, callback) { if (typeof query !== 'string') query = JSON.stringify(query); var url = '/fql?q=' + encodeURIComponent(query); if (typeof params === 'function') { callback = params; params = null; return this.get(url, callback); } else { return this.get(url, params, callback); } }; /** * @param {object} params containing: * - client_id * - redirect_uri * @param {object} opts Options hash. { mobile: true } will return mobile oAuth URL * @returns the oAuthDialogUrl based on params */ exports.getOauthUrl = function (params, opts) { var url = (opts && opts.mobile) ? oauthDialogUrlMobile : oauthDialogUrl; return url + qs.stringify(params); }; /** * Authorizes user and sets the * accessToken if everything worked out * * @param {object} params containing: * - client_id * - redirect_uri * - client_secret * - code * @param {function} callback */ exports.authorize = function (params, callback) { var self = this; return this.get("/oauth/access_token", params, function(err, res) { if (!err) self.setAccessToken(res.access_token); callback(err, res); }); }; /** * Extends the expiration time of accessToken * * @param {object} params containing: * - client_id * - client_secret * - access_token (optional) * @param {function} callback */ exports.extendAccessToken = function (params, callback) { var self = this; params.grant_type = 'fb_exchange_token'; params.fb_exchange_token = params.access_token ? params.access_token : this.getAccessToken(); return this.get("/oauth/access_token", params, function(err, res) { if (!err && !params.access_token) { self.setAccessToken(res.access_token); } callback(err, res); }); }; /** * Set request options. * These are mapped directly to the * `request` module options object * @param {Object} options */ exports.setOptions = function (options) { if (typeof options === 'object') requestOptions = options; return this; }; /** * @returns the request options object */ exports.getOptions = function() { return requestOptions; }; /** * Sets the access token * @param {string} token */ exports.setAccessToken = function(token) { accessToken = token; return this; }; /** * @returns the access token */ exports.getAccessToken = function () { return accessToken; }; /** * Set's the Graph API version. * Note that you don't need to specify the 'v', just * add '2.1', '1.1' etc * @param {string} version */ exports.setVersion = function (version) { // set version graphVersion = version; // update auth urls oauthDialogUrl = "https://www.facebook.com/v"+version+"/dialog/oauth?"; // oldest version for auth oauthDialogUrlMobile = "https://m.facebook.com/v"+version+"/dialog/oauth?"; // oldest version for auth return this; }; /** * Sets the app secret, used to verify all API calls if provided * @param {string} token */ exports.setAppSecret = function(token) { appSecret = token; return this; }; /** * @returns the app secret */ exports.getAppSecret = function () { return appSecret; }; /** * sets graph url */ exports.setGraphUrl = function (url) { graphUrl = url; return this; }; /** * @returns the graphUrl */ exports.getGraphUrl = function() { return graphUrl; };