UNPKG

11.8 kBJavaScriptView Raw
1/**
2 * Module Dependencies
3 */
4
5var request = require('request')
6 , qs = require('qs')
7 , url = require('url')
8 , crypto = require('crypto')
9 , noop = function(){};
10
11
12// Using `extend` from https://github.com/Raynos/xtend
13function extend(target) {
14 for (var i = 1; i < arguments.length; i++) {
15 var source = arguments[i]
16 , keys = Object.keys(source);
17
18 for (var j = 0; j < keys.length; j++) {
19 var name = keys[j];
20 target[name] = source[name];
21 }
22 }
23
24 return target;
25}
26
27
28/**
29 * @private
30 */
31
32var accessToken = null
33 , appSecret = null
34 , graphUrl = 'https://graph.facebook.com'
35 , oauthDialogUrl = "http://www.facebook.com/dialog/oauth?"
36 , oauthDialogUrlMobile = "http://m.facebook.com/dialog/oauth?"
37 , requestOptions = {};
38
39/**
40 * Library version
41 */
42
43exports.version = '0.2.13';
44
45/**
46 * Graph Stream
47 *
48 * @param {String} method
49 * @param {String} url
50 * @param {object/function} - postData
51 * - object to be used for post
52 * - assumed to be a callback function if callback is undefined
53 * @param {function/undefined} - callback function
54 */
55
56function Graph(method, url, postData, callback) {
57 if (typeof callback === 'undefined') {
58 callback = postData;
59 postData = {};
60 }
61
62 url = this.prepareUrl(url);
63 this.callback = callback || noop;
64 this.postData = postData;
65
66 this.options = extend({}, requestOptions);
67 this.options.encoding = this.options.encoding || 'utf-8';
68
69 // these particular set of options should be immutable
70 this.options.method = method;
71 this.options.uri = url;
72 this.options.followRedirect = false;
73
74 this[method.toLowerCase()]();
75
76 return this;
77}
78
79
80/**
81 * "Prepares" given url string
82 * - adds protocol and host prefix if none is given
83 * @param {string} url string
84 */
85Graph.prototype.prepareUrl = function(url) {
86 url = this.cleanUrl(url);
87
88 if (url.substr(0,4) !== 'http') {
89 url = graphUrl + url;
90 }
91
92 return url;
93};
94
95/**
96 * "Cleans" given url string
97 * - adds lading slash
98 * - adds access token if we have one
99 * - adds appsecret_proof if we have an accessToken and appSecret
100 * @param {string} url string
101 */
102
103Graph.prototype.cleanUrl = function(url) {
104 url = url.trim();
105
106 // prep access token in url for appsecret proofing
107 var regex = /access_token=([^&]*)/;
108 var results = regex.exec(url);
109 var sessionAccessToken = results ? results[1] : accessToken;
110
111 // add leading slash
112 if (url.charAt(0) !== '/' && url.substr(0,4) !== 'http') url = '/' + url;
113
114 // add access token to url
115 if (accessToken && url.indexOf('access_token=') === -1) {
116 url += ~url.indexOf('?') ? '&' : '?';
117 url += "access_token=" + accessToken;
118 }
119
120 // add appsecret_proof to the url
121 if (sessionAccessToken && appSecret && url.indexOf('appsecret_proof') === -1) {
122 var hmac = crypto.createHmac('sha256', appSecret);
123 hmac.update(sessionAccessToken);
124
125 url += ~url.indexOf('?') ? '&' : '?';
126 url += "appsecret_proof=" + hmac.digest('hex');
127 }
128
129 return url;
130};
131
132/**
133 * Gets called on response.end
134 * @param {String|Object} body
135 */
136
137Graph.prototype.end = function (body) {
138 var json = typeof body === 'string' ? null : body
139 , err = null;
140
141 if (!json) {
142 try {
143
144 // this accounts for `real` json strings
145 if (~body.indexOf('{') && ~body.indexOf('}')) {
146 json = JSON.parse(body);
147
148 } else {
149 // this accounts for responses that are plain strings
150 // access token responses have format of "accessToken=....&..."
151 // but facebook has random responses that just return "true"
152 // so we'll convert those to { data: true }
153 if (!~body.indexOf('=')) body = 'data=' + body;
154 if (body.charAt(0) !== '?') body = '?' + body;
155
156 json = url.parse(body, true).query;
157 }
158
159 } catch (e) {
160 err = {
161 message: 'Error parsing json'
162 , exception: e
163 };
164 }
165 }
166
167 if (!err && (json && json.error)) err = json.error;
168
169 this.callback(err, json);
170};
171
172
173/**
174 * https.get request wrapper
175 */
176
177Graph.prototype.get = function () {
178 var self = this;
179
180 request.get(this.options, function(err, res, body) {
181 if (err) {
182 self.callback({
183 message: 'Error processing https request'
184 , exception: err
185 }, null);
186
187 return;
188 }
189
190 if (~res.headers['content-type'].indexOf('image')) {
191 body = {
192 image: true
193 , location: res.headers.location
194 };
195 }
196
197 self.end(body);
198 });
199};
200
201
202/**
203 * https.post request wrapper
204 */
205
206Graph.prototype.post = function() {
207
208 var self = this
209 , postData = qs.stringify(this.postData);
210
211 this.options.body = postData;
212
213 request(this.options, function (err, res, body) {
214 if (err) {
215 self.callback({
216 message: 'Error processing https request'
217 , exception: err
218 }, null);
219
220 return;
221 }
222
223 self.end(body);
224 });
225
226};
227
228/**
229 * Accepts an url an returns facebook
230 * json data to the callback provided
231 *
232 * if the response is an image
233 * ( FB redirects profile image requests directly to the image )
234 * We'll send back json containing {image: true, location: imageLocation }
235 *
236 * Ex:
237 *
238 * Passing params directly in the url
239 *
240 * graph.get("zuck?fields=picture", callback)
241 *
242 * OR
243 *
244 * var params = { fields: picture };
245 * graph.get("zuck", params, callback);
246 *
247 * GraphApi calls that redirect directly to an image
248 * will return a json response with relavant fields
249 *
250 * graph.get("/zuck/picture", callback);
251 *
252 * {
253 * image: true,
254 * location: "http://profile.ak.fbcdn.net/hprofile-ak-snc4/157340_4_3955636_q.jpg"
255 * }
256 *
257 *
258 * @param {object} params
259 * @param {string} url
260 * @param {function} callback
261 */
262
263exports.get = function(url, params, callback) {
264 if (typeof params === 'function') {
265 callback = params;
266 params = null;
267 }
268
269 if (typeof url !== 'string') {
270 return callback({ message: 'Graph api url must be a string' }, null);
271 }
272
273 if (params) {
274 url += ~url.indexOf('?') ? '&' : '?';
275 url += qs.stringify(params);
276 }
277
278 return new Graph('GET', url, callback);
279};
280
281/**
282 * Publish to the facebook graph
283 * access token will be needed for posts
284 * Ex:
285 *
286 * var wallPost = { message: "heyooo budday" };
287 * graph.post(friendID + "/feed", wallPost, callback);
288 *
289 * @param {string} url
290 * @param {object} postData
291 * @param {function} callback
292 */
293
294exports.post = function (url, postData, callback) {
295 if (typeof url !== 'string') {
296 return callback({ message: 'Graph api url must be a string' }, null);
297 }
298
299 if (typeof postData === 'function') {
300 callback = postData;
301 postData = url.indexOf('access_token') !== -1 ? {} : {access_token: accessToken};
302 }
303
304 return new Graph('POST', url, postData, callback);
305};
306
307/**
308 * Deletes an object from the graph api
309 * by sending a "DELETE", which is really
310 * a post call, along with a method=delete param
311 *
312 * @param {string} url
313 * @param {object} postData (optional)
314 * @param {function} callback
315 */
316
317exports.del = function (url, postData, callback) {
318 if (!url.match(/[?|&]method=delete/i)) {
319 url += ~url.indexOf('?') ? '&' : '?';
320 url += 'method=delete';
321 }
322
323 if (typeof postData === 'function') {
324 callback = postData;
325 postData = url.indexOf('access_token') !== -1 ? {} : {access_token: accessToken};
326 }
327
328 this.post(url, postData, callback);
329};
330
331
332/**
333 * Perform a search on the graph api
334 *
335 * @param {object} options (search options)
336 * @param {function} callback
337 */
338
339exports.search = function (options, callback) {
340 options = options || {};
341 var url = '/search?' + qs.stringify(options);
342 return this.get(url, callback);
343};
344
345/**
346 * Perform a batch query on the graph api
347 *
348 * @param {Array} reqs An array containing queries
349 * @param {[Object]} additionalData Additional data to send, e.g. attachments or the `include_headers` parameter.
350 * @param {Function} callback
351 *
352 * @see https://developers.facebook.com/docs/graph-api/making-multiple-requests
353 */
354
355exports.batch = function (reqs, additionalData, callback) {
356 if (!(reqs instanceof Array)) {
357 return callback({ message: 'Graph api batch requests must be an array' }, null);
358 }
359
360 if (typeof additionalData === 'function') {
361 callback = additionalData;
362 additionalData = {};
363 }
364
365 return new Graph('POST', '', extend({}, additionalData, {
366 access_token: accessToken,
367 batch: JSON.stringify(reqs)
368 }), callback);
369};
370
371
372/**
373 * Perform a fql query or mutliquery
374 * multiqueries are done by sending in
375 * an object :
376 *
377 * var query = {
378 * name: "SELECT name FROM user WHERE uid = me()"
379 * , permissions: "SELECT " + FBConfig.scope + " FROM permissions WHERE uid = me()"
380 * };
381 *
382 * @param {string/object} query
383 * @param {object} params
384 * @param {function} callback
385 */
386exports.fql = function (query, params, callback) {
387 if (typeof query !== 'string') query = JSON.stringify(query);
388
389 var url = '/fql?q=' + encodeURIComponent(query);
390
391 if (typeof params === 'function') {
392 callback = params;
393 params = null;
394 return this.get(url, callback);
395 } else {
396 return this.get(url, params, callback);
397 }
398};
399
400
401/**
402 * @param {object} params containing:
403 * - client_id
404 * - redirect_uri
405 * @param {object} opts Options hash. { mobile: true } will return mobile oAuth URL
406 * @returns the oAuthDialogUrl based on params
407 */
408exports.getOauthUrl = function (params, opts) {
409 var url = (opts && opts.mobile) ? oauthDialogUrlMobile : oauthDialogUrl;
410 return url + qs.stringify(params);
411};
412
413/**
414 * Authorizes user and sets the
415 * accessToken if everything worked out
416 *
417 * @param {object} params containing:
418 * - client_id
419 * - redirect_uri
420 * - client_secret
421 * - code
422 * @param {function} callback
423 */
424
425exports.authorize = function (params, callback) {
426 var self = this;
427
428 this.get("/oauth/access_token", params, function(err, res) {
429 if (!err) self.setAccessToken(res.access_token);
430
431 callback(err, res);
432 });
433};
434
435/**
436 * Extends the expiration time of accessToken
437 *
438 * @param {object} params containing:
439 * - client_id
440 * - client_secret
441 * - access_token (optional)
442 * @param {function} callback
443 */
444
445exports.extendAccessToken = function (params, callback) {
446 var self = this;
447
448 params.grant_type = 'fb_exchange_token';
449 params.fb_exchange_token = params.access_token ? params.access_token : this.getAccessToken();
450
451 this.get("/oauth/access_token", params, function(err, res) {
452 if (!err && !params.access_token) {
453 self.setAccessToken(res.access_token);
454 }
455
456 callback(err, res);
457 });
458};
459
460/**
461 * Set request options.
462 * These are mapped directly to the
463 * `request` module options object
464 * @param {Object} options
465 */
466
467exports.setOptions = function (options) {
468 if (typeof options === 'object') requestOptions = options;
469
470 return this;
471};
472
473/**
474 * @returns the request options object
475 */
476
477exports.getOptions = function() {
478 return requestOptions;
479};
480
481/**
482 * Sets the access token
483 * @param {string} token
484 */
485
486exports.setAccessToken = function(token) {
487 accessToken = token;
488 return this;
489};
490
491/**
492 * @returns the access token
493 */
494
495exports.getAccessToken = function () {
496 return accessToken;
497};
498
499/**
500 * Sets the app secret, used to verify all API calls if provided
501 * @param {string} token
502 */
503
504exports.setAppSecret = function(token) {
505 appSecret = token;
506 return this;
507};
508
509/**
510 * @returns the app secret
511 */
512
513exports.getAppSecret = function () {
514 return appSecret;
515};
516
517/**
518 * sets graph url
519 */
520
521exports.setGraphUrl = function (url) {
522 graphUrl = url;
523 return this;
524};
525
526/**
527 * @returns the graphUrl
528 */
529
530exports.getGraphUrl = function() {
531 return graphUrl;
532};