snoowrap
Advanced tools
Comparing version 0.2.0 to 0.3.0
module.exports = { | ||
ENDPOINT_DOMAIN: 'reddit.com', | ||
MODULE_NAME: 'snoowrap', | ||
ISSUE_REPORT_LINK: require('../package.json').bugs.url, | ||
API_RULES_LINK: 'https://github.com/reddit/reddit/wiki/API', | ||
USER_KEYS: ['author', 'approved_by', 'banned_by'], | ||
@@ -15,5 +16,6 @@ SUBREDDIT_KEYS: ['subreddit'], | ||
Listing: 'Listing', | ||
more: 'more', | ||
UserList: 'UserList' | ||
}, | ||
username_regex: /^[\w-]{1,20}$/ | ||
USERNAME_REGEX: /^[\w-]{1,20}$/ | ||
}; |
'use strict'; | ||
let constants = require('./constants'); | ||
let e = ''; // Used for newlines in template strings without breaking indentation | ||
let errors = { | ||
@@ -9,4 +8,4 @@ RateLimitError: class extends Error { | ||
super(); | ||
this.message = `ERROR: ${ constants.MODULE_NAME }.errors.ratelimit_exceeded: ${ e }${ constants.MODULE_NAME } refused to continue because reddit's ratelimit was exceeded. ${ e }For more information about reddit\'s ratelimit, please consult reddit\'s API rules at ${ e }https://github.com/reddit/reddit/wiki/API. To avoid hitting the ratelimit again, you should probably ${ e }wait at least ${ expiry_time_from_now } seconds before making any more requests.`; | ||
this.name = 'RateLimitError'; | ||
this.message = `${ constants.MODULE_NAME }.errors.${ this.name }: ${ constants.MODULE_NAME } refused to continue because reddit's ratelimit was exceeded. For more information about reddit's ratelimit, please consult reddit's API rules at ${ constants.API_RULES_LINK }. To avoid hitting the ratelimit again, you should probably wait at least ${ expiry_time_from_now } seconds before making any more requests.`; | ||
} | ||
@@ -17,7 +16,15 @@ }, | ||
super(); | ||
this.message = `Cannot fetch information on the user '${ username }'. Please be sure you have the right username.`; | ||
this.name = 'InvalidUserError'; | ||
this.message = `${ constants.MODULE_NAME }.errors.${ this.name }: Cannot fetch information on the user '${ username }'. Please be sure you have the right username.`; | ||
} | ||
} | ||
}, | ||
InvalidMethodCallError: class extends Error { | ||
constructor(reason) { | ||
super(); | ||
this.name = 'InvalidMethodCallError'; | ||
this.message = `${ constants.MODULE_NAME }.errors.${ this.name }: ${ reason }`; | ||
} | ||
}, | ||
RateLimitWarning: time_until_reset => `Warning: ${ constants.MODULE_NAME } temporarily stopped sending requests because reddit's ratelimit was exceeded. The request you attempted to send was queued, and will be sent to reddit when the current ratelimit period expires in ${ time_until_reset } seconds.` | ||
}; | ||
module.exports = errors; |
@@ -5,4 +5,5 @@ 'use strict'; | ||
require('harmony-reflect'); // temp dependency until node implements Proxies properly | ||
require('harmony-reflect'); // temp dependency until v8 implements Proxies properly | ||
let Promise = require('bluebird'); | ||
Promise.config({ longStackTraces: true }); | ||
let _ = require('lodash'); | ||
@@ -15,2 +16,4 @@ let request = require('request-promise').defaults({ json: true }); | ||
let errors = require('./errors'); | ||
let default_config = require('./default_config'); | ||
let actions = require('./actions'); | ||
let objects = {}; | ||
@@ -29,5 +32,13 @@ let helpers = {}; | ||
this.ratelimit_reset_point = options.ratelimit_reset_point; | ||
this.config = default_config; | ||
this.throttle = Promise.resolve(); | ||
} | ||
get_me() { | ||
return this.get('api/v1/me').then(result => { | ||
this.own_user_info = new objects.RedditUser(result, this, true); | ||
return this.own_user_info; | ||
}); | ||
} | ||
get_user(name) { | ||
return new objects.RedditUser({ name: name }, this); | ||
return new objects.RedditUser({ name }, this); | ||
} | ||
@@ -38,4 +49,7 @@ get_comment(comment_id) { | ||
get_subreddit(display_name) { | ||
return new objects.Subreddit({ display_name: display_name }, this); | ||
return new objects.Subreddit({ display_name }, this); | ||
} | ||
get_submission(submission_id) { | ||
return new objects.Submission({ name: `t3_${ submission_id }` }, this); | ||
} | ||
_update_access_token() { | ||
@@ -45,30 +59,18 @@ var _this = this; | ||
return _asyncToGenerator(function* () { | ||
let token_response = yield request.post({ | ||
url: `https://www.${ constants.ENDPOINT_DOMAIN }/api/v1/access_token`, | ||
headers: { | ||
Authorization: `Basic ${ Buffer(`${ _this.client_id }:${ _this.client_secret }`).toString('base64') }`, | ||
'User-Agent': _this.user_agent | ||
}, | ||
let token_info = yield request.post({ | ||
url: `https://www.${ _this.config.endpoint_domain }/api/v1/access_token`, | ||
auth: { user: _this.client_id, pass: _this.client_secret }, | ||
headers: { 'user-agent': _this.user_agent }, | ||
form: { grant_type: 'refresh_token', refresh_token: _this.refresh_token } | ||
}); | ||
_this.access_token = token_response.access_token; | ||
_this.token_expiration = moment().add(token_response.expires_in, 'seconds'); | ||
_this.scopes = token_response.scope.split(' '); | ||
_this.access_token = token_info.access_token; | ||
_this.token_expiration = moment().add(token_info.expires_in, 'seconds'); | ||
_this.scopes = token_info.scope.split(' '); | ||
})(); | ||
} | ||
get oauth_requester() { | ||
let request_filter_proxy = (requester, thisArg, args) => { | ||
if (this.ratelimit_remaining < 1 && this.ratelimit_reset_point.isAfter()) { | ||
throw new errors.RateLimitError(this.ratelimit_reset_point.diff(moment(), 'seconds')); | ||
} | ||
let needs_refresh = !this.token_expiration || moment(this.token_expiration).subtract(10, 'seconds').isBefore(); | ||
return promise_wrap((needs_refresh ? this._update_access_token() : Promise.resolve()).then(() => { | ||
return requester.defaults({ headers: { Authorization: `bearer ${ this.access_token }` } }).apply(thisArg, args); | ||
})); | ||
}; | ||
get _oauth_requester() { | ||
let default_requester = request.defaults({ | ||
headers: { 'User-Agent': this.user_agent }, | ||
baseUrl: `https://oauth.${ constants.ENDPOINT_DOMAIN }`, | ||
qs: { raw_json: 1 }, // This tells reddit to unescape html characters, so that it sends '<' instead of '<' | ||
headers: { 'user-agent': this.user_agent }, | ||
baseUrl: `https://oauth.${ this.config.endpoint_domain }`, | ||
qs: { raw_json: 1 }, // This tells reddit to unescape html characters, e.g. it will send '<' instead of '<' | ||
resolveWithFullResponse: true, | ||
@@ -81,33 +83,109 @@ transform: (body, response) => { | ||
}); | ||
return new Proxy(default_requester, { | ||
apply: request_filter_proxy, | ||
get: (target, key) => { | ||
// Allow both request(args) and request.post(args) | ||
if (_.includes(['get', 'head', 'post', 'put', 'patch', 'del'], key)) { | ||
return new Proxy(target.defaults({ method: key }), { apply: request_filter_proxy }); | ||
let handle_request = (() => { | ||
var _this2 = this; | ||
var ref = _asyncToGenerator(function* (requester, self, args) { | ||
let attempts = arguments.length <= 3 || arguments[3] === undefined ? 0 : arguments[3]; | ||
if (_this2.ratelimit_remaining < 1 && _this2.ratelimit_reset_point.isAfter()) { | ||
let seconds_until_expiry = _this2.ratelimit_reset_point.diff(moment(), 'seconds'); | ||
if (_this2.config.continue_after_ratelimit_error) { | ||
_this2.warn(errors.RateLimitWarning(seconds_until_expiry)); | ||
yield Promise.delay(_this2.ratelimit_reset_point.diff()); | ||
} else { | ||
throw new errors.RateLimitError(seconds_until_expiry); | ||
} | ||
} | ||
return target[key]; | ||
/* this.throttle is a timer that gets reset to this.config.request_delay whenever a request is sent. | ||
This ensures that requests are ratelimited and that no requests are lost. The await statement is wrapped | ||
in a loop to make sure that if the throttle promise resolves while multiple requests are pending, only | ||
one of the requests will be sent, and the others will await the throttle again. (The loop is non-blocking | ||
due to its await statement.) */ | ||
while (!_this2.throttle.isFulfilled()) { | ||
yield _this2.throttle; | ||
} | ||
_this2.throttle = Promise.delay(_this2.config.request_delay); | ||
// If the access token has expired (or will expire in the next 10 seconds), refresh it. | ||
if (!_this2.token_expiration || moment(_this2.token_expiration).subtract(10, 'seconds').isBefore()) { | ||
yield _this2._update_access_token(); | ||
} | ||
// Send the request and return the response. | ||
return yield requester.defaults({ auth: { bearer: _this2.access_token } }).apply(self, args).catch(function (err) { | ||
if (attempts < _this2.config.max_retry_attempts && _.includes(_this2.config.retry_error_codes, err.statusCode)) { | ||
return handle_request(requester, self, args, attempts + 1); | ||
} | ||
throw err; | ||
}); | ||
}), | ||
_this = this; | ||
return function handle_request(_x, _x2, _x3, _x4) { | ||
return ref.apply(_this, arguments); | ||
}; | ||
})(); | ||
return new Proxy(default_requester, { apply: handle_request }); | ||
} | ||
inspect() { | ||
// Hide confidential information (tokens, client IDs, etc.) from the console.log output. | ||
// Also, hide some things that aren't converted to text well. | ||
let keys_for_hidden_values = ['client_secret', 'refresh_token', 'access_token']; | ||
let hidden_keys = ['throttle']; | ||
let formatted = util.inspect(_(this).omit(hidden_keys).mapValues((value, key) => { | ||
if (_.includes(keys_for_hidden_values, key)) { | ||
return value && '(redacted)'; | ||
} | ||
}); | ||
if (value instanceof moment) { | ||
return value.format(); | ||
} | ||
return value; | ||
}).value()); | ||
return `<${ constants.MODULE_NAME }.objects.${ this.constructor.name }> ${ formatted }`; | ||
} | ||
/*gotta*/get get() { | ||
return this._oauth_requester.defaults({ method: 'get' }); | ||
} | ||
get post() { | ||
return this._oauth_requester.defaults({ method: 'post' }); | ||
} | ||
get patch() { | ||
return this._oauth_requester.defaults({ method: 'patch' }); | ||
} | ||
get put() { | ||
return this._oauth_requester.defaults({ method: 'put' }); | ||
} | ||
get delete() { | ||
return this._oauth_requester.defaults({ method: 'delete' }); | ||
} | ||
// TODO: probably combine the above getters to avoid repetition, though that would require deleting the 'get get' joke :\ | ||
warn() { | ||
if (!this.config.suppress_warnings) { | ||
console.warn(...arguments); | ||
} | ||
} | ||
}; | ||
objects.RedditContent = class RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
var _this2 = this; | ||
this._fetcher = _fetcher; | ||
constructor(options, _ac, has_fetched) { | ||
this._ac = _ac; | ||
this.has_fetched = !!has_fetched; | ||
_.assign(this, options); | ||
this._fetch = _.once(_asyncToGenerator(function* () { | ||
let response = yield _this2._fetcher.oauth_requester.get({ uri: _this2._uri }); | ||
let transformed = _this2._transform_api_response(response); | ||
_.assign(_this2, transformed); | ||
_this2.has_fetched = true; | ||
return _this2; | ||
})); | ||
_.assignIn(this, options); | ||
this.fetch = this.fetch || _.once(() => { | ||
return promise_wrap(this._ac.get({ uri: this._uri }).then(this._transform_api_response.bind(this)).then(response => { | ||
/* The line below is equivalent to _.assign(this, response);, but _.assign ends up triggering warning messages when | ||
used on Proxies, since the patched globals from harmony-reflect aren't applied to lodash. This won't be a problem once | ||
Proxies are correctly implemented natively. https://github.com/tvcutsem/harmony-reflect#dependencies */ | ||
_.forIn(response, (value, key) => { | ||
this[key] = value; | ||
}); | ||
this.has_fetched = true; | ||
return this; | ||
})); | ||
}); | ||
return new Proxy(this, { get: (target, key) => { | ||
if (key in target || key === 'length' || key in Promise.prototype || this.has_fetched) { | ||
if (key in target || key === 'length' || key in Promise.prototype || target.has_fetched) { | ||
return target[key]; | ||
} | ||
if (key === '_raw') { | ||
return target; | ||
} | ||
return this.fetch()[key]; | ||
@@ -117,78 +195,91 @@ } }); | ||
inspect() { | ||
// TODO: Simplify this when lodash fixes _.pickBy | ||
let property_display = _.pick(this, _.filter(_.keys(this), key => { | ||
return key.indexOf('_') !== 0 && typeof this[key] !== 'function'; | ||
})); | ||
return `<${ constants.MODULE_NAME }.objects.${ this.constructor.name }> ${ util.inspect(property_display) }`; | ||
let public_properties = _.omitBy(this, (value, key) => key.startsWith('_') || typeof value === 'function'); | ||
return `<${ constants.MODULE_NAME }.objects.${ this.constructor.name }> ${ util.inspect(public_properties) }`; | ||
} | ||
get oauth_requester() { | ||
return this._fetcher.oauth_requester; | ||
_transform_api_response(response_object) { | ||
return response_object; | ||
} | ||
fetch() { | ||
if (this.has_fetched) { | ||
return this; | ||
} | ||
return promise_wrap(this._fetch()); | ||
} | ||
_transform_api_response(response_obj) { | ||
return response_obj; | ||
} | ||
}; | ||
objects.Comment = class Comment extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
constructor(options, _ac, has_fetched) { | ||
super(options, _ac, has_fetched); | ||
} | ||
_transform_api_response(response_object) { | ||
return response_object.children[0]; | ||
_transform_api_response(response_obj) { | ||
let replies_uri = `comments/${ response_obj[0].link_id.slice(3) }`; | ||
let replies_query = { comment: this.name.slice(3) }; | ||
let _transform = item => item[1][0].replies; | ||
response_obj[0].replies = new objects.Listing({ _uri: replies_uri, query: replies_query, _transform }, this._ac); | ||
return response_obj[0]; | ||
} | ||
get _uri() { | ||
return `/api/info?id=${ this.name }`; | ||
return `api/info?id=${ this.name }`; | ||
} | ||
static get inherited_action_categories() { | ||
return ['reply', 'vote', 'moderate', 'more_comments']; | ||
} | ||
}; | ||
objects.RedditUser = class RedditUser extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
constructor(options, _ac, has_fetched) { | ||
super(options, _ac, has_fetched); | ||
} | ||
get _uri() { | ||
if (typeof this.name !== 'string' || !constants.username_regex.test(this.name)) { | ||
if (typeof this.name !== 'string' || !constants.USERNAME_REGEX.test(this.name)) { | ||
throw new errors.InvalidUserError(this.name); | ||
} | ||
return `/user/${ this.name }/about`; | ||
return `user/${ this.name }/about`; | ||
} | ||
static get inherited_action_categories() { | ||
return []; | ||
} | ||
}; | ||
objects.Submission = class Submission extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
constructor(options, _ac, has_fetched) { | ||
super(options, _ac, has_fetched); | ||
let _transform = response => response[1]; | ||
this.comments = new objects.Listing({ _uri: `comments/${ this.name.slice(3) }`, _transform }, _ac); | ||
} | ||
get _uri() { | ||
return `/api/info?id=${ this.name }`; | ||
return `api/info?id=${ this.name }`; | ||
} | ||
_transform_api_response(response_object) { | ||
return response_object[0]; | ||
} | ||
static get inherited_action_categories() { | ||
return ['reply', 'vote', 'moderate', 'more_comments']; | ||
} | ||
}; | ||
objects.PrivateMessage = class PrivateMessage extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
constructor(options, _ac, has_fetched) { | ||
super(options, _ac, has_fetched); | ||
} | ||
get _uri() { | ||
return `/message/messages/${ this.id }`; | ||
return `message/messages/${ this.id }`; | ||
} | ||
static get inherited_action_categories() { | ||
return ['reply', 'moderate']; // more_comments? Need to check whether this ever applies with PMs | ||
} | ||
}; | ||
objects.Subreddit = class Subreddit extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
constructor(options, _ac, has_fetched) { | ||
super(options, _ac, has_fetched); | ||
} | ||
get _uri() { | ||
return `/r/${ this.display_name }/about`; | ||
return `r/${ this.display_name }/about`; | ||
} | ||
get_moderators() { | ||
return this._fetcher.oauth_requester.get(`/r/${ this.display_name }/about/moderators`); | ||
return this._ac.get(`r/${ this.display_name }/about/moderators`); | ||
} | ||
static get inherited_action_categories() { | ||
return []; | ||
} | ||
}; | ||
objects.Trophy = class Trophy extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
constructor(options, _ac, has_fetched) { | ||
super(options, _ac, has_fetched); | ||
} | ||
@@ -198,17 +289,147 @@ }; | ||
objects.PromoCampaign = class PromoCampaign extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
constructor(options, _ac, has_fetched) { | ||
super(options, _ac, has_fetched); | ||
} | ||
}; | ||
objects.Listing = class Listing extends objects.RedditContent { | ||
constructor(options, _fetcher, has_fetched) { | ||
super(options, _fetcher, has_fetched); | ||
objects.Listing = class Listing extends Array { | ||
constructor() { | ||
var _ref = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var _ref$children = _ref.children; | ||
let children = _ref$children === undefined ? [] : _ref$children; | ||
var _ref$query = _ref.query; | ||
let query = _ref$query === undefined ? {} : _ref$query; | ||
var _ref$show_all = _ref.show_all; | ||
let show_all = _ref$show_all === undefined ? true : _ref$show_all; | ||
let limit = _ref.limit; | ||
var _ref$_transform = _ref._transform; | ||
let _transform = _ref$_transform === undefined ? _.identity : _ref$_transform; | ||
let _uri = _ref._uri; | ||
let method = _ref.method; | ||
let after = _ref.after; | ||
let before = _ref.before; | ||
var _ref$_is_comment_list = _ref._is_comment_list; | ||
let _is_comment_list = _ref$_is_comment_list === undefined ? false : _ref$_is_comment_list; | ||
let _ac = arguments[1]; | ||
super(); | ||
_.assign(this, children); | ||
let constant_params = _.assign(query, { show: show_all ? 'all' : undefined, limit }); | ||
this._ac = _ac; | ||
this._requester = _ac._oauth_requester.defaults({ uri: _uri, method, qs: constant_params }); | ||
this._transform = _transform; | ||
this.limit = limit; | ||
this.after = after; | ||
this.before = before; | ||
this._is_comment_list = _is_comment_list; | ||
return new Proxy(this, { get: (target, key, thisArg) => { | ||
if (!isNaN(key) && key >= target.length) { | ||
return promise_wrap(target.fetch({ amount: key - target.length + 1 }).then(_.last)); | ||
} | ||
return Reflect.get(target, key, thisArg); | ||
} }); | ||
} | ||
get is_finished() { | ||
if (this._is_comment_list) { | ||
return !this._more || !this._more.children.length; | ||
} | ||
return !!this.uri && this.after === null && this.before === null; | ||
} | ||
fetch(_ref2) { | ||
var _ref2$amount = _ref2.amount; | ||
let amount = _ref2$amount === undefined ? this.limit : _ref2$amount; | ||
if (typeof amount !== 'number') { | ||
throw new errors.InvalidMethodCallError('Failed to fetch listing. (amount must be a Number.)'); | ||
} | ||
if (amount <= 0 || this.is_finished) { | ||
return []; | ||
} | ||
if (this._is_comment_list) { | ||
return promise_wrap(this._fetch_more_comments({ amount })); | ||
} | ||
return promise_wrap(this._fetch_more_regular({ amount })); | ||
} | ||
_fetch_more_regular(_ref3) { | ||
var _this3 = this; | ||
let amount = _ref3.amount; | ||
return _asyncToGenerator(function* () { | ||
let limit_for_request = Math.min(amount, _this3.limit || 0); | ||
let request_params = { qs: { after: _this3.after, before: _this3.before, limit: limit_for_request } }; | ||
let response = yield _this3._requester(request_params).then(_this3._transform); | ||
if (_this3.length === 0 && _.last(response) instanceof objects.more) { | ||
_this3._more = response.pop(); | ||
_this3._is_comment_list = true; | ||
} | ||
_this3.push(..._.toArray(response)); | ||
_this3.before = response.before; | ||
_this3.after = response.after; | ||
return _this3.slice(0, amount).concat((yield _this3.fetch({ amount: amount - response.length }))); | ||
})(); | ||
} | ||
/* Pagination for comments works differently than it does for most other things; rather than sending a link to the next page | ||
within a listing, reddit sends the last comment in the list as as a `more` object, with links to all the remaining comments | ||
in the thread. */ | ||
_fetch_more_comments() { | ||
var _this4 = this; | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
return _asyncToGenerator(function* () { | ||
let new_comments = _this4._more ? yield _this4._more.fetch(...args) : []; | ||
_this4.push(..._.toArray(new_comments)); | ||
return new_comments; | ||
})(); | ||
} | ||
fetch_all() { | ||
return this.fetch({ amount: Infinity }); | ||
} | ||
inspect() { | ||
return util.inspect(_.omitBy(this, (value, key) => key.startsWith('_'))); | ||
} | ||
}; | ||
objects.UserList = class UserList extends objects.RedditContent { | ||
constructor(options, _fetcher) { | ||
objects.more = class more extends objects.RedditContent { | ||
constructor(properties, _ac) { | ||
super(properties, _ac); | ||
} | ||
/* Requests to /api/morechildren are capped at 20 comments at a time, but requests to /api/info are capped at 100, so | ||
it's easier to send to the latter. The disadvantage is that comment replies are not automatically sent from requests | ||
to /api/info. */ | ||
fetch(_ref4) { | ||
var _this5 = this; | ||
var _ref4$amount = _ref4.amount; | ||
let amount = _ref4$amount === undefined ? Infinity : _ref4$amount; | ||
return _asyncToGenerator(function* () { | ||
if (isNaN(amount)) { | ||
throw new errors.InvalidMethodCallError('Failed to fetch listing. (`amount` must be a Number.)'); | ||
} | ||
if (amount <= 0 || _this5.children.length === 0) { | ||
return []; | ||
} | ||
let ids_for_this_request = _this5.children.splice(0, Math.min(amount, 100)).map(function (id) { | ||
return `t1_${ id }`; | ||
}); | ||
// Requests are capped at 100 comments. Send lots of requests recursively to get the comments, then concatenate them. | ||
// (This speed-requesting is only possible with comment listings since the entire list of ids is present initially.) | ||
let promise_for_this_batch = _this5._ac.get({ uri: 'api/info', qs: { id: ids_for_this_request.join(',') } }); | ||
let promise_for_remaining_items = _this5.fetch({ amount: amount - ids_for_this_request.length }); | ||
return _.toArray((yield promise_for_this_batch)).concat((yield promise_for_remaining_items)); | ||
})(); | ||
} | ||
}; | ||
objects.UserList = class UserList { | ||
constructor(options, _ac) { | ||
return options.children.map(user => { | ||
return new objects.RedditUser(user, _fetcher); | ||
return new objects.RedditUser(user, _ac); | ||
}); | ||
@@ -218,8 +439,11 @@ } | ||
helpers._populate = (response_tree, _fetcher) => { | ||
helpers._populate = (response_tree, _ac) => { | ||
if (typeof response_tree === 'object' && response_tree !== null) { | ||
// Map {kind: 't2', data: {name: 'some_username', ... }} to a RedditUser (e.g.) with the same properties | ||
if (_.keys(response_tree).length === 2 && response_tree.kind && constants.KINDS[response_tree.kind]) { | ||
let remainder_of_tree = helpers._populate(response_tree.data, _fetcher); | ||
return new objects[constants.KINDS[response_tree.kind]](remainder_of_tree, _fetcher, true); | ||
if (_.keys(response_tree).length === 2 && response_tree.kind) { | ||
let remainder_of_tree = helpers._populate(response_tree.data, _ac); | ||
if (constants.KINDS[response_tree.kind]) { | ||
return new objects[constants.KINDS[response_tree.kind]](remainder_of_tree, _ac, true); | ||
} | ||
_ac.warn(`Unknown type ${ response_tree.kind }. This may be a bug; please report it at ${ constants.ISSUE_REPORT_LINK }.`); | ||
} | ||
@@ -230,8 +454,8 @@ let mapFunction = Array.isArray(response_tree) ? _.map : _.mapValues; | ||
if (_.includes(constants.USER_KEYS, key)) { | ||
return new objects.RedditUser({ name: value }, _fetcher); | ||
return new objects.RedditUser({ name: value }, _ac); | ||
} | ||
if (_.includes(constants.SUBREDDIT_KEYS, key)) { | ||
return new objects.Subreddit({ display_name: value }, _fetcher); | ||
return new objects.Subreddit({ display_name: value }, _ac); | ||
} | ||
return helpers._populate(value, _fetcher); | ||
return helpers._populate(value, _ac); | ||
}); | ||
@@ -242,4 +466,13 @@ } | ||
/* Assign all the action functions to the class prototypes. Actions are split into categories (moderation, voting, etc.), | ||
which are defined in actions.js. Each class should have the inherited_action_categories property, which is a list of strings | ||
corresponding to the action categories which apply to objects of that class. */ | ||
_(objects).forOwn(object_class => { | ||
_(actions).pick(object_class.inherited_action_categories || []).forOwn(action_category => { | ||
_.assign(object_class.prototype, action_category); | ||
}); | ||
}); | ||
snoowrap.objects = objects; | ||
snoowrap.helpers = helpers; | ||
module.exports = snoowrap; |
{ | ||
"name": "snoowrap", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"license": "Apache-2.0", | ||
@@ -9,4 +9,5 @@ "description": "A Node.js wrapper for the reddit API", | ||
"compile": "babel -d lib/ src/", | ||
"prepublish": "npm run compile", | ||
"test": "eslint --ignore-path .gitignore . && mocha --harmony_proxies --compilers js:babel-core/register" | ||
"pretest": "eslint --ignore-path .gitignore . && npm run compile", | ||
"test": "mocha --harmony_proxies --compilers js:babel-core/register", | ||
"prepublish": "npm test" | ||
}, | ||
@@ -17,3 +18,9 @@ "repository": { | ||
}, | ||
"keywords": [], | ||
"keywords": [ | ||
"reddit", | ||
"api", | ||
"wrapper", | ||
"praw", | ||
"snoo" | ||
], | ||
"author": "not-an-aardvark <not-an-aardvark@users.noreply.github.com>", | ||
@@ -25,3 +32,5 @@ "bugs": { | ||
"plugins": [ | ||
"transform-async-to-generator" | ||
"transform-async-to-generator", | ||
"transform-es2015-destructuring", | ||
"transform-es2015-parameters" | ||
] | ||
@@ -31,18 +40,20 @@ }, | ||
"dependencies": { | ||
"bluebird": "^3.1.1", | ||
"bluebird": "^3.1.5", | ||
"harmony-reflect": "^1.4.2", | ||
"lodash": "^4.0.0", | ||
"lodash": "^4.1.0", | ||
"moment": "^2.11.1", | ||
"promise-chains": "^0.1.1", | ||
"promise-chains": "^0.1.2", | ||
"request-promise": "^2.0.0" | ||
}, | ||
"devDependencies": { | ||
"babel-cli": "^6.4.0", | ||
"babel-core": "^6.4.0", | ||
"babel-eslint": "^4.1.6", | ||
"babel-plugin-transform-async-to-generator": "^6.4.0", | ||
"chai": "^3.4.1", | ||
"babel-cli": "^6.4.5", | ||
"babel-core": "^6.4.5", | ||
"babel-eslint": "^5.0.0-beta8", | ||
"babel-plugin-transform-async-to-generator": "^6.4.6", | ||
"babel-plugin-transform-es2015-destructuring": "^6.4.0", | ||
"babel-plugin-transform-es2015-parameters": "^6.4.5", | ||
"chai": "^3.5.0", | ||
"chai-as-promised": "^5.2.0", | ||
"mocha": "^2.3.4" | ||
"mocha": "^2.4.5" | ||
} | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
37466
8
556
9
1
Updatedbluebird@^3.1.5
Updatedlodash@^4.1.0
Updatedpromise-chains@^0.1.2