├── index.js
├── examples
├── uploading_photo
│ ├── .gitignore
│ ├── README.md
│ ├── images
│ │ └── nodejs.png
│ ├── package.json
│ └── index.js
└── express.js
├── .npmignore
├── .gitignore
├── test
├── image.jpg
├── cli.js
├── client.js
├── api-common.js
├── auth.js
├── authUtils.js
└── api-authed.js
├── Makefile
├── .travis.yml
├── package.json
├── lib
├── encode.js
├── flickr.js
└── flapi.js
└── README.md
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/flapi');
--------------------------------------------------------------------------------
/examples/uploading_photo/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .git*
2 | test/
3 | examples/
4 | Makefile
5 | *.db
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.DS_Store
3 | npm-debug.log
4 | *.db
--------------------------------------------------------------------------------
/test/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/flapi/master/test/image.jpg
--------------------------------------------------------------------------------
/examples/uploading_photo/README.md:
--------------------------------------------------------------------------------
1 | A simple example illustrating uploading a photo to Flickr.
--------------------------------------------------------------------------------
/examples/uploading_photo/images/nodejs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/krasimir/flapi/master/examples/uploading_photo/images/nodejs.png
--------------------------------------------------------------------------------
/examples/uploading_photo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "projectname",
3 | "description": "description",
4 | "version": "0.0.1",
5 | "dependencies": {
6 | "flapi": "*",
7 | "open": "*"
8 | }
9 | }
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | @./node_modules/.bin/mocha test/client test/auth test/api-common test/api-authed -R spec auth true
3 |
4 | test-without-auth:
5 | @./node_modules/.bin/mocha test/client test/auth test/api-common test/api-authed -R spec auth false
6 |
7 | .PHONY: test
--------------------------------------------------------------------------------
/test/cli.js:
--------------------------------------------------------------------------------
1 | // Parse cli arguments to determine whether or not
2 | // we need to build the client with a pre determined
3 | // secret
4 | var args = process.argv;
5 | var index = args.indexOf('auth');
6 | var authArg = index != -1 ? args[index + 1] : 'true';
7 | exports.useAuth = authArg == 'false' ? false : true;
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.10'
4 | - '0.8'
5 | env:
6 | global:
7 | - secure: NwrIhj4rebxsL6Y1HgeJ9QB8iqdXZHynsukovhTp8QUnsKakSrJ0ob1p3whxaB53g5UHYowuPl8kY1MxbMXjItYRgHiT4gyu4wffDAQQqe+Qg22NDHGAR5a61S8JmkPkEqTVi/cWNcxLcSqrFDgidieoIWrlApdYpSxvglx/hGU=
8 | - secure: R+foJLm2h3t2AA6uMXZ0aCUUEw8ZLuSP+XK4e/hQcOtiJ49k6enbzTr3NMLJlQ1xpLQgdaSF2SQnhgwJLmuLF1MI3kPblHTu5eCZul5EYZO6yy4lA/7Tt24UsqWzY/Et0s41o+o3w2nbij52PvUUMl09MRlMM9HW35EYhnwh6ec=
9 | - secure: NHuXQdSRHgtOr4fvwOdisnMQ+hy64zqPlNZ4OgyGoM8AXmutDgHdxw956lv0SOt5so9K09sNo1tjHyQ7Tht8jgLyqMygHGr+qSgfPWeTUMZJwQFMh2WbUKuWoin3oyVJFx0TG04PQxNUwvlh9gU643VDP7+uGMJg6v5EINiKdrI=
10 | - secure: AP4fc2pc4qd50CuTJrEikmfZJytpwa69c35VPRLP/pVeNUuEvtkNuMuQrf2iXjPRj19pWUn/hJxVVo8D9ts01XSCvmjVGMJtIT02RhrCDkejh310dCWeueu/huihDBkEv4f7eoTS9dw96OwfmLscaRSqyjtypGj9AtJquaABF7w=
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flapi",
3 | "version": "1.1.0",
4 | "author": "Joe Longstreet",
5 | "homepage": "https://github.com/joelongstreet/flapi",
6 | "repository": {
7 | "type": "git",
8 | "url": "http://github.com/joelongstreet/flapi.git"
9 | },
10 | "main" : "./index",
11 | "license": "MIT",
12 | "description": "A fully testable, dependency light, Flickr API wrapper. Authenticates and makes web requests",
13 | "keywords": [
14 | "flickr",
15 | "flapi",
16 | "api",
17 | "oauth",
18 | "upload",
19 | "photo"
20 | ],
21 | "scripts": {
22 | "test": "make test"
23 | },
24 | "engines": {"node":">=0.8.0"},
25 | "devDependencies": {
26 | "mocha": "~1.14.0",
27 | "should": "~2.1.0",
28 | "jsdom": "~0.8.8",
29 | "request": "~2.27.0"
30 | },
31 | "dependencies" : {
32 | "restler": "~2.0.1"
33 | }
34 | }
--------------------------------------------------------------------------------
/test/client.js:
--------------------------------------------------------------------------------
1 | var should = require('should');
2 | var Flapi = require('../index');
3 | var cli = require('./cli');
4 | var flapiClient = new Flapi({
5 | oauth_consumer_key : process.env.FLICKR_KEY,
6 | oauth_consumer_secret : process.env.FLICKR_SECRET,
7 | perms : 'delete'
8 | });
9 |
10 | if(!cli.useAuth){
11 | flapiClient.settings = {
12 | oauth_consumer_key : process.env.FLICKR_KEY,
13 | oauth_consumer_secret : process.env.FLICKR_SECRET,
14 | oauth_token : process.env.FLICKR_OAUTH_TOKEN,
15 | oauth_token_secret : process.env.FLICKR_OAUTH_SECRET,
16 | perms : 'delete'
17 | };
18 | }
19 |
20 |
21 | describe('initialization', function(){
22 |
23 | it('should return all settings defined', function(){
24 | flapiClient.settings.should.have.property('oauth_consumer_key');
25 | flapiClient.settings.oauth_consumer_key.should.equal(process.env.FLICKR_KEY);
26 | });
27 |
28 | });
29 |
30 | exports.client = flapiClient;
--------------------------------------------------------------------------------
/test/api-common.js:
--------------------------------------------------------------------------------
1 | var should = require('should');
2 | var flapiClient = require('./client').client;
3 |
4 | describe('common api', function(){
5 | this.timeout(30000);
6 |
7 | it('should be able to fetch a list of camera brand models', function(done){
8 | flapiClient.api({
9 | method : 'flickr.cameras.getBrandModels',
10 | params : { brand : 'apple' },
11 | next : function(data){
12 | data.should.have.properties('cameras', 'stat');
13 | data.stat.should.equal('ok');
14 | data.cameras.should.have.property('camera');
15 | done();
16 | }
17 | });
18 | });
19 |
20 |
21 | it('should be able to fetch a list of interesting photos', function(done){
22 | flapiClient.api({
23 | method : 'flickr.interestingness.getList',
24 | next : function(data){
25 | data.should.have.properties('photos', 'stat');
26 | data.stat.should.equal('ok');
27 | data.photos.should.have.property('photo');
28 | done();
29 | }
30 | });
31 | });
32 |
33 | });
--------------------------------------------------------------------------------
/lib/encode.js:
--------------------------------------------------------------------------------
1 | var crypto = require('crypto');
2 |
3 | // Flickr requires a signature to be built for auth requests
4 | // There is a lot of encodeURIComponents here, but they're all needed...
5 | // at least i think so
6 | exports.sign = function(req, consumerSecret, tokenSecret){
7 | if(!req.queryParams){ req.queryParams = {}; }
8 | if(!req.method) {req.method = 'GET';}
9 | if(!tokenSecret) { tokenSecret = ''; }
10 |
11 | req.queryParams.oauth_signature_method = 'HMAC-SHA1';
12 |
13 | // Sort parameters and encode key value pairs
14 | // ignore banned parameters
15 | var paramKeys = Object.keys(req.queryParams).sort();
16 | var urlParams = [];
17 | paramKeys.forEach(function(key){
18 | var keyValuePair = encodeURIComponent(key + '=' + encodeURIComponent(req.queryParams[key]));
19 | urlParams.push(keyValuePair);
20 | });
21 |
22 | var url = 'http://' + req.host + req.path;
23 | var urlString = req.method + '&' + encodeURIComponent(url) + '&' + urlParams.join(encodeURIComponent('&'));
24 | var encodeKey = consumerSecret + '&' + tokenSecret;
25 | var encoded = crypto.createHmac('sha1', encodeKey).update(urlString).digest('base64');
26 |
27 | return encoded;
28 | };
--------------------------------------------------------------------------------
/examples/uploading_photo/index.js:
--------------------------------------------------------------------------------
1 | var open = require('open');
2 | var http = require('http');
3 | var url = require('url');
4 | var Flapi = require('flapi');
5 | var flapiClient;
6 |
7 | var options = {
8 | oauth_consumer_key: "[your api key]",
9 | oauth_consumer_secret: "[your app secret]",
10 | // oauth_token: '...', // <-- fill this to prevent the browser opening
11 | // oauth_token_secret: '...', // <-- fill this to prevent the browser opening
12 | perms: 'write'
13 | };
14 |
15 | var runServer = function(callback) {
16 | http.createServer(function (req, res) {
17 | res.writeHead(200, {'Content-Type': 'text/html'});
18 | var urlParts = url.parse(req.url, true);
19 | var query = urlParts.query;
20 | if(query.oauth_token) {
21 | flapiClient.getUserAccessToken(query.oauth_verifier, function(result) {
22 | options.oauth_token = result.oauth_token;
23 | options.oauth_token_secret = result.oauth_token_secret;
24 | var message = '';
25 | for(var prop in result) {
26 | message += prop + ' = ' + result[prop] + '
';
27 | }
28 | res.end(message);
29 | uploadPhotos();
30 | });
31 | } else {
32 | res.end('Missing oauth_token parameter.');
33 | }
34 | }).listen(3000, '127.0.0.1');
35 | console.log('Server running at http://127.0.0.1:3000/');
36 | callback();
37 | }
38 |
39 | var uploadPhotos = function() {
40 | var file = __dirname + "/images/nodejs.png";
41 | console.log("Uploading " + file);
42 | flapiClient.api({
43 | method: 'upload',
44 | params: { photo : file },
45 | accessToken : {
46 | oauth_token: options.oauth_token,
47 | oauth_token_secret: options.oauth_token_secret
48 | },
49 | next: function(data){
50 | console.log('New Photo: ', data);
51 | }
52 | });
53 | }
54 |
55 | var createFlapiClient = function(options){
56 | flapiClient = new Flapi(options);
57 | if(!options.oauth_token) {
58 | flapiClient.authApp('http://127.0.0.1:3000', function(oauthResults){
59 | runServer(function() {
60 | open(flapiClient.getUserAuthURL());
61 | })
62 | });
63 | } else {
64 | uploadPhotos();
65 | }
66 | };
67 |
68 | createFlapiClient(options);
--------------------------------------------------------------------------------
/test/auth.js:
--------------------------------------------------------------------------------
1 | var should = require('should');
2 | var authUtils = require('./authUtils');
3 | var cli = require('./cli');
4 | var flapiClient = require('./client').client;
5 | var port = 3001;
6 | var userToken = {};
7 |
8 |
9 | if(cli.useAuth){
10 |
11 | describe('authorization', function(){
12 | this.timeout(30000);
13 |
14 | it('should throw an error if no auth callback is defined', function(){
15 | flapiClient.authApp.should.throw()
16 | });
17 |
18 |
19 | it('should reach out to flickr and receive an oauth_token and an oauth_token_secret', function(done){
20 | var callbackURL = 'http://localhost:' + port + '/auth_callback';
21 |
22 | flapiClient.authApp(callbackURL, function(settings){
23 | settings.should.have.properties('oauth_callback_confirmed', 'oauth_token', 'oauth_token_secret');
24 | settings.oauth_callback_confirmed.should.equal('true');
25 |
26 | done();
27 | });
28 | });
29 |
30 |
31 | it('should return a user authorization URL when prompted', function(){
32 | var url = flapiClient.getUserAuthURL();
33 | var query = url.split('?')[1];
34 | var token = query.split('=')[1];
35 | token.should.not.equal('undefined');
36 | token.length.should.be.above(10);
37 |
38 | // Make the auth url available so we can later verify it works
39 | this.authURL = url;
40 | });
41 |
42 |
43 | it('should be able to fetch a user access token', function(done){
44 | var matched = function(queryParams){
45 | flapiClient.getUserAccessToken(queryParams.oauth_verifier, function(accessToken){
46 | accessToken.should.have.properties('oauth_token', 'oauth_token_secret', 'user_nsid', 'username');
47 | accessToken.oauth_token_secret.should.not.equal('undefined');
48 | accessToken.oauth_token_secret.should.not.equal('');
49 | userToken = accessToken;
50 | done();
51 | });
52 | };
53 |
54 | authUtils.createRouteListener('/auth_callback', matched);
55 | authUtils.simulateUserApproval(flapiClient.getUserAuthURL());
56 | });
57 |
58 | });
59 | }
60 |
61 |
62 | exports.getUserAccessToken = function(){
63 | if(cli.useAuth) return userToken
64 | else return {
65 | fullname : 'First Last',
66 | oauth_token : process.env.FLICKR_OAUTH_USER_TOKEN,
67 | oauth_token_secret : process.env.FLICKR_OAUTH_USER_SECRET,
68 | user_nsid : process.env.FLICKR_NSID,
69 | username : process.env.FLICKR_USERNAME
70 | }
71 | };
--------------------------------------------------------------------------------
/examples/express.js:
--------------------------------------------------------------------------------
1 | // Below is a sample express app using flapi.
2 | // Make sure to install the express and dirty db dependencies.
3 |
4 | // Notice how data persistence works in relation to the creation
5 | // of the flapi client. It's important to reuse the same `oauth_token`
6 | // and `oauth_token_secret` so your user's do not have to
7 | // reauthenticate every time your node application restarts.
8 |
9 |
10 | var express = require('express');
11 | var http = require('http');
12 | var dirty = require('dirty');
13 | var Flapi = require('../index');
14 | var users = dirty('users.db');
15 | var settings = dirty('settings.db');
16 | var app = express();
17 | var flapiClient;
18 |
19 |
20 | var createFlapiClient = function(opts){
21 | flapiClient = new Flapi(opts);
22 |
23 | if(!opts.oauth_token){
24 | flapiClient.authApp('http://localhost:3000/auth_callback', function(oauthResults){
25 | settings.set('oauth', oauthResults);
26 | });
27 | }
28 | };
29 |
30 |
31 | // See if there's already an oauth object saved that we can reuse.
32 | // Not doing it this way will cause users to be forced to reauth
33 | // everytime your app gets restarted
34 | settings.on('load', function(){
35 | var opts = {
36 | oauth_consumer_key : process.env.FLICKR_KEY,
37 | oauth_consumer_secret : process.env.FLICKR_SECRET,
38 | perms : 'delete'
39 | };
40 |
41 | var oauthOpts = settings.get('oauth');
42 | if(oauthOpts){
43 | opts.oauth_token = oauthOpts.oauth_token;
44 | opts.oauth_token_secret = oauthOpts.oauth_token_secret;
45 | }
46 |
47 | createFlapiClient(opts);
48 | });
49 |
50 |
51 | // Redirect to the authorize page
52 | app.get('/', function(req, res){
53 | res.redirect('/user/authorize');
54 | });
55 |
56 |
57 | // Forward to flickr's authorize page
58 | app.get('/user/authorize', function(req, res){
59 | res.redirect(flapiClient.getUserAuthURL());
60 | });
61 |
62 |
63 | // If there's an oauth_token, we know this is a user auth.
64 | // otherwise, it's just the app trying to auth, let it pass
65 | app.get('/auth_callback', function(req, res){
66 | if(req.query.oauth_token){
67 | flapiClient.getUserAccessToken(req.query.oauth_verifier, function(accessToken){
68 | users.set(accessToken.user_nsid, accessToken);
69 |
70 | var message = ['Succesfully Authorized. Visit,',
71 | 'http://localhost:3000/user/' + accessToken.user_nsid + '/photos to see',
72 | 'a full listing of your photos'].join('');
73 | res.send(message);
74 | });
75 | } else {
76 | res.writeHead(200, {'Content-Type': 'text/plain'});
77 | res.end();
78 | }
79 | });
80 |
81 |
82 | // Show a users photos
83 | app.get('/user/:id/photos', function(req, res){
84 | var accessToken = users.get(req.params.id);
85 | flapiClient.api({
86 | method : 'flickr.people.getPhotos',
87 | params : { user_id : req.params.id },
88 | accessToken : accessToken,
89 | next : function(data){
90 | res.send(data);
91 | }
92 | });
93 | });
94 |
95 |
96 | // Create a new server
97 | http.createServer(app).listen(3000, function(){
98 | console.log('Express server listening on port 3000');
99 | });
--------------------------------------------------------------------------------
/test/authUtils.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var request = require('request').defaults({ jar : true, followAllRedirects : true });
3 | var jsdom = require('jsdom');
4 | var qs = require('querystring');
5 | var url = require('url');
6 | var port = 3001;
7 |
8 |
9 | // Follow login redirect(s), enter username and password,
10 | // 'click' the "OK I'll authorize button"... which then
11 | // in turn triggers the http request from flickr
12 | exports.simulateUserApproval = function(authURL){
13 | // Bounce around until we find the login page
14 | var findLogin = function(earl){
15 | console.log('\n ...... finding the login page ......');
16 |
17 | request(earl, function(err, res, body){
18 | var redirectUrl = this.redirects[this.redirects.length-1].redirectUri;
19 | login(body, redirectUrl);
20 | });
21 | }(authURL);
22 |
23 | // Fetch form data from DOM and login via the redirected
24 | // web page from flickr
25 | var login = function(body, earl){
26 | console.log(' ...... attempting to login ......');
27 |
28 | getFormDataFromLogin(body, function(formData){
29 |
30 | formData.login = process.env.FLICKR_USERNAME;
31 | formData.passwd = process.env.FLICKR_PASSWORD;
32 |
33 | request.post(earl, { form : formData }, function(err, res, body){
34 | getClientRedirectURL(body, loadAppApprovalPage)
35 | });
36 | });
37 | };
38 |
39 | // Load the app authorization page, get the form data from
40 | // the DOM and move to approval
41 | var loadAppApprovalPage = function(earl){
42 | console.log(' ...... loading the app approval page ......');
43 |
44 | request(earl, function(err, res, body){
45 | getFormDataFromAppApproval(body, approveApplication);
46 | });
47 | };
48 |
49 | // Attempt to approve the application with
50 | var approveApplication = function(formAction, formData){
51 | console.log(' ...... attempting to approve the app ......');
52 |
53 | var earl = 'http://www.flickr.com' + formAction;
54 | request.post(earl, { form : formData });
55 | };
56 |
57 | // The yahoo login page is FILLED with hidden input fields.
58 | // collect all inputs, then pass them back to next
59 | var getFormDataFromLogin = function(webpage, next){
60 | jsdom.env(webpage, ['http://code.jquery.com/jquery.js'], function(errors, window) {
61 | var $ = window.$;
62 | var form = {};
63 |
64 | $('#fsLogin input').each(function(){
65 | form[$(this).attr('name')] = $(this).val()
66 | });
67 |
68 | next(form);
69 | });
70 | };
71 |
72 | // After a succesful login, yahoo redirects to a webpage with
73 | // a client side redirect. Find the redirect and follow
74 | var getClientRedirectURL = function(webpage, next){
75 | jsdom.env(webpage, ['http://code.jquery.com/jquery.js'], function(errors, window) {
76 | var $ = window.$;
77 | var href = $('a').attr('href');
78 | next(href);
79 | });
80 | };
81 |
82 | // Find all input fields and return them along
83 | // with the form action
84 | var getFormDataFromAppApproval = function(webpage, next){
85 | jsdom.env(webpage, ['http://code.jquery.com/jquery.js'], function(errors, window) {
86 | var $ = window.$;
87 | var action = $('.authPerms form').attr('action');
88 | var form = {};
89 |
90 | $('.authPerms input').each(function(){
91 | var name = $(this).attr('name');
92 | if(name) form[name] = $(this).val()
93 | });
94 |
95 | next(action, form);
96 | });
97 | };
98 | };
99 |
100 |
101 | // Create a simple http server to listen for the
102 | exports.createRouteListener = function(route, isMatched){
103 | var server = http.createServer(function(req, res){
104 |
105 | var parsedEarl = url.parse(req.url);
106 | var queryParams = qs.parse(parsedEarl.query);
107 |
108 | if(parsedEarl.pathname == route){
109 | res.writeHead(200, {'Content-Type': 'text/plain'});
110 | res.end();
111 | isMatched(queryParams);
112 | }
113 | }).listen(port);
114 | };
--------------------------------------------------------------------------------
/lib/flickr.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var queryString = require('querystring');
3 | var http = require('http');
4 | var restler = require('restler');
5 |
6 |
7 | // Make HTTP requests to flickr
8 | exports.makeRequest = function(request, next, photo){
9 | var query = (request.queryParams) ? '?' + queryString.stringify(request.queryParams) : '';
10 | var reqOpts = {
11 | host : request.host,
12 | path : request.path + query,
13 | method : request.method
14 | };
15 |
16 | // When posting, we need to pass all data in the body instead of the query
17 | // this is used only for image creation
18 | if(photo){
19 | var earl = 'http://' + reqOpts.host + reqOpts.path;
20 | var form = {};
21 | for(var key in request.queryParams){
22 | if(request.queryParams.hasOwnProperty(key)){
23 | form[key] = request.queryParams[key];
24 | }
25 | }
26 |
27 | var postForm = function(){
28 | restler.post(earl, {
29 | multipart : true,
30 | data : form
31 | }).on('complete', function(xml, res) {
32 | var photoNode = xml.match('(.*)');
33 | var msgNode = xml.match('(.*)');
34 | var body = {
35 | stat : photoNode ? 'ok' : 'fail'
36 | };
37 |
38 | if(photoNode){ body.photoid = photoNode[1]; }
39 | if(msgNode) { body.message = msgNode[1]; }
40 |
41 | next(body, res);
42 | });
43 | };
44 |
45 | fs.stat(photo, function (err, stats){
46 | form.photo = restler.file(photo, null, stats.size, null);
47 | postForm();
48 | });
49 | } else {
50 | var data = [];
51 | var httpReq = http.request(reqOpts, function(res){
52 | res.on('data',function(chunk){
53 | data.push(chunk);
54 | });
55 |
56 | res.on('end',function(){
57 | next(data.join(''), res);
58 | });
59 | });
60 |
61 | httpReq.end();
62 | }
63 | };
64 |
65 |
66 | // Convenience for adding nonce, time stamp, and version
67 | // ...also adds two objects together
68 | // ...also strips out banned parameters
69 | exports.addDefaultParams = function(original, additional){
70 | var obj = {
71 | oauth_nonce : Math.floor(Math.random()*100) + new Date().getTime(),
72 | oauth_timestamp : Math.floor(new Date()/1000),
73 | oauth_version : '1.0'
74 | };
75 |
76 | // Make a copy of the original object
77 | for(var oKey in original){
78 | if(original.hasOwnProperty(oKey)){
79 | obj[oKey] = original[oKey];
80 | }
81 | }
82 |
83 | // If an object to add is passed, add it
84 | if(additional){
85 | for(var aKey in additional){
86 | if(additional.hasOwnProperty(aKey)){
87 | obj[aKey] = additional[aKey];
88 | }
89 | }
90 | }
91 |
92 | // Delete the banned params
93 | var bannedParams = ['consumer_secret', 'photo', 'perms', 'oauth_callback_confirmed'];
94 | bannedParams.forEach(function(banned){
95 | delete obj[banned];
96 | });
97 |
98 | return obj;
99 | };
100 |
101 |
102 | exports.getHttpMethod = function(methodName){
103 | var name = methodName.replace('flickr.', '');
104 | return postMethods.indexOf(name) === -1 ? 'GET' : 'POST';
105 | };
106 |
107 |
108 | var postMethods = [
109 | 'upload',
110 | 'blogs.postPhoto',
111 | 'favorites.add',
112 | 'favorites.remove',
113 | 'galleries.addPhoto',
114 | 'galleries.create',
115 | 'galleries.editMeta',
116 | 'galleries.editPhoto',
117 | 'galleries.editPhotos',
118 | 'groups.join',
119 | 'groups.joinRequest',
120 | 'groups.leave',
121 | 'groups.discuss.replies.add',
122 | 'groups.discuss.replies.delete',
123 | 'groups.discuss.replies.edit',
124 | 'groups.discuss.topics.add',
125 | 'groups.pools.addPhoto',
126 | 'groups.pools.remove',
127 | 'photos.addTags',
128 | 'photos.delete',
129 | 'photos.removeTag',
130 | 'photos.setContentType',
131 | 'photos.setDates',
132 | 'photos.setMeta',
133 | 'photos.setPerms',
134 | 'photos.setSafetyLevel',
135 | 'photos.setTags',
136 | 'photos.comments.addComment',
137 | 'photos.comments.deleteComment',
138 | 'photos.comments.editComment',
139 | 'photos.geo.batchCorrectLocation',
140 | 'photos.geo.correctLocation',
141 | 'photos.geo.removeLocation',
142 | 'photos.geo.setContext',
143 | 'photos.geo.setLocation',
144 | 'photos.geo.setPerms',
145 | 'photos.licenses.setLicense',
146 | 'photos.notes.add',
147 | 'photos.notes.delete',
148 | 'photos.notes.edit',
149 | 'photos.people.add',
150 | 'photos.people.delete',
151 | 'photos.people.deleteCoords',
152 | 'photos.people.editCoords',
153 | 'photos.suggestions.approveSuggestion',
154 | 'photos.suggestions.rejectSuggestion',
155 | 'photos.suggestions.removeSuggestion',
156 | 'photos.suggestions.suggestLocation',
157 | 'photos.transform.rotate',
158 | 'photosets.addPhoto',
159 | 'photosets.create',
160 | 'photosets.delete',
161 | 'photosets.editMeta',
162 | 'photosets.editPhotos',
163 | 'photosets.orderSets',
164 | 'photosets.removePhoto',
165 | 'photosets.removePhotos',
166 | 'photosets.reorderPhotos',
167 | 'photosets.setPrimaryPhoto',
168 | 'photosets.comments.addComment',
169 | 'photosets.comments.deleteComment',
170 | 'photosets.comments.editComment'
171 | ];
--------------------------------------------------------------------------------
/lib/flapi.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var queryString = require('querystring');
3 | var flickr = require('./flickr');
4 | var encode = require('./encode');
5 | var apiHost = 'api.flickr.com';
6 |
7 | // Any options passed in are defined as settings
8 | var flapi = function(opts){
9 | this.settings = {};
10 | for(var key in opts){
11 | if(opts.hasOwnProperty(key)){
12 | this.settings[key] = opts[key];
13 | }
14 | }
15 | };
16 |
17 |
18 | // Automatically called when the app starts. Authorizes your app and returns
19 | // a request token and signature you'll use to auth users
20 | flapi.prototype.authApp = function(authCallbackURL, next){
21 | var self = this;
22 | var message = 'Please visit the flickr documentation http://www.flickr.com/services/api/, or retrieve your app creds here http://www.flickr.com/services/apps, or create an app here http://www.flickr.com/services/apps/create/ \n';
23 |
24 | if(!this.settings.oauth_consumer_key){
25 | throw new Error('An oauth_consumer_key (app key) is required. ' + message);
26 | }
27 |
28 | if(!this.settings.oauth_consumer_secret){
29 | throw new Error('An oauth_consumer_secret (app secret) is required. ' + message);
30 | }
31 |
32 | if(!authCallbackURL || typeof(authCallbackURL) !== 'string') {
33 | throw new Error('Please define a an auth callback url. ' + message);
34 | }
35 |
36 | var request = {
37 | method : 'GET',
38 | host : apiHost,
39 | path : '/services/oauth/request_token',
40 | queryParams : flickr.addDefaultParams(this.settings, {
41 | oauth_callback : authCallbackURL
42 | })
43 | };
44 |
45 | request.queryParams.oauth_signature = encode.sign(request, this.settings.oauth_consumer_secret);
46 |
47 | flickr.makeRequest(request, function(body, res){
48 | var returnObj = queryString.parse(body);
49 | for(var key in returnObj){
50 | if(returnObj.hasOwnProperty(key)){
51 | self.settings[key] = returnObj[key];
52 | }
53 | }
54 |
55 | if(next){ next(self.settings, res); }
56 | });
57 | };
58 |
59 |
60 | // Return a URL with the oauth token
61 | flapi.prototype.getUserAuthURL = function(){
62 | var perms = (this.settings.perms) ? '&perms=' + this.settings.perms : '';
63 | return 'http://' + apiHost + '/services/oauth/authorize?oauth_token=' + this.settings.oauth_token + perms;
64 | };
65 |
66 |
67 | // The "token" is really an object with an oauth_token and an
68 | // oauth_token_secret unique to this individual
69 | flapi.prototype.getUserAccessToken = function(oauth_verifier, next){
70 | var request = {
71 | method : 'GET',
72 | path : '/services/oauth/access_token',
73 | host : apiHost,
74 | queryParams : flickr.addDefaultParams(this.settings, {
75 | oauth_verifier : oauth_verifier
76 | })
77 | };
78 |
79 | request.queryParams.oauth_signature = encode.sign(request,
80 | this.settings.oauth_consumer_secret,
81 | this.settings.oauth_token_secret
82 | );
83 |
84 | flickr.makeRequest(request, function(body, res){
85 | next(queryString.parse(body), res);
86 | });
87 | };
88 |
89 |
90 | // General method for handling all api calls
91 | flapi.prototype.api = function(opts){
92 |
93 | if(!opts.method){
94 | throw new Error('Please pass an api method option as "method". You can find all available flickr methods within the flickr documentation at http://www.flickr.com/services/api/');
95 | }
96 |
97 | var queryParams = flickr.addDefaultParams(this.settings, {
98 | method : opts.method,
99 | format : 'json',
100 | nojsoncallback : 1
101 | });
102 |
103 | // Any url params which are passed in should be added
104 | // to the query params object, ignore the photo
105 | opts.params = opts.params || {};
106 | for(var key in opts.params){
107 | if(key !== 'photo' && opts.params.hasOwnProperty(key)){
108 | queryParams[key] = opts.params[key];
109 | }
110 | }
111 |
112 | var request = {
113 | method : flickr.getHttpMethod(opts.method),
114 | path : '/services/rest/',
115 | host : apiHost,
116 | queryParams : queryParams
117 | };
118 |
119 | // If the method is a photo upload, make a few changes
120 | if(opts.method === 'upload'){
121 | request.host = 'up.flickr.com';
122 | request.path = '/services/upload/';
123 | }
124 |
125 | // Attach the oath_token if passed
126 | if(opts.accessToken){
127 | request.queryParams.oauth_token = opts.accessToken.oauth_token;
128 | request.queryParams.oauth_signature = encode.sign(request,
129 | this.settings.oauth_consumer_secret,
130 | opts.accessToken.oauth_token_secret
131 | );
132 | }
133 |
134 | if(!opts.preventCall){
135 | flickr.makeRequest(request, function(body, res){
136 | if(opts.next){
137 | var json = body;
138 | if(typeof body !== 'object'){
139 | if(body[0] === '{' || body[0] === '['){
140 | json = JSON.parse(body);
141 | } else {
142 | json = { stat : 'fail', message : body };
143 | }
144 | }
145 | opts.next(json, res);
146 | }
147 | }, opts.params.photo);
148 | }
149 |
150 | // Return the url just in case the end user wants to make
151 | // the request themselves
152 | var query = queryString.stringify(request.queryParams);
153 | var earl = 'http://' + path.join(request.host, request.path);
154 | return earl + '?' + query;
155 | };
156 |
157 |
158 | module.exports = flapi;
--------------------------------------------------------------------------------
/test/api-authed.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var should = require('should');
3 | var queryString = require('querystring');
4 | var flapiClient = require('./client').client;
5 | var photoData = {};
6 |
7 | describe('authorized api', function(){
8 | this.timeout(30000);
9 |
10 | beforeEach(function(){
11 | this.accessToken = require('./auth').getUserAccessToken();
12 | });
13 |
14 |
15 | it('should throw an error when no api method is submitted', function(){
16 | flapiClient.api.should.throw();
17 | });
18 |
19 |
20 | it('should return a flickr request url', function(done){
21 | var url = flapiClient.api({
22 | method : 'flickr.people.getPhotos',
23 | params : { user_id : this.accessToken.user_nsid },
24 | accessToken : this.accessToken,
25 | preventCall : true
26 | });
27 |
28 | var queryParams = queryString.parse(url);
29 | queryParams.should.have.properties('oauth_signature', 'oauth_timestamp', 'method');
30 | done();
31 | });
32 |
33 |
34 | it('should be able to fetch a list of the user\'s photos', function(done){
35 | var urlParams = { user_id : this.accessToken.user_nsid };
36 |
37 | flapiClient.api({
38 | method : 'flickr.people.getPhotos',
39 | params : urlParams,
40 | accessToken : this.accessToken,
41 | next : function(data){
42 | data.should.have.properties('photos', 'stat');
43 | data.stat.should.equal('ok');
44 | data.photos.should.have.property('photo');
45 |
46 | var photoArray = data.photos.photo;
47 | photoData.randomPhotoId = photoArray[Math.floor(Math.random(0, photoArray.length) + 1)].id;
48 |
49 | done();
50 | }
51 | });
52 | });
53 |
54 |
55 | it('should be able to fetch a single photo', function(done){
56 | var urlParams = { photo_id : photoData.randomPhotoId };
57 |
58 | flapiClient.api({
59 | method : 'flickr.photos.getInfo',
60 | params : urlParams,
61 | accessToken : this.accessToken,
62 | next : function(data){
63 | data.should.have.properties('photo', 'stat');
64 | data.stat.should.equal('ok');
65 | done();
66 | }
67 | });
68 | });
69 |
70 |
71 | it('should be able to create a photo', function(done){
72 | flapiClient.api({
73 | method : 'upload',
74 | params : { photo : 'test/image.jpg' },
75 | accessToken : this.accessToken,
76 | next : function(data){
77 | data.should.have.properties('stat');
78 | data.stat.should.equal('ok');
79 | photoData.newPhotoId = data.photoid;
80 | done();
81 | }
82 | });
83 | });
84 |
85 |
86 | it('should be able to add tags to a photo', function(done){
87 | var params = {
88 | photo_id : photoData.newPhotoId,
89 | tags : 'oscar, meyer, weiner'
90 | };
91 |
92 | flapiClient.api({
93 | method : 'flickr.photos.addTags',
94 | params : params,
95 | accessToken : this.accessToken,
96 | next : function(data){
97 | data.should.have.properties('tags', 'stat');
98 | data.stat.should.equal('ok');
99 | done();
100 | }
101 | });
102 | });
103 |
104 |
105 | it('should be able to create a set', function(done){
106 | var params = {
107 | title : 'weiner mobiles',
108 | description : 'A collection of my most favorite photos of the oscar meyer weiner mobile',
109 | primary_photo_id : photoData.randomPhotoId
110 | };
111 |
112 | flapiClient.api({
113 | method : 'flickr.photosets.create',
114 | params : params,
115 | accessToken : this.accessToken,
116 | next : function(data){
117 | data.stat.should.equal('ok');
118 | data.photoset.should.have.property('id');
119 | photoData.photoSetId = data.photoset.id;
120 | done();
121 | }
122 | });
123 | });
124 |
125 |
126 | it('should be able to put a photo into a set', function(done){
127 | var params = {
128 | photo_id : photoData.newPhotoId,
129 | photoset_id : photoData.photoSetId
130 | };
131 |
132 | flapiClient.api({
133 | method : 'flickr.photosets.addPhoto',
134 | params : params,
135 | accessToken : this.accessToken,
136 | next : function(data){
137 | data.stat.should.equal('ok');
138 | done();
139 | }
140 | });
141 | });
142 |
143 |
144 | it('should be able to remove tags from a photo', function(done){
145 | var self = this;
146 |
147 | // When creating a tag, the full id isn't passed back... only the
148 | // abbreviated form. Therefore, to fully test, we need to re-request
149 | // the created photo to get the tag list
150 | flapiClient.api({
151 | method : 'flickr.photos.getInfo',
152 | params : { photo_id : photoData.newPhotoId},
153 | accessToken : this.accessToken,
154 | next : function(photoResponse){
155 | var tagId = photoResponse.photo.tags.tag[0].id;
156 | flapiClient.api({
157 | method : 'flickr.photos.removeTag',
158 | params : { tag_id : tagId },
159 | accessToken : self.accessToken,
160 | next : function(data){
161 | data.stat.should.equal('ok');
162 | done();
163 | }
164 | });
165 | }
166 | });
167 | });
168 |
169 |
170 | it('should be able to remove a photo from a set', function(done){
171 | var params = {
172 | photo_id : photoData.newPhotoId,
173 | photoset_id : photoData.photoSetId
174 | };
175 |
176 | flapiClient.api({
177 | method : 'flickr.photosets.removePhoto',
178 | params : params,
179 | accessToken : this.accessToken,
180 | next : function(data){
181 | data.stat.should.equal('ok');
182 | done();
183 | }
184 | });
185 | });
186 |
187 |
188 | it('should be able to delete a photo', function(done){
189 | flapiClient.api({
190 | method : 'flickr.photos.delete',
191 | params : { photo_id : photoData.newPhotoId },
192 | accessToken : this.accessToken,
193 | next : function(data){
194 | data.stat.should.equal('ok');
195 | done();
196 | }
197 | });
198 | });
199 |
200 |
201 | it('should be able to delete a set', function(done){
202 | flapiClient.api({
203 | method : 'flickr.photosets.delete',
204 | params : { photoset_id : photoData.photoSetId },
205 | accessToken : this.accessToken,
206 | next : function(data){
207 | data.stat.should.equal('ok');
208 | done();
209 | }
210 | });
211 | });
212 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flapi [](https://travis-ci.org/joelongstreet/flapi) [](http://badge.fury.io/js/flapi) [](https://codeclimate.com/github/joelongstreet/flapi)
2 |
3 | ## Feature Set
4 | I know there are several existing node flickr modules, but... I thought the community could use one with the following features:
5 |
6 | * Simple api wrapper
7 | * Fully tested
8 | * Oauth support
9 | * Extra light dependency tree
10 | * Example code
11 |
12 |
13 | ### Quick Start (...quickish)
14 | 1 - Instantiate the flapi client:
15 | ``` javascript
16 | var Flapi = require('flapi');
17 | var flapiClient = new Flapi({
18 | oauth_consumer_key : FLICKR_KEY,
19 | oauth_consumer_secret : FLICKR_SECRET
20 | });
21 | ```
22 |
23 | 2 - Listen for an http request from flickr and respond
24 | ``` javascript
25 | var server = http.createServer(function(req, res){
26 | res.writeHead(200, {'Content-Type': 'text/plain'});
27 | res.end();
28 | });
29 | ```
30 |
31 | 3 - Authenticate your application
32 | ``` javascript
33 | flapiClient.authApp('http://localhost:3000/auth_callback');
34 | ```
35 |
36 | 4 - When application authentication is finished, prompt users to authenticate with your app by giving them the app auth url.
37 | ``` javascript
38 | var url = flapiClient.getUserAuthURL();
39 | ```
40 |
41 | 5 - Get the user's access token to make individual requests.
42 | ``` javascript
43 | var userAccessToken;
44 | flapiClient.getUserAccessToken(function(accessToken){
45 | userAccessToken = accessToken;
46 | });
47 | ```
48 |
49 | 6 - Make requests on the user's behalf
50 | ``` javascript
51 | flapiClient.api({
52 | method : 'flickr.people.getPhotos',
53 | params : { user_id : userAccessToken.user_nsid },
54 | accessToken : userAccessToken,
55 | next : function(data){
56 | console.log('User Photos: ', data)
57 | }
58 | });
59 | ```
60 |
61 |
62 |
63 | ## Making API requests
64 | Once you've authorized your application and have permissions from a user, you should be able to make api requests on their behalf.
65 |
66 | With the exception of photo uploading, all api methods match the [flickr documentation](http://www.flickr.com/services/api/). To get a [listing of photos](http://www.flickr.com/services/api/flickr.people.getPhotos.html), you'll need a `user_id` and the user's access token (the `api_key` is automatically sent via the access token). The request can be completed with the following:
67 |
68 | ``` javascript
69 | flapiClient.api({
70 | method : 'flickr.people.getPhotos',
71 | params : { user_id : userAccessToken.user_nsid },
72 | accessToken : userAccessToken,
73 | next : function(data){
74 | console.log('User Photos: ', data)
75 | }
76 | });
77 | ```
78 |
79 | * `method` - The flickr API Method
80 | * `params` - Any API options to send
81 | * `accessToken` - A user's unique access token
82 | * `next` - The callback used when the api call is complete
83 |
84 |
85 | ### Uploading Photos
86 | To upload a photo, use the method `upload` and pass the file path as param. Example:
87 |
88 | ``` javascript
89 | flapiClient.api({
90 | method : 'upload',
91 | params : { photo : 'test/image.jpg' },
92 | accessToken : {
93 | oauth_token: [access token returned after successful authorization],
94 | oauth_token_secret: [access token secret returned after successful authorization]
95 | },
96 | next : function(data){
97 | console.log('New Photo: ', data)
98 | }
99 | });
100 | ```
101 |
102 |
103 | ### Setting Application permissions
104 | According to the [flickr documentation](http://www.flickr.com/services/api/), you should be able to set your application's permission set (`read`, `write`, or `delete`) from the edit screen of your app. I've never been able to find these settings. Instead, you can set it in the flickr module constructor with the `perms` option:
105 |
106 | ``` javascript
107 | var flapiClient = new Flapi({
108 | oauth_consumer_key : FLICKR_KEY,
109 | oauth_consumer_secret : FLICKR_SECRET,
110 | perms : 'delete'
111 | });
112 | ```
113 |
114 |
115 | ### Unauthorized API Requests
116 | Some of flickr's api methods don't require authorization (ex: `flickr.cameras.getBrandModels` and `flickr.interestingness.getList`). To use these without user authentication, omit the `accessToken` option from the api call:
117 |
118 | ``` javascript
119 | flapiClient.api({
120 | method : 'flickr.interestingness.getList',
121 | params : { brand : 'apple' },
122 | next : function(data){
123 | console.log('TADA!', data)
124 | }
125 | });
126 | ```
127 |
128 |
129 | ### Handling Errors
130 | API errors are passed directly to the next function. You can catch them by checking the stat property of the data returned. In most scenarios a message property should also be passed.
131 | ``` javascript
132 | flapiClient.api({
133 | method : 'flickr.class.method',
134 | params : {},
135 | next : function(data){
136 | if(data.stat == 'fail'){
137 | console.log(data.code);
138 | console.log(data.message);
139 | }
140 | }
141 | });
142 | ```
143 |
144 |
145 | ### Other Notes
146 | If you're looking to have a client make the http request, delay the request for some reason, or want to handle the request on your own... you can prevent the call to flickr by passing the option `preventCall : true`. Regardless of this option, the full url of the intended request will always be passed back.
147 | ``` javascript
148 | var flickrAPIurl = flapiClient.api({
149 | method : 'flickr.people.getPhotos',
150 | params : { user_id : this.accessToken.user_nsid },
151 | accessToken : this.accessToken,
152 | preventCall : true
153 | });
154 | ```
155 |
156 |
157 |
158 | ## Data Persistence
159 | To keep users from having to authenticate every time your node application restarts, you'll need to persist your application's `oauth_token` and `oauth_token_secret` as well as each user's individual `access_token`.
160 |
161 | To instantiate the client with a token and secret, pass those properties when creating the object.
162 |
163 | ``` javascript
164 | var flapiClient = new Flapi({
165 | oauth_consumer_key : FLICKR_KEY,
166 | oauth_consumer_secret : FLICKR_SECRET,
167 | oauth_token : FLICKR_OAUTH_TOKEN,
168 | oauth_token_secret : FLICKR_OAUTH_TOKEN_SECRET
169 | });
170 | ```
171 |
172 | You only need to authorize your application once. If you're passing the token and secret, you can skip steps 2 and 3 described in the quick start above.
173 |
174 |
175 |
176 | ## Examples
177 | Using this module within express or any other node server framework should be fairly straight forward. Check out the examples directory of this project for more details.
178 |
179 |
180 |
181 | ## Running the Tests
182 | To run the tests, please make sure you've installed the project's dev dependencies and have the following environment variables set:
183 |
184 | * `FLICKR_KEY` - Your flickr application's key
185 | * `FLICKR_SECRET` - Your flickr application's secret
186 | * `FLICKR_USERNAME` - Your flickr user name, used to simulate a yahoo and flickr app authentication flow
187 | * `FLICKR_PASSWORD` - Your flickr password, used to simulate a yahoo and flickr app authentication flow
188 |
189 | ### With Auth
190 | Calling `make test` from the root of the project will run all tests in the correct order.
191 |
192 | On occasion, the numerous flickr redirects required to simulate authentication will fail. This causes every subsequent test to then fail. If you're running tests and experience this oddity, just run again.
193 |
194 | ### Without Auth
195 | You can skip the auth steps and still test all the API methods by running `make test-without-auth`. For this to work, you'll also need the following environment variables set:
196 |
197 | * `FLICKR_OAUTH_USER_TOKEN` - Can retrieve this from the standard auth above.
198 | * `FLICKR_OAUTH_USER_SECRET` - Can retrieve this from the standard auth above.
199 | * `FLICKR_NSID` - Can retrieve this from the standard auth above.
200 | * `FLICKR_USERNAME` - Not currently used in the tests, but good to include anyways
--------------------------------------------------------------------------------