├── .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 |
--------------------------------------------------------------------------------