Repository: technoweenie/twitter-node Branch: master Commit: 39ab640327c5 Files: 7 Total size: 17.6 KB Directory structure: gitextract_bwrkckrb/ ├── .gitignore ├── LICENSE ├── README.md ├── lib/ │ └── twitter-node/ │ ├── index.js │ └── parser.js ├── package.json └── test/ └── twitter_node_config_test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *build/ *.lock-wscript *.node .DS_Store ================================================ FILE: LICENSE ================================================ Copyright (c) 2010 rick 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. ================================================ FILE: README.md ================================================ # twitter-node Creates a streaming connection with twitter, and pushes any incoming statuses to a tweet event. ## Installation Depends on ntest. Use NPM: npm install twitter-node Otherwise create a symlink in `~/.node_libraries` $ ln -s /path/to/twitter-node/lib/twitter-node ~/.node_libraries/twitter-node ## Events TwitterNode emits these events: * tweet(json) - This is emitted when a new tweet comes in. This will be a parsed JSON object. * limit(json) - This is emitted when a new limit command comes in. Currently, limit detection only works with parsed JSON objects. * delete(json) - This is emitted when a new delete command comes in. Currently, delete detection only works with parsed JSON objects. * end(response) - This is emitted when the http connection is closed. The HTTP response object is sent. See the [streaming API docs][api-docs] for examples of the limit and delete commands. [api-docs]: http://apiwiki.twitter.com/Streaming-API-Documentation ## Usage // twitter-node does not modify GLOBAL, that's so rude var TwitterNode = require('twitter-node').TwitterNode , util = require('util') // you can pass args to create() or set them on the TwitterNode instance var twit = new TwitterNode({ user: 'username', password: 'password', host: 'my_proxy.my_company.com', // proxy server name or ip addr port: 8080, // proxy port! track: ['baseball', 'football'], // sports! follow: [12345, 67890], // follow these random users locations: [-122.75, 36.8, -121.75, 37.8] // tweets in SF }); // adds to the track array set above twit.track('foosball'); // adds to the following array set above twit.follow(2345); // follow tweets from NYC twit.location(-74, 40, -73, 41) // http://apiwiki.twitter.com/Streaming-API-Documentation#QueryParameters twit.params['count'] = 100; // http://apiwiki.twitter.com/Streaming-API-Documentation#Methods twit.action = 'sample'; // 'filter' is default twit.headers['User-Agent'] = 'whatever'; // Make sure you listen for errors, otherwise // they are thrown twit.addListener('error', function(error) { console.log(error.message); }); twit .addListener('tweet', function(tweet) { util.puts("@" + tweet.user.screen_name + ": " + tweet.text); }) .addListener('limit', function(limit) { util.puts("LIMIT: " + util.inspect(limit)); }) .addListener('delete', function(del) { util.puts("DELETE: " + util.inspect(del)); }) .addListener('end', function(resp) { util.puts("wave goodbye... " + resp.statusCode); }) .stream(); // We can also add things to track on-the-fly twit.track('#nowplaying'); twit.follow(1234); // This will reset the stream twit.stream(); ## Pre-Launch Checklist See http://apiwiki.twitter.com/Streaming-API-Documentation. Keep these points in mind when getting ready to use TwitterNode in production: * Not purposefully attempting to circumvent access limits and levels? * Creating the minimal number of connections? * Avoiding duplicate logins? * Backing off from failures: none for first disconnect, seconds for repeated network (TCP/IP) level issues, minutes for repeated HTTP (4XX codes)? * Using long-lived connections? * Tolerant of other objects and newlines in markup stream? (Non objects...) * Tolerant of duplicate messages? ## TODO * Handle failures as recommended from the Twitter stream documentation. ## \m/ * Tim Smart * Matt Secoske (secos) * kompozer * Twitter ## Note on Patches/Pull Requests * Fork the project. * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with version or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) * Send me a pull request. Bonus points for topic branches. ## Copyright Copyright (c) 2010 rick. See LICENSE for details. ================================================ FILE: lib/twitter-node/index.js ================================================ var https = require('https'), query = require('querystring'), Parser = require('./parser'), EventEmitter = require('events').EventEmitter, Buffer = require('buffer').Buffer; // process.mixin is gone, a function for replacement function extend(a, b) { Object.keys(b).forEach(function (key) { a[key] = b[key]; }); return a; } // Creates a streaming connection with twitter, and pushes any incoming // statuses to a tweet event. // // options - optional Object that specifies custom configuration values. // // Valid option keys: // // port - Integer of proxy port // host - String or ip address of the proxy server. Defaults to 'stream.twitter.com'. // path - String of the base path for the request. // action - String part of the URL that specifies what to query for. // track - Array of keywords to filter. See track() // following - Array of userIDs to filter. See follow() // locations - Array of lat/long tuples. See location() // params - Extra HTTP params Object to send with the request. // user - String Twitter login name or email. // password - String Twitter password. // // Returns TwitterNode instance. var TwitterNode = exports.TwitterNode = function(options) { EventEmitter.call(this); if(!options) options = {}; var self = this; this.port = options.port || 443; this.host = options.host || 'stream.twitter.com'; this.path = options.path || '/1/statuses/'; this.action = options.action || 'filter'; this.trackKeywords = options.track || []; this.following = options.follow || []; this.locations = options.locations || []; this.params = options.params || {}; this.user = options.user; this.password = options.password; this.headers = { "User-Agent": 'Twitter-Node' }; this.debug = false; this.parser = new Parser(); this.parser.addListener('object', processJSONObject(this)); this.parser.addListener('error', function (error) { self.emit('error', new Error('TwitterNode parser error: ' + error.message)); }); if (options.headers) { extend(this.headers, options.headers); } } TwitterNode.prototype = Object.create(EventEmitter.prototype); // Track the following keyword. If called multiple times, all words are sent // as a comma-separated parameter to Twitter. // // See: http://apiwiki.twitter.com/Streaming-API-Documentation#track // // word - String word to track. // // Returns nothing. TwitterNode.prototype.track = function track(word) { this.trackKeywords.push(word); return this; }; // Follow the given twitter user (specified by their userID, not screen name) // If called multiple times, all userIDs are sent as a comma-separated // parameter to Twitter. // // See: http://apiwiki.twitter.com/Streaming-API-Documentation#follow // // userID - Integer userID to track. // // Returns nothing. TwitterNode.prototype.follow = function follow(userId) { this.following.push(userId); return this; }; // Match tweets in the given bounding box. // // See: http://apiwiki.twitter.com/Streaming-API-Documentation#locations // // Example: location(-122.75, 36.8, -121.75, 37.8) // SF // // lng1, lat1 - southwest corner of the bounding box. // lng2, lat2 - northeast corner. // // Returns nothing. TwitterNode.prototype.location = function location(lng1, lat1, lng2, lat2) { this.locations.push(lng1, lat1, lng2, lat2) return this; }; TwitterNode.prototype.stream = function stream() { if (this._clientResponse && this._clientResponse.connection) { this._clientResponse.socket.end(); } if (this.action === 'filter' && this.buildParams() === '') return; var headers = extend({}, this.headers), twit = this, request; headers['Host'] = this.host; if (this.user) { headers['Authorization'] = basicAuth(this.user, this.password); } postdata = this.buildParams(); headers['Content-Type'] = 'application/x-www-form-urlencoded'; headers['Content-Length'] = postdata.length; var requestOptions = { host: this.host, port: this.port, path: this.requestUrl(), method: 'POST', headers: headers }; request = https.request(requestOptions, function(response) { twit._clientResponse = response; response.on('close', function(err){ // If underlaying connection was terminated reconnect to the stream twit.stream(); }); response.on('data', function(chunk){ twit._receive(chunk); }); response.on('end', function() { twit.emit('end', this); }); response.on('close', function() { twit.emit('close', this); }); }); request.write(postdata); request.end(); return this; }; // UTILITY METHODS // Passes the received data to the streaming JSON parser. // // chunk - String data received from the HTTP stream. // // Returns nothing. TwitterNode.prototype._receive = function(chunk) { this.parser.receive(chunk); return this; }; // Builds the URL for the streaming request. // // Returns a String absolute URL. TwitterNode.prototype.requestUrl = function() { return this.path + this.action + ".json"; }; // Builds the GET params for the streaming request. // // Returns URI encoded string: "?track=LOST" TwitterNode.prototype.buildParams = function() { var options = {}; extend(options, this.params); if (this.trackKeywords.length > 0) options.track = this.trackKeywords.join(","); if (this.following.length > 0) options.follow = this.following.join(","); if (this.locations.length > 0) options.locations = this.locations.join(","); if (options.track || options.follow || options.locations) { return query.stringify(options); } return ""; }; // Base64 encodes the given username and password. // // user - String Twitter screen name or email. // pass - String password. // // Returns a Basic Auth header fit for HTTP. var basicAuth = function basicAuth(user, pass) { return "Basic " + new Buffer(user + ":" + pass).toString('base64'); }; // Creates a callback for the object Event of the JSON Parser. // // twit - an instance of this TwitterNode. // // Returns a function to be passed to the addListener call on the parser. var processJSONObject = function processJSONObject(twit) { return function(tweet) { if (tweet.limit) { twit.emit('limit', tweet.limit); } else if (tweet['delete']) { twit.emit('delete', tweet['delete']); } else { twit.emit('tweet', tweet); } }; }; ================================================ FILE: lib/twitter-node/parser.js ================================================ // glorious streaming json parser, built specifically for the twitter streaming api // assumptions: // 1) ninjas are mammals // 2) tweets come in chunks of text, surrounded by {}'s, separated by line breaks // 3) only one tweet per chunk // // p = new parser.instance() // p.addListener('object', function...) // p.receive(data) // p.receive(data) // ... var EventEmitter = require('events').EventEmitter; var Parser = module.exports = function Parser() { // Make sure we call our parents constructor EventEmitter.call(this); this.buffer = ''; return this; }; // The parser emits events! Parser.prototype = Object.create(EventEmitter.prototype); Parser.END = '\r\n'; Parser.END_LENGTH = 2; Parser.prototype.receive = function receive(buffer) { this.buffer += buffer.toString('utf8'); var index, json; // We have END? while ((index = this.buffer.indexOf(Parser.END)) > -1) { json = this.buffer.slice(0, index); this.buffer = this.buffer.slice(index + Parser.END_LENGTH); if (json.length > 0) { try { json = JSON.parse(json); this.emit('object', json); } catch (error) { this.emit('error', error); } } } }; ================================================ FILE: package.json ================================================ { "name": "twitter-node", "description": "node.js stream API for the twitter streaming HTTP API", "version": "0.1.1", "author": {"name": "technoweenie" }, "contributors": [ { "name": "tim-smart" }, { "name": "Chris Verwymeren", "email": "verwymeren@gmail.com", "web": "http://www.github.com/cvee" }, { "name": "Makis Tracend", "email": "makis.tracend@gmail.com", "web": "http://www.github.com/tracend" }, { "name": "Dustin Dobervich", "email": "ddobervich@gmail.com", "web": "http://www.github.com/dustin10" }, { "name": "Chris M. Welsh", "email": "chris@cmwelsh.com", "web": "http://www.github.com/cmwelsh" }, { "name": "Michael Nutt", "email": "michael@nuttnet.net", "web": "http://www.github.com/mnutt" } ], "repository": { "type": "git", "url": "http://github.com/istrategylabs/twitter-node.git" }, "engine": [ "node >=0.2.0" ], "main": "./lib/twitter-node" } ================================================ FILE: test/twitter_node_config_test.js ================================================ var TwitterNode = require('../lib/twitter-node').TwitterNode, assert = require('assert'), util = require('util'); process.mixin(GLOBAL, require('ntest')); describe("streaming json parser") it("accepts JSON in chunks", function() { var parser = require("../lib/twitter-node/parser"), p = new parser.instance(), result p.addListener('object', function(tweet) { result = tweet }) p.receive("") p.receive(" ") p.receive("{") p.receive('"a":{') p.receive('"b":1') p.receive("}\n}\n{\"a\":1}") assert.ok(result) assert.equal(1, result.a.b) }) describe("json TwitterNode instance") before(function() { this.twit = new TwitterNode(); }) it("emits tweet with parsed JSON tweet", function() { var result; this.twit .addListener('tweet', function(tweet) { result = tweet }) .addListener('limit', function(tweet) { result = {a:null} }) .addListener('delete', function(tweet) { result = {a:null} }) .receive('{"a":1}') assert.equal(1, result.a) }) it("emits delete with parsed JSON delete command", function() { var result; this.twit .addListener('tweet', function(tweet) { result = {status:null} }) .addListener('limit', function(tweet) { result = {status:null} }) .addListener('delete', function(tweet) { result = tweet }) .receive('{"delete":{"status":{"id": 1234}}}') assert.equal(1234, result.status.id) }) it("emits limit with parsed JSON limit command", function() { var result; this.twit .addListener('tweet', function(tweet) { result = {track:null} }) .addListener('delete', function(tweet) { result = {track:null} }) .addListener('limit', function(tweet) { result = tweet }) .receive('{"limit":{"track": 1234}}') assert.equal(1234, result.track) }) describe("default TwitterNode instance") before(function() { this.twit = new TwitterNode(); }) it("has default requestUrl()", function() { assert.equal("/1/statuses/filter.json", this.twit.requestUrl()) }) it("has empty params", function() { assert.equal('', this.twit.buildParams()) }) it("has default port", function() { assert.equal(80, this.twit.port) }) it("has default host", function() { assert.equal('stream.twitter.com', this.twit.host) }) it("adds tracking keywords", function() { this.twit.track('abc+def') this.twit.track('ghi') assert.equal('?track=abc%2Bdef%2Cghi', this.twit.buildParams()) }) it("adds following users", function() { this.twit.follow(123) this.twit.follow(456) assert.equal('?follow=123%2C456', this.twit.buildParams()) }) it("adds locations", function() { this.twit.location(122.75, 36.8, -121.75, 37.8) // SF this.twit.location(-74, 40, -73, 41) // NYC assert.equal('?locations=122.75%2C36.8%2C-121.75%2C37.8%2C-74%2C40%2C-73%2C41', this.twit.buildParams()) }) describe("custom TwitterNode instance") before(function() { this.options = { port: 81, host: '10.0.0.1', path: 'abc/', action: 'retweet', follow: [123,456], track: ['abc', 'def'], headers: {'a': 'abc'}, params: {count: 5} } this.twit = new TwitterNode(this.options); }) it("has default requestUrl()", function() { assert.equal("abc/retweet.json" + this.twit.buildParams(), this.twit.requestUrl()) }) it("merges given headers with defaults", function() { assert.equal('abc', this.twit.headers.a) assert.ok(this.twit.headers['User-Agent']) }) it("has empty params", function() { assert.equal('?count=5&track=abc%2Cdef&follow=123%2C456', this.twit.buildParams()) }) it("sets port", function() { assert.equal(this.options.port, this.twit.port) }) it("sets host", function() { assert.equal(this.options.host, this.twit.host) })