Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit a54830fe2f5914b42a06d39d380ab4e1837ebdd1 0 parents
@mncaudill authored
Showing with 216 additions and 0 deletions.
  1. +7 −0 LICENSE
  2. +23 −0 README.md
  3. +136 −0 flickr-conduit.js
  4. +19 −0 package.json
  5. +31 −0 sample.js
7 LICENSE
@@ -0,0 +1,7 @@
+Copyright (c) 2011 Nolan Caudill
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 README.md
@@ -0,0 +1,23 @@
+flickr-conduit: a PubSub subscriber endpoint for Flickr's real-time PuSH feed
+===================
+
+## Description
+
+flickr-conduit is a subsriber endpoint for Flickr's implementation of the PubSubHubbub spec. It handles the the 'subscribe', 'unsubscribe', and the parsing of the XML that Flickr pushes out.
+
+The server works in publish/subscribe model itself, with users registering events they're interested in and then flickr-conduit answering these subscription requests. This works identically to node's own EventEmitter class and in fact uses that under the covers.
+
+## Flow
+
+1. The server is running on user-defined port.
+2. Some userland code calls the Flickr API using this listening server as an endpoint. flickr-conduit assumes that the only thing hitting this endpoint is the Flickr PuSH feed.
+3. flickr-conduit will then use the subsriptionCallback method to look at the verify_token the subscription callback brings in. You should be using the verify_token, so the code requires this. The subscriptionCallback returns true or false and if true, then flickr-conduit will echo the challenge screen. Unsubsribe works the same way.
+4. After the challenge-response step is completed, Flickr will start posting XML blobs to the endpoint. flickr-conduit will parse this and then emit an event with the parsed payload. There is a function called getEventName (also user-defined) that takes the parsed URL and returns a string that signfies the event name. By default, I use the method of base64-encoding the Flickr NSID and the feed topic_type to create unique callbacks and just use this as the event name. If you want something different, override the getEventName function for your conduit instance.
+5. flickr-conduit will then run any callbacks you've registered with it (using your instance's "on" method) with the image payload.
+
+## Tips
+
+* Make sure you override the subscribeCallback (and probably unsubscribeCallback) methods as by default these return true. I use an HMACof the callback URL and my Flickr API secret as my verify_token as this was easy to write in both PHP and node.
+* Also, this code has been running for me for a couple of weeks with no problems, but if any long-running server, you may want to use node's "process.on("uncaughtException", function(){})" to catch bad things.
+* This library is fun with socket.io. I've included an example of me using flickr-conduit to shove things to socket.io.
+
136 flickr-conduit.js
@@ -0,0 +1,136 @@
+/*
+Copyright (c) 2011 Nolan Caudill
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+var EventEmitter = require('events').EventEmitter
+ , urlParser = require('url').parse
+ , xml2js = require('xml2js')
+ , http = require('http')
+;
+
+var Conduit = function() {
+
+ // Create new emitter
+ var emitter = new EventEmitter();
+ emitter.setMaxListeners(0);
+ this.emitter = emitter;
+}
+
+exports.Conduit = Conduit;
+
+// Receives parser URL object and verifyToken
+// Returns true or false
+Conduit.prototype.unsubscribeCallback = function(urlParts, verifyToken) {
+ return true;
+}
+
+// Receives parser URL object and verifyToken
+// Returns true or false
+Conduit.prototype.subscribeCallback = function(urlParts, verifyToken) {
+ return true;
+}
+
+// By default, if you have a format of /callback?sub=$SUB where $SUB is the base64-encoded "nsid-topic_type",
+// you're good to go. Otherwise you'll want to pass in a function that takes the urlParts (a parsed URL)
+// and returns a string. I haven't retooled this yet to support the parameterized topic types like tags and geo.
+Conduit.prototype.getEventName = function(urlParts) {
+ var sub = new Buffer(urlParts.query.sub, 'base64').toString('ascii');
+ var eventPieces = sub.split('-');
+
+ var nsid = eventPieces[0];
+ var stream = eventPieces[1];
+ var eventName = nsid + '-' + stream;
+ return eventName;
+}
+
+var parseFlickrPost = function(content, callback) {
+ var xml = new xml2js.Parser();
+ var imgObjs = [];
+ xml.on('end', function(data) {
+ // We possibly get multiple entries per POST
+ var entries = Array.isArray(data.entry) ? data.entry : [data.entry];
+
+ var imgData = null;
+ var photoUrl= null;
+ for (var i in entries) {
+ try {
+ imgData = entries[i]['media:content']['@'];
+
+ // Dumb, but there's a bug in the xml2js that messes up on the <link> tab. (Or I'm missing something.)
+ var id = entries[i]['id'].split(':')[2].split('/')[2];
+ photoUrl = entries[i].author.uri.replace("http://www.flickr.com/people/", 'http://www.flickr.com/photos/');
+ photoUrl += id + '/';
+
+ imgObjs.push({
+ url: imgData.url,
+ width: imgData.width,
+ height: imgData.height,
+ link: photoUrl,
+ });
+ } catch (e) {
+ // Noop
+ }
+ }
+ callback(imgObjs);
+ });
+
+ xml.parseString(content);
+}
+
+var pushHandler = function(req, res) {
+ var urlParts = urlParser(req.url, true);
+ var content = '';
+ var me = this;
+
+ req.on('data', function(data) {
+ content += data;
+ });
+
+ req.on('end', function() {
+ var verifyToken = urlParts.query.verify_token;
+
+ if (urlParts.query.mode == 'unsubscribe') {
+ if (me.unsubscribeCallback(urlParts, verifyToken)) {
+ if (urlParts.query.challenge) {
+ res.write(urlParts.query.challenge);
+ }
+ }
+ } else if (urlParts.query.mode == 'subscribe') {
+ if (me.subscribeCallback(urlParts, verifyToken)) {
+ if (urlParts.query.challenge) {
+ res.write(urlParts.query.challenge);
+ }
+ }
+ } else {
+ // Parse what we've gotten
+ var eventName = me.getEventName(urlParts);
+ parseFlickrPost(content, function(imgObjs) {
+ for (var i in imgObjs) {
+ me.emitter.emit(eventName, imgObjs[i]);
+ }
+ });
+
+ }
+ res.end();
+ });
+}
+
+Conduit.prototype.on = function(ev, listener) {
+ return this.emitter.on(ev, listener);
+}
+
+Conduit.prototype.listen = function(port) {
+ var me = this;
+ var callback = function () {
+ return pushHandler.apply(me, arguments);
+ };
+
+ http.createServer(callback).listen(port);
+}
19 package.json
@@ -0,0 +1,19 @@
+{
+ "author": "Nolan Caudill <nolan@nolancaudill.com> (http://nolancaudill.com)",
+ "name": "flickr-conduit",
+ "description": "A subscriber endpoint for Flickr's real-time PuSH feed",
+ "version": "0.1.0",
+ "homepage": "https://github.com/mncaudill/flickr-conduit",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/mncaudill/flickr-conduit.git"
+ },
+ "main": "conduit.js",
+ "engines": {
+ "node": "~v0.4.9"
+ },
+ "dependencies": {
+ "xml2js": "0.1.9"
+ },
+ "devDependencies": {}
+}
31 sample.js
@@ -0,0 +1,31 @@
+var io = require('socket.io').listen(1340)
+ , Conduit = require('flickr-conduit').Conduit
+ , crypto = require('crypto')
+;
+
+var conduit = new Conduit();
+conduit.subscribeCallback = function(urlParts, verifyToken) {
+
+ var hmac = crypto.createHmac('sha1', 'FLICKRSECRET');
+ hmac.update(urlParts.query.sub);
+ var digest = hmac.digest('hex');
+
+ if (digest == verifyToken) {
+ console.log("Successful verify token for " + urlParts);
+ return true;
+ }
+
+ return false;
+}
+conduit.listen(1338);
+
+io.sockets.on('connection', function(socket) {
+ socket.on('subscribe', function(data) {
+ for (var i in data.events) {
+ conduit.on(data.events[i], function(img) {
+ socket.emit('publish', img);
+ });
+ }
+ });
+});
+
Please sign in to comment.
Something went wrong with that request. Please try again.