tweeter.js | |
---|---|
Tweeter.js A node.js module with simple OAuth 1.0a dancing with Twitter's API. It also provides a simple structure for lazy-loading (mixins?) API wrappers. All requests emit Tweeter.js doesn't provide any session management. It's a new project, so that should be coming in the future. | var EventEmitter = require('events').EventEmitter,
util = require('./util'), // includes util.format, remove in later version
https = require('https'), // for SSL requests
http = require('http'), // for normal requests
qs = require('querystring'), // create and parse parameter strings
crypto = require('crypto'), // provides HMAC-SHA1
url = require('url'), // parses values sent from twitter to oauthCallback
Log = require('./tweeter.log'), // simple logging with configurable streams
noop = function() { }; // caches an empty function |
Creates a Tweeter object.
* REQUIRED Tweeter.js can be used easily with everyauth. If so, everyauth remains responsible for the authentication and session management. Pass the access token and access token secret into the configuration object and Tweeter.js doesn't do authentication, just API request calls. | var Tweeter = exports = module.exports = function Tweeter(o) {
if(!o || typeof o !== 'object') {
throw Error('Invalid initialization object');
}
if( typeof o.consumerKey !== 'string' || typeof o.consumerSecret !== 'string') {
throw Error('Required call: new Tweeter({ consumerKey: "your key", consumerSecret: "your secret" }');
}
this.config = o;
this._init();
}; |
Initializes Tweeter object and attached APIs. | Tweeter.prototype._init = function() {
var self = this;
self.apiBase = 'api.twitter.com';
self.useSSL = false;
self.api = {};
for(var api in self.include){
if(typeof self.include[api] === 'function') {
self.api[api] = new self.include[api];
self.api[api].tweeter = this;
} else { self.log( self.logLevel.ERROR, "Could not create %j", api); }
}
}; |
Instantiates tweeter.log object. -1 disables log output. | Tweeter.prototype.logger = new Log(-1); |
TODO: Remove this for a defined getter/setter. I'd much rather a more robust getter/setter: | Tweeter.prototype.setLogLevel = function(level) {
this.logger.logLevel = level;
}; |
TODO: Remove this at some point? | Tweeter.prototype.logLevels = Log.prototype.logLevels; |
Placeholder for API function references to be included. | Tweeter.prototype.include = {}; |
Creates a base options object for | Tweeter.prototype.__defineGetter__('base', function() {
var self = this;
return {
host: self.apiBase,
headers: {
'Content-Type':'application/json',
'X-Target-URI': util.format('http%s://%s', (self.useSSL?'s':''), self.apiBase),
'Connection':'Keep-Alive'
}
};
}); |
extend method modified from underscore.js
| Tweeter.prototype.extend = function(obj) {
[].slice.call(arguments, 1).forEach(function(source) {
for (var prop in source) {
if(source[prop] !== void 0) obj[prop] = source[prop];
}
});
return obj;
}; |
The generic version of the main functions. This is the same as: Or, you could include the timelines API module: | Tweeter.prototype.apiCall = function(method, path, opt, fn) {
var self = this;
if(! (method in ['get','post','delete','put', 'head']) ){
method = 'get';
}
var req = self.prepareCall(path, opt, fn);
req.method = method;
self[method].call(this, path, self.extend({}, self.base, req), fn);
return this;
}; |
Caution: side effects. These are on purpose. | Tweeter.prototype.prepareCall = function(path, opt, fn) {
if(typeof opt === 'function') {
fn = opt;
opt = { };
} else { opt = opt || { }; }
var query = qs.stringify(opt);
var req = { path: path + (query?'?'+query:'') };
return req;
}; |
Creates a new client request for each call | Tweeter.prototype.execute = function(path, options, cb) {
var self = this, req, client, fn = cb || noop;
var header = {
Authorization: self.oauthHeader(options.method, options.path || path)
};
self.extend(options.headers, header);
self.log(self.logLevels.INFO,
'execute() path: %s\n\toptions: %j\n',
path, options ); |
client is either node.js https or http object, depending in useSSL | client = self.useSSL ? https : http; |
Tweeter inherits from EventEmitter for great control over events.
It also allows you to pass in a callback with the signature | req = client.request(options, function(res) {
res.setEncoding('utf8');
var responseData = [];
res.on('data', function(d) {
responseData += d;
self.emit('data', d);
});
res.on('end', function() {
fn.call(this, null, responseData);
self.emit('end');
});
res.on('close', function() {
req.emit('end');
});
});
req.on('error', function(err) {
fn.call(this, err, null);
self.emit('error', err);
});
if(options.body) {
req.write(options.body, 'utf8');
req.end();
} else {
req.end();
}
}; |
get(path, options, cb)A convenience method for HTTP GET requests | Tweeter.prototype.get = function(path, options, cb) {
var req = this.prepareCall(path, options, cb);
this.execute(path, this.extend({}, this.base, options, req, { method: 'GET' }), cb );
return this;
}; |
post(path, options, get)A convenience method for HTTP POST requests | Tweeter.prototype.post = function(path, options, cb) {
var req = this.prepareCall(path, options, cb);
this.execute(path, this.extend({}, this.base, options, req, { method: 'POST' }), cb );
return this;
}; |
TODO: This is a little broken atm This is the only time | Tweeter.prototype.upload = function(path, options, cb) {
var apiBase = this.apiBase; |
Get the media to upload | var media = options['media[]'];
var filename = options.filename;
var filetype = options.filetype; |
These don'e need to be in options object anymore | delete options['media[]'];
delete options['filename'];
delete options['filetype'];
var boundary = ((new Date()).getTime()) + '.' + Math.random();
var req = this.prepareCall(path, {}, cb); |
Set Multipart header information | var multipart = this.extend({}, this.base);
this.apiBase = 'upload.twitter.com';
multipart.host = 'upload.twitter.com';
multipart.headers['Content-Type'] = 'multipart/form-data' + (media != null ? '; boundary=' + boundary : '');
multipart.headers['X-Target-URI'] = 'https://upload.twitter.com'; |
Create the Multipart body | var body = '';
for(var key in options) {
body = body +
util.format('--%s\r\nContent-Disposition: form-data; name="%s"\r\n\r\n%s\r\n',
boundary, key, options[key]);
}
if(media) {
body = body + util.format('--%s\r\n' +
'Content-Disposition: form-data; name="media[]"; filename="%s"\r\n' +
'Content-Type: %s\r\n\r\n%s\r\n',
boundary, filename || 'new-file.png', filetype || 'image/png', media
);
}
body = body + util.format('--%s--\r\n', boundary);
multipart.headers['Content-length'] = body.length;
options.body = body; |
Start the upload | this.useSSL = true;
this.execute(path, this.extend({}, multipart, options, req, { method: 'POST' }),
function() {
if(cb && typeof cb === 'function') {
cb();
} |
reset apiBase. | this.apiBase = apiBase;
});
return this;
}; |
put(path, options, cb)A convenience method for HTTP PUT requests | Tweeter.prototype.put = function(path, options, cb) {
var req = this.prepareCall(path, options, cb);
this.execute(path, this.extend({}, this.base, options, { method: 'PUT' }), cb );
return this;
}; |
delete(path, options, cb)A convenience method for HTTP DELETE requests | Tweeter.prototype.delete = function(path, options, cb) {
var req = this.prepareCall(path, options, cb);
this.execute(path, this.extend({}, this.base, options, { method: 'DELETE' }), cb );
return this;
}; |
head(path, options, cb)A convenience method for HTTP HEAD requests | Tweeter.prototype.head = function(path, options, cb) {
var req = this.prepareCall(path, options, cb);
this.execute(path, this.extend({}, this.base, options, { method: 'HEAD' }), cb );
return this;
}; |
log(level, msg)A convenience method for logging. Example with a string:
Example with a format string: | Tweeter.prototype.log = function(level, msg) {
this.logger.log.apply(this.logger, arguments);
}; |
Generates the oauth header object | Tweeter.prototype.oauthHeader = function(method, request, mergeProps) {
var self = this,
d = new Date(),
parts = request.split('?'),
path = parts[0],
query = parts.length > 1 ? parts[1] : '';
var headerObj = {
oauth_consumer_key: self.config.consumerKey,
oauth_nonce: self.config.test_nonce || Math.ceil( d.getTime() / Math.random() ),
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: self.config.test_timestamp || (''+( (d.getTime() - d.getMilliseconds() )/1000)),
oauth_version: '1.0'
}; |
only add the access token if it exists | if(self.config.accessToken) {
headerObj.oauth_token = self.config.accessToken;
} else if(self.config.token && !headerObj.oauth_token) { |
if the request token exists, but the access token doesn't, add the request token | headerObj.oauth_token = self.config.token;
}
self.extend(headerObj, mergeProps); |
If the requested path contains /oauth/*, we have to provide the callback | if( path.match(/\/oauth\//gi) ) {
headerObj.oauth_callback = qs.escape(self.config.oauthCallback);
} |
Gets the oauth signature and adds it to the header | var signature = self.oauthSignature(method, path, headerObj, query);
headerObj.oauth_signature = qs.escape(signature); |
concat the header object into a csv string | var header = 'OAuth realm="Twitter API",';
var oauthParts = [];
for (var h in headerObj) {
oauthParts.push(h + '="'+headerObj[h]+'"');
}
header+= oauthParts.join(',');
self.log(self.logLevels.DEBUG,
'oauthHeader()\n\tHeader: %j\n', header);
return header;
}; |
Creates the HMAC-SHA1 encrypted signature string | Tweeter.prototype.oauthSignature = function(method, path, props, query) {
var self = this; |
twitter expects either | var composite = qs.escape(self.config.consumerSecret) + '&' +
qs.escape(self.config.accessTokenSecret||'');
self.log(self.logLevels.DEBUG,
'oauthSignature() options:\n\tComposite: %s\n\tconsumerSecret: %s\n\taccessTokenSecret: %s\n',
composite, self.config.consumerSecret, self.config.accessTokenSecret);
var hmac = crypto.createHmac('sha1', new Buffer(composite) );
var sig = self.createSignatureBaseString(method, path, props, query);
hmac.update(sig);
var hmacDigest = hmac.digest('base64');
self.log(self.logLevels.DEBUG,
'oauthSignature() encryption parts:\n\tHMAC-SHA1: %s\n\tHashing: %s\n\tSalt: %s\n',
hmacDigest, sig, composite);
return hmacDigest;
}; |
Creates the signature base string which is to be encrypted. | Tweeter.prototype.createSignatureBaseString = function(method, path, props, query) {
var self = this,
oauthProperties;
if(props) {
oauthProperties = []; |
If the query string is included, it must be added. | if(query) oauthProperties.push( qs.escape(query) ); |
properties should be sorted by key, then by property. if we push everything as a single string to the array, then sort the array, we end up with the same effect and less error-prone code. | for( var key in props ) {
if(props[key]) {
oauthProperties.push( qs.escape(key)+'%3D'+qs.escape(props[key]) );
}
}
oauthProperties.sort();
self.log(self.logLevels.DEBUG,
'createSignatureBaseString()\n\tSorted array: %j', oauthProperties );
}
var uri = util.format('http%s://%s%s', (self.useSSL?'s':''), self.apiBase, path||'');
var sig = method+'&'+qs.escape(uri) +'&' + oauthProperties.join('%26');
self.log(self.logLevels.DEBUG,
'createSignatureBaseString()\n\tSignature: %s\n', sig);
return sig;
}; |
authenticate(callback)Begins the OAuth 'dance'. | Tweeter.prototype.authenticate = function(cb) {
var self = this,
cb = cb || noop; |
These aren't currently set anywhere. | if(self.isAuthenticated && !self.authenticationError) {
cb();
return;
}
return self.getRequestToken(cb);
}; |
returns the authorizationUrl to the callback's data object
| Tweeter.prototype.getRequestToken = function(cb) {
cb = cb||noop;
var self = this;
self.useSSL = true;
return this.post('/oauth/request_token', { }, function(err,data) {
if(data && data.match(/Failed/gi)) {
cb(new Error(data));
return;
}
var response = qs.parse(data);
if(response.oauth_callback_confirmed) {
self.config.token = response.oauth_token;
self.config.tokenSecret = response.oauth_token_secret;
} else {
cb(new Error("Callback was not accepted by Twitter API"));
return;
} |
TODO: just return the string as data instead of creating a new object | cb(null, { authUrl: util.format('%s?oauth_token=%s',
(self.config.authorizeUrl||'http://api.twitter.com/oauth/authorize'),
self.config.token)
});
});
}; |
parseCallback(requestUrl, callback)Parses the callback URL (token and verifier) Note: fn can be your call to the api. | Tweeter.prototype.parseCallback = function(reqUrl,fn) {
var self = this;
var values = url.parse(reqUrl||'');
var response = qs.parse(values.query);
if(response && self.config.token == response.oauth_token) {
self.config.verifier = response.oauth_verifier;
}
self.getAccessToken(fn);
}; |
Requests an access token from twitter's API | Tweeter.prototype.getAccessToken = function(fn) {
var self = this,
fn = fn||noop;
return self.get('/oauth/access_token', { }, function(err,data) {
if(err) { fn.call(self, err, null); }
if(data) {
var response = qs.parse(data);
if(response) {
self.config.accessToken = response.oauth_token;
self.config.accessTokenSecret =
response.oauth_token_secret;
}
}
fn.call(self);
});
};
/**
* Inherit from `EventEmitter`.
*/
Tweeter.prototype.__proto__ = EventEmitter.prototype;
|