UNPKG

11.6 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.12';
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 {function} callback
314 */
315
316exports.del = function (url, callback) {
317 if (!url.match(/[?|&]method=delete/i)) {
318 url += ~url.indexOf('?') ? '&' : '?';
319 url += 'method=delete';
320 }
321
322 this.post(url, callback);
323};
324
325
326/**
327 * Perform a search on the graph api
328 *
329 * @param {object} options (search options)
330 * @param {function} callback
331 */
332
333exports.search = function (options, callback) {
334 options = options || {};
335 var url = '/search?' + qs.stringify(options);
336 return this.get(url, callback);
337};
338
339/**
340 * Perform a batch query on the graph api
341 *
342 * @param {Array} reqs An array containing queries
343 * @param {[Object]} additionalData Additional data to send, e.g. attachments or the `include_headers` parameter.
344 * @param {Function} callback
345 *
346 * @see https://developers.facebook.com/docs/graph-api/making-multiple-requests
347 */
348
349exports.batch = function (reqs, additionalData, callback) {
350 if (!(reqs instanceof Array)) {
351 return callback({ message: 'Graph api batch requests must be an array' }, null);
352 }
353
354 if (typeof additionalData === 'function') {
355 callback = additionalData;
356 additionalData = {};
357 }
358
359 return new Graph('POST', '', extend({}, additionalData, {
360 access_token: accessToken,
361 batch: JSON.stringify(reqs)
362 }), callback);
363};
364
365
366/**
367 * Perform a fql query or mutliquery
368 * multiqueries are done by sending in
369 * an object :
370 *
371 * var query = {
372 * name: "SELECT name FROM user WHERE uid = me()"
373 * , permissions: "SELECT " + FBConfig.scope + " FROM permissions WHERE uid = me()"
374 * };
375 *
376 * @param {string/object} query
377 * @param {object} params
378 * @param {function} callback
379 */
380exports.fql = function (query, params, callback) {
381 if (typeof query !== 'string') query = JSON.stringify(query);
382
383 var url = '/fql?q=' + encodeURIComponent(query);
384
385 if (typeof params === 'function') {
386 callback = params;
387 params = null;
388 return this.get(url, callback);
389 } else {
390 return this.get(url, params, callback);
391 }
392};
393
394
395/**
396 * @param {object} params containing:
397 * - client_id
398 * - redirect_uri
399 * @param {object} opts Options hash. { mobile: true } will return mobile oAuth URL
400 * @returns the oAuthDialogUrl based on params
401 */
402exports.getOauthUrl = function (params, opts) {
403 var url = (opts && opts.mobile) ? oauthDialogUrlMobile : oauthDialogUrl;
404 return url + qs.stringify(params);
405};
406
407/**
408 * Authorizes user and sets the
409 * accessToken if everything worked out
410 *
411 * @param {object} params containing:
412 * - client_id
413 * - redirect_uri
414 * - client_secret
415 * - code
416 * @param {function} callback
417 */
418
419exports.authorize = function (params, callback) {
420 var self = this;
421
422 this.get("/oauth/access_token", params, function(err, res) {
423 if (!err) self.setAccessToken(res.access_token);
424
425 callback(err, res);
426 });
427};
428
429/**
430 * Extends the expiration time of accessToken
431 *
432 * @param {object} params containing:
433 * - client_id
434 * - client_secret
435 * - access_token (optional)
436 * @param {function} callback
437 */
438
439exports.extendAccessToken = function (params, callback) {
440 var self = this;
441
442 params.grant_type = 'fb_exchange_token';
443 params.fb_exchange_token = params.access_token ? params.access_token : this.getAccessToken();
444
445 this.get("/oauth/access_token", params, function(err, res) {
446 if (!err && !params.access_token) {
447 self.setAccessToken(res.access_token);
448 }
449
450 callback(err, res);
451 });
452};
453
454/**
455 * Set request options.
456 * These are mapped directly to the
457 * `request` module options object
458 * @param {Object} options
459 */
460
461exports.setOptions = function (options) {
462 if (typeof options === 'object') requestOptions = options;
463
464 return this;
465};
466
467/**
468 * @returns the request options object
469 */
470
471exports.getOptions = function() {
472 return requestOptions;
473};
474
475/**
476 * Sets the access token
477 * @param {string} token
478 */
479
480exports.setAccessToken = function(token) {
481 accessToken = token;
482 return this;
483};
484
485/**
486 * @returns the access token
487 */
488
489exports.getAccessToken = function () {
490 return accessToken;
491};
492
493/**
494 * Sets the app secret, used to verify all API calls if provided
495 * @param {string} token
496 */
497
498exports.setAppSecret = function(token) {
499 appSecret = token;
500 return this;
501};
502
503/**
504 * @returns the app secret
505 */
506
507exports.getAppSecret = function () {
508 return appSecret;
509};
510
511/**
512 * sets graph url
513 */
514
515exports.setGraphUrl = function (url) {
516 graphUrl = url;
517 return this;
518};
519
520/**
521 * @returns the graphUrl
522 */
523
524exports.getGraphUrl = function() {
525 return graphUrl;
526};