lots of important stuff (#1883)

* lots of important stuff

* Update Constants.js
This commit is contained in:
Gus Caplan
2017-09-04 10:49:44 -05:00
committed by Crawl
parent 3a503ef56e
commit 18e3801bb7
15 changed files with 212 additions and 253 deletions

126
src/client/BaseClient.js Normal file
View File

@@ -0,0 +1,126 @@
const EventEmitter = require('events');
const RESTManager = require('../rest/RESTManager');
const ClientDataResolver = require('./ClientDataResolver');
const Util = require('../util/Util');
const Constants = require('../util/Constants');
/**
* The base class for all clients.
* @extends {EventEmitter}
*/
class BaseClient extends EventEmitter {
constructor(options = {}) {
super();
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
/**
* The REST manager of the client
* @type {RESTManager}
* @private
*/
this.rest = new RESTManager(this, options._tokenType);
/**
* The data resolver of the client
* @type {ClientDataResolver}
* @private
*/
this.resolver = new ClientDataResolver(this);
/**
* Timeouts set by {@link WebhookClient#setTimeout} that are still active
* @type {Set<Timeout>}
* @private
*/
this._timeouts = new Set();
/**
* Intervals set by {@link WebhookClient#setInterval} that are still active
* @type {Set<Timeout>}
* @private
*/
this._intervals = new Set();
}
/**
* Whether the client is in a browser environment
* @type {boolean}
* @readonly
*/
get browser() {
return typeof window !== 'undefined';
}
/**
* API shortcut
* @type {Object}
* @private
*/
get api() {
return this.rest.api;
}
/**
* Destroys all assets used by the base client.
*/
destroy() {
for (const t of this._timeouts) clearTimeout(t);
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
}
module.exports = BaseClient;

View File

@@ -1,10 +1,7 @@
const EventEmitter = require('events');
const Constants = require('../util/Constants');
const BaseClient = require('./BaseClient');
const Permissions = require('../util/Permissions');
const Util = require('../util/Util');
const RESTManager = require('./rest/RESTManager');
const RESTManager = require('../rest/RESTManager');
const ClientManager = require('./ClientManager');
const ClientDataResolver = require('./ClientDataResolver');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
const WebSocketManager = require('./websocket/WebSocketManager');
const ActionsManager = require('./actions/ActionsManager');
@@ -19,28 +16,24 @@ const UserStore = require('../stores/UserStore');
const ChannelStore = require('../stores/ChannelStore');
const GuildStore = require('../stores/GuildStore');
const ClientPresenceStore = require('../stores/ClientPresenceStore');
const Constants = require('../util/Constants');
const { Error, TypeError, RangeError } = require('../errors');
/**
* The main hub for interacting with the Discord API, and the starting point for any bot.
* @extends {EventEmitter}
* @extends {BaseClient}
*/
class Client extends EventEmitter {
class Client extends BaseClient {
/**
* @param {ClientOptions} [options] Options for the client
*/
constructor(options = {}) {
super();
super(Object.assign({ _tokenType: 'Bot' }, options));
// Obtain shard details from environment
if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID);
if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT);
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
this._validateOptions();
/**
@@ -64,13 +57,6 @@ class Client extends EventEmitter {
*/
this.ws = new WebSocketManager(this);
/**
* The data resolver of the client
* @type {ClientDataResolver}
* @private
*/
this.resolver = new ClientDataResolver(this);
/**
* The action manager of the client
* @type {ActionsManager}
@@ -184,15 +170,6 @@ class Client extends EventEmitter {
return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0;
}
/**
* API shortcut
* @type {Object}
* @private
*/
get api() {
return this.rest.api;
}
/**
* Current status of the client's connection to Discord
* @type {?Status}
@@ -252,15 +229,6 @@ class Client extends EventEmitter {
return this.readyAt ? this.readyAt.getTime() : null;
}
/**
* Whether the client is in a browser environment
* @type {boolean}
* @readonly
*/
get browser() {
return typeof window !== 'undefined';
}
/**
* Creates a voice broadcast.
* @returns {VoiceBroadcast}
@@ -298,10 +266,7 @@ class Client extends EventEmitter {
* @returns {Promise}
*/
destroy() {
for (const t of this._timeouts) clearTimeout(t);
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
super.destroy();
return this.manager.destroy();
}
@@ -423,53 +388,6 @@ class Client extends EventEmitter {
);
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
/**
* Adds a ping to {@link Client#pings}.
* @param {number} startTime Starting time of the ping

View File

@@ -1,14 +1,12 @@
const Webhook = require('../structures/Webhook');
const RESTManager = require('./rest/RESTManager');
const ClientDataResolver = require('./ClientDataResolver');
const Constants = require('../util/Constants');
const Util = require('../util/Util');
const BaseClient = require('./BaseClient');
/**
* The webhook client.
* @extends {Webhook}
* @extends {BaseClient}
*/
class WebhookClient extends Webhook {
class WebhookClient extends BaseClient {
/**
* @param {Snowflake} id ID of the webhook
* @param {string} token Token of the webhook
@@ -19,109 +17,11 @@ class WebhookClient extends Webhook {
* hook.sendMessage('This will send a message').catch(console.error);
*/
constructor(id, token, options) {
super(null, id, token);
/**
* The options the client was instantiated with
* @type {ClientOptions}
*/
this.options = Util.mergeDefault(Constants.DefaultOptions, options);
/**
* The REST manager of the client
* @type {RESTManager}
* @private
*/
this.rest = new RESTManager(this);
/**
* The data resolver of the client
* @type {ClientDataResolver}
* @private
*/
this.resolver = new ClientDataResolver(this);
/**
* Timeouts set by {@link WebhookClient#setTimeout} that are still active
* @type {Set<Timeout>}
* @private
*/
this._timeouts = new Set();
/**
* Intervals set by {@link WebhookClient#setInterval} that are still active
* @type {Set<Timeout>}
* @private
*/
this._intervals = new Set();
}
/**
* API shortcut
* @type {Object}
* @private
*/
get api() {
return this.rest.api;
}
/**
* Sets a timeout that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setTimeout(fn, delay, ...args) {
const timeout = setTimeout(() => {
fn(...args);
this._timeouts.delete(timeout);
}, delay);
this._timeouts.add(timeout);
return timeout;
}
/**
* Clears a timeout.
* @param {Timeout} timeout Timeout to cancel
*/
clearTimeout(timeout) {
clearTimeout(timeout);
this._timeouts.delete(timeout);
}
/**
* Sets an interval that will be automatically cancelled if the client is destroyed.
* @param {Function} fn Function to execute
* @param {number} delay Time to wait before executing (in milliseconds)
* @param {...*} args Arguments for the function
* @returns {Timeout}
*/
setInterval(fn, delay, ...args) {
const interval = setInterval(fn, delay, ...args);
this._intervals.add(interval);
return interval;
}
/**
* Clears an interval.
* @param {Timeout} interval Interval to cancel
*/
clearInterval(interval) {
clearInterval(interval);
this._intervals.delete(interval);
}
/**
* Destroys the client.
*/
destroy() {
for (const t of this._timeouts) clearTimeout(t);
for (const i of this._intervals) clearInterval(i);
this._timeouts.clear();
this._intervals.clear();
super(options);
Webhook.call(this, null, id, token);
}
}
Object.assign(WebhookClient.prototype, Object.create(Webhook.prototype));
module.exports = WebhookClient;

View File

@@ -1,48 +0,0 @@
const querystring = require('querystring');
const snekfetch = require('snekfetch');
const { Error } = require('../../errors');
class APIRequest {
constructor(rest, method, path, options) {
this.rest = rest;
this.client = rest.client;
this.method = method;
this.path = path.toString();
this.route = options.route;
this.options = options;
}
getAuth() {
if (this.client.token && this.client.user && this.client.user.bot) {
return `Bot ${this.client.token}`;
} else if (this.client.token) {
return this.client.token;
}
throw new Error('TOKEN_MISSING');
}
gen() {
const API = `${this.client.options.http.api}/v${this.client.options.http.version}`;
if (this.options.query) {
const queryString = (querystring.stringify(this.options.query).match(/[^=&?]+=[^=&?]+/g) || []).join('&');
this.path += `?${queryString}`;
}
const request = snekfetch[this.method](`${API}${this.path}`);
if (this.options.auth !== false) request.set('Authorization', this.getAuth());
if (this.options.reason) request.set('X-Audit-Log-Reason', encodeURIComponent(this.options.reason));
if (!this.rest.client.browser) request.set('User-Agent', this.rest.userAgentManager.userAgent);
if (this.options.files) {
for (const file of this.options.files) if (file && file.file) request.attach(file.name, file.file, file.name);
if (typeof this.options.data !== 'undefined') request.attach('payload_json', JSON.stringify(this.options.data));
} else if (typeof this.options.data !== 'undefined') {
request.send(this.options.data);
}
return request;
}
}
module.exports = APIRequest;

View File

@@ -1,34 +0,0 @@
const util = require('util');
const noop = () => {}; // eslint-disable-line no-empty-function
const methods = ['get', 'post', 'delete', 'patch', 'put'];
const reflectors = [
'toString', 'valueOf', 'inspect', 'constructor',
Symbol.toPrimitive, util.inspect.custom,
];
function buildRoute(manager) {
const route = [''];
const handler = {
get(target, name) {
if (reflectors.includes(name)) return () => route.join('/');
if (methods.includes(name)) {
return options => manager.request(name, route.join('/'), Object.assign({
route: route.map((r, i) => {
if (/\d{16,19}/g.test(r)) return /channels|guilds/.test(route[i - 1]) ? r : ':id';
return r;
}).join('/'),
}, options));
}
route.push(name);
return new Proxy(noop, handler);
},
apply(target, _, args) {
route.push(...args.filter(x => x != null)); // eslint-disable-line eqeqeq
return new Proxy(noop, handler);
},
};
return new Proxy(noop, handler);
}
module.exports = buildRoute;

View File

@@ -1,54 +0,0 @@
/**
* Represents an error from the Discord API.
* @extends Error
*/
class DiscordAPIError extends Error {
constructor(path, error) {
super();
const flattened = this.constructor.flattenErrors(error.errors || error).join('\n');
this.name = 'DiscordAPIError';
this.message = error.message && flattened ? `${error.message}\n${flattened}` : error.message || flattened;
/**
* The path of the request relative to the HTTP endpoint
* @type {string}
*/
this.path = path;
/**
* HTTP error code returned by Discord
* @type {number}
*/
this.code = error.code;
}
/**
* Flattens an errors object returned from the API into an array.
* @param {Object} obj Discord errors object
* @param {string} [key] Used internally to determine key names of nested fields
* @returns {string[]}
* @private
*/
static flattenErrors(obj, key = '') {
let messages = [];
for (const [k, v] of Object.entries(obj)) {
if (k === 'message') continue;
const newKey = key ? isNaN(k) ? `${key}.${k}` : `${key}[${k}]` : k;
if (v._errors) {
messages.push(`${newKey}: ${v._errors.map(e => e.message).join(' ')}`);
} else if (v.code || v.message) {
messages.push(`${v.code ? `${v.code}: ` : ''}${v.message}`.trim());
} else if (typeof v === 'string') {
messages.push(v);
} else {
messages = messages.concat(this.flattenErrors(v, newKey));
}
}
return messages;
}
}
module.exports = DiscordAPIError;

View File

@@ -1,59 +0,0 @@
const UserAgentManager = require('./UserAgentManager');
const handlers = require('./handlers');
const APIRequest = require('./APIRequest');
const routeBuilder = require('./APIRouter');
const Constants = require('../../util/Constants');
const { Error } = require('../../errors');
class RESTManager {
constructor(client) {
this.client = client;
this.handlers = {};
this.userAgentManager = new UserAgentManager(this);
this.rateLimitedEndpoints = {};
this.globallyRateLimited = false;
}
get api() {
return routeBuilder(this);
}
get cdn() {
return Constants.Endpoints.CDN(this.client.options.http.cdn);
}
destroy() {
for (const handler of Object.values(this.handlers)) {
if (handler.destroy) handler.destroy();
}
}
push(handler, apiRequest) {
return new Promise((resolve, reject) => {
handler.push({
request: apiRequest,
resolve,
reject,
});
});
}
getRequestHandler() {
const method = this.client.options.apiRequestMethod;
if (typeof method === 'function') return method;
const handler = handlers[method];
if (!handler) throw new Error('RATELIMIT_INVALID_METHOD');
return handler;
}
request(method, url, options = {}) {
const apiRequest = new APIRequest(this, method, url, options);
if (!this.handlers[apiRequest.route]) {
this.handlers[apiRequest.route] = new handlers.RequestHandler(this, this.getRequestHandler());
}
return this.push(this.handlers[apiRequest.route], apiRequest);
}
}
module.exports = RESTManager;

View File

@@ -1,25 +0,0 @@
const Constants = require('../../util/Constants');
class UserAgentManager {
constructor() {
this.build(this.constructor.DEFAULT);
}
set({ url, version } = {}) {
this.build({
url: url || this.constructor.DFEAULT.url,
version: version || this.constructor.DEFAULT.version,
});
}
build(ua) {
this.userAgent = `DiscordBot (${ua.url}, ${ua.version}) Node.js/${process.version}`;
}
}
UserAgentManager.DEFAULT = {
url: Constants.Package.homepage.split('#')[0],
version: Constants.Package.version,
};
module.exports = UserAgentManager;

View File

@@ -1,70 +0,0 @@
const DiscordAPIError = require('../DiscordAPIError');
class RequestHandler {
constructor(manager, handler) {
this.manager = manager;
this.client = this.manager.client;
this.handle = handler.bind(this);
this.limit = Infinity;
this.resetTime = null;
this.remaining = 1;
this.timeDifference = 0;
this.queue = [];
}
get limited() {
return this.manager.globallyRateLimited || this.remaining <= 0;
}
set globallyLimited(limited) {
this.manager.globallyRateLimited = limited;
}
push(request) {
this.queue.push(request);
this.handle();
}
execute(item) {
return new Promise((resolve, reject) => {
const finish = timeout => {
// eslint-disable-next-line prefer-promise-reject-errors
if (timeout || this.limited) reject({ timeout, limited: this.limited });
else resolve();
};
item.request.gen().end((err, res) => {
if (res && res.headers) {
if (res.headers['x-ratelimit-global']) this.globallyLimited = true;
this.limit = Number(res.headers['x-ratelimit-limit']);
this.resetTime = Number(res.headers['x-ratelimit-reset']) * 1000;
this.remaining = Number(res.headers['x-ratelimit-remaining']);
this.timeDifference = Date.now() - new Date(res.headers.date).getTime();
}
if (err) {
if (err.status === 429) {
this.queue.unshift(item);
finish(Number(res.headers['retry-after']) + this.client.options.restTimeOffset);
} else if (err.status >= 500 && err.status < 600) {
this.queue.unshift(item);
finish(1e3 + this.client.options.restTimeOffset);
} else {
item.reject(err.status >= 400 && err.status < 500 ? new DiscordAPIError(res.request.path, res.body) : err);
finish();
}
} else {
const data = res && res.body ? res.body : {};
item.resolve(data);
finish();
}
});
});
}
reset() {
this.globallyLimited = false;
this.remaining = 1;
}
}
module.exports = RequestHandler;

View File

@@ -1,13 +0,0 @@
module.exports = function burst() {
if (this.limited || this.queue.length === 0) return;
this.execute(this.queue.shift())
.then(this.handle.bind(this))
.catch(({ timeout }) => {
this.client.setTimeout(() => {
this.reset();
this.handle();
}, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset));
});
this.remaining--;
this.handle();
};

View File

@@ -1,5 +0,0 @@
module.exports = {
sequential: require('./sequential'),
burst: require('./burst'),
RequestHandler: require('./RequestHandler'),
};

View File

@@ -1,16 +0,0 @@
module.exports = function sequential() {
if (this.busy || this.limited || this.queue.length === 0) return;
this.busy = true;
this.execute(this.queue.shift())
.then(() => {
this.busy = false;
this.handle();
})
.catch(({ timeout }) => {
this.client.setTimeout(() => {
this.reset();
this.busy = false;
this.handle();
}, timeout || (this.resetTime - Date.now() + this.timeDifference + this.client.options.restTimeOffset));
});
};