├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── parser.js └── twitter.js ├── package.json └── test ├── memory.js └── memory.txt /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .*.swp 3 | s*.js 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .*.swp 3 | s*.js 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | node-twitter: Copyright (c) 2010 Jeff Waugh 2 | parser.js: Copyright (c) 2010 rick 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Twitter API client library for node.js 2 | ====================================== 3 | 4 | [node-twitter](https://github.com/jdub/node-twitter) aims to provide a complete, asynchronous client library for the Twitter API, including the REST, search and streaming endpoints. It was inspired by, and uses some code from, [@technoweenie](https://github.com/technoweenie)'s [twitter-node](https://github.com/technoweenie/twitter-node). 5 | 6 | ## Requirements 7 | 8 | You can install node-twitter and its dependencies with npm: `npm install twitter`. 9 | 10 | - [node](http://nodejs.org/) v0.6+ 11 | - [node-oauth](https://github.com/ciaranj/node-oauth) 12 | - [cookies](https://github.com/jed/cookies) 13 | 14 | ## Getting started 15 | 16 | It's early days for node-twitter, so I'm going to assume a fair amount of knowledge for the moment. Better documentation to come as we head towards a stable release. 17 | 18 | ### Setup API (stable) 19 | 20 | var util = require('util'), 21 | twitter = require('twitter'); 22 | var twit = new twitter({ 23 | consumer_key: 'STATE YOUR NAME', 24 | consumer_secret: 'STATE YOUR NAME', 25 | access_token_key: 'STATE YOUR NAME', 26 | access_token_secret: 'STATE YOUR NAME' 27 | }); 28 | 29 | ### Basic OAuth-enticated GET/POST API (stable) 30 | 31 | The convenience APIs aren't finished, but you can get started with the basics: 32 | 33 | twit.get('/statuses/show/27593302936.json', {include_entities:true}, function(data) { 34 | console.log(util.inspect(data)); 35 | }); 36 | 37 | ### REST API (unstable, may change) 38 | 39 | Note that all functions may be chained: 40 | 41 | twit 42 | .verifyCredentials(function(data) { 43 | console.log(util.inspect(data)); 44 | }) 45 | .updateStatus('Test tweet from node-twitter/' + twitter.VERSION, 46 | function(data) { 47 | console.log(util.inspect(data)); 48 | } 49 | ); 50 | 51 | ### Search API (unstable, may change) 52 | 53 | twit.search('nodejs OR #node', function(data) { 54 | console.log(util.inspect(data)); 55 | }); 56 | 57 | ### Streaming API (stable) 58 | 59 | The stream() callback receives a Stream-like EventEmitter: 60 | 61 | twit.stream('statuses/sample', function(stream) { 62 | stream.on('data', function(data) { 63 | console.log(util.inspect(data)); 64 | }); 65 | }); 66 | 67 | node-twitter also supports user and site streams: 68 | 69 | twit.stream('user', {track:'nodejs'}, function(stream) { 70 | stream.on('data', function(data) { 71 | console.log(util.inspect(data)); 72 | }); 73 | // Disconnect stream after five seconds 74 | setTimeout(stream.destroy, 5000); 75 | }); 76 | 77 | ## Contributors 78 | 79 | - [Jeff Waugh](https://github.com/jdub) (author) 80 | - [@technoweenie](https://github.com/technoweenie) (parser.js and, of course, twitter-node!) 81 | - Lots of [wonderful helper elves](https://github.com/jdub/node-twitter/contributors) on GitHub 82 | 83 | ## TODO 84 | 85 | - Complete the convenience functions, preferably generated 86 | - Fix ALL THE THINGS! on the GitHub [issues list](https://github.com/jdub/node-twitter/issues) 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/twitter'); 2 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | // glorious streaming json parser, built specifically for the twitter streaming api 2 | // assumptions: 3 | // 1) ninjas are mammals 4 | // 2) tweets come in chunks of text, surrounded by {}'s, separated by line breaks 5 | // 3) only one tweet per chunk 6 | // 7 | // p = new parser.instance() 8 | // p.addListener('object', function...) 9 | // p.receive(data) 10 | // p.receive(data) 11 | // ... 12 | 13 | var EventEmitter = require('events').EventEmitter; 14 | 15 | var Parser = module.exports = function Parser() { 16 | // Make sure we call our parents constructor 17 | EventEmitter.call(this); 18 | this.buffer = ''; 19 | return this; 20 | }; 21 | 22 | // The parser emits events! 23 | Parser.prototype = Object.create(EventEmitter.prototype); 24 | 25 | Parser.END = '\r\n'; 26 | Parser.END_LENGTH = 2; 27 | 28 | Parser.prototype.receive = function receive(buffer) { 29 | this.buffer += buffer.toString('utf8'); 30 | var index, json; 31 | 32 | // We have END? 33 | while ((index = this.buffer.indexOf(Parser.END)) > -1) { 34 | json = this.buffer.slice(0, index); 35 | this.buffer = this.buffer.slice(index + Parser.END_LENGTH); 36 | if (json.length > 0) { 37 | try { 38 | json = JSON.parse(json); 39 | this.emit('data', json); 40 | } catch (error) { 41 | this.emit('error', error); 42 | } 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /lib/twitter.js: -------------------------------------------------------------------------------- 1 | var VERSION = '0.1.18', 2 | http = require('http'), 3 | querystring = require('querystring'), 4 | oauth = require('oauth'), 5 | Cookies = require('cookies'), 6 | Keygrip = require('keygrip'), 7 | streamparser = require('./parser'); 8 | 9 | function merge(defaults, options) { 10 | defaults = defaults || {}; 11 | if (options && typeof options === 'object') { 12 | var keys = Object.keys(options); 13 | for (var i = 0, len = keys.length; i < len; i++) { 14 | var k = keys[i]; 15 | if (options[k] !== undefined) defaults[k] = options[k]; 16 | } 17 | } 18 | return defaults; 19 | } 20 | 21 | 22 | function Twitter(options) { 23 | if (!(this instanceof Twitter)) return new Twitter(options); 24 | 25 | var defaults = { 26 | consumer_key: null, 27 | consumer_secret: null, 28 | access_token_key: null, 29 | access_token_secret: null, 30 | 31 | headers: { 32 | 'Accept': '*/*', 33 | 'Connection': 'close', 34 | 'User-Agent': 'node-twitter/' + VERSION 35 | }, 36 | 37 | request_token_url: 'https://api.twitter.com/oauth/request_token', 38 | access_token_url: 'https://api.twitter.com/oauth/access_token', 39 | authenticate_url: 'https://api.twitter.com/oauth/authenticate', 40 | authorize_url: 'https://api.twitter.com/oauth/authorize', 41 | callback_url: null, 42 | 43 | rest_base: 'https://api.twitter.com/1', 44 | search_base: 'https://search.twitter.com', 45 | stream_base: 'https://stream.twitter.com/1', 46 | user_stream_base: 'https://userstream.twitter.com/2', 47 | site_stream_base: 'https://sitestream.twitter.com/2b', 48 | 49 | secure: false, // force use of https for login/gatekeeper 50 | cookie: 'twauth', 51 | cookie_options: {}, 52 | cookie_secret: null 53 | }; 54 | this.options = merge(defaults, options); 55 | 56 | this.keygrip = this.options.cookie_secret === null ? null : 57 | new Keygrip([this.options.cookie_secret]); 58 | 59 | this.oauth = new oauth.OAuth( 60 | this.options.request_token_url, 61 | this.options.access_token_url, 62 | this.options.consumer_key, 63 | this.options.consumer_secret, 64 | '1.0A', 65 | this.options.callback_url, 66 | 'HMAC-SHA1', null, 67 | this.options.headers); 68 | } 69 | Twitter.VERSION = VERSION; 70 | module.exports = Twitter; 71 | 72 | 73 | /* 74 | * GET 75 | */ 76 | Twitter.prototype.get = function(url, params, callback) { 77 | if (typeof params === 'function') { 78 | callback = params; 79 | params = null; 80 | } 81 | 82 | if ( typeof callback !== 'function' ) { 83 | throw "FAIL: INVALID CALLBACK."; 84 | return this; 85 | } 86 | 87 | if (url.charAt(0) == '/') 88 | url = this.options.rest_base + url; 89 | 90 | this.oauth.get(url + '?' + querystring.stringify(params), 91 | this.options.access_token_key, 92 | this.options.access_token_secret, 93 | function(error, data, response) { 94 | if (error) { 95 | var err = new Error('HTTP Error ' 96 | + error.statusCode + ': ' 97 | + http.STATUS_CODES[error.statusCode]); 98 | err.statusCode = error.statusCode; 99 | err.data = error.data; 100 | callback(err); 101 | } else { 102 | try { 103 | var json = JSON.parse(data); 104 | callback(json); 105 | } catch(err) { 106 | callback(err); 107 | } 108 | } 109 | }); 110 | return this; 111 | } 112 | 113 | 114 | /* 115 | * POST 116 | */ 117 | Twitter.prototype.post = function(url, content, content_type, callback) { 118 | if (typeof content === 'function') { 119 | callback = content; 120 | content = null; 121 | content_type = null; 122 | } else if (typeof content_type === 'function') { 123 | callback = content_type; 124 | content_type = null; 125 | } 126 | 127 | if ( typeof callback !== 'function' ) { 128 | throw "FAIL: INVALID CALLBACK."; 129 | return this; 130 | } 131 | 132 | if (url.charAt(0) == '/') 133 | url = this.options.rest_base + url; 134 | 135 | // Workaround: oauth + booleans == broken signatures 136 | if (content && typeof content === 'object') { 137 | Object.keys(content).forEach(function(e) { 138 | if ( typeof content[e] === 'boolean' ) 139 | content[e] = content[e].toString(); 140 | }); 141 | } 142 | 143 | this.oauth.post(url, 144 | this.options.access_token_key, 145 | this.options.access_token_secret, 146 | content, content_type, 147 | function(error, data, response) { 148 | if (error) { 149 | var err = new Error('HTTP Error ' 150 | + error.statusCode + ': ' 151 | + http.STATUS_CODES[error.statusCode]); 152 | err.statusCode = error.statusCode; 153 | err.data = error.data; 154 | callback(err); 155 | } else { 156 | try { 157 | var json = JSON.parse(data); 158 | callback(json); 159 | } catch(err) { 160 | callback(err); 161 | } 162 | } 163 | }); 164 | return this; 165 | } 166 | 167 | 168 | /* 169 | * SEARCH (not API stable!) 170 | */ 171 | Twitter.prototype.search = function(q, params, callback) { 172 | if (typeof params === 'function') { 173 | callback = params; 174 | params = null; 175 | } 176 | 177 | if ( typeof callback !== 'function' ) { 178 | throw "FAIL: INVALID CALLBACK."; 179 | return this; 180 | } 181 | 182 | var url = this.options.search_base + '/search.json'; 183 | params = merge(params, {q:q}); 184 | this.get(url, params, callback); 185 | return this; 186 | } 187 | 188 | 189 | /* 190 | * STREAM 191 | */ 192 | Twitter.prototype.stream = function(method, params, callback) { 193 | if (typeof params === 'function') { 194 | callback = params; 195 | params = null; 196 | } 197 | 198 | var stream_base = this.options.stream_base; 199 | 200 | // Stream type customisations 201 | if (method === 'user') { 202 | stream_base = this.options.user_stream_base; 203 | // Workaround for node-oauth vs. twitter commas-in-params bug 204 | if ( params && params.track && Array.isArray(params.track) ) { 205 | params.track = params.track.join(',') 206 | } 207 | 208 | } else if (method === 'site') { 209 | stream_base = this.options.site_stream_base; 210 | // Workaround for node-oauth vs. twitter double-encode-commas bug 211 | if ( params && params.follow && Array.isArray(params.follow) ) { 212 | params.follow = params.follow.join(',') 213 | } 214 | } 215 | 216 | 217 | var url = stream_base + '/' + escape(method) + '.json'; 218 | 219 | var request = this.oauth.post(url, 220 | this.options.access_token_key, 221 | this.options.access_token_secret, 222 | params); 223 | 224 | var stream = new streamparser(); 225 | stream.destroy = function() { 226 | // FIXME: should we emit end/close on explicit destroy? 227 | if ( typeof request.abort === 'function' ) 228 | request.abort(); // node v0.4.0 229 | else 230 | request.socket.destroy(); 231 | }; 232 | 233 | request.on('response', function(response) { 234 | // FIXME: Somehow provide chunks of the response when the stream is connected 235 | // Pass HTTP response data to the parser, which raises events on the stream 236 | response.on('data', function(chunk) { 237 | stream.receive(chunk); 238 | }); 239 | response.on('error', function(error) { 240 | stream.emit('error', error); 241 | }); 242 | response.on('end', function() { 243 | stream.emit('end', response); 244 | }); 245 | }); 246 | request.on('error', function(error) { 247 | stream.emit('error', error); 248 | }); 249 | request.end(); 250 | 251 | if ( typeof callback === 'function' ) callback(stream); 252 | return this; 253 | } 254 | 255 | 256 | /* 257 | * TWITTER "O"AUTHENTICATION UTILITIES, INCLUDING THE GREAT 258 | * CONNECT/STACK STYLE TWITTER "O"AUTHENTICATION MIDDLEWARE 259 | * and helpful utilities to retrieve the twauth cookie etc. 260 | */ 261 | Twitter.prototype.cookie = function(req) { 262 | // Fetch the cookie 263 | var cookies = new Cookies(req, null, this.keygrip); 264 | return this._readCookie(cookies); 265 | } 266 | 267 | Twitter.prototype.login = function(mount, success) { 268 | var self = this, 269 | url = require('url'); 270 | 271 | // Save the mount point for use in gatekeeper 272 | this.options.login_mount = mount = mount || '/twauth'; 273 | 274 | // Use secure cookie if forced to https and haven't configured otherwise 275 | if ( this.options.secure && !this.options.cookie_options.secure ) 276 | this.options.cookie_options.secure = true; 277 | 278 | return function handle(req, res, next) { 279 | var path = url.parse(req.url, true); 280 | 281 | // We only care about requests against the exact mount point 282 | if ( path.pathname !== mount ) return next(); 283 | 284 | // Set the oauth_callback based on this request if we don't have it 285 | if ( !self.oauth._authorize_callback ) { 286 | // have to get the entire url because this is an external callback 287 | // but it's only done once... 288 | var scheme = (req.socket.secure || self.options.secure) ? 'https://' : 'http://', 289 | path = url.parse(scheme + req.headers.host + req.url, true); 290 | self.oauth._authorize_callback = path.href; 291 | } 292 | 293 | // Fetch the cookie 294 | var cookies = new Cookies(req, res, self.keygrip); 295 | var twauth = self._readCookie(cookies); 296 | 297 | // We have a winner, but they're in the wrong place 298 | if ( twauth && twauth.user_id && twauth.access_token_secret ) { 299 | res.writeHead(302, {'Location': success || '/'}); 300 | res.end(); 301 | return; 302 | 303 | // Returning from Twitter with oauth_token 304 | } else if ( path.query && path.query.oauth_token && path.query.oauth_verifier && twauth && twauth.oauth_token_secret ) { 305 | self.oauth.getOAuthAccessToken( 306 | path.query.oauth_token, 307 | twauth.oauth_token_secret, 308 | path.query.oauth_verifier, 309 | function(error, access_token_key, access_token_secret, params) { 310 | // FIXME: if we didn't get these, explode 311 | var user_id = (params && params.user_id) || null, 312 | screen_name = (params && params.screen_name) || null; 313 | 314 | if ( error ) { 315 | // FIXME: do something more intelligent 316 | return next(500); 317 | } else { 318 | cookies.set(self.options.cookie, JSON.stringify({ 319 | user_id: user_id, 320 | screen_name: screen_name, 321 | access_token_key: access_token_key, 322 | access_token_secret: access_token_secret 323 | }), self.options.cookie_options); 324 | res.writeHead(302, {'Location': success || '/'}); 325 | res.end(); 326 | return; 327 | } 328 | }); 329 | 330 | // Begin OAuth transaction if we have no cookie or access_token_secret 331 | } else if ( !(twauth && twauth.access_token_secret) ) { 332 | self.oauth.getOAuthRequestToken( 333 | function(error, oauth_token, oauth_token_secret, oauth_authorize_url, params) { 334 | if ( error ) { 335 | // FIXME: do something more intelligent 336 | return next(500); 337 | } else { 338 | cookies.set(self.options.cookie, JSON.stringify({ 339 | oauth_token: oauth_token, 340 | oauth_token_secret: oauth_token_secret 341 | }), self.options.cookie_options); 342 | res.writeHead(302, { 343 | 'Location': self.options.authorize_url + '?' 344 | + querystring.stringify({oauth_token: oauth_token}) 345 | }); 346 | res.end(); 347 | return; 348 | } 349 | }); 350 | 351 | // Broken cookie, clear it and return to originating page 352 | // FIXME: this is dumb 353 | } else { 354 | cookies.set(self.options.cookie, null, self.options.cookie_options); 355 | res.writeHead(302, {'Location': mount}); 356 | res.end(); 357 | return; 358 | } 359 | }; 360 | } 361 | 362 | Twitter.prototype.gatekeeper = function(failure) { 363 | var self = this, 364 | mount = this.options.login_mount || '/twauth'; 365 | 366 | return function(req, res, next) { 367 | var twauth = self.cookie(req); 368 | 369 | // We have a winner 370 | if ( twauth && twauth.user_id && twauth.access_token_secret ) 371 | return next(); 372 | 373 | // I pity the fool! 374 | // FIXME: use 'failure' param to fail with: a) 401, b) redirect 375 | // possibly using configured login mount point 376 | // perhaps login can save the mount point, then we can use it? 377 | res.writeHead(401, {}); // {} for bug in stack 378 | res.end([ 379 | '', 380 | '', 381 | '', 382 | '

Twitter authentication required.

', 383 | '' 384 | ].join('')); 385 | }; 386 | } 387 | 388 | 389 | /* 390 | * CONVENIENCE FUNCTIONS (not API stable!) 391 | */ 392 | 393 | // Timeline resources 394 | 395 | Twitter.prototype.getHomeTimeline = function(params, callback) { 396 | var url = '/statuses/home_timeline.json'; 397 | this.get(url, params, callback); 398 | return this; 399 | } 400 | 401 | Twitter.prototype.getMentions = function(params, callback) { 402 | var url = '/statuses/mentions.json'; 403 | this.get(url, params, callback); 404 | return this; 405 | } 406 | 407 | Twitter.prototype.getRetweetedByMe = function(params, callback) { 408 | var url = '/statuses/retweeted_by_me.json'; 409 | this.get(url, params, callback); 410 | return this; 411 | } 412 | 413 | Twitter.prototype.getRetweetedToMe = function(params, callback) { 414 | var url = '/statuses/retweeted_to_me.json'; 415 | this.get(url, params, callback); 416 | return this; 417 | } 418 | 419 | Twitter.prototype.getRetweetsOfMe = function(params, callback) { 420 | var url = '/statuses/retweets_of_me.json'; 421 | this.get(url, params, callback); 422 | return this; 423 | } 424 | 425 | Twitter.prototype.getUserTimeline = function(params, callback) { 426 | var url = '/statuses/user_timeline.json'; 427 | this.get(url, params, callback); 428 | return this; 429 | } 430 | 431 | Twitter.prototype.getRetweetedToUser = function(params, callback) { 432 | var url = '/statuses/retweeted_to_user.json'; 433 | this.get(url, params, callback); 434 | return this; 435 | } 436 | 437 | Twitter.prototype.getRetweetedByUser = function(params, callback) { 438 | var url = '/statuses/retweeted_by_user.json'; 439 | this.get(url, params, callback); 440 | return this; 441 | } 442 | 443 | // Tweets resources 444 | 445 | Twitter.prototype.showStatus = function(id, callback) { 446 | var url = '/statuses/show/' + escape(id) + '.json'; 447 | this.get(url, null, callback); 448 | return this; 449 | } 450 | Twitter.prototype.getStatus 451 | = Twitter.prototype.showStatus; 452 | 453 | Twitter.prototype.updateStatus = function(text, params, callback) { 454 | if (typeof params === 'function') { 455 | callback = params; 456 | params = null; 457 | } 458 | 459 | var url = '/statuses/update.json'; 460 | var defaults = { 461 | status: text, 462 | include_entities: 1 463 | }; 464 | params = merge(defaults, params); 465 | this.post(url, params, null, callback); 466 | return this; 467 | } 468 | 469 | Twitter.prototype.destroyStatus = function(id, callback) { 470 | var url = '/statuses/destroy/' + escape(id) + '.json'; 471 | this.post(url, null, null, callback); 472 | return this; 473 | } 474 | Twitter.prototype.deleteStatus 475 | = Twitter.prototype.destroyStatus; 476 | 477 | Twitter.prototype.retweetStatus = function(id, callback) { 478 | var url = '/statuses/retweet/' + escape(id) + '.json'; 479 | this.post(url, null, null, callback); 480 | return this; 481 | } 482 | 483 | Twitter.prototype.getRetweets = function(id, params, callback) { 484 | var url = '/statuses/retweets/' + escape(id) + '.json'; 485 | this.get(url, params, callback); 486 | return this; 487 | } 488 | 489 | Twitter.prototype.getRetweetedBy = function(id, params, callback) { 490 | var url = '/statuses/' + escape(id) + '/retweeted_by.json'; 491 | this.post(url, params, null, callback); 492 | return this; 493 | } 494 | 495 | Twitter.prototype.getRetweetedByIds = function(id, params, callback) { 496 | var url = '/statuses/' + escape(id) + '/retweeted_by/ids.json'; 497 | this.post(url, params, null, callback); 498 | return this; 499 | } 500 | 501 | // User resources 502 | 503 | Twitter.prototype.showUser = function(id, callback) { 504 | // FIXME: handle id-array and id-with-commas as lookupUser 505 | // NOTE: params with commas b0rk between node-oauth and twitter 506 | // https://github.com/ciaranj/node-oauth/issues/7 507 | var url = '/users/show.json'; 508 | 509 | var params = {}; 510 | if (typeof id === 'string') 511 | params.screen_name = id; 512 | else 513 | params.user_id = id; 514 | 515 | this.get(url, params, callback); 516 | return this; 517 | } 518 | Twitter.prototype.lookupUser 519 | = Twitter.prototype.lookupUsers 520 | = Twitter.prototype.showUser; 521 | 522 | Twitter.prototype.searchUser = function(q, params, callback) { 523 | if (typeof params === 'function') { 524 | callback = params; 525 | params = null; 526 | } 527 | 528 | var url = '/users/search.json'; 529 | params = merge(params, {q:q}); 530 | this.get(url, params, callback); 531 | return this; 532 | } 533 | Twitter.prototype.searchUsers 534 | = Twitter.prototype.searchUser; 535 | 536 | // FIXME: users/suggestions** 537 | 538 | Twitter.prototype.userProfileImage = function(id, params, callback) { 539 | if (typeof params === 'function') { 540 | callback = params; 541 | params = null; 542 | } else if (typeof params === 'string') { 543 | params = { size: params }; 544 | } 545 | 546 | var url = '/users/profile_image/' + escape(id) + '.json?' + querystring.stringify(params); 547 | 548 | // Do our own request, so we can return the 302 location header 549 | var request = this.oauth.get(this.options.rest_base + url, 550 | this.options.access_token_key, 551 | this.options.access_token_secret); 552 | request.on('response', function(response) { 553 | // return the location or an HTTP error 554 | callback(response.headers.location || new Error('HTTP Error ' 555 | + response.statusCode + ': ' 556 | + http.STATUS_CODES[response.statusCode])); 557 | }); 558 | request.end(); 559 | 560 | return this; 561 | } 562 | 563 | // FIXME: statuses/friends, statuses/followers 564 | 565 | // Trends resources 566 | 567 | Twitter.prototype.getTrends = function(callback) { 568 | var url = '/trends.json'; 569 | this.get(url, null, callback); 570 | return this; 571 | } 572 | 573 | Twitter.prototype.getCurrentTrends = function(params, callback) { 574 | var url = '/trends/current.json'; 575 | this.get(url, params, callback); 576 | return this; 577 | } 578 | 579 | Twitter.prototype.getDailyTrends = function(params, callback) { 580 | var url = '/trends/daily.json'; 581 | this.get(url, params, callback); 582 | return this; 583 | } 584 | 585 | Twitter.prototype.getWeeklyTrends = function(params, callback) { 586 | var url = '/trends/weekly.json'; 587 | this.get(url, params, callback); 588 | return this; 589 | } 590 | 591 | // Local Trends resources 592 | 593 | // List resources 594 | 595 | Twitter.prototype.getLists = function(id, params, callback) { 596 | if (typeof params === 'function') { 597 | callback = params; 598 | params = null; 599 | } 600 | 601 | var defaults = {key:'lists'}; 602 | if (typeof id === 'string') 603 | defaults.screen_name = id; 604 | else 605 | defaults.user_id = id; 606 | params = merge(defaults, params); 607 | 608 | var url = '/lists.json'; 609 | this._getUsingCursor(url, params, callback); 610 | return this; 611 | } 612 | 613 | Twitter.prototype.getListMemberships = function(id, params, callback) { 614 | if (typeof params === 'function') { 615 | callback = params; 616 | params = null; 617 | } 618 | 619 | var defaults = {key:'lists'}; 620 | if (typeof id === 'string') 621 | defaults.screen_name = id; 622 | else 623 | defaults.user_id = id; 624 | params = merge(defaults, params); 625 | 626 | var url = '/lists/memberships.json'; 627 | this._getUsingCursor(url, params, callback); 628 | return this; 629 | } 630 | 631 | Twitter.prototype.getListSubscriptions = function(id, params, callback) { 632 | if (typeof params === 'function') { 633 | callback = params; 634 | params = null; 635 | } 636 | 637 | var defaults = {key:'lists'}; 638 | if (typeof id === 'string') 639 | defaults.screen_name = id; 640 | else 641 | defaults.user_id = id; 642 | params = merge(defaults, params); 643 | 644 | var url = '/lists/subscriptions.json'; 645 | this._getUsingCursor(url, params, callback); 646 | return this; 647 | } 648 | 649 | // FIXME: Uses deprecated Twitter lists API 650 | Twitter.prototype.showList = function(screen_name, list_id, callback) { 651 | var url = '/' + escape(screen_name) + '/lists/' + escape(list_id) + '.json'; 652 | this.get(url, null, callback); 653 | return this; 654 | } 655 | 656 | // FIXME: Uses deprecated Twitter lists API 657 | Twitter.prototype.getListTimeline = function(screen_name, list_id, params, callback) { 658 | var url = '/' + escape(screen_name) + '/lists/' + escape(list_id) + '/statuses.json'; 659 | this.get(url, params, callback); 660 | return this; 661 | } 662 | Twitter.prototype.showListStatuses 663 | = Twitter.prototype.getListTimeline; 664 | 665 | // FIXME: Uses deprecated Twitter lists API 666 | Twitter.prototype.createList = function(screen_name, list_name, params, callback) { 667 | if (typeof params === 'function') { 668 | callback = params; 669 | params = null; 670 | } 671 | 672 | var url = '/' + escape(screen_name) + '/lists.json'; 673 | params = merge(params, {name:list_name}); 674 | this.post(url, params, null, callback); 675 | return this; 676 | } 677 | 678 | // FIXME: Uses deprecated Twitter lists API 679 | Twitter.prototype.updateList = function(screen_name, list_id, params, callback) { 680 | var url = '/' + escape(screen_name) + '/lists/' + escape(list_id) + '.json'; 681 | this.post(url, params, null, callback); 682 | return this; 683 | } 684 | 685 | // FIXME: Uses deprecated Twitter lists API 686 | Twitter.prototype.deleteList = function(screen_name, list_id, callback) { 687 | var url = '/' + escape(screen_name) + '/lists/' + escape(list_id) + '.json?_method=DELETE'; 688 | this.post(url, null, callback); 689 | return this; 690 | } 691 | Twitter.prototype.destroyList 692 | = Twitter.prototype.deleteList; 693 | 694 | // List Members resources 695 | 696 | // FIXME: Uses deprecated Twitter lists API 697 | Twitter.prototype.getListMembers = function(screen_name, list_id, params, callback) { 698 | if (typeof params === 'function') { 699 | callback = params; 700 | params = null; 701 | } 702 | 703 | var url = '/' + escape(screen_name) + '/' + escape(list_id) + '/members.json'; 704 | params = merge(params, {key:'users'}); 705 | this._getUsingCursor(url, params, callback); 706 | return this; 707 | } 708 | 709 | // FIXME: the rest of list members 710 | 711 | // List Subscribers resources 712 | 713 | // FIXME: Uses deprecated Twitter lists API 714 | Twitter.prototype.getListSubscribers = function(screen_name, list_id, params, callback) { 715 | if (typeof params === 'function') { 716 | callback = params; 717 | params = null; 718 | } 719 | 720 | var url = '/' + escape(screen_name) + '/' + escape(list_id) + '/subscribers.json'; 721 | params = merge(params, {key:'users'}); 722 | this._getUsingCursor(url, params, callback); 723 | return this; 724 | } 725 | 726 | // FIXME: the rest of list subscribers 727 | 728 | // Direct Messages resources 729 | 730 | Twitter.prototype.getDirectMessages = function(params, callback) { 731 | var url = '/direct_messages.json'; 732 | this.get(url, params, callback); 733 | return this; 734 | } 735 | 736 | Twitter.prototype.getDirectMessagesSent = function(params, callback) { 737 | var url = '/direct_messages/sent.json'; 738 | this.get(url, params, callback); 739 | return this; 740 | } 741 | Twitter.prototype.getSentDirectMessages 742 | = Twitter.prototype.getDirectMessagesSent; 743 | 744 | Twitter.prototype.newDirectMessage = function(id, text, params, callback) { 745 | if (typeof params === 'function') { 746 | callback = params; 747 | params = null; 748 | } 749 | 750 | var defaults = { 751 | text: text, 752 | include_entities: 1 753 | }; 754 | if (typeof id === 'string') 755 | defaults.screen_name = id; 756 | else 757 | defaults.user_id = id; 758 | params = merge(defaults, params); 759 | 760 | var url = '/direct_messages/new.json'; 761 | this.post(url, params, null, callback); 762 | return this; 763 | } 764 | Twitter.prototype.updateDirectMessage 765 | = Twitter.prototype.sendDirectMessage 766 | = Twitter.prototype.newDirectMessage; 767 | 768 | Twitter.prototype.destroyDirectMessage = function(id, callback) { 769 | var url = '/direct_messages/destroy/' + escape(id) + '.json?_method=DELETE'; 770 | this.post(url, null, callback); 771 | return this; 772 | } 773 | Twitter.prototype.deleteDirectMessage 774 | = Twitter.prototype.destroyDirectMessage; 775 | 776 | // Friendship resources 777 | 778 | Twitter.prototype.createFriendship = function(id, params, callback) { 779 | if (typeof params === 'function') { 780 | callback = params; 781 | params = null; 782 | } 783 | 784 | var defaults = { 785 | include_entities: 1 786 | }; 787 | if (typeof id === 'string') 788 | defaults.screen_name = id; 789 | else 790 | defaults.user_id = id; 791 | params = merge(defaults, params); 792 | 793 | var url = '/friendships/create.json'; 794 | this.post(url, params, null, callback); 795 | return this; 796 | } 797 | 798 | Twitter.prototype.destroyFriendship = function(id, callback) { 799 | if (typeof id === 'function') { 800 | callback = id; 801 | id = null; 802 | } 803 | 804 | var params = { 805 | include_entities: 1 806 | }; 807 | if (typeof id === 'string') 808 | params.screen_name = id; 809 | else 810 | params.user_id = id; 811 | 812 | var url = '/friendships/destroy.json?_method=DELETE'; 813 | this.post(url, params, null, callback); 814 | return this; 815 | } 816 | Twitter.prototype.deleteFriendship 817 | = Twitter.prototype.destroyFriendship; 818 | 819 | // Only exposing friendships/show instead of friendships/exist 820 | 821 | Twitter.prototype.showFriendship = function(source, target, callback) { 822 | var params = {}; 823 | 824 | if (typeof source === 'string') 825 | params.source_screen_name = source; 826 | else 827 | params.source_id = source; 828 | 829 | if (typeof target === 'string') 830 | params.target_screen_name = target; 831 | else 832 | params.target_id = target; 833 | 834 | var url = '/friendships/show.json'; 835 | this.get(url, params, callback); 836 | return this; 837 | } 838 | 839 | Twitter.prototype.incomingFriendship = function(callback) { 840 | var url = '/friendships/incoming.json'; 841 | this._getUsingCursor(url, {key:'ids'}, callback); 842 | return this; 843 | } 844 | Twitter.prototype.incomingFriendships 845 | = Twitter.prototype.incomingFriendship; 846 | 847 | Twitter.prototype.outgoingFriendship = function(callback) { 848 | var url = '/friendships/outgoing.json'; 849 | this._getUsingCursor(url, {key:'ids'}, callback); 850 | return this; 851 | } 852 | Twitter.prototype.outgoingFriendships 853 | = Twitter.prototype.outgoingFriendship; 854 | 855 | // Friends and Followers resources 856 | 857 | Twitter.prototype.getFriendsIds = function(id, callback) { 858 | if (typeof id === 'function') { 859 | callback = id; 860 | id = null; 861 | } 862 | 863 | var params = { key: 'ids' }; 864 | if (typeof id === 'string') 865 | params.screen_name = id; 866 | else if (typeof id === 'number') 867 | params.user_id = id; 868 | 869 | var url = '/friends/ids.json'; 870 | this._getUsingCursor(url, params, callback); 871 | return this; 872 | } 873 | 874 | Twitter.prototype.getFollowersIds = function(id, callback) { 875 | if (typeof id === 'function') { 876 | callback = id; 877 | id = null; 878 | } 879 | 880 | var params = { key: 'ids' }; 881 | if (typeof id === 'string') 882 | params.screen_name = id; 883 | else if (typeof id === 'number') 884 | params.user_id = id; 885 | 886 | var url = '/followers/ids.json'; 887 | this._getUsingCursor(url, params, callback); 888 | return this; 889 | } 890 | 891 | // Account resources 892 | 893 | Twitter.prototype.verifyCredentials = function(callback) { 894 | var url = '/account/verify_credentials.json'; 895 | this.get(url, null, callback); 896 | return this; 897 | } 898 | 899 | Twitter.prototype.rateLimitStatus = function(callback) { 900 | var url = '/account/rate_limit_status.json'; 901 | this.get(url, null, callback); 902 | return this; 903 | } 904 | 905 | Twitter.prototype.updateProfile = function(params, callback) { 906 | // params: name, url, location, description 907 | var defaults = { 908 | include_entities: 1 909 | }; 910 | params = merge(defaults, params); 911 | 912 | var url = '/account/update_profile.json'; 913 | this.post(url, params, null, callback); 914 | return this; 915 | } 916 | 917 | // FIXME: Account resources section not complete 918 | 919 | // Favorites resources 920 | 921 | Twitter.prototype.getFavorites = function(params, callback) { 922 | var url = '/favorites.json'; 923 | this.get(url, params, callback); 924 | return this; 925 | } 926 | 927 | Twitter.prototype.createFavorite = function(id, params, callback) { 928 | var url = '/favorites/create/' + escape(id) + '.json'; 929 | this.post(url, params, null, callback); 930 | return this; 931 | } 932 | Twitter.prototype.favoriteStatus 933 | = Twitter.prototype.createFavorite; 934 | 935 | Twitter.prototype.destroyFavorite = function(id, params, callback) { 936 | var url = '/favorites/destroy/' + escape(id) + '.json'; 937 | this.post(url, params, null, callback); 938 | return this; 939 | } 940 | Twitter.prototype.deleteFavorite 941 | = Twitter.prototype.destroyFavorite; 942 | 943 | // Notification resources 944 | 945 | // Block resources 946 | 947 | Twitter.prototype.createBlock = function(id, callback) { 948 | var url = '/blocks/create.json'; 949 | 950 | var params = {}; 951 | if (typeof id === 'string') 952 | params.screen_name = id; 953 | else 954 | params.user_id = id; 955 | 956 | this.post(url, params, null, callback); 957 | return this; 958 | } 959 | Twitter.prototype.blockUser 960 | = Twitter.prototype.createBlock; 961 | 962 | Twitter.prototype.destroyBlock = function(id, callback) { 963 | var url = '/blocks/destroy.json'; 964 | 965 | var params = {}; 966 | if (typeof id === 'string') 967 | params.screen_name = id; 968 | else 969 | params.user_id = id; 970 | 971 | this.post(url, params, null, callback); 972 | return this; 973 | } 974 | Twitter.prototype.unblockUser 975 | = Twitter.prototype.destroyBlock; 976 | 977 | Twitter.prototype.blockExists = function(id, callback) { 978 | var url = '/blocks/exists.json'; 979 | 980 | var params = {}; 981 | if (typeof id === 'string') 982 | params.screen_name = id; 983 | else 984 | params.user_id = id; 985 | 986 | this.get(url, params, null, callback); 987 | return this; 988 | } 989 | Twitter.prototype.isBlocked 990 | = Twitter.prototype.blockExists; 991 | 992 | // FIXME: blocking section not complete (blocks/blocking + blocks/blocking/ids) 993 | 994 | // Spam Reporting resources 995 | 996 | Twitter.prototype.reportSpam = function(id, callback) { 997 | var url = '/report_spam.json'; 998 | 999 | var params = {}; 1000 | if (typeof id === 'string') 1001 | params.screen_name = id; 1002 | else 1003 | params.user_id = id; 1004 | 1005 | this.post(url, params, null, callback); 1006 | return this; 1007 | } 1008 | 1009 | // Saved Searches resources 1010 | 1011 | Twitter.prototype.savedSearches = function(callback) { 1012 | var url = '/saved_searches.json'; 1013 | this.get(url, null, callback); 1014 | return this; 1015 | } 1016 | 1017 | Twitter.prototype.showSavedSearch = function(id, callback) { 1018 | var url = '/saved_searches/' + escape(id) + '.json'; 1019 | this.get(url, null, callback); 1020 | return this; 1021 | } 1022 | 1023 | Twitter.prototype.createSavedSearch = function(query, callback) { 1024 | var url = '/saved_searches/create.json'; 1025 | this.post(url, {query: query}, null, callback); 1026 | return this; 1027 | } 1028 | Twitter.prototype.newSavedSearch = 1029 | Twitter.prototype.createSavedSearch; 1030 | 1031 | Twitter.prototype.destroySavedSearch = function(id, callback) { 1032 | var url = '/saved_searches/destroy/' + escape(id) + '.json?_method=DELETE'; 1033 | this.post(url, null, null, callback); 1034 | return this; 1035 | } 1036 | Twitter.prototype.deleteSavedSearch = 1037 | Twitter.prototype.destroySavedSearch; 1038 | 1039 | // OAuth resources 1040 | 1041 | // Geo resources 1042 | 1043 | Twitter.prototype.geoSearch = function(params, callback) { 1044 | var url = '/geo/search.json'; 1045 | this.get(url, params, callback); 1046 | return this; 1047 | } 1048 | 1049 | Twitter.prototype.geoSimilarPlaces = function(lat, lng, name, params, callback) { 1050 | if (typeof params === 'function') { 1051 | callback = params; 1052 | params = {}; 1053 | } else if (typeof params !== 'object') { 1054 | params = {}; 1055 | } 1056 | 1057 | if (typeof lat !== 'number' || typeof lng !== 'number' || !name) { 1058 | callback(new Error('FAIL: You must specify latitude, longitude (as numbers) and name.')); 1059 | } 1060 | 1061 | var url = '/geo/similar_places.json'; 1062 | params.lat = lat; 1063 | params.long = lng; 1064 | params.name = name; 1065 | this.get(url, params, callback); 1066 | return this; 1067 | } 1068 | 1069 | Twitter.prototype.geoReverseGeocode = function(lat, lng, params, callback) { 1070 | if (typeof params === 'function') { 1071 | callback = params; 1072 | params = {}; 1073 | } else if (typeof params !== 'object') { 1074 | params = {}; 1075 | } 1076 | 1077 | if (typeof lat !== 'number' || typeof lng !== 'number') { 1078 | callback(new Error('FAIL: You must specify latitude and longitude as numbers.')); 1079 | } 1080 | 1081 | var url = '/geo/reverse_geocode.json'; 1082 | params.lat = lat; 1083 | params.long = lng; 1084 | this.get(url, params, callback); 1085 | return this; 1086 | } 1087 | 1088 | Twitter.prototype.geoGetPlace = function(place_id, callback) { 1089 | var url = '/geo/id/' + escape(place_id) + '.json'; 1090 | this.get(url, callback); 1091 | return this; 1092 | } 1093 | 1094 | // Legal resources 1095 | 1096 | // Help resources 1097 | 1098 | // Streamed Tweets resources 1099 | 1100 | // Search resources 1101 | 1102 | // Deprecated resources 1103 | 1104 | Twitter.prototype.getPublicTimeline = function(params, callback) { 1105 | var url = '/statuses/public_timeline.json'; 1106 | this.get(url, params, callback); 1107 | return this; 1108 | } 1109 | 1110 | Twitter.prototype.getFriendsTimeline = function(params, callback) { 1111 | var url = '/statuses/friends_timeline.json'; 1112 | this.get(url, params, callback); 1113 | return this; 1114 | } 1115 | 1116 | 1117 | /* 1118 | * INTERNAL UTILITY FUNCTIONS 1119 | */ 1120 | 1121 | Twitter.prototype._getUsingCursor = function(url, params, callback) { 1122 | var self = this, 1123 | params = params || {}, 1124 | key = params.key || null, 1125 | result = []; 1126 | 1127 | // if we don't have a key to fetch, we're screwed 1128 | if (!key) 1129 | callback(new Error('FAIL: Results key must be provided to _getUsingCursor().')); 1130 | delete params.key; 1131 | 1132 | // kick off the first request, using cursor -1 1133 | params = merge(params, {cursor:-1}); 1134 | this.get(url, params, fetch); 1135 | 1136 | function fetch(data) { 1137 | // FIXME: what if data[key] is not a list? 1138 | if (data[key]) result = result.concat(data[key]); 1139 | 1140 | if (data.next_cursor_str === '0') { 1141 | callback(result); 1142 | } else { 1143 | params.cursor = data.next_cursor_str; 1144 | self.get(url, params, fetch); 1145 | } 1146 | } 1147 | 1148 | return this; 1149 | } 1150 | 1151 | Twitter.prototype._readCookie = function(cookies) { 1152 | // parse the auth cookie 1153 | try { 1154 | var twauth = JSON.parse(cookies.get(this.options.cookie)); 1155 | } catch (error) { 1156 | var twauth = null; 1157 | } 1158 | return twauth; 1159 | } 1160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "twitter" 2 | , "version": "v0.1.18" 3 | , "description": "Twitter API client library for node.js" 4 | , "keywords": ["twitter","streaming","oauth"] 5 | , "homepage": "https://github.com/jdub/node-twitter" 6 | , "author": "jdub" 7 | , "licenses": 8 | [ { "type": "MIT" 9 | , "url": "http://github.com/jdub/node-twitter/raw/master/LICENSE" 10 | } ] 11 | , "repository": 12 | { "type": "git" 13 | , "url": "http://github.com/jdub/node-twitter.git" 14 | } 15 | , "dependencies": 16 | { "oauth": ">=0.8.4" 17 | , "cookies": ">=0.1.6" 18 | , "keygrip": ">=0.1.7" 19 | } 20 | , "engines": ["node >=0.2.0"] 21 | , "main": "./lib/twitter" 22 | } 23 | -------------------------------------------------------------------------------- /test/memory.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'), 2 | twitter = require('twitter'); 3 | 4 | var count = 0, 5 | lastc = 0; 6 | 7 | function tweet(data) { 8 | count++; 9 | if ( typeof data === 'string' ) 10 | sys.puts(data); 11 | else if ( data.text && data.user && data.user.screen_name ) 12 | sys.puts('"' + data.text + '" -- ' + data.user.screen_name); 13 | else if ( data.message ) 14 | sys.puts('ERROR: ' + sys.inspect(data)); 15 | else 16 | sys.puts(sys.inspect(data)); 17 | } 18 | 19 | function memrep() { 20 | var rep = process.memoryUsage(); 21 | rep.tweets = count - lastc; 22 | lastc = count; 23 | console.log(JSON.stringify(rep)); 24 | // next report in 60 seconds 25 | setTimeout(memrep, 60000); 26 | } 27 | 28 | var twit = new twitter({ 29 | consumer_key: 'STATE YOUR NAME', 30 | consumer_secret: 'STATE YOUR NAME', 31 | access_token_key: 'STATE YOUR NAME', 32 | access_token_secret: 'STATE YOUR NAME' 33 | }) 34 | .stream('statuses/sample', function(stream) { 35 | stream.on('data', tweet); 36 | // first report in 15 seconds 37 | setTimeout(memrep, 15000); 38 | }) 39 | -------------------------------------------------------------------------------- /test/memory.txt: -------------------------------------------------------------------------------- 1 | twitter.stream() memory test 2 | ============================ 3 | 4 | Add your oauth details to memory.js and run: node memory.js | grep '^{"rss' 5 | All tests run with node-twitter 0.1.4 on amd64. 6 | 7 | 8 | node 0.2.5 9 | ---------- 10 | 11 | First report 15s after connection: 12 | 13 | {"rss":15527936,"vsize":641900544,"heapTotal":9790784,"heapUsed":6038032,"tweets":141} 14 | 15 | Then every 60s thereafter, for 10min: 16 | 17 | {"rss":27459584,"vsize":647766016,"heapTotal":19803168,"heapUsed":13285208,"tweets":657} 18 | {"rss":22380544,"vsize":643698688,"heapTotal":22838656,"heapUsed":6190456,"tweets":748} 19 | {"rss":30773248,"vsize":643698688,"heapTotal":22642048,"heapUsed":5910000,"tweets":704} 20 | {"rss":29765632,"vsize":642478080,"heapTotal":22895008,"heapUsed":9168464,"tweets":739} 21 | {"rss":30179328,"vsize":642478080,"heapTotal":23147968,"heapUsed":11336200,"tweets":733} 22 | {"rss":30265344,"vsize":642641920,"heapTotal":23147968,"heapUsed":12100416,"tweets":718} 23 | {"rss":30265344,"vsize":642641920,"heapTotal":23147968,"heapUsed":13586760,"tweets":749} 24 | {"rss":29929472,"vsize":642641920,"heapTotal":23147968,"heapUsed":13601368,"tweets":704} 25 | {"rss":17620992,"vsize":642904064,"heapTotal":10565056,"heapUsed":7058816,"tweets":696} 26 | {"rss":22384640,"vsize":643166208,"heapTotal":15265280,"heapUsed":7695208,"tweets":684} 27 | 28 | 29 | node 0.3.2 30 | ---------- 31 | 32 | First report 15s after connection: 33 | 34 | {"rss":15224832,"vsize":643862528,"heapTotal":8507456,"heapUsed":5220336,"tweets":181} 35 | 36 | Then every 60s thereafter, for 10min: 37 | 38 | {"rss":16572416,"vsize":643997696,"heapTotal":9029696,"heapUsed":5498744,"tweets":696} 39 | {"rss":11419648,"vsize":643899392,"heapTotal":4716608,"heapUsed":2667984,"tweets":707} 40 | {"rss":14233600,"vsize":644161536,"heapTotal":6548544,"heapUsed":3583680,"tweets":749} 41 | {"rss":16539648,"vsize":644161536,"heapTotal":8906816,"heapUsed":3567664,"tweets":735} 42 | {"rss":16592896,"vsize":644161536,"heapTotal":8906816,"heapUsed":5034392,"tweets":734} 43 | {"rss":16592896,"vsize":644161536,"heapTotal":8906816,"heapUsed":5489256,"tweets":711} 44 | {"rss":12730368,"vsize":643899392,"heapTotal":5765184,"heapUsed":3313272,"tweets":698} 45 | {"rss":13983744,"vsize":644161536,"heapTotal":6287424,"heapUsed":4020752,"tweets":761} 46 | {"rss":16306176,"vsize":644161536,"heapTotal":8645696,"heapUsed":4063720,"tweets":742} 47 | {"rss":15040512,"vsize":644161536,"heapTotal":8645696,"heapUsed":5595896,"tweets":695} 48 | --------------------------------------------------------------------------------