├── test ├── _fixtures │ └── ellipsis.txt ├── mocha.opts ├── .eslintrc.json ├── lint.js ├── _supports │ └── notError.js ├── interface │ ├── library.js │ ├── misc.js │ ├── import.js │ └── extend.js ├── api │ ├── batch.js │ ├── header.js │ ├── post.js │ └── get.js ├── signed_request │ └── parseSignedRequest.js ├── access_token │ └── api.js ├── options │ └── options.spec.js └── login_url │ └── getLoginUrl.js ├── .release.json ├── .eslintignore ├── test_live ├── mocha.opts ├── _fixtures │ └── 2x2-check.jpg ├── .eslintrc.json ├── _supports │ ├── chai.js │ ├── ci-safe.js │ └── fb.js ├── basic │ ├── agent.js │ ├── token.js │ ├── error.js │ ├── batch.js │ ├── me.js │ └── post.js └── README.md ├── .npmignore ├── .gitignore ├── .babelrc ├── .travis.yml ├── src ├── FacebookApiException.js └── fb.js ├── LICENSE ├── .eslintrc.json ├── package.json ├── CHANGELOG.md └── README.md /test/_fixtures/ellipsis.txt: -------------------------------------------------------------------------------- 1 | ... 2 | -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagName": "v%s" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | samples/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | --reporter spec 3 | --timeout 10000 4 | -------------------------------------------------------------------------------- /test_live/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-register 2 | --reporter spec 3 | --timeout 10000 4 | -------------------------------------------------------------------------------- /test_live/_fixtures/2x2-check.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-facebook/facebook-node-sdk/HEAD/test_live/_fixtures/2x2-check.jpg -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | 3 | Thumbs.db 4 | # WebStrom IDE ignore 5 | .idea/ 6 | test/ 7 | samples/ 8 | .travis.yml 9 | .release.json 10 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "prefer-arrow-callback": [0] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | /lib/ 4 | .env 5 | 6 | .DS_Store 7 | Thumbs.db 8 | # WebStrom IDE ignore 9 | .idea/ 10 | -------------------------------------------------------------------------------- /test_live/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "prefer-arrow-callback": [0] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", {"targets": {"node": 6}}], "stage-0"], 3 | "plugins": ["transform-decorators-legacy", "transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /test/lint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var lint = require('mocha-eslint'); 3 | 4 | if ( !process.env.CI_TEST || process.env.CI_TEST !== 'no-lint' ) { 5 | lint(['.']); 6 | } 7 | -------------------------------------------------------------------------------- /test/_supports/notError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | 4 | module.exports = function(result) { 5 | expect(result.error && result.error.Error).to.not.exist; 6 | expect(result.error).to.not.be.instanceof(Error); 7 | }; 8 | -------------------------------------------------------------------------------- /test_live/_supports/chai.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import chai, {expect} from 'chai'; 3 | import cap from 'chai-as-promised'; 4 | import sinon from 'sinon-chai'; 5 | 6 | chai.use(cap); 7 | chai.use(sinon); 8 | 9 | export default chai; 10 | export {expect}; 11 | -------------------------------------------------------------------------------- /test_live/_supports/ci-safe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if ( process.env.CI === 'true' && process.env.TRAVIS_EVENT_TYPE === 'pull_request' ) { 4 | console.warn('Skipping live tests, encrypted environment variables are not available in pull requests'); // eslint-disable-line no-console 5 | process.exit(0); 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | - "8" 5 | - "6" 6 | env: 7 | global: 8 | - CI_TEST=no-lint 9 | matrix: 10 | include: 11 | - node_js: "9" 12 | script: npm run lint 13 | env: 14 | - CI_TEST=lint-only 15 | - node_js: "9" 16 | script: npm run test-live 17 | env: 18 | - CI_TEST=test-live 19 | cache: 20 | directories: 21 | - node_modules 22 | notifications: 23 | webhooks: https://app.fossa.io/hooks/travisci 24 | -------------------------------------------------------------------------------- /src/FacebookApiException.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default function FacebookApiException(res) { 4 | this.name = 'FacebookApiException'; 5 | this.message = JSON.stringify(res || {}); 6 | this.response = res; 7 | Error.captureStackTrace(this, this.constructor.name); 8 | } 9 | 10 | FacebookApiException.prototype = Object.create(Error.prototype, { 11 | constructor: { 12 | value: FacebookApiException, 13 | enumerable: false, 14 | writable: true, 15 | configurable: true 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Thuzi LLC 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /test_live/basic/agent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {expect} from '../_supports/chai'; 3 | import FB from '../_supports/fb'; 4 | import https from 'https'; 5 | 6 | describe('FB.options({agent})', function() { 7 | it('should override the https.Agent used with a custom one', async function() { 8 | const agent = new https.Agent({ 9 | keepAlive: true, 10 | maxSockets: 0, 11 | }); 12 | FB.options({agent}); 13 | 14 | expect(Object.keys(agent.sockets)).to.be.empty; 15 | const res = FB.api('/me'); 16 | expect(Object.keys(agent.sockets)).to.not.be.empty; 17 | await res; 18 | 19 | FB.options({agent: undefined}); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/interface/library.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import FB, {Facebook} from '../..'; 3 | var expect = require('chai').expect; 4 | 5 | describe('Facebook', function() { 6 | describe('new Facebook()', function() { 7 | it('should create an instance that behaves like FB', function() { 8 | var fb = new Facebook(); 9 | expect(fb).property('api').to.be.a('function'); 10 | }); 11 | }); 12 | 13 | describe("new Facebook({appId: '42'})", function() { 14 | it('should set options', function() { 15 | var fb = new Facebook({appId: '42'}); 16 | expect(fb.options('appId')).to.equal('42'); 17 | }); 18 | 19 | it('should not share options with FB', function() { 20 | var fb = new Facebook({appId: '42'}); 21 | expect(fb.options('appId')).to.not.equal(FB.options('appId')); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/interface/misc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var FBmodule = require('../..'), 3 | FacebookApiException = require('../../lib/FacebookApiException').default, 4 | {version} = require('../../package.json'), 5 | expect = require('chai').expect; 6 | 7 | describe('exports.FacebookApiException', function() { 8 | it('should be a function', function() { 9 | expect(FBmodule.FacebookApiException) 10 | .to.exist 11 | .and.to.be.a('function'); 12 | }); 13 | 14 | it('should create a FacebookApiException instance that derives from Error', function() { 15 | var obj = {}; 16 | expect(new FBmodule.FacebookApiException(obj)) 17 | .to.be.an.instanceof(FacebookApiException) 18 | .and.to.be.an.instanceof(Error) 19 | .and.to.include({ 20 | name: 'FacebookApiException', 21 | message: '{}', 22 | response: obj 23 | }); 24 | }); 25 | }); 26 | 27 | describe('exports.version', function() { 28 | it("should be a string with this package's current version", function() { 29 | expect(FBmodule.version) 30 | .to.be.a('string') 31 | .and.to.equal(version); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test_live/basic/token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {expect} from '../_supports/chai'; 3 | import FB from '../_supports/fb'; 4 | 5 | describe("FB.api('/debug_token')", function() { 6 | let debug_token; 7 | before(function() { 8 | debug_token = FB.api('/debug_token', { 9 | access_token: `${process.env.FB_APP_ID}|${process.env.FB_APP_SECRET}`, 10 | input_token: FB.options('accessToken'), 11 | }); 12 | }); 13 | 14 | it('should be valid', async function() { 15 | const {data} = await debug_token; 16 | expect(data).to.have.a.property('is_valid', true); 17 | }); 18 | 19 | it('should be for a USER', async function() { 20 | const {data} = await debug_token; 21 | expect(data).to.have.a.property('type', 'USER'); 22 | }); 23 | 24 | it('should be for the same APP_ID', async function() { 25 | const {data} = await debug_token; 26 | expect(data).to.have.a.property('app_id', process.env.FB_APP_ID); 27 | }); 28 | 29 | it('should have the publish_actions and user_posts scope', async function() { 30 | const {data} = await debug_token; 31 | expect(data).to.have.a.property('scopes') 32 | .which.is.a('array') 33 | .and.includes('publish_actions') 34 | .and.includes('user_posts'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parser": "babel-eslint", 8 | "rules": { 9 | "array-bracket-spacing": [1, "never"], 10 | "brace-style": [1, "1tbs", { "allowSingleLine": true }], 11 | "comma-spacing": [1], 12 | "comma-style": [1, "last"], 13 | "computed-property-spacing": [1, "never"], 14 | "eol-last": [1], 15 | "indent": [2, "tab"], 16 | "key-spacing": [1, {"beforeColon": false, "afterColon": true, "mode": "minimum"}], 17 | "linebreak-style": [2, "unix"], 18 | "no-spaced-func": [1], 19 | "no-trailing-spaces": [1], 20 | "no-unneeded-ternary": [2], 21 | "object-curly-spacing": [1, "never"], 22 | "prefer-arrow-callback": [1], 23 | "quote-props": [2, "as-needed"], 24 | "quotes": [1, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 25 | "semi": [2, "always"], 26 | "spaced-comment": [2, "always"], 27 | "keyword-spacing": [1, {"before": true, "after": true}], 28 | "space-before-blocks": [1, "always"], 29 | "space-before-function-paren": [1, "never"], 30 | "space-infix-ops": [1], 31 | "space-unary-ops": [1], 32 | "strict": [0, "global"] 33 | }, 34 | "extends": "eslint:recommended" 35 | } 36 | -------------------------------------------------------------------------------- /test_live/_supports/fb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import 'dotenv/config'; 3 | import FB from '../..'; 4 | 5 | if ( !process.env.FB_APP_ID || !process.env.FB_APP_SECRET || !(process.env.FB_APP_ACCESS_TOKEN || process.env.FB_APP_TEST_USER) ) { 6 | console.warn("FB_APP_ID, FB_APP_SECRET, and FB_APP_ACCESS_TOKEN environment variables must be defined for live tests against Facebook's Graph API to function."); // eslint-disable-line no-console 7 | } 8 | 9 | FB.options({ 10 | appId: process.env.FB_APP_ID, 11 | appSecret: process.env.FB_APP_SECRET, 12 | accessToken: process.env.FB_APP_ACCESS_TOKEN, 13 | version: 'v2.12', 14 | }); 15 | 16 | before(async function() { 17 | if ( !process.env.FB_APP_ACCESS_TOKEN && process.env.FB_APP_TEST_USER ) { 18 | const {data} = await FB.api(process.env.FB_APP_ID + '/accounts/test-users', 19 | {access_token: process.env.FB_APP_ID + '|' + process.env.FB_APP_SECRET}); 20 | 21 | const testUser = data.find(({id}) => id === process.env.FB_APP_TEST_USER); 22 | if ( testUser ) { 23 | FB.options({ 24 | accessToken: testUser.access_token, 25 | }); 26 | } else { 27 | console.warn(`Could not find test user ${process.env.FB_APP_TEST_USER}`); // eslint-disable-line no-console 28 | } 29 | } 30 | }); 31 | 32 | export default FB; 33 | -------------------------------------------------------------------------------- /test_live/basic/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {expect} from '../_supports/chai'; 3 | import FB, {FacebookApiException} from '../_supports/fb'; 4 | import sinon from 'sinon'; 5 | 6 | describe('error', function() { 7 | describe("FB.api('/404')", function() { 8 | it('should throw a FacebookApiException', async function() { 9 | const res = FB.api('/404'); 10 | await expect(res).to.eventually.be.rejectedWith(FacebookApiException); 11 | await expect(res).to.eventually.be.rejected 12 | .and.have.a.property('response') 13 | .that.is.a('object') 14 | .with.property('error') 15 | .that.has.keys(['message', 'type', 'code', 'error_subcode', 'fbtrace_id']); 16 | }); 17 | }); 18 | 19 | describe("FB.api('/me', {fields: ['id', 'name']})", function() { 20 | beforeEach(function() { 21 | sinon.spy(console, 'warn'); 22 | }); 23 | 24 | afterEach(function() { 25 | console.warn.restore(); // eslint-disable-line no-console 26 | }); 27 | 28 | it('should emit a warning when fields is an array', async function() { 29 | await FB.api('/me', {fields: ['id', 'name']}); 30 | expect(console.warn).to.have.been.calledWith( // eslint-disable-line no-console 31 | `The fields param should be a comma separated list, not an array, try changing it to: ["id","name"].join(',')` 32 | ); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test_live/README.md: -------------------------------------------------------------------------------- 1 | Graph API Live Tests 2 | ==================== 3 | ```js 4 | npm run test-live 5 | ``` 6 | 7 | The normal `test/` tests locally ensure that the various ways of making API calls all make API calls in the same expected format. These tests are run against the live Graph API in just one format to ensure that the SDK functions with real-world API calls. 8 | 9 | To run these live tests you will have to setup a Facebook app for testing, create some test users, and include the credentials as ENV variables when running the test suite. 10 | 11 | * Create a Facebook app on https://developers.facebook.com/ 12 | * Using "Roles > Test Users" add a test user with the following options: 13 | * Enable "Authorize Test Users for This App?" 14 | * Set "Login Permissions" to "publish_actions","user_posts" 15 | * Use the "Edit" button to get an access token for that user 16 | * Run the test suite with the following environment variables (to simplify configuration you can save these to an `.env` file) 17 | * **`FB_APP_ID`**=Your test app's App ID. 18 | * **`FB_APP_SECRET`**=Your test app's App Secret. 19 | * **`FB_APP_ACCESS_TOKEN`**=The access token for the test user you created, you'll need to periodically get a new one. 20 | 21 | **WARNING**: These tests make test posts and photo uploads, **do not** run them using access tokens for your personal Facebook account. 22 | -------------------------------------------------------------------------------- /test_live/basic/batch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {expect} from '../_supports/chai'; 3 | import FB from '../_supports/fb'; 4 | 5 | describe('batch', function() { 6 | it('should make a batch API call and return results for each', async function() { 7 | const response = await FB.api('', 'post', { 8 | batch: [ 9 | {method: 'get', relative_url: 'me'}, 10 | {method: 'get', relative_url: 'me/friends?limit=1', name: 'one-friend'} 11 | ], 12 | }); 13 | 14 | const res1 = response[0]; 15 | expect(res1).to.be.a('object'); 16 | expect(res1).to.have.a.property('code', 200); 17 | expect(res1).to.have.a.property('body') 18 | .that.satisfies((body) => JSON.parse(body)); 19 | expect(JSON.parse(res1.body)).to.have.a.property('id') 20 | .that.is.a('string').which.matches(/^\d+$/); 21 | expect(JSON.parse(res1.body)).to.have.a.property('name') 22 | .that.is.a('string'); 23 | 24 | const res2 = response[1]; 25 | expect(res2).to.be.a('object'); 26 | expect(res2).to.have.a.property('code', 200); 27 | expect(res2).to.have.a.property('body') 28 | .that.satisfies((body) => JSON.parse(body)); 29 | expect(JSON.parse(res2.body)).to.have.a.property('data') 30 | .that.is.a('array'); 31 | expect(JSON.parse(res2.body)).to.have.a.property('summary') 32 | .that.is.a('object') 33 | .with.a.property('total_count').that.is.a('number'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test_live/basic/me.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {expect} from '../_supports/chai'; 3 | import FB from '../_supports/fb'; 4 | 5 | describe('me', function() { 6 | describe("FB.api('/me')", function() { 7 | it('should have a numeric `id` and string `name`', async function() { 8 | const response = await FB.api('/me'); 9 | expect(response).to.have.a.property('id') 10 | .that.is.a('string').which.matches(/^\d+$/); 11 | expect(response).to.have.a.property('name') 12 | .that.is.a('string'); 13 | }); 14 | 15 | it('should have an `id` that matches the one belonging to the access token', async function() { 16 | const {data} = await FB.api('/debug_token', { 17 | access_token: `${process.env.FB_APP_ID}|${process.env.FB_APP_SECRET}`, 18 | input_token: FB.options('accessToken'), 19 | }); 20 | const response = await FB.api('/me', {fields: 'id'}); 21 | expect(response).to.have.a.property('id', data.user_id); 22 | }); 23 | }); 24 | 25 | describe("FB.api('/me', {fields: 'locale'})", function() { 26 | it('should have a `locale` matching a locale string and not have a `name`', async function() { 27 | const response = await FB.api('/me', {fields: 'locale'}); 28 | expect(response).to.have.a.property('locale') 29 | .that.is.a('string').which.matches(/^[a-z]{2}_[A-Z]{2}$/); 30 | expect(response).to.not.have.a.property('name'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test_live/basic/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import crypto from 'crypto'; 5 | import {expect} from '../_supports/chai'; 6 | import FB from '../_supports/fb'; 7 | 8 | function getTestID() { 9 | const now = new Date(); 10 | const uniq = crypto.randomBytes(3).toString('base64'); 11 | const id = `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}-${uniq}`; 12 | return id; 13 | } 14 | 15 | describe('post', function() { 16 | describe("FB.api('/me/feed', 'post', {message})", function() { 17 | it('should post a message', async function() { 18 | const id = getTestID(); 19 | const message = `A test suite post using facebook-node-sdk (#${id})`; 20 | const response = await FB.api('/me/feed', 'post', {message, fields: 'message'}); 21 | expect(response).to.have.a.property('id') 22 | .that.is.a('string').which.matches(/^\d+_\d+$/); 23 | expect(response).to.have.a.property('message') 24 | .that.is.a('string').which.equals(message); 25 | }); 26 | }); 27 | 28 | describe("FB.api('/me/photos', 'post', {source: readStream, caption})", function() { 29 | it('should post an image', async function() { 30 | const id = getTestID(); 31 | const caption = `A test suite upload using facebook-node-sdk (#${id})`; 32 | const readStream = fs.createReadStream(path.join(__dirname, '../_fixtures/2x2-check.jpg')); 33 | const response = await FB.api('/me/photos', 'post', {readStream, caption, fields: 'name'}); 34 | expect(response).to.have.a.property('id') 35 | .that.is.a('string').which.matches(/^\d+$/); 36 | expect(response).to.have.a.property('name') 37 | .that.is.a('string').which.equals(caption); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/interface/import.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import FBdefault, {FB, FacebookApiException as FacebookApiExceptionImport} from '../..'; 3 | var FacebookApiException = require('../../lib/FacebookApiException').default, 4 | nock = require('nock'), 5 | expect = require('chai').expect, 6 | notError = require('../_supports/notError'), 7 | omit = require('lodash.omit'), 8 | defaultOptions = omit(FB.options(), 'appId'); 9 | 10 | nock.disableNetConnect(); 11 | 12 | beforeEach(function() { 13 | FB.options(defaultOptions); 14 | }); 15 | 16 | afterEach(function() { 17 | nock.cleanAll(); 18 | FB.options(defaultOptions); 19 | }); 20 | 21 | describe('import', function() { 22 | describe("import FB from 'fb';", function() { 23 | it('should expose FB.api', function() { 24 | expect(FBdefault).property('api') 25 | .to.be.a('function'); 26 | }); 27 | 28 | it('FB.api should work without `this`', function(done) { 29 | nock('https://graph.facebook.com:443') 30 | .get('/v2.5/4') 31 | .reply(200, { 32 | id: '4' 33 | }); 34 | 35 | FBdefault.api.call(undefined, '/4', function(result) { 36 | notError(result); 37 | expect(result).to.have.property('id', '4'); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | 43 | describe("import {FB} from 'fb';", function() { 44 | it('should expose FB.api', function() { 45 | expect(FB).property('api') 46 | .to.be.a('function'); 47 | }); 48 | 49 | it('FB.api should work without `this`', function(done) { 50 | nock('https://graph.facebook.com:443') 51 | .get('/v2.5/4') 52 | .reply(200, { 53 | id: '4' 54 | }); 55 | 56 | FB.api.call(undefined, '/4', function(result) { 57 | notError(result); 58 | expect(result).to.have.property('id', '4'); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | describe("import {FacebookApiException} from 'fb';", function() { 65 | it('should expose the FacebookApiException error type', function() { 66 | expect(FacebookApiExceptionImport).to.equal(FacebookApiException); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fb", 3 | "version": "2.0.0", 4 | "description": "NodeJS Library for Facebook", 5 | "keywords": [ 6 | "facebook", 7 | "fb", 8 | "graph" 9 | ], 10 | "author": "Thuzi LLC (https://github.com/Thuzi)", 11 | "contributors": [ 12 | "Daniel Friesen (http://danf.ca)" 13 | ], 14 | "homepage": "https://github.com/node-facebook/facebook-node-sdk", 15 | "license": "Apache-2.0", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/node-facebook/facebook-node-sdk.git" 19 | }, 20 | "main": "./lib/fb.js", 21 | "scripts": { 22 | "precommit": "lint-staged", 23 | "lint": "eslint .", 24 | "build": "babel src/ -d lib/", 25 | "buildw": "babel -w src/ -d lib/", 26 | "test": "npm run build && node ./node_modules/mocha/bin/mocha --recursive", 27 | "test-live": "npm run build && node ./node_modules/mocha/bin/mocha --require test_live/_supports/ci-safe --recursive test_live", 28 | "prepublish": "npm run build" 29 | }, 30 | "dependencies": { 31 | "babel-runtime": "^6.23.0", 32 | "core-decorators": "^0.17.0", 33 | "debug": "^2.6.3", 34 | "form-data": "^2.3.1", 35 | "needle": "^2.1.0" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.24.1", 39 | "babel-eslint": "^7.2.2", 40 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 41 | "babel-plugin-transform-runtime": "^6.23.0", 42 | "babel-preset-env": "^1.4.0", 43 | "babel-preset-stage-0": "^6.24.1", 44 | "babel-register": "^6.24.1", 45 | "bluebird": "^3.5.0", 46 | "chai": "^3.5.0", 47 | "chai-as-promised": "^7.1.1", 48 | "dotenv": "^5.0.0", 49 | "eslint": "^3.19.0", 50 | "husky": "^0.14.3", 51 | "lint-staged": "^7.0.0", 52 | "lodash.omit": "^4.5.0", 53 | "mocha": "^3.2.0", 54 | "mocha-eslint": "^3.0.1", 55 | "nock": "^9.2.1", 56 | "sinon": "^4.4.2", 57 | "sinon-chai": "^2.14.0" 58 | }, 59 | "engines": { 60 | "node": ">=6" 61 | }, 62 | "lint-staged": { 63 | "*.js": [ 64 | "eslint" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/interface/extend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import FBdefault, {FB, Facebook} from '../..'; 3 | var expect = require('chai').expect, 4 | omit = require('lodash.omit'), 5 | defaultOptions = omit(FB.options()); 6 | 7 | beforeEach(function() { 8 | FB.options(defaultOptions); 9 | }); 10 | 11 | afterEach(function() { 12 | FB.options(defaultOptions); 13 | }); 14 | 15 | describe('FB.extend', function() { 16 | describe('FB.extend()', function() { 17 | it('should create a Facebook instance', function() { 18 | var fb = FB.extend(); 19 | expect(fb).to.be.instanceof(Facebook); 20 | }); 21 | 22 | it('should not be the same instance as FB', function() { 23 | var fb = FB.extend(); 24 | expect(fb).to.not.equal(FBdefault) 25 | .and.to.not.equal(FB); 26 | }); 27 | }); 28 | 29 | describe("FB.extend({appId: '42'})", function() { 30 | it('should set options passed to it', function() { 31 | var fb = FB.extend({appId: '42'}); 32 | expect(fb.options('appId')).to.equal('42'); 33 | }); 34 | 35 | it('should inherit other options from FB', function() { 36 | FB.options({appSecret: 'the_secret'}); 37 | var fb = FB.extend({appId: '42'}); 38 | expect(fb.options('appSecret')).to.equal(FB.options('appSecret')); 39 | }); 40 | 41 | it('should inherit options from FB set after its creation', function() { 42 | FB.options({appSecret: 'the_secret'}); 43 | var fb = FB.extend({appId: '42'}); 44 | FB.options({appSecret: 'another_secret'}); 45 | expect(fb.options('appSecret')).to.equal('another_secret'); 46 | }); 47 | }); 48 | 49 | describe("fb.extend({appId: '42'})", function() { 50 | it('should work on an instance made by `new Facebook()` like it does on `FB`', function() { 51 | var fb = new Facebook(), 52 | fb2 = fb.extend({appId: '42'}); 53 | 54 | expect(fb2).to.not.equal(fb); 55 | expect(fb.options('appId')).to.not.equal('42'); 56 | expect(fb2.options('appId')).to.equal('42'); 57 | }); 58 | }); 59 | 60 | describe("FB.withAccessToken('access_token')", function() { 61 | it('should create a new instance with accessToken set', function() { 62 | var fb = FB.withAccessToken('access_token'); 63 | expect(fb).to.not.equal(FB); 64 | expect(fb.options('accessToken')).to.equal('access_token'); 65 | expect(FB.options('accessToken')).to.not.equal(fb.options('accessToken')); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/api/batch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var nock = require('nock'), 3 | expect = require('chai').expect, 4 | FB = require('../..').default, 5 | notError = require('../_supports/notError'), 6 | omit = require('lodash.omit'), 7 | defaultOptions = omit(FB.options(), 'appId'); 8 | 9 | nock.disableNetConnect(); 10 | 11 | beforeEach(function() { 12 | FB.options(defaultOptions); 13 | }); 14 | 15 | afterEach(function() { 16 | nock.cleanAll(); 17 | FB.options(defaultOptions); 18 | }); 19 | 20 | describe('FB.api', function() { 21 | describe('batch', function() { 22 | 23 | describe("FB.api('', 'post', { batch: [ { method: 'get', relative_url: '4' }, { method: 'get', relative_url: 'me/friends?limit=50' } ], cb)", function() { 24 | beforeEach(function() { 25 | nock('https://graph.facebook.com:443') 26 | .post('/v2.5/', 'batch=%5B%7B%22method%22%3A%22get%22%2C%22relative_url%22%3A%224%22%7D%2C%7B%22method%22%3A%22get%22%2C%22relative_url%22%3A%22me%2Ffriends%3Flimit%3D50%22%7D%5D') 27 | .reply(200, [ 28 | { 29 | code: 200, 30 | headers: [ 31 | { 32 | name: 'Access-Control-Allow-Origin', 33 | value: '*' 34 | }, 35 | { 36 | name: 'Content-Type', 37 | value: 'text/javascript; charset=UTF-8' 38 | }, 39 | { 40 | name: 'Facebook-API-Version', 41 | value: 'v2.4' 42 | } 43 | ], 44 | body: JSON.stringify({ 45 | id: '4', 46 | name: 'Mark Zuckerberg' 47 | }) 48 | }, 49 | { 50 | code: 200, 51 | headers: [ 52 | { 53 | name: 'Access-Control-Allow-Origin', 54 | value: '*' 55 | }, 56 | { 57 | name: 'Content-Type', 58 | value: 'text/javascript; charset=UTF-8' 59 | }, 60 | { 61 | name: 'Facebook-API-Version', 62 | value: 'v2.4' 63 | } 64 | ], 65 | body: JSON.stringify({ 66 | data: [], 67 | summary: { 68 | total_count: 0 69 | } 70 | }) 71 | } 72 | ]); 73 | }); 74 | 75 | it('should return batch results', function(done) { 76 | FB.api('', 'post', { 77 | batch: [ 78 | {method: 'get', relative_url: '4'}, 79 | {method: 'get', relative_url: 'me/friends?limit=50'} 80 | ] 81 | }, function(result) { 82 | notError(result); 83 | expect(result).to.be.a('array'); 84 | expect(result[0]).to.have.property('code', 200); 85 | expect(result[1]).to.have.property('code', 200); 86 | done(); 87 | }); 88 | }); 89 | }); 90 | 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/api/header.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var nock = require('nock'), 3 | expect = require('chai').expect, 4 | notError = require('../_supports/notError'), 5 | FB = require('../..').default, 6 | omit = require('lodash.omit'), 7 | defaultOptions = omit(FB.options(), 'appId'); 8 | 9 | nock.disableNetConnect(); 10 | 11 | beforeEach(function() { 12 | FB.options(defaultOptions); 13 | }); 14 | 15 | afterEach(function() { 16 | nock.cleanAll(); 17 | FB.options(defaultOptions); 18 | }); 19 | 20 | describe('FB.api', function() { 21 | describe('headers', function() { 22 | describe('FB.getAppUsage()', function() { 23 | it('should be updated', function(done) { 24 | nock('https://graph.facebook.com:443') 25 | .get('/v2.5/4') 26 | .reply(200, {}, { 27 | 'X-App-Usage': '{"call_count":50, "total_time":60, "total_cputime":70}' 28 | }); 29 | 30 | FB.api('4', function(result) { 31 | notError(result); 32 | let appUsage = FB.getAppUsage(); 33 | expect(appUsage).to.have.property('callCount', 50); 34 | expect(appUsage).to.have.property('totalTime', 60); 35 | expect(appUsage).to.have.property('totalCPUTime', 70); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('should be set back to 0 if no header sent', function(done) { 41 | nock('https://graph.facebook.com:443') 42 | .get('/v2.5/5') 43 | .reply(200, {}); 44 | 45 | FB.api('5', function(result) { 46 | notError(result); 47 | let appUsage = FB.getAppUsage(); 48 | expect(appUsage).to.have.property('callCount', 0); 49 | expect(appUsage).to.have.property('totalTime', 0); 50 | expect(appUsage).to.have.property('totalCPUTime', 0); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('FB.getPageUsage()', function() { 57 | it('should be updated', function(done) { 58 | nock('https://graph.facebook.com:443') 59 | .get('/v2.5/4') 60 | .reply(200, {}, { 61 | 'X-Page-Usage': '{"call_count":10, "total_time":20, "total_cputime":30}' 62 | }); 63 | 64 | FB.api('4', function(result) { 65 | notError(result); 66 | let pageUsage = FB.getPageUsage(); 67 | expect(pageUsage).to.have.property('callCount', 10); 68 | expect(pageUsage).to.have.property('totalTime', 20); 69 | expect(pageUsage).to.have.property('totalCPUTime', 30); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should be set back to 0 if no header sent', function(done) { 75 | nock('https://graph.facebook.com:443') 76 | .get('/v2.5/5') 77 | .reply(200, {}); 78 | FB.api('5', function(result) { 79 | notError(result); 80 | let pageUsage = FB.getPageUsage(); 81 | expect(pageUsage).to.have.property('callCount', 0); 82 | expect(pageUsage).to.have.property('totalTime', 0); 83 | expect(pageUsage).to.have.property('totalCPUTime', 0); 84 | done(); 85 | }); 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/signed_request/parseSignedRequest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect, 3 | FB = require('../..').default, 4 | omit = require('lodash.omit'), 5 | defaultOptions = omit(FB.options(), 'appId'), 6 | signature = 'U0_O1MqqNKUt32633zAkdd2Ce-jGVgRgJeRauyx_zC8', 7 | app_secret = 'foo_app_secret', 8 | payload = 'eyJvYXV0aF90b2tlbiI6ImZvb190b2tlbiIsImFsZ29yaXRobSI6IkhNQUMtU0hBMjU2IiwiaXNzdWVkX2F0IjozMjEsImNvZGUiOiJmb29fY29kZSIsInN0YXRlIjoiZm9vX3N0YXRlIiwidXNlcl9pZCI6MTIzLCJmb28iOiJiYXIifQ==', 9 | payloadData = { 10 | oauth_token: 'foo_token', 11 | algorithm: 'HMAC-SHA256', 12 | issued_at: 321, 13 | code: 'foo_code', 14 | state: 'foo_state', 15 | user_id: 123, 16 | foo: 'bar' 17 | }, 18 | signedRequest = signature + '.' + payload; 19 | 20 | beforeEach(function() { 21 | FB.options(defaultOptions); 22 | }); 23 | 24 | afterEach(function() { 25 | FB.options(defaultOptions); 26 | }); 27 | 28 | describe('FB.parseSignedRequest', function() { 29 | describe('FB.parseSignedRequest(signedRequest, app_secret)', function() { 30 | describe('when app_secret is defined', function() { 31 | it('should decode the correct payload', function() { 32 | expect(FB.parseSignedRequest(signedRequest, app_secret)).to.exist 33 | .and.include(payloadData); 34 | }); 35 | 36 | it('should prefer the app_secret argument over the appSecret option', function() { 37 | FB.options({appSecret: 'wrong_secret'}); 38 | expect(FB.parseSignedRequest(signedRequest, app_secret)).to.exist 39 | .and.include(payloadData); 40 | }); 41 | }); 42 | 43 | describe('when signedRequest is undefined', function() { 44 | it('should return undefined', function() { 45 | expect(FB.parseSignedRequest(undefined, app_secret)).to.be.undefined; 46 | }); 47 | }); 48 | 49 | describe('when signedRequest is not two pieces separated by a .', function() { 50 | it('should return undefined', function() { 51 | expect(FB.parseSignedRequest('wrong', app_secret)).to.be.undefined; 52 | }); 53 | }); 54 | 55 | describe('when signedRequest is not base64 encoded', function() { 56 | it('should return undefined', function() { 57 | expect(FB.parseSignedRequest('wrong.token', app_secret)).to.be.undefined; 58 | }); 59 | }); 60 | 61 | describe('when signature is incorrect', function() { 62 | it('should return undefined', function() { 63 | expect(FB.parseSignedRequest('YmFkc2ln.' + payload, app_secret)).to.be.undefined; 64 | }); 65 | }); 66 | 67 | describe('when app_secret is undefined', function() { 68 | it('should use the appSecret option to decode the payload', function() { 69 | FB.options({appSecret: app_secret}); 70 | expect(FB.parseSignedRequest(signedRequest)).to.exist 71 | .and.include(payloadData); 72 | }); 73 | 74 | it('should throw when the appSecret option is not defined', function() { 75 | expect(function() { return FB.parseSignedRequest(signedRequest); }).to.throw; 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## 3.0.0 5 | 6 | * Added `getAppUsage` API 7 | * New minimum API version `v2.5` (v2.4 is no longer available) 8 | * Debug headers are now logged with DEBUG=fb:fbdebug 9 | * Log messages to stderr using console.warn instead of console.log 10 | * **BREAKING CHANGE**: Switched from the `request` library to `needle` 11 | * **BREAKING CHANGE**: Passing invalid methods or arguments to `FB.api` now throws a TypeError instead of logging and returning undefined 12 | * Use of the [function-bind operator](https://github.com/tc39/proposal-bind-operator) has been removed 13 | * **BREAKING CHANGE**: Raised the minimum supported version of Node to `6.x` 14 | * **BREAKING CHANGE**: `any-promise` has been removed, native promises are now returned by default. If you wish to use a 3rd party promise library like `bluebird` you must explicitly set the `Promise` option instead of relying on `any-promise`'s behaviour 15 | * * **BREAKING CHANGE**: CommonJS `require('fb')` must now use the `.default` or `.FB` export 16 | 17 | ## 2.0.0 18 | 19 | * **BREAKING CHANGE**: Dropped support for FQL and Legacy REST Api 20 | * **BREAKING CHANGE**: New minimum API version `v2.3` 21 | * **BREAKING CHANGE**: `FacebookApiException` and `version` are no longer available on `Facebook` instances. 22 | * This means `FB.FacebookApiException` cannot be used when doing `import FB from 'fb';` or `var {FB} = require('fb');` you must import `FacebookApiException` separately. 23 | * **BREAKING CHANGE**: Drop support for node `0.10` and `0.12`, node `4` is the new minimum 24 | * **BREAKING CHANGE**: The old broken samples/ directory has been removed 25 | * `FB.api` now supports usage with promises 26 | * Update deps 27 | * Migrate to babel-preset-env 28 | 29 | ## 1.1.1 30 | 31 | * Fix #54: FB methods should be bound 32 | 33 | ## 1.1.0 34 | 35 | * Update deps: 36 | * `request`: `^2.62.0` -> `^2.67.0` 37 | * `chai`: `^3.2.0` -> `^3.4.1` 38 | * `lodash.omit`: `^3.1.0` -> `^4.0.1` 39 | * `mocha`: `^2.3.2` -> `^2.3.4` 40 | * `nock`: `^2.12.0` -> `^5.2.1` 41 | * Explicitly support ES2015 `import` in Babel 42 | * Add `new Facebook(options)` for library usage 43 | * Add `FB.extend` for multi-app usage 44 | * Add `FB.withAccessToken` for alternate multi-user usage 45 | * Support file uploads using Buffers and read streams 46 | 47 | ## 1.0.2 48 | 49 | * #22 Fix accidental global / strict mode bug 50 | 51 | ## 1.0.1 52 | 53 | * `1.0.0` was accidentally tagged as `next` instead of `latest`, making `1.0.1` the first official `1.0.x` version. 54 | 55 | ## 1.0.0 56 | 57 | * Add `beta` option 58 | * Update dep: `request` 59 | * Remove unused dependency on `crypto` package 60 | * **BREAKING CHANGE**: Drop support for node `0.6` and `0.8` 61 | * Fix __dirname relative required that breaks browserify builds 62 | * Add `userAgent` option 63 | * Add `version` option 64 | * Fix incorrect merging of string and object query params 65 | * Add `DEBUG=fb:req,fb:sig` support 66 | * `pingFacebook` removed 67 | 68 | ## 0.7.3 and before 69 | 70 | Versions before 1.0.0 were maintained by Thuzi and were never given an official changelog. 71 | -------------------------------------------------------------------------------- /test/access_token/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var nock = require('nock'), 3 | expect = require('chai').expect, 4 | notError = require('../_supports/notError'), 5 | FB = require('../..').default, 6 | omit = require('lodash.omit'), 7 | defaultOptions = omit(FB.options(), 'appId'); 8 | 9 | nock.disableNetConnect(); 10 | 11 | beforeEach(function() { 12 | FB.options(defaultOptions); 13 | }); 14 | 15 | afterEach(function() { 16 | nock.cleanAll(); 17 | FB.options(defaultOptions); 18 | }); 19 | 20 | describe('access_token', function() { 21 | describe("FB.setAccessToken('access_token')", function() { 22 | it('should set an access_token used by api calls', function(done) { 23 | FB.setAccessToken('access_token'); 24 | 25 | var expectedRequest = nock('https://graph.facebook.com:443') 26 | .get('/v2.5/me') 27 | .query({ 28 | access_token: 'access_token' 29 | }) 30 | .reply(200, { 31 | id: '4', 32 | name: 'Mark Zuckerberg' 33 | }); 34 | 35 | FB.api('/me', function(result) { 36 | notError(result); 37 | expectedRequest.done(); 38 | done(); 39 | }); 40 | 41 | }); 42 | }); 43 | 44 | describe("FB.api('/me', { access_token: 'access_token' }, cb)", function() { 45 | it('should override an access_token set with FB.setAccessToken()', function(done) { 46 | FB.setAccessToken('wrong_token'); 47 | 48 | var expectedRequest = nock('https://graph.facebook.com:443') 49 | .get('/v2.5/me') 50 | .query({ 51 | access_token: 'access_token' 52 | }) 53 | .reply(200, { 54 | id: '4', 55 | name: 'Mark Zuckerberg' 56 | }); 57 | 58 | FB.api('/me', {access_token: 'access_token'}, function(result) { 59 | notError(result); 60 | expectedRequest.done(); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('FB.getAccessToken()', function() { 67 | describe('when accessToken is not set', function() { 68 | it('should return null', function() { 69 | expect(FB.getAccessToken()).to.be.null; 70 | }); 71 | }); 72 | 73 | describe('when accessToken is set', function() { 74 | it('should return the access_token', function() { 75 | FB.setAccessToken('access_token'); 76 | expect(FB.getAccessToken()) 77 | .to.exist 78 | .and.equal('access_token'); 79 | }); 80 | }); 81 | }); 82 | 83 | describe("FB.api('/me', { access_token: 'access_token' }, cb)", function() { 84 | it('should include the correct appsecret_proof in the query', function(done) { 85 | FB.options({appSecret: 'app_secret'}); 86 | 87 | var expectedRequest = nock('https://graph.facebook.com:443') 88 | .get('/v2.5/me') 89 | .query({ 90 | access_token: 'access_token', 91 | appsecret_proof: 'd52ddf968d622d8af8677906b7fbae09ac1f89f7cd5c1584b27544624cc23e5a' 92 | }) 93 | .reply(200, { 94 | id: '4', 95 | name: 'Mark Zuckerberg' 96 | }); 97 | 98 | FB.api('/me', {access_token: 'access_token'}, function(result) { 99 | notError(result); 100 | expectedRequest.done(); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/api/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'), 3 | fs = require('fs'), 4 | nock = require('nock'), 5 | expect = require('chai').expect, 6 | notError = require('../_supports/notError'), 7 | FB = require('../..').default, 8 | omit = require('lodash.omit'), 9 | defaultOptions = omit(FB.options(), 'appId'); 10 | 11 | nock.disableNetConnect(); 12 | 13 | beforeEach(function() { 14 | FB.options(defaultOptions); 15 | }); 16 | 17 | afterEach(function() { 18 | nock.cleanAll(); 19 | FB.options(defaultOptions); 20 | }); 21 | 22 | describe('FB.api', function() { 23 | describe('POST', function() { 24 | describe("FB.api('me/feed', 'post', { message: 'My first post using facebook-node-sdk' }, cb)", function() { 25 | beforeEach(function() { 26 | nock('https://graph.facebook.com:443') 27 | .post('/v2.5/me/feed', 'message=My%20first%20post%20using%20facebook-node-sdk') 28 | .reply(200, function() { 29 | return { 30 | contentType: this.req.headers['content-type'], 31 | id: '4_14' 32 | }; 33 | }); 34 | }); 35 | 36 | it('should have id 4_14', function(done) { 37 | FB.api('me/feed', 'post', {message: 'My first post using facebook-node-sdk'}, function(result) { 38 | notError(result); 39 | expect(result.contentType).to.equal('application/x-www-form-urlencoded'); 40 | expect(result).to.have.property('id', '4_14'); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | 46 | describe("FB.api('path', 'post', { file: { value: new Buffer('...', 'utf8'), options: { contentType: 'text/plain' } }, cb)", function() { 47 | beforeEach(function() { 48 | nock('https://graph.facebook.com:443') 49 | .post('/v2.5/path') 50 | .reply(200, function(uri, body) { 51 | return { 52 | contentType: this.req.headers['content-type'], 53 | body: body 54 | }; 55 | }); 56 | }); 57 | 58 | it("should upload a file containing '...'", function(done) { 59 | FB.api('path', 'post', {file: {value: new Buffer('...', 'utf8'), options: {contentType: 'text/plain'}}}, function(result) { 60 | notError(result); 61 | expect(result.contentType).to.match(/^multipart\/form-data; boundary=/); 62 | let [, boundary] = result.contentType.match(/boundary=(.+)/); 63 | expect(result.body).to.equal(`--${boundary}\r\nContent-Disposition: form-data; name="file"\r\nContent-Type: text/plain\r\n\r\n...\r\n--${boundary}--\r\n`); 64 | done(); 65 | }); 66 | }); 67 | }); 68 | 69 | describe("FB.api('path', 'post', { file: fs.createReadStream('./ellipsis.txt') }, cb)", function() { 70 | beforeEach(function() { 71 | nock('https://graph.facebook.com:443') 72 | .post('/v2.5/path') 73 | .reply(200, function(uri, body) { 74 | return { 75 | contentType: this.req.headers['content-type'], 76 | body: body 77 | }; 78 | }); 79 | }); 80 | 81 | it("should upload a file containing '...'", function(done) { 82 | FB.api('path', 'post', {file: fs.createReadStream(path.join(__dirname, '../_fixtures/ellipsis.txt'))}, function(result) { 83 | notError(result); 84 | expect(result.contentType).to.match(/^multipart\/form-data; boundary=/); 85 | let [, boundary] = result.contentType.match(/boundary=(.+)/); 86 | expect(result.body).to.equal(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="ellipsis.txt"\r\nContent-Type: text/plain\r\n\r\n...\n\r\n--${boundary}--\r\n`); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /test/options/options.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var FB = require('../..').default, 3 | nock = require('nock'), 4 | expect = require('chai').expect, 5 | notError = require('../_supports/notError'), 6 | omit = require('lodash.omit'), 7 | defaultOptions = omit(FB.options(), 'appId'), 8 | {version} = require('../../package.json'); 9 | 10 | nock.disableNetConnect(); 11 | 12 | beforeEach(function() { 13 | FB.options(defaultOptions); 14 | }); 15 | 16 | afterEach(function() { 17 | nock.cleanAll(); 18 | FB.options(defaultOptions); 19 | }); 20 | 21 | describe('FB.options', function() { 22 | describe('beta', function() { 23 | it('Should default beta to false', function() { 24 | expect(FB.options('beta')).to.be.false; 25 | }); 26 | 27 | it('Should allow beta option to be set', function() { 28 | FB.options({beta: true}); 29 | 30 | expect(FB.options('beta')).to.be.true; 31 | 32 | FB.options({beta: false}); 33 | 34 | expect(FB.options('beta')).to.be.false; 35 | }); 36 | 37 | it('Should make use graph.facebook when beta is false', function(done) { 38 | var expectedRequest = nock('https://graph.facebook.com:443').get('/v2.5/4').reply(200, {}); 39 | 40 | FB.options({beta: false}); 41 | 42 | FB.api('/4', function(result) { 43 | notError(result); 44 | expectedRequest.done(); // verify non-beta request was made 45 | 46 | done(); 47 | }); 48 | }); 49 | 50 | it('Should make use graph.beta.facebook when beta is true', function(done) { 51 | var expectedRequest = nock('https://graph.beta.facebook.com:443').get('/v2.5/4').reply(200, {}); 52 | 53 | FB.options({beta: true}); 54 | 55 | FB.api('/4', function(result) { 56 | notError(result); 57 | expectedRequest.done(); // verify beta request was made 58 | 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('userAgent', function() { 65 | beforeEach(function() { 66 | nock('https://graph.facebook.com:443') 67 | .get('/v2.5/4') 68 | .reply(function() { 69 | return { 70 | userAgent: this.req.headers['user-agent'] 71 | }; 72 | }); 73 | }); 74 | 75 | it('Should default to thuzi_nodejssdk/' + version, function() { 76 | expect(FB.options('userAgent')).to.equal('thuzi_nodejssdk/' + version); 77 | }); 78 | 79 | it('Should default the userAgent for FB.api requests to thuzi_nodejssdk/' + version, function(done) { 80 | FB.api('/4', function(result) { 81 | notError(result); 82 | expect(result.userAgent).to.equal('thuzi_nodejssdk/' + version); 83 | 84 | done(); 85 | }); 86 | }); 87 | 88 | it('Should be used as the userAgent for FB.api requests', function(done) { 89 | FB.options({userAgent: 'faux/0.0.1'}); 90 | 91 | FB.api('/4', function(result) { 92 | notError(result); 93 | expect(result.userAgent).to.equal('faux/0.0.1'); 94 | 95 | done(); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('version', function() { 101 | it('Should default version to v2.5', function() { 102 | expect(FB.options('version')).to.equal('v2.5'); 103 | }); 104 | 105 | it('Should change the version used in FB.api requests', function(done) { 106 | FB.options({version: 'v2.9'}); 107 | 108 | var expectedRequest = nock('https://graph.facebook.com:443') 109 | .get('/v2.9/4') 110 | .reply(200, { 111 | id: '4', 112 | name: 'Mark Zuckerberg', 113 | first_name: 'Mark', 114 | last_name: 'Zuckerberg', 115 | link: 'http://www.facebook.com/zuck', 116 | gender: 'male', 117 | locale: 'en_US' 118 | }); 119 | 120 | FB.api('4', function(result) { 121 | notError(result); 122 | expectedRequest.done(); 123 | done(); 124 | }); 125 | }); 126 | 127 | it("Should not prepend a version to FB.api('/v2.12/4', cb) style requests", function(done) { 128 | FB.options({version: 'v2.9'}); 129 | 130 | var expectedRequest = nock('https://graph.facebook.com:443') 131 | .get('/v2.12/4') 132 | .reply(200, { 133 | id: '4', 134 | name: 'Mark Zuckerberg', 135 | first_name: 'Mark', 136 | last_name: 'Zuckerberg', 137 | link: 'http://www.facebook.com/zuck', 138 | gender: 'male', 139 | locale: 'en_US' 140 | }); 141 | 142 | FB.api('/v2.12/4', function(result) { 143 | notError(result); 144 | expectedRequest.done(); 145 | done(); 146 | }); 147 | }); 148 | 149 | it('Should change the version used in FB.getLoginUrl', function() { 150 | FB.options({version: 'v2.9'}); 151 | expect(FB.getLoginUrl({appId: 'app_id'})) 152 | .to.equal('https://www.facebook.com/v2.9/dialog/oauth?response_type=code&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/login_url/getLoginUrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var nock = require('nock'), 3 | expect = require('chai').expect, 4 | FB = require('../..').default, 5 | omit = require('lodash.omit'), 6 | defaultOptions = omit(FB.options(), 'appId'); 7 | 8 | nock.disableNetConnect(); 9 | 10 | beforeEach(function() { 11 | FB.options(defaultOptions); 12 | }); 13 | 14 | afterEach(function() { 15 | nock.cleanAll(); 16 | FB.options(defaultOptions); 17 | }); 18 | 19 | describe('FB.getLoginUrl', function() { 20 | var base = 'https://www.facebook.com/v2.5/dialog/oauth'; 21 | describe('when no options are set', function() { 22 | describe('FB.getLoginUrl({}})', function() { 23 | it('should throw', function() { 24 | expect(function() { return FB.getLoginUrl({}); }).to.throw; 25 | }); 26 | }); 27 | 28 | describe("FB.getLoginUrl({ appId: 'app_id' }})", function() { 29 | var url = base + '?response_type=code&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 30 | it('should equal ' + url, function() { 31 | expect(FB.getLoginUrl({appId: 'app_id'})) 32 | .to.equal(url); 33 | }); 34 | }); 35 | 36 | describe("FB.getLoginUrl({ client_id: 'app_id' }})", function() { 37 | var url = base + '?response_type=code&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 38 | it('should equal ' + url, function() { 39 | expect(FB.getLoginUrl({client_id: 'app_id'})) 40 | .to.equal(url); 41 | }); 42 | }); 43 | 44 | describe("FB.getLoginUrl({ client_id: 'app_id', scope: 'email' }})", function() { 45 | var url = base + '?response_type=code&scope=email&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 46 | it('should equal ' + url, function() { 47 | expect(FB.getLoginUrl({client_id: 'app_id', scope: 'email'})) 48 | .to.equal(url); 49 | }); 50 | }); 51 | 52 | describe("FB.getLoginUrl({ client_id: 'app_id', state: 'state_data' }})", function() { 53 | var url = base + '?response_type=code&state=state_data&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 54 | it('should equal ' + url, function() { 55 | expect(FB.getLoginUrl({client_id: 'app_id', state: 'state_data'})) 56 | .to.equal(url); 57 | }); 58 | }); 59 | 60 | describe("FB.getLoginUrl({ client_id: 'app_id', redirectUri: 'http://example.com/' }})", function() { 61 | var url = base + '?response_type=code&redirect_uri=http%3A%2F%2Fexample.com%2F&client_id=app_id'; 62 | it('should equal ' + url, function() { 63 | expect(FB.getLoginUrl({client_id: 'app_id', redirectUri: 'http://example.com/'})) 64 | .to.equal(url); 65 | }); 66 | }); 67 | 68 | describe("FB.getLoginUrl({ client_id: 'app_id', redirect_uri: 'http://example.com/' }})", function() { 69 | var url = base + '?response_type=code&redirect_uri=http%3A%2F%2Fexample.com%2F&client_id=app_id'; 70 | it('should equal ' + url, function() { 71 | expect(FB.getLoginUrl({client_id: 'app_id', redirect_uri: 'http://example.com/'})) 72 | .to.equal(url); 73 | }); 74 | }); 75 | 76 | describe("FB.getLoginUrl({ client_id: 'app_id', display: 'popup' }})", function() { 77 | var url = base + '?response_type=code&display=popup&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 78 | it('should equal ' + url, function() { 79 | expect(FB.getLoginUrl({client_id: 'app_id', display: 'popup'})) 80 | .to.equal(url); 81 | }); 82 | }); 83 | 84 | describe("FB.getLoginUrl({ client_id: 'app_id', responseType: 'token' }})", function() { 85 | var url = base + '?response_type=token&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 86 | it('should equal ' + url, function() { 87 | expect(FB.getLoginUrl({client_id: 'app_id', responseType: 'token'})) 88 | .to.equal(url); 89 | }); 90 | }); 91 | 92 | describe("FB.getLoginUrl({ client_id: 'app_id', response_type: 'token' }})", function() { 93 | var url = base + '?response_type=token&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 94 | it('should equal ' + url, function() { 95 | expect(FB.getLoginUrl({client_id: 'app_id', response_type: 'token'})) 96 | .to.equal(url); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('when the redirectUri option is set to http://example.com/', function() { 102 | beforeEach(function() { 103 | FB.options({redirectUri: 'http://example.com/'}); 104 | }); 105 | 106 | describe("FB.getLoginUrl({ client_id: 'app_id' }})", function() { 107 | var url = base + '?response_type=code&redirect_uri=http%3A%2F%2Fexample.com%2F&client_id=app_id'; 108 | it('should equal ' + url, function() { 109 | expect(FB.getLoginUrl({client_id: 'app_id'})) 110 | .to.equal(url); 111 | }); 112 | }); 113 | 114 | describe("FB.getLoginUrl({ client_id: 'app_id', redirectUri: 'http://example.org/' }})", function() { 115 | var url = base + '?response_type=code&redirect_uri=http%3A%2F%2Fexample.org%2F&client_id=app_id'; 116 | it('should equal ' + url, function() { 117 | expect(FB.getLoginUrl({client_id: 'app_id', redirectUri: 'http://example.org/'})) 118 | .to.equal(url); 119 | }); 120 | }); 121 | 122 | describe("FB.getLoginUrl({ client_id: 'app_id', redirect_uri: 'http://example.org/' }})", function() { 123 | var url = base + '?response_type=code&redirect_uri=http%3A%2F%2Fexample.org%2F&client_id=app_id'; 124 | it('should equal ' + url, function() { 125 | expect(FB.getLoginUrl({client_id: 'app_id', redirect_uri: 'http://example.org/'})) 126 | .to.equal(url); 127 | }); 128 | }); 129 | }); 130 | 131 | describe("when the scope option is set to 'email'", function() { 132 | beforeEach(function() { 133 | FB.options({scope: 'email'}); 134 | }); 135 | 136 | describe("FB.getLoginUrl({ client_id: 'app_id' }})", function() { 137 | var url = base + '?response_type=code&scope=email&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 138 | it('should equal ' + url, function() { 139 | expect(FB.getLoginUrl({client_id: 'app_id'})) 140 | .to.equal(url); 141 | }); 142 | }); 143 | 144 | describe("FB.getLoginUrl({ client_id: 'app_id', scope: 'email,user_likes' }})", function() { 145 | var url = base + '?response_type=code&scope=email%2Cuser_likes&redirect_uri=https%3A%2F%2Fwww.facebook.com%2Fconnect%2Flogin_success.html&client_id=app_id'; 146 | it('should equal ' + url, function() { 147 | expect(FB.getLoginUrl({client_id: 'app_id', scope: 'email,user_likes'})) 148 | .to.equal(url); 149 | }); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/api/get.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Bluebird = require('bluebird'), 3 | nock = require('nock'), 4 | expect = require('chai').expect, 5 | notError = require('../_supports/notError'), 6 | {FB, Facebook} = require('../..'), 7 | omit = require('lodash.omit'), 8 | defaultOptions = omit(FB.options(), 'appId'); 9 | 10 | nock.disableNetConnect(); 11 | 12 | beforeEach(function() { 13 | FB.options(defaultOptions); 14 | }); 15 | 16 | afterEach(function() { 17 | nock.cleanAll(); 18 | FB.options(defaultOptions); 19 | }); 20 | 21 | describe('FB.api', function() { 22 | describe('GET', function() { 23 | describe("FB.api('4', cb)", function() { 24 | beforeEach(function() { 25 | nock('https://graph.facebook.com:443') 26 | .get('/v2.5/4') 27 | .reply(200, { 28 | id: '4', 29 | name: 'Mark Zuckerberg', 30 | first_name: 'Mark', 31 | last_name: 'Zuckerberg', 32 | link: 'http://www.facebook.com/zuck', 33 | gender: 'male', 34 | locale: 'en_US' 35 | }); 36 | }); 37 | 38 | it('should have id 4', function(done) { 39 | FB.api('4', function(result) { 40 | notError(result); 41 | expect(result).to.have.property('id', '4'); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | describe("FB.api('/4', cb)", function() { 48 | it('should have id 4', function(done) { 49 | nock('https://graph.facebook.com:443') 50 | .get('/v2.5/4') 51 | .reply(200, { 52 | id: '4', 53 | name: 'Mark Zuckerberg', 54 | first_name: 'Mark', 55 | last_name: 'Zuckerberg', 56 | link: 'http://www.facebook.com/zuck', 57 | username: 'zuck', 58 | gender: 'male', 59 | locale: 'en_US' 60 | }); 61 | 62 | FB.api('/4', function(result) { 63 | notError(result); 64 | expect(result).to.have.property('id', '4'); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should work without `this`', function(done) { 70 | nock('https://graph.facebook.com:443') 71 | .get('/v2.5/4') 72 | .reply(200, { 73 | id: '4', 74 | name: 'Mark Zuckerberg', 75 | first_name: 'Mark', 76 | last_name: 'Zuckerberg', 77 | link: 'http://www.facebook.com/zuck', 78 | username: 'zuck', 79 | gender: 'male', 80 | locale: 'en_US' 81 | }); 82 | 83 | FB.api.call(undefined, '/4', function(result) { 84 | notError(result); 85 | expect(result).to.have.property('id', '4'); 86 | done(); 87 | }); 88 | }); 89 | }); 90 | 91 | describe('FB.api(4, cb)', function() { 92 | // this is the default behavior of client side js sdk 93 | it('should throw synchronously: Path is of type number, not string', function(done) { 94 | try { 95 | FB.api(4, function() { 96 | }); 97 | 98 | done(new Error('Passing in a number should throw an exception')); 99 | } catch (e) { 100 | done(); 101 | } 102 | }); 103 | }); 104 | 105 | describe("FB.api('4', { fields: 'id' }), cb)", function() { 106 | it("should return { id: '4' } object", function(done) { 107 | nock('https://graph.facebook.com:443') 108 | .get('/v2.5/4?fields=id') 109 | .reply(200, { 110 | id: '4' 111 | }); 112 | 113 | FB.api('4', {fields: 'id'}, function(result) { 114 | notError(result); 115 | expect(result).to.include({id: '4'}); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | 121 | describe("FB.api('/4?fields=name', cb)", function() { 122 | it("should return { id: '4', name: 'Mark Zuckerberg' } object", function(done) { 123 | nock('https://graph.facebook.com:443') 124 | .get('/v2.5/4?fields=name') 125 | .reply(200, { 126 | name: 'Mark Zuckerberg', 127 | id: '4' 128 | }); 129 | 130 | FB.api('/4?fields=name', function(result) { 131 | notError(result); 132 | expect(result).to.have.keys('id', 'name') 133 | .and.include({id: '4', name: 'Mark Zuckerberg'}); 134 | done(); 135 | }); 136 | }); 137 | }); 138 | 139 | describe("FB.api('/', { yes: true, no: false }), cb)", function() { 140 | it('should serialize boolean parameter values to true/false strings ?yes=true&no=false', function(done) { 141 | nock('https://graph.facebook.com:443') 142 | .get('/v2.5/4?yes=true&no=false') 143 | .reply(200, { 144 | }); 145 | 146 | FB.api('4', {yes: true, no: false}, function(result) { 147 | notError(result); 148 | done(); 149 | }); 150 | }); 151 | }); 152 | 153 | describe("FB.api('/', { object: { a: false, b: 2, c: 'cat' } }), cb)", function() { 154 | it('should serialize object parameter values to json ?object={"a":false,"b":2,"c":"cat"}', function(done) { 155 | nock('https://graph.facebook.com:443') 156 | .get('/v2.5/4?object=%7B%22a%22%3Afalse%2C%22b%22%3A2%2C%22c%22%3A%22cat%22%7D') 157 | .reply(200, { 158 | }); 159 | 160 | FB.api('4', {object: {a: false, b: 2, c: 'cat'}}, function(result) { 161 | notError(result); 162 | done(); 163 | }); 164 | }); 165 | }); 166 | 167 | describe("FB.api('/', { array: [false, 2, 'c'] }), cb)", function() { 168 | it('should serialize array parameter values to json ?array=[false,2,"c"]', function(done) { 169 | nock('https://graph.facebook.com:443') 170 | .get('/v2.5/4?array=%5Bfalse%2C2%2C%22c%22%5D') 171 | .reply(200, { 172 | }); 173 | 174 | FB.api('4', {array: [false, 2, 'c']}, function(result) { 175 | notError(result); 176 | done(); 177 | }); 178 | }); 179 | }); 180 | 181 | describe("FB.api('/4?fields=name', { fields: 'id,first_name' }, cb)", function() { 182 | it("should return { id: '4', name: 'Mark Zuckerberg' } object", function(done) { 183 | var expectedRequest = nock('https://graph.facebook.com:443') 184 | .get('/v2.5/4?fields=id%2Cname') 185 | .reply(200, { 186 | id: '4', 187 | name: 'Mark Zuckerberg' 188 | }); 189 | 190 | FB.api('4?fields=name', {fields: 'id,name'}, function(result) { 191 | notError(result); 192 | expectedRequest.done(); 193 | expect(result).to.include({id: '4', name: 'Mark Zuckerberg'}); 194 | done(); 195 | }); 196 | }); 197 | }); 198 | 199 | }); 200 | 201 | describe('oauth', function() { 202 | describe("FB.api('oauth/access_token', { ..., grant_type: 'client_credentials' }, cb)", function() { 203 | it("should return an { access_token: '...' } object", function(done) { 204 | nock('https://graph.facebook.com:443') 205 | .get('/v2.5/oauth/access_token') 206 | .query({ 207 | client_id: 'app_id', 208 | client_secret: 'app_secret', 209 | grant_type: 'client_credentials' 210 | }) 211 | .reply(200, { 212 | access_token: '...', 213 | }); 214 | 215 | FB.api('oauth/access_token', { 216 | client_id: 'app_id', 217 | client_secret: 'app_secret', 218 | grant_type: 'client_credentials' 219 | }, function(result) { 220 | notError(result); 221 | expect(result).to.have.keys('access_token') 222 | .and.include({access_token: '...'}); 223 | done(); 224 | }); 225 | }); 226 | }); 227 | 228 | describe("FB.api('oauth/access_token', { grant_type: 'fb_exchange_token', ..., fb_exchange_token: ... }, cb)", function() { 229 | it('should return an object with expires as a number', function(done) { 230 | nock('https://graph.facebook.com:443') 231 | .get('/v2.5/oauth/access_token') 232 | .query({ 233 | grant_type: 'fb_exchange_token', 234 | client_id: 'app_id', 235 | client_secret: 'app_secret', 236 | fb_exchange_token: 'access_token' 237 | }) 238 | .reply(200, { 239 | access_token: '...', 240 | expires_in: 99999, 241 | }); 242 | 243 | FB.api('oauth/access_token', { 244 | grant_type: 'fb_exchange_token', 245 | client_id: 'app_id', 246 | client_secret: 'app_secret', 247 | fb_exchange_token: 'access_token' 248 | }, function(result) { 249 | notError(result); 250 | expect(result).to.have.property('expires_in') 251 | .and.be.a('number') 252 | .and.equal(99999); 253 | done(); 254 | }); 255 | }); 256 | }); 257 | }); 258 | }); 259 | 260 | describe('FB.api', function() { 261 | describe('GET', function() { 262 | describe("FB.napi('/4', cb)", function() { 263 | it('should have id 4', function(done) { 264 | nock('https://graph.facebook.com:443') 265 | .get('/v2.5/4') 266 | .reply(200, { 267 | id: '4', 268 | name: 'Mark Zuckerberg', 269 | first_name: 'Mark', 270 | last_name: 'Zuckerberg', 271 | link: 'http://www.facebook.com/zuck', 272 | username: 'zuck', 273 | gender: 'male', 274 | locale: 'en_US' 275 | }); 276 | 277 | FB.napi('/4', function(err, result) { 278 | notError(result); 279 | expect(result).to.have.property('id', '4'); 280 | done(); 281 | }); 282 | }); 283 | 284 | it('should work without `this`', function(done) { 285 | nock('https://graph.facebook.com:443') 286 | .get('/v2.5/4') 287 | .reply(200, { 288 | id: '4', 289 | name: 'Mark Zuckerberg', 290 | first_name: 'Mark', 291 | last_name: 'Zuckerberg', 292 | link: 'http://www.facebook.com/zuck', 293 | username: 'zuck', 294 | gender: 'male', 295 | locale: 'en_US' 296 | }); 297 | 298 | FB.napi.call(undefined, '/4', function(err, result) { 299 | notError(result); 300 | expect(result).to.have.property('id', '4'); 301 | done(); 302 | }); 303 | }); 304 | }); 305 | 306 | describe("FB.api('/4')", function() { 307 | it('should return a Promise', function(done) { 308 | nock('https://graph.facebook.com:443') 309 | .get('/v2.5/4') 310 | .reply(200, { 311 | id: '4', 312 | name: 'Mark Zuckerberg', 313 | first_name: 'Mark', 314 | last_name: 'Zuckerberg', 315 | link: 'http://www.facebook.com/zuck', 316 | username: 'zuck', 317 | gender: 'male', 318 | locale: 'en_US' 319 | }); 320 | 321 | var ret = FB.api('/4'); 322 | 323 | expect(ret).to.have.property('then').that.is.a('function'); 324 | expect(ret).to.have.property('catch').that.is.a('function'); 325 | expect(ret).to.be.an.instanceof(Promise); 326 | ret 327 | .then((result) => { 328 | expect(result).to.have.property('id', '4'); 329 | done(); 330 | }); 331 | }); 332 | 333 | it('should work when the Promise option is Bluebird', function(done) { 334 | nock('https://graph.facebook.com:443') 335 | .get('/v2.5/4') 336 | .reply(200, { 337 | id: '4', 338 | name: 'Mark Zuckerberg', 339 | first_name: 'Mark', 340 | last_name: 'Zuckerberg', 341 | link: 'http://www.facebook.com/zuck', 342 | username: 'zuck', 343 | gender: 'male', 344 | locale: 'en_US' 345 | }); 346 | 347 | var fb = new Facebook({Promise: Bluebird}); 348 | var ret = fb.api('/4'); 349 | 350 | expect(ret).to.have.property('then').that.is.a('function'); 351 | expect(ret).to.have.property('catch').that.is.a('function'); 352 | expect(ret).to.be.an.instanceof(Bluebird); 353 | ret 354 | .then((result) => { 355 | expect(result).to.have.property('id', '4'); 356 | done(); 357 | }); 358 | }); 359 | }); 360 | }); 361 | }); 362 | -------------------------------------------------------------------------------- /src/fb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {autobind} from 'core-decorators'; 3 | import debug from 'debug'; 4 | import FormData from 'form-data'; 5 | import needle from 'needle'; 6 | import URL from 'url'; 7 | import QS from 'querystring'; 8 | import crypto from 'crypto'; 9 | import FacebookApiException from './FacebookApiException'; 10 | 11 | var {version} = require('../package.json'), 12 | debugReq = debug('fb:req'), 13 | debugSig = debug('fb:sig'), 14 | debugFbDebug = debug('fb:fbdebug'), 15 | METHODS = ['get', 'post', 'delete', 'put'], 16 | isString = (self, ...args) => Object.prototype.toString.call(self, ...args) === '[object String]', 17 | has = (self, ...args) => Object.prototype.hasOwnProperty.call(self, ...args), 18 | log = function(d) { 19 | // todo 20 | console.warn(d); // eslint-disable-line no-console 21 | }, 22 | defaultOptions = Object.assign(Object.create(null), { 23 | Promise: Promise, 24 | accessToken: null, 25 | appId: null, 26 | appSecret: null, 27 | appSecretProof: null, 28 | beta: false, 29 | version: 'v2.5', 30 | timeout: null, 31 | scope: null, 32 | redirectUri: null, 33 | proxy: null, 34 | userAgent: `thuzi_nodejssdk/${version}`, 35 | agent: null, 36 | }), 37 | emptyRateLimit = Object.assign(Object.create(null), { 38 | callCount: 0, 39 | totalTime: 0, 40 | totalCPUTime: 0 41 | }), 42 | isValidOption = function(key) { 43 | return has(defaultOptions, key); 44 | }, 45 | parseResponseHeaderAppUsage = function(header) { 46 | if ( !has(header, 'x-app-usage') ) { 47 | return null; 48 | } 49 | return JSON.parse(header['x-app-usage']); 50 | }, 51 | parseResponseHeaderPageUsage = function(header) { 52 | if ( !has(header, 'x-page-usage') ) { 53 | return null; 54 | } 55 | return JSON.parse(header['x-page-usage']); 56 | }, 57 | buildRateLimitObjectFromJson = function(rateLimit) { 58 | return Object.assign(Object.create(null), { 59 | callCount: rateLimit['call_count'], 60 | totalTime: rateLimit['total_time'], 61 | totalCPUTime: rateLimit['total_cputime'] 62 | }); 63 | }, 64 | stringifyParams = function(params) { 65 | var data = {}; 66 | 67 | // https://developers.facebook.com/bugs/1925316137705574/ 68 | // fields=[] as json is not officialy supported, however the README has made people mistakenly do so 69 | if ( Array.isArray(params.fields) ) { 70 | log(`The fields param should be a comma separated list, not an array, try changing it to: ${JSON.stringify(params.fields)}.join(',')`); 71 | } 72 | 73 | for ( let key in params ) { 74 | let value = params[key]; 75 | if ( value && typeof value !== 'string' ) { 76 | value = JSON.stringify(value); 77 | } 78 | if ( value !== undefined ) { 79 | data[key] = value; 80 | } 81 | } 82 | 83 | return QS.stringify(data); 84 | }, 85 | postParamData = function(params) { 86 | var data = {}, 87 | multipart = false; 88 | 89 | for ( let key in params ) { 90 | let value = params[key]; 91 | if ( value && typeof value !== 'string' ) { 92 | let val = typeof value === 'object' && has(value, 'value') && has(value, 'options') ? value.value : value; 93 | if ( Buffer.isBuffer(val) ) { 94 | multipart = true; 95 | } else if ( typeof val.read === 'function' && typeof val.pipe === 'function' && val.readable ) { 96 | multipart = true; 97 | } else { 98 | value = JSON.stringify(value); 99 | } 100 | } 101 | if ( value !== undefined ) { 102 | data[key] = value; 103 | } 104 | } 105 | 106 | if ( multipart ) { 107 | const formData = new FormData(); 108 | for (const key in data) { 109 | const value = data[key]; 110 | if ( typeof value === 'object' && has(value, 'value') && has(value, 'options') ) { 111 | formData.append(key, value.value, value.options); 112 | } else { 113 | formData.append(key, value); 114 | } 115 | } 116 | const formHeaders = formData.getHeaders(); 117 | return {data: formData, formHeaders}; 118 | } 119 | return {data}; 120 | }, 121 | getAppSecretProof = function(accessToken, appSecret) { 122 | var hmac = crypto.createHmac('sha256', appSecret); 123 | hmac.update(accessToken); 124 | return hmac.digest('hex'); 125 | }, 126 | base64UrlDecode = function(str) { 127 | var base64String = str.replace(/\-/g, '+').replace(/_/g, '/'); 128 | var buffer = new Buffer(base64String, 'base64'); 129 | return buffer.toString('utf8'); 130 | }, 131 | nodeifyCallback = function(originalCallback) { 132 | // normalizes the callback parameters so that the 133 | // first parameter is always error and second is response 134 | return function(res) { 135 | if ( !res || res.error ) { 136 | originalCallback(new FacebookApiException(res)); 137 | } else { 138 | originalCallback(null, res); 139 | } 140 | }; 141 | }; 142 | 143 | const _opts = Symbol('opts'); 144 | const graph = Symbol('graph'); 145 | const oauthRequest = Symbol('oauthRequest'); 146 | 147 | class Facebook { 148 | constructor(opts, _internalInherit) { 149 | if ( _internalInherit instanceof Facebook ) { 150 | this[_opts] = Object.create(_internalInherit[_opts]); 151 | } else { 152 | this[_opts] = Object.create(defaultOptions); 153 | } 154 | 155 | if ( typeof opts === 'object' ) { 156 | this.options(opts); 157 | } 158 | 159 | this._pageUsage = Object.create(emptyRateLimit); 160 | this._appUsage = Object.create(emptyRateLimit); 161 | } 162 | 163 | /** 164 | * 165 | * @access public 166 | * @param path {String} the url path 167 | * @param method {String} the http method (default: `"GET"`) 168 | * @param params {Object} the parameters for the query 169 | * @param cb {Function} the callback function to handle the response 170 | * @return {Promise|undefined} 171 | */ 172 | @autobind 173 | api(...args) { 174 | // 175 | // FB.api('/platform', function(response) { 176 | // console.log(response.company_overview); 177 | // }); 178 | // 179 | // FB.api('/platform/posts', { limit: 3 }, function(response) { 180 | // }); 181 | // 182 | // FB.api('/me/feed', 'post', { message: body }, function(response) { 183 | // if(!response || response.error) { 184 | // console.log('Error occured'); 185 | // } else { 186 | // console.log('Post ID:' + response.id); 187 | // } 188 | // }); 189 | // 190 | // var postId = '1234567890'; 191 | // FB.api(postId, 'delete', function(response) { 192 | // if(!response || response.error) { 193 | // console.log('Error occurred'); 194 | // } else { 195 | // console.log('Post was deleted'); 196 | // } 197 | // }); 198 | // 199 | // 200 | 201 | let ret; 202 | 203 | if ( args.length > 0 && typeof args[args.length - 1] !== 'function' ) { 204 | let Promise = this.options('Promise'); 205 | ret = new Promise((resolve, reject) => { 206 | args.push((res) => { 207 | if ( !res || res.error ) { 208 | reject(new FacebookApiException(res)); 209 | } else { 210 | resolve(res); 211 | } 212 | }); 213 | }); 214 | } 215 | 216 | this[graph](...args); 217 | 218 | return ret; 219 | } 220 | 221 | /** 222 | * 223 | * @access public 224 | * @param path {String} the url path 225 | * @param method {String} the http method (default: `"GET"`) 226 | * @param params {Object} the parameters for the query 227 | * @param cb {Function} the callback function to handle the error and response 228 | */ 229 | // this method does not exist in fb js sdk 230 | @autobind 231 | napi(...args) { 232 | // 233 | // normalizes to node style callback so can use the sdk with async control flow node libraries 234 | // first parameters: error (always type of FacebookApiException) 235 | // second callback parameter: response 236 | // 237 | // FB.napi('/platform', function(err, response) { 238 | // console.log(response.company_overview); 239 | // }); 240 | // 241 | // FB.napi('/platform/posts', { limit: 3 }, function(err, response) { 242 | // }); 243 | // 244 | // FB.napi('/me/feed', 'post', { message: body }, function(error, response) { 245 | // if(error) { 246 | // console.log('Error occured'); 247 | // } else { 248 | // console.log('Post ID:' + response.id); 249 | // } 250 | // }); 251 | // 252 | // var postId = '1234567890'; 253 | // FB.napi(postId, 'delete', function(error, response) { 254 | // if(error) { 255 | // console.log('Error occurred'); 256 | // } else { 257 | // console.log('Post was deleted'); 258 | // } 259 | // }); 260 | // 261 | // 262 | 263 | if ( args.length > 0 ) { 264 | let originalCallback = args.pop(); 265 | args.push(typeof originalCallback === 'function' ? nodeifyCallback(originalCallback) : originalCallback); 266 | } 267 | 268 | this.api(...args); 269 | } 270 | 271 | /** 272 | * 273 | * Make a api call to Graph server. 274 | * 275 | * Except the path, all arguments to this function are optiona. So any of 276 | * these are valid: 277 | * 278 | * FB.api('/me') // throw away the response 279 | * FB.api('/me', function(r) { console.log(r) }) 280 | * FB.api('/me', { fields: 'email' }); // throw away response 281 | * FB.api('/me', { fields: 'email' }, function(r) { console.log(r) }); 282 | * FB.api('/123456789', 'delete', function(r) { console.log(r) } ); 283 | * FB.api( 284 | * '/me/feed', 285 | * 'post', 286 | * { body: 'hi there' }, 287 | * function(r) { console.log(r) } 288 | * ); 289 | * 290 | */ 291 | [graph](path, next, ...args) { 292 | var method, 293 | params, 294 | cb; 295 | 296 | if ( typeof path !== 'string' ) { 297 | throw new Error(`Path is of type ${typeof path}, not string`); 298 | } 299 | 300 | while ( next ) { 301 | let type = typeof next; 302 | if ( type === 'string' && !method ) { 303 | method = next.toLowerCase(); 304 | } else if ( type === 'function' && !cb ) { 305 | cb = next; 306 | } else if ( type === 'object' && !params ) { 307 | params = next; 308 | } else { 309 | throw new TypeError('Invalid argument passed to FB.api(): ' + next); 310 | } 311 | next = args.shift(); 312 | } 313 | 314 | method = method || 'get'; 315 | params = params || {}; 316 | 317 | // remove prefix slash if one is given, as it's already in the base url 318 | if ( path[0] === '/' ) { 319 | path = path.substr(1); 320 | } 321 | 322 | if ( METHODS.indexOf(method) < 0 ) { 323 | throw new TypeError('Invalid method passed to FB.api(): ' + method); 324 | } 325 | 326 | this[oauthRequest](path, method, params, cb); 327 | } 328 | 329 | /** 330 | * Add the oauth parameter, and fire of a request. 331 | * 332 | * @access private 333 | * @param path {String} the request path 334 | * @param method {String} the http method 335 | * @param params {Object} the parameters for the query 336 | * @param cb {Function} the callback function to handle the response 337 | */ 338 | [oauthRequest](path, method, params, cb) { 339 | let uri, 340 | parsedUri, 341 | parsedQuery, 342 | postData, 343 | formHeaders, 344 | requestOptions; 345 | 346 | cb = cb || function() {}; 347 | if ( !params.access_token ) { 348 | if ( this.options('accessToken') ) { 349 | params.access_token = this.options('accessToken'); 350 | if ( this.options('appSecret') ) { 351 | params.appsecret_proof = this.options('appSecretProof'); 352 | } 353 | } 354 | } else if ( !params.appsecret_proof && this.options('appSecret') ) { 355 | params.appsecret_proof = getAppSecretProof(params.access_token, this.options('appSecret')); 356 | } 357 | 358 | if ( !/^v\d+\.\d+\//.test(path) ) { 359 | path = this.options('version') + '/' + path; 360 | } 361 | uri = `https://graph.${this.options('beta') ? 'beta.' : ''}facebook.com/${path}`; 362 | 363 | parsedUri = URL.parse(uri); 364 | delete parsedUri.search; 365 | parsedQuery = QS.parse(parsedUri.query); 366 | 367 | if ( method === 'post' ) { 368 | if ( params.access_token ) { 369 | parsedQuery.access_token = params.access_token; 370 | delete params.access_token; 371 | 372 | if ( params.appsecret_proof ) { 373 | parsedQuery.appsecret_proof = params.appsecret_proof; 374 | delete params.appsecret_proof; 375 | } 376 | } 377 | 378 | ({data: postData, formHeaders} = postParamData(params)); 379 | } else { 380 | for ( let key in params ) { 381 | parsedQuery[key] = params[key]; 382 | } 383 | } 384 | 385 | parsedUri.search = stringifyParams(parsedQuery); 386 | uri = URL.format(parsedUri); 387 | 388 | requestOptions = {parse: false}; 389 | if ( this.options('proxy') ) { 390 | requestOptions['proxy'] = this.options('proxy'); 391 | } 392 | if ( this.options('timeout') ) { 393 | requestOptions['response_timeout'] = this.options('timeout'); 394 | } 395 | if ( this.options('userAgent') ) { 396 | requestOptions['headers'] = { 397 | 'User-Agent': this.options('userAgent'), 398 | ...formHeaders 399 | }; 400 | } 401 | if ( this.options('agent') ) { 402 | requestOptions['agent'] = this.options('agent'); 403 | } 404 | 405 | debugReq(method.toUpperCase() + ' ' + uri); 406 | needle.request( 407 | method, 408 | uri, 409 | postData, 410 | requestOptions, 411 | (error, response) => { 412 | if ( error !== null ) { 413 | if ( error === Object(error) && has(error, 'error') ) { 414 | return cb(error); 415 | } 416 | return cb({error}); 417 | } 418 | 419 | debugFbDebug(`x-fb-trace-id: ${response.headers['x-fb-trace-id']}`); 420 | debugFbDebug(`x-fb-rev: ${response.headers['x-fb-rev']}`); 421 | debugFbDebug(`x-fb-debug: ${response.headers['x-fb-debug']}`); 422 | 423 | let appUsage = parseResponseHeaderAppUsage(response.headers); 424 | if ( appUsage !== null ) { 425 | this._appUsage = buildRateLimitObjectFromJson(appUsage); 426 | } else { 427 | this._appUsage = emptyRateLimit; 428 | } 429 | 430 | let pageUsage = parseResponseHeaderPageUsage(response.headers); 431 | if ( pageUsage !== null ) { 432 | this._pageUsage = buildRateLimitObjectFromJson(pageUsage); 433 | } else { 434 | this._pageUsage = emptyRateLimit; 435 | } 436 | 437 | let json; 438 | try { 439 | json = JSON.parse(response.body); 440 | } catch (ex) { 441 | // sometimes FB is has API errors that return HTML and a message 442 | // of "Sorry, something went wrong". These are infrequent and unpredictable but 443 | // let's not let them blow up our application. 444 | json = { 445 | error: { 446 | code: 'JSONPARSE', 447 | Error: ex 448 | } 449 | }; 450 | } 451 | cb(json); 452 | }); 453 | } 454 | 455 | /** 456 | * 457 | * @access public 458 | * @param signedRequest {String} the signed request value 459 | * @param appSecret {String} the application secret 460 | * @return {Object} the parsed signed request or undefined if failed 461 | * 462 | * throws error if appSecret is not defined 463 | * 464 | * FB.parseSignedRequest('signedRequest', 'appSecret') 465 | * FB.parseSignedRequest('signedRequest') // will use appSecret from options('appSecret') 466 | * 467 | */ 468 | @autobind 469 | parseSignedRequest(signedRequest, ...args) { 470 | // this method does not exist in fb js sdk 471 | var appSecret = args.shift() || this.options('appSecret'), 472 | split, 473 | encodedSignature, 474 | encodedEnvelope, 475 | envelope, 476 | hmac, 477 | base64Digest, 478 | base64UrlDigest; 479 | 480 | if ( !signedRequest ) { 481 | debugSig('invalid signedRequest'); 482 | return; 483 | } 484 | 485 | if ( !appSecret ) { 486 | throw new Error('appSecret required'); 487 | } 488 | 489 | split = signedRequest.split('.'); 490 | 491 | if ( split.length !== 2 ) { 492 | debugSig('invalid signedRequest'); 493 | return; 494 | } 495 | 496 | [encodedSignature, encodedEnvelope] = split; 497 | 498 | if ( !encodedSignature || !encodedEnvelope ) { 499 | debugSig('invalid signedRequest'); 500 | return; 501 | } 502 | 503 | try { 504 | envelope = JSON.parse(base64UrlDecode(encodedEnvelope)); 505 | } catch (ex) { 506 | debugSig('encodedEnvelope is not a valid base64 encoded JSON'); 507 | return; 508 | } 509 | 510 | if (!(envelope && has(envelope, 'algorithm') && envelope.algorithm.toUpperCase() === 'HMAC-SHA256') ) { 511 | debugSig(envelope.algorithm + ' is not a supported algorithm, must be one of [HMAC-SHA256]'); 512 | return; 513 | } 514 | 515 | hmac = crypto.createHmac('sha256', appSecret); 516 | hmac.update(encodedEnvelope); 517 | base64Digest = hmac.digest('base64'); 518 | 519 | // remove Base64 padding 520 | base64UrlDigest = base64Digest.replace(/={1,3}$/, ''); 521 | 522 | // Replace illegal characters 523 | base64UrlDigest = base64UrlDigest.replace(/\+/g, '-').replace(/\//g, '_'); 524 | 525 | if ( base64UrlDigest !== encodedSignature ) { 526 | debugSig('invalid signature'); 527 | return; 528 | } 529 | 530 | return envelope; 531 | } 532 | 533 | /** 534 | * 535 | * @access public 536 | * @param opt {Object} the parameters for appId and scope 537 | */ 538 | @autobind 539 | getLoginUrl(opt = {}) { 540 | // this method does not exist in fb js sdk 541 | var clientId = opt.appId || opt.client_id || this.options('appId'), 542 | redirectUri = opt.redirectUri || opt.redirect_uri || this.options('redirectUri') || 'https://www.facebook.com/connect/login_success.html', 543 | scope = opt.scope || this.options('scope'), 544 | display = opt.display, 545 | state = opt.state, 546 | scopeQuery = '', 547 | displayQuery = '', 548 | stateQuery = ''; 549 | 550 | if ( !clientId ) { 551 | throw new Error('client_id required'); 552 | } 553 | 554 | if ( scope ) { 555 | scopeQuery = '&scope=' + encodeURIComponent(scope); 556 | } 557 | 558 | if ( display ) { 559 | displayQuery = '&display=' + display; 560 | } 561 | 562 | if ( state ) { 563 | stateQuery = '&state=' + state; 564 | } 565 | 566 | return `https://www.facebook.com/${this.options('version')}/dialog/oauth` 567 | + '?response_type=' + (opt.responseType || opt.response_type || 'code') 568 | + scopeQuery 569 | + displayQuery 570 | + stateQuery 571 | + '&redirect_uri=' + encodeURIComponent(redirectUri) 572 | + '&client_id=' + clientId; 573 | } 574 | 575 | @autobind 576 | options(keyOrOptions) { 577 | // this method does not exist in the fb js sdk 578 | var o = this[_opts]; 579 | if ( !keyOrOptions ) { 580 | return o; 581 | } 582 | if ( isString(keyOrOptions) ) { 583 | return isValidOption(keyOrOptions) && keyOrOptions in o ? o[keyOrOptions] : null; 584 | } 585 | for ( let key in o ) { 586 | if ( isValidOption(key) && key in o && has(keyOrOptions, key) ) { 587 | o[key] = keyOrOptions[key]; 588 | switch (key) { 589 | case 'appSecret': 590 | case 'accessToken': 591 | o.appSecretProof = 592 | (o.appSecret && o.accessToken) ? 593 | getAppSecretProof(o.accessToken, o.appSecret) : 594 | null; 595 | break; 596 | } 597 | } 598 | } 599 | } 600 | 601 | /** 602 | * Return a new instance of Facebook with a different set of options 603 | * that inherit unset options from the current instance. 604 | * @access public 605 | * @param {Object} [opts] Options to set 606 | */ 607 | @autobind 608 | extend(opts) { 609 | return new Facebook(opts, this); 610 | } 611 | 612 | @autobind 613 | getAccessToken() { 614 | return this.options('accessToken'); 615 | } 616 | 617 | @autobind 618 | getAppUsage() { 619 | return this._appUsage; 620 | } 621 | 622 | @autobind 623 | getPageUsage() { 624 | return this._pageUsage; 625 | } 626 | 627 | @autobind 628 | setAccessToken(accessToken) { 629 | // this method does not exist in fb js sdk 630 | this.options({accessToken}); 631 | } 632 | 633 | @autobind 634 | withAccessToken(accessToken) { 635 | return this.extend({accessToken}); 636 | } 637 | } 638 | 639 | export var FB = new Facebook(); 640 | export default FB; 641 | export {Facebook, FacebookApiException, version}; 642 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeJS Library for Facebook [![Build Status](https://travis-ci.org/node-facebook/facebook-node-sdk.svg?branch=master)](https://travis-ci.org/node-facebook/facebook-node-sdk) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fnode-facebook%2Ffacebook-node-sdk.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fnode-facebook%2Ffacebook-node-sdk?ref=badge_shield) 2 | 3 | With facebook-node-sdk you can now easily write the same code and share between your server (nodejs) and the client ([Facebook Javascript SDK](https://developers.facebook.com/docs/reference/javascript/)). 4 | 5 | **Author:** [Thuzi](http://www.thuzi.com) 6 | 7 | **Maintainer** [Daniel Friesen](https://github.com/dantman) 8 | 9 | **License:** Apache v2 10 | 11 | # Installing facebook-node-sdk 12 | 13 | ``` 14 | npm install fb 15 | ``` 16 | 17 | ```js 18 | // Using ES2015 import 19 | import FB, {FacebookApiException} from 'fb'; 20 | 21 | // Using require() in ES2015 22 | const {FB, FacebookApiException} = require('fb'); 23 | 24 | // Using require() in ES5 25 | var FB = require('fb').default; 26 | ``` 27 | 28 | ## Library usage 29 | 30 | Libraries can isolate themselves from the options belonging to the default `FB` by creating an instance of the `Facebook` class. 31 | 32 | ```js 33 | // ES2015 modules 34 | import {Facebook, FacebookApiException} from 'fb'; 35 | const fb = new Facebook(options); 36 | 37 | // ES2015 w/ require() 38 | const {Facebook, FacebookApiException} = require('fb'), 39 | const fb = new Facebook(options); 40 | 41 | // ES5 42 | var Facebook = require('fb').Facebook, 43 | fb = new Facebook(options); 44 | ``` 45 | 46 | ## Multi-app usage 47 | 48 | Applications that run on behalf of multiple apps with different Facebook appIds and secrets can use `.extend` (on `FB` or any `Facebook` instance) to create a new instance which inherits options not set on it from the instance it is created from (like the API `version` your application is coded against). 49 | 50 | ```js 51 | FB.options({version: 'v2.4'}); 52 | var fooApp = FB.extend({appId: 'foo_id', appSecret: 'secret'}), 53 | barApp = FB.extend({appId: 'bar_id', appSecret: 'secret'}); 54 | ``` 55 | 56 | ## Graph Api 57 | 58 | ### Get 59 | 60 | ```js 61 | FB.api('4', function (res) { 62 | if(!res || res.error) { 63 | console.log(!res ? 'error occurred' : res.error); 64 | return; 65 | } 66 | console.log(res.id); 67 | console.log(res.name); 68 | }); 69 | ``` 70 | 71 | __Passing Parameters__ 72 | 73 | ```js 74 | FB.api('4', { fields: 'id,name,picture.type(large)' }, function (res) { 75 | if(!res || res.error) { 76 | console.log(!res ? 'error occurred' : res.error); 77 | return; 78 | } 79 | console.log(res.id); 80 | console.log(res.name); 81 | }); 82 | ``` 83 | 84 | ### Post 85 | 86 | ```js 87 | FB.setAccessToken('access_token'); 88 | 89 | var body = 'My first post using facebook-node-sdk'; 90 | FB.api('me/feed', 'post', { message: body }, function (res) { 91 | if(!res || res.error) { 92 | console.log(!res ? 'error occurred' : res.error); 93 | return; 94 | } 95 | console.log('Post Id: ' + res.id); 96 | }); 97 | ``` 98 | 99 | #### Upload 100 | 101 | ```js 102 | FB.setAccessToken('access_token'); 103 | 104 | FB.api('me/photos', 'post', { source: fs.createReadStream('my-vacation.jpg'), caption: 'My vacation' }, function (res) { 105 | if(!res || res.error) { 106 | console.log(!res ? 'error occurred' : res.error); 107 | return; 108 | } 109 | console.log('Post Id: ' + res.post_id); 110 | }); 111 | 112 | FB.api('me/photos', 'post', { source: { value: photoBuffer, options: { filename: 'upload.jpg', contentType: 'image/jpeg' } }, caption: 'My vacation' }, function (res) { 113 | if(!res || res.error) { 114 | console.log(!res ? 'error occurred' : res.error); 115 | return; 116 | } 117 | console.log('Post Id: ' + res.post_id); 118 | }); 119 | ``` 120 | 121 | ### Delete 122 | 123 | ```js 124 | FB.setAccessToken('access_token'); 125 | 126 | var postId = '1234567890'; 127 | FB.api(postId, 'delete', function (res) { 128 | if(!res || res.error) { 129 | console.log(!res ? 'error occurred' : res.error); 130 | return; 131 | } 132 | console.log('Post was deleted'); 133 | }); 134 | ``` 135 | 136 | ## Batch Requests 137 | 138 | ```js 139 | FB.setAccessToken('access_token'); 140 | 141 | var extractEtag; 142 | FB.api('', 'post', { 143 | batch: [ 144 | { method: 'get', relative_url: '4' }, 145 | { method: 'get', relative_url: 'me/friends?limit=50' }, 146 | { method: 'get', relative_url: '4', headers: { 'If-None-Match': '"7de572574f2a822b65ecd9eb8acef8f476e983e1"' } }, /* etags */ 147 | { method: 'get', relative_url: 'me/friends?limit=1', name: 'one-friend' /* , omit_response_on_success: false */ }, 148 | { method: 'get', relative_url: '{result=one-friend:$.data.0.id}/feed?limit=5'} 149 | ] 150 | }, function(res) { 151 | var res0, res1, res2, res3, res4, 152 | etag1; 153 | 154 | if(!res || res.error) { 155 | console.log(!res ? 'error occurred' : res.error); 156 | return; 157 | } 158 | 159 | res0 = JSON.parse(res[0].body); 160 | res1 = JSON.parse(res[1].body); 161 | res2 = res[2].code === 304 ? undefined : JSON.parse(res[2].body); // special case for not-modified responses 162 | // set res2 as undefined if response wasn't modified. 163 | res3 = res[3] === null ? null : JSON.parse(res[3].body); 164 | res4 = res3 === null ? JSON.parse(res[4].body) : undefined; // set result as undefined if previous dependency failed 165 | 166 | if(res0.error) { 167 | console.log(res0.error); 168 | } else { 169 | console.log('Hi ' + res0.name); 170 | etag1 = extractETag(res[0]); // use this etag when making the second request. 171 | console.log(etag1); 172 | } 173 | 174 | if(res1.error) { 175 | console.log(res1.error); 176 | } else { 177 | console.log(res1); 178 | } 179 | 180 | // check if there are any new updates 181 | if(typeof res2 !== "undefined") { 182 | // make sure there was no error 183 | if(res2.error) { 184 | console.log(error); 185 | } else { 186 | console.log('new update available'); 187 | console.log(res2); 188 | } 189 | } 190 | else { 191 | console.log('no updates'); 192 | } 193 | 194 | // check if dependency executed successfully 195 | if(res[3] === null) { 196 | // then check if the result it self doesn't have any errors. 197 | if(res4.error) { 198 | console.log(res4.error); 199 | } else { 200 | console.log(res4); 201 | } 202 | } else { 203 | console.log(res3.error); 204 | } 205 | }); 206 | 207 | extractETag = function(res) { 208 | var etag, header, headerIndex; 209 | for(headerIndex in res.headers) { 210 | header = res.headers[headerIndex]; 211 | if(header.name === 'ETag') { 212 | etag = header.value; 213 | } 214 | } 215 | return etag; 216 | }; 217 | ``` 218 | 219 | ### Post 220 | 221 | ```js 222 | FB.setAccessToken('access_token'); 223 | 224 | var message = 'Hi from facebook-node-js'; 225 | FB.api('', 'post', { 226 | batch: [ 227 | { method: 'post', relative_url: 'me/feed', body:'message=' + encodeURIComponent(message) } 228 | ] 229 | }, function (res) { 230 | var res0; 231 | 232 | if(!res || res.error) { 233 | console.log(!res ? 'error occurred' : res.error); 234 | return; 235 | } 236 | 237 | res0 = JSON.parse(res[0].body); 238 | 239 | if(res0.error) { 240 | console.log(res0.error); 241 | } else { 242 | console.log('Post Id: ' + res0.id); 243 | } 244 | }); 245 | ``` 246 | 247 | ## OAuth Requests 248 | 249 | *This is a non-standard behavior and does not work in the official client side FB JS SDK.* 250 | 251 | facebook-node-sdk is capable of handling oauth requests which return non-json responses. You can use it by calling `api` method. 252 | 253 | ### Get facebook application access token 254 | 255 | ```js 256 | 257 | FB.api('oauth/access_token', { 258 | client_id: 'app_id', 259 | client_secret: 'app_secret', 260 | grant_type: 'client_credentials' 261 | }, function (res) { 262 | if(!res || res.error) { 263 | console.log(!res ? 'error occurred' : res.error); 264 | return; 265 | } 266 | 267 | var accessToken = res.access_token; 268 | }); 269 | ``` 270 | 271 | ### Exchange code for access token 272 | 273 | ```js 274 | 275 | FB.api('oauth/access_token', { 276 | client_id: 'app_id', 277 | client_secret: 'app_secret', 278 | redirect_uri: 'http://yoururl.com/callback', 279 | code: 'code' 280 | }, function (res) { 281 | if(!res || res.error) { 282 | console.log(!res ? 'error occurred' : res.error); 283 | return; 284 | } 285 | 286 | var accessToken = res.access_token; 287 | var expires = res.expires ? res.expires : 0; 288 | }); 289 | ``` 290 | 291 | You can safely extract the code from the url using the `url` module. Always make sure to handle invalid oauth callback as 292 | well as error. 293 | 294 | ```js 295 | var url = require('url'); 296 | 297 | var urlToParse = 'http://yoururl.com/callback?code=.....#_=_'; 298 | var result = url.parse(urlToParse, true); 299 | if(result.query.error) { 300 | if(result.query.error_description) { 301 | console.log(result.query.error_description); 302 | } else { 303 | console.log(result.query.error); 304 | } 305 | return; 306 | } else if (!result.query.code) { 307 | console.log('not a oauth callback'); 308 | return; 309 | } 310 | 311 | var code = result.query.code; 312 | ``` 313 | 314 | ### Extend expiry time of the access token 315 | 316 | ```js 317 | 318 | FB.api('oauth/access_token', { 319 | client_id: 'client_id', 320 | client_secret: 'client_secret', 321 | grant_type: 'fb_exchange_token', 322 | fb_exchange_token: 'existing_access_token' 323 | }, function (res) { 324 | if(!res || res.error) { 325 | console.log(!res ? 'error occurred' : res.error); 326 | return; 327 | } 328 | 329 | var accessToken = res.access_token; 330 | var expires = res.expires ? res.expires : 0; 331 | }); 332 | ``` 333 | 334 | ## Access Tokens 335 | 336 | ### setAccessToken 337 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 338 | 339 | **Warning**: Due to Node's asynchronous nature, you should not use `setAccessToken` when `FB` is used on behalf of for multiple users. 340 | 341 | ```js 342 | FB.setAccessToken('access_token'); 343 | ``` 344 | 345 | If you want to use the api compatible with FB JS SDK, pass `access_token` as parameter. 346 | 347 | ```js 348 | FB.api('me', { fields: 'id,name', access_token: 'access_token' }, function (res) { 349 | console.log(res); 350 | }); 351 | ``` 352 | 353 | ### withAccessToken 354 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 355 | 356 | Using `FB.extend` this returns a new FB object that inherits the same options but has an accessToken specific to it set. 357 | 358 | ```js 359 | var fb = FB.withAccessToken('access_token'); 360 | ``` 361 | 362 | ### getAccessToken 363 | *Unlike `setAccessToken` this is a standard api and exists in FB JS SDK.* 364 | 365 | ```js 366 | FB.setAccessToken('access_token'); 367 | var accessToken = FB.getAccessToken(); 368 | ``` 369 | 370 | ### AppSecret Proof 371 | For improved security, as soon as you provide an app secret and an access token, the 372 | library automatically computes and adds the appsecret_proof parameter to your requests. 373 | 374 | ## Rate limiting 375 | Current rate limit values are updated after each response received. 376 | >If your app is making enough calls to be considered for rate limiting by our system, we return an X-App-Usage HTTP header. 377 | 378 | That means if your API usage is low enough, Facebook don't send any information about rate limits and we consider them as 0 379 | ### getAppUsage 380 | ```js 381 | var appUsage = FB.getAppUsage(); 382 | /* 383 | { 384 | callCount: 0, 385 | totalTime: 0, 386 | totalCPUTime: 0 387 | } 388 | */ 389 | ``` 390 | 391 | ### getPageUsage 392 | ```js 393 | var pageUsage = FB.getPageUsage(); 394 | /* 395 | { 396 | callCount: 0, 397 | totalTime: 0, 398 | totalCPUTime: 0 399 | } 400 | */ 401 | ``` 402 | 403 | ## Configuration options 404 | 405 | ### options 406 | 407 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 408 | 409 | When this method is called with no parameters it will return all of the current options. 410 | 411 | ```js 412 | var options = FB.options(); 413 | ``` 414 | 415 | When this method is called with a string it will return the value of the option if exists, null if it does not. 416 | 417 | ```js 418 | var timeout = FB.options('timeout'); 419 | ``` 420 | 421 | When this method is called with an object it will merge the object onto the previous options object. 422 | ```js 423 | FB.options({accessToken: 'abc'}); //equivalent to calling setAccessToken('abc') 424 | FB.options({timeout: 1000, accessToken: 'XYZ'}); //will set timeout and accessToken options 425 | var timeout = FB.options('timeout'); //will get a timeout of 1000 426 | var accessToken = FB.options('accessToken'); //will get the accessToken of 'XYZ' 427 | ``` 428 | 429 | The existing options are: 430 | * `'accessToken'` string representing the Facebook accessToken to be used for requests. This is the same option that is updated by the `setAccessToken` and `getAccessToken` methods. 431 | * `'appId'` The ID of your app, found in your app's dashboard. 432 | * `'appSecret'` string representing the Facebook application secret. 433 | * `'version'` [default=`'v2.3'`] string representing the Facebook api version to use. Defaults to the oldest available version of the api. 434 | * `'proxy'` string representing an HTTP proxy to be used. Support proxy Auth with Basic Auth, embedding the auth info in the uri: 'http://[username:password@]proxy[:port]' (parameters in brackets are optional). 435 | * `'agent'` a custom `https.Agent` to use for the request. 436 | * `'timeout'` integer number of milliseconds to wait for a response. Requests that have not received a response in *X* ms. If set to null or 0 no timeout will exist. On timeout an error object will be returned to the api callback with the error code of `'ETIMEDOUT'` (example below). 437 | * `'scope'` string representing the Facebook scope to use in `getLoginUrl`. 438 | * `'redirectUri'` string representing the Facebook redirect_uri to use in `getLoginUrl`. 439 | * `'Promise'` Promise implementation to use when `FB.api` is called without a callback. Defaults to the Promise implementation returned by `require('any-promise')`. 440 | 441 | ### version 442 | 443 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 444 | 445 | Gets the string representation of the facebook-node-sdk library version. 446 | 447 | ```js 448 | var version = FB.version; 449 | ``` 450 | 451 | ## Parsing Signed Request 452 | 453 | ### parseSignedRequest 454 | 455 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 456 | 457 | ```js 458 | var signedRequestValue = 'signed_request_value'; 459 | var appSecret = 'app_secret'; 460 | 461 | var signedRequest = FB.parseSignedRequest(signedRequestValue, appSecret); 462 | if(signedRequest) { 463 | var accessToken = signedRequest.oauth_token; 464 | var userId = signedRequest.user_id; 465 | var userCountry = signedRequest.user.country; 466 | } 467 | ``` 468 | 469 | *Note: parseSignedRequest will return undefined if validation fails. Always remember to check the result of parseSignedRequest before accessing the result.* 470 | 471 | If you already set the appSecret in options, you can ignore the second parameter when calling parseSignedRequest. If you do pass the second parameter it will use the appSecret passed in parameter instead of using appSecret from options. 472 | 473 | If appSecret is absent, parseSignedRequest will throw an error. 474 | 475 | ```js 476 | FB.options({'appSecret': 'app_secret'}); 477 | 478 | var signedRequestValue = 'signed_request_value'; 479 | 480 | var signedRequest = FB.parseSignedRequest(signedRequestValue); 481 | if(signedRequest) { 482 | var accessToken = signedRequest.oauth_token; 483 | var userId = signedRequest.user_id; 484 | var userCountry = signedRequest.user.country; 485 | } 486 | ``` 487 | 488 | ## Manual Login Flow 489 | 490 | ### getLoginUrl 491 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 492 | 493 | This returns the redirect url for a [manual login flow](https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow). 494 | 495 | ```js 496 | FB.getLoginUrl({ 497 | scope: 'email,user_likes', 498 | redirect_uri: 'http://example.com/' 499 | }); 500 | ``` 501 | 502 | These options are accepted and all correspond to url parameters documented in Facebook's manual login flow documentation. 503 | 504 | * `'appId'`/`'client_id'` [default=`FB.options('appId')`] The ID of your app, found in your app's dashboard. 505 | * `'redirectUri'`/`'redirect_uri'` [default=`FB.options('redirectUri')`] The URL that you want to redirect the person logging in back to. This URL will capture the response from the Login Dialog. 506 | * `'scope'` [default=`FB.options('scope')`] A comma separated list of Permissions to request from the person using your app. 507 | * `'display'` Can be set to 'popup'. 508 | * `'state'` An arbitrary unique string created by your app to guard against Cross-site Request Forgery. 509 | * `'responseType'`/`'response_type'` [default=`'code'`] Determines whether the response data included when the redirect back to the app occurs is in URL parameters or fragments. 510 | 511 | 512 | ## Error handling 513 | 514 | *Note: Facebook is not consistent with their error format, and different systems can fail causing different error formats* 515 | 516 | Some examples of various error codes you can check for: 517 | * `'ECONNRESET'` - connection reset by peer 518 | * `'ETIMEDOUT'` - connection timed out 519 | * `'ESOCKETTIMEDOUT'` - socket timed out 520 | * `'JSONPARSE'` - could not parse JSON response, happens when the FB API has availability issues. It sometimes returns HTML 521 | 522 | ```js 523 | FB.options({timeout: 1, accessToken: 'access_token'}); 524 | 525 | FB.api('/me', function (res) { 526 | if(res && res.error) { 527 | if(res.error.code === 'ETIMEDOUT') { 528 | console.log('request timeout'); 529 | } 530 | else { 531 | console.log('error', res.error); 532 | } 533 | } 534 | else { 535 | console.log(res); 536 | } 537 | }); 538 | ``` 539 | 540 | ### Debugging 541 | 542 | If you need to submit a bug report to Facebook you can run your application with `DEBUG=fb:req,fb:fbdebug` and request information will be output to your console along with `x-fb-trace-id`, `x-fb-rev`, and `x-fb-debug` headers you can include. 543 | 544 | ## Promise based interface 545 | 546 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 547 | 548 | When `FB.api` is called without a callback it will instead return a Promise that will either resolve with the same response as `FB.api` or be rejected with a `FacebookApiException` error. 549 | 550 | ```js 551 | // In an async function 552 | async function example() { 553 | try { 554 | var response = await FB.api('4'); 555 | console.log(response); 556 | } 557 | catch(error) { 558 | if(error.response.error.code === 'ETIMEDOUT') { 559 | console.log('request timeout'); 560 | } 561 | else { 562 | console.log('error', error.message); 563 | } 564 | } 565 | } 566 | 567 | // Using plain promise callbacks 568 | FB.api('4') 569 | .then((response) => { 570 | console.log(response); 571 | }) 572 | .catch((error) => { 573 | if(error.response.error.code === 'ETIMEDOUT') { 574 | console.log('request timeout'); 575 | } 576 | else { 577 | console.log('error', error.message); 578 | } 579 | }); 580 | ``` 581 | 582 | The promises returned are native Promises. However you can override the promise implementation used with a 3rd party library by setting the `Promise` option. 583 | 584 | ```js 585 | // Promise option 586 | import FB from 'fb'; 587 | 588 | FB.options({ 589 | Promise: require('bluebird') 590 | }); 591 | let response = await FB.api('4'); 592 | 593 | // Promise option in a library 594 | import {Facebook} from 'fb'; 595 | var fb = new Facebook({ 596 | Promise: require('bluebird') 597 | }); 598 | let response = await fb.api('4'); 599 | ``` 600 | 601 | ## Node style callback with FB.napi 602 | 603 | *This is a non-standard api and does not exist in the official client side FB JS SDK.* 604 | 605 | `FB.napi` takes the same input as `FB.api`. Only the callback parameters is different. In the original `FB.api`, the callback expects one parameter which is the response. In `FB.napi` the callback expects two parameters instead of one and follows the node standards. The first parameter is an error which is always of type `FacebookApiException` and the second parameter is the same response as in `FB.api`. Error response can be accessed using `error.response` which is the same response as the response when using `FB.api`. 606 | 607 | ```js 608 | FB.napi('4', function(error, response) { 609 | if(error) { 610 | if(error.response.error.code === 'ETIMEDOUT') { 611 | console.log('request timeout'); 612 | } 613 | else { 614 | console.log('error', error.message); 615 | } 616 | } else { 617 | console.log(response); 618 | } 619 | }); 620 | ``` 621 | 622 | `FB.napi` was added especially to make it easier to work with async control flow libraries. 623 | 624 | Here are some examples of using facebook-node-sdk with [Step](https://npmjs.org/package/step). 625 | 626 | You will need to install `step`. 627 | 628 | ```fb 629 | npm install step 630 | ``` 631 | 632 | ### FB.api with Step 633 | 634 | ```js 635 | var FB = require('fb').default, 636 | Step = require('step'); 637 | 638 | Step( 639 | function getUser() { 640 | var self = this; 641 | FB.api('4', function(res) { 642 | if(!res || res.error) { 643 | self(new Error('Error occured')); 644 | } else { 645 | self(null, res); 646 | } 647 | }); 648 | }, 649 | function processResult(err, res) { 650 | if(err) throw err; 651 | console.log(res); 652 | } 653 | ); 654 | ``` 655 | 656 | ### FB.napi with Step 657 | 658 | Simplified version of facebook-node-sdk async callbacks using `FB.napi`. 659 | 660 | ```js 661 | var FB = require('fb').default, 662 | Step = require('step'); 663 | 664 | Step( 665 | function getUser() { 666 | FB.napi('4', this); 667 | }, 668 | function processResult(err, res) { 669 | if(err) throw err; 670 | console.log(res); 671 | } 672 | ); 673 | ``` 674 | 675 | ## License 676 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fnode-facebook%2Ffacebook-node-sdk.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fnode-facebook%2Ffacebook-node-sdk?ref=badge_large) 677 | --------------------------------------------------------------------------------