mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-11 00:53:31 +01:00
lots of important stuff (#1883)
* lots of important stuff * Update Constants.js
This commit is contained in:
40
src/rest/APIRequest.js
Normal file
40
src/rest/APIRequest.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const querystring = require('querystring');
|
||||
const snekfetch = require('snekfetch');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
gen() {
|
||||
const API = this.options.versioned === false ? this.client.options.http.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.rest.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.headers) request.set(this.options.headers);
|
||||
|
||||
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;
|
||||
35
src/rest/APIRouter.js
Normal file
35
src/rest/APIRouter.js
Normal file
@@ -0,0 +1,35 @@
|
||||
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({
|
||||
versioned: manager.versioned,
|
||||
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;
|
||||
54
src/rest/DiscordAPIError.js
Normal file
54
src/rest/DiscordAPIError.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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;
|
||||
73
src/rest/RESTManager.js
Normal file
73
src/rest/RESTManager.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const UserAgentManager = require('./UserAgentManager');
|
||||
const handlers = require('./handlers');
|
||||
const APIRequest = require('./APIRequest');
|
||||
const routeBuilder = require('./APIRouter');
|
||||
const { Error } = require('../errors');
|
||||
const Constants = require('../util/Constants');
|
||||
|
||||
class RESTManager {
|
||||
constructor(client, tokenPrefix = 'Bot') {
|
||||
this.client = client;
|
||||
this.handlers = {};
|
||||
this.userAgentManager = new UserAgentManager(this);
|
||||
this.rateLimitedEndpoints = {};
|
||||
this.globallyRateLimited = false;
|
||||
this.tokenPrefix = tokenPrefix;
|
||||
this.versioned = true;
|
||||
}
|
||||
|
||||
get api() {
|
||||
return routeBuilder(this);
|
||||
}
|
||||
|
||||
getAuth() {
|
||||
const token = this.client.token || this.client.accessToken;
|
||||
const prefixed = !!this.client.application || (this.client.user && this.client.user.bot);
|
||||
if (token && prefixed) return `${this.tokenPrefix} ${token}`;
|
||||
else if (token) return token;
|
||||
throw new Error('TOKEN_MISSING');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
set endpoint(endpoint) {
|
||||
this.client.options.http.api = endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RESTManager;
|
||||
25
src/rest/UserAgentManager.js
Normal file
25
src/rest/UserAgentManager.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
70
src/rest/handlers/RequestHandler.js
Normal file
70
src/rest/handlers/RequestHandler.js
Normal file
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
13
src/rest/handlers/burst.js
Normal file
13
src/rest/handlers/burst.js
Normal file
@@ -0,0 +1,13 @@
|
||||
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();
|
||||
};
|
||||
5
src/rest/handlers/index.js
Normal file
5
src/rest/handlers/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
sequential: require('./sequential'),
|
||||
burst: require('./burst'),
|
||||
RequestHandler: require('./RequestHandler'),
|
||||
};
|
||||
16
src/rest/handlers/sequential.js
Normal file
16
src/rest/handlers/sequential.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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));
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user