raw.js
Version:
API wrapper for reddit.com
255 lines (215 loc) • 7.03 kB
JavaScript
var request = require('request');
var Exception = require('./exception.js');
module.exports = reddit;
require('util').inherits(reddit, require('events').EventEmitter);
require('./handlers.js');
require('./simplerequests.js');
require('./commentstream.js');
require('./livethreadreader.js');
require('./api/captcha.js');
require('./api/edit.js');
require('./api/history.js');
require('./api/identity.js');
require('./api/livemanage.js');
require('./api/modcontributors.js');
require('./api/modflair.js');
require('./api/modlog.js');
require('./api/modothers.js');
require('./api/modposts.js');
require('./api/privatemessages.js');
require('./api/read.js');
require('./api/report.js');
require('./api/submit.js');
function reddit(userAgent) {
this._userAgent = userAgent;
}
reddit.prototype.setupOAuth2 = function(id, secret, redirectUri) {
this._oauth2 = {
id: id,
secret: secret,
redirectUri: redirectUri
};
this._rateLimit;
};
reddit.prototype.authUrl = function(state, scopes, permanent) {
if(!this._oauth2) {
throw new Exception("OAuth2 has not yet been set up, use reddit.setupOAuth2(id, secret, redirectUri) to set up OAuth2");
}
return "https://ssl.reddit.com/api/v1/authorize?client_id=" + encodeURIComponent(this._oauth2.id)
+ "&response_type=code"
+ "&state=" + encodeURIComponent(state)
+ "&redirect_uri=" + encodeURIComponent(this._oauth2.redirectUri)
+ "&duration=" + ((!!permanent) ? "permanent" : "temporary")
+ "&scope=" + encodeURIComponent(scopes.join(','));
};
reddit.prototype._apiRequest = function(endpoint, options, callback) {
var defaults = {
"domain": "https://oauth.reddit.com",
"method": "GET",
"path": "/api"
};
if(!options.domain && !this.bearerToken) {
options.domain = "http://www.reddit.com";
}
for(var i in defaults) {
if(options[i] == undefined) {
options[i] = defaults[i];
}
}
if(options.version) {
options.path += "/v" + options.version;
}
options.headers = options.headers || {};
options.headers["User-Agent"] = this._userAgent;
if(options.domain != "http://www.reddit.com") {
if(this.bearerToken && !options.inAuthorizationFlow) {
options.headers["Authorization"] = "bearer " + this.bearerToken;
} else if(this._oauth2) {
options.auth = {"user": this._oauth2.id, "pass": this._oauth2.secret};
}
}
var req = {
"uri": options.domain + options.path + '/' + endpoint,
"method": options.method,
"form": options.form,
"qs": options.qs,
"headers": options.headers,
"auth": options.auth
};
var self = this;
this.emit('debug-apirequest', req);
request(req, function(err, response, body) {
if(response && response.headers && response.headers['x-ratelimit-remaining'] != undefined && response.headers['x-ratelimit-used'] != undefined && response.headers['x-ratelimit-reset'] != undefined) {
self._rateLimit = {
"remaining": parseInt(response.headers['x-ratelimit-remaining']),
"used": parseInt(response.headers['x-ratelimit-used']),
"reset": Math.floor(Date.now() / 1000) + parseInt(response.headers['x-ratelimit-reset'])
};
}
if(!err && response.statusCode >= 400) {
var codeDescriptions = {
'400': "Bad Request",
'403': "Forbidden",
'404': "Not Found",
'500': "Internal Server Error"
};
callback((codeDescriptions[response.statusCode] != undefined) ? response.statusCode + ": " + codeDescriptions[response.statusCode] : response.statusCode);
return;
}
callback(err, response, body);
});
};
reddit.prototype.auth = function(options, callback) {
var self = this;
if(typeof options == 'function') {
callback = options;
options = {};
}
if(options.code) {
// Normal OAuth2 authorization flow
form = {
"grant_type": "authorization_code",
"code": options.code,
"redirect_uri": this._oauth2.redirectUri
};
} else if(options.username) {
// Script authorization flow with client username and password
form = {
"grant_type": "password",
"username": options.username,
"password": options.password
};
} else {
// Getting a new bearer token from a refresh token
form = {
"grant_type": "refresh_token",
"refresh_token": self.refreshToken
};
}
self._apiRequest("access_token", {"domain": "https://ssl.reddit.com", "method": "POST", "version": 1, "form": form, "inAuthorizationFlow": true}, function(err, response, body) {
if(err) {
callback(err);
return;
}
var json;
try {
var json = JSON.parse(body);
} catch(e) {
callback("reddit API returned invalid JSON: " + e);
return;
}
if(json.error) {
callback(json.error);
return;
}
self.bearerToken = json.access_token;
if(json.refresh_token) {
self.refreshToken = json.refresh_token;
}
if(self.refreshToken) {
// If we have a refresh token, set a timer to automatically refresh it when our bearer token expires
self._refreshTimeout = setTimeout(function() {
refreshBearerToken(self);
}, (json.expires_in - 60) * 1000);
} else if(options.username) {
// If we're logged in as a script, set a timer to automatically refresh the login when our bearer token expires
self._refreshTimeout = setTimeout(function() {
refreshBearerToken(self, options);
}, (json.expires_in - 60) * 1000);
}
json.scope = json.scope.split(',');
callback(null, json);
});
};
function refreshBearerToken(self, options) {
if(options) {
self.auth(options, function(err, response) {
if(err) {
self.emit('debug-error', "Unable to refresh bearer token: " + err);
if(err == "invalid_grant") {
// Password is no longer valid
self.emit('error', "Unable to refresh bearer token: password is no longer valid");
self.logout();
return;
}
refreshBearerToken(self, options); // Try again!
}
});
} else {
self.auth(function(err, response) {
if(err) {
self.emit('debug-error', "Unable to refresh bearer token: " + err);
if(err == "invalid_request") {
// Access was revoked
self.emit('error', "Unable to refresh bearer token: app access was revoked");
self.logout();
return;
}
refreshBearerToken(self); // Try again!
}
});
}
}
reddit.prototype.logout = function() {
if(this._refreshTimeout) {
clearTimeout(this._refreshTimeout);
}
delete this.bearerToken;
delete this.refreshToken;
};
reddit.prototype.getRateLimitDetails = function() {
if(!this._rateLimit) {
return false;
}
var now = Math.floor(Date.now() / 1000);
if(this._rateLimit.reset < now) {
this._rateLimit.remaining += this._rateLimit.used;
this._rateLimit.used = 0;
this._rateLimit.reset = 0;
}
return {
"used": this._rateLimit.used,
"remaining": this._rateLimit.remaining,
"reset": this._rateLimit.reset - now
};
};