refactor: use @discordjs/rest (#7298)

Co-authored-by: ckohen <chaikohen@gmail.com>
This commit is contained in:
Jan
2022-01-26 10:45:04 +01:00
committed by GitHub
parent ac26d9b130
commit ec0fba1ed0
72 changed files with 525 additions and 1546 deletions

View File

@@ -49,12 +49,10 @@
"dependencies": {
"@discordjs/builders": "^0.12.0",
"@discordjs/collection": "^0.5.0",
"@sapphire/async-queue": "^1.1.9",
"@discordjs/rest": "^0.3.0",
"@sapphire/snowflake": "^3.0.1",
"@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.26.1",
"form-data": "^4.0.0",
"node-fetch": "^2.6.7",
"ws": "^8.4.2"
},

View File

@@ -1,9 +1,8 @@
'use strict';
const EventEmitter = require('node:events');
const { clearInterval } = require('node:timers');
const { REST } = require('@discordjs/rest');
const { TypeError } = require('../errors');
const RESTManager = require('../rest/RESTManager');
const Options = require('../util/Options');
const Util = require('../util/Util');
@@ -27,20 +26,9 @@ class BaseClient extends EventEmitter {
/**
* The REST manager of the client
* @type {RESTManager}
* @private
* @type {REST}
*/
this.rest = new RESTManager(this);
}
/**
* API shortcut
* @type {Object}
* @readonly
* @private
*/
get api() {
return this.rest.api;
this.rest = new REST(this.options.rest);
}
/**
@@ -48,7 +36,8 @@ class BaseClient extends EventEmitter {
* @returns {void}
*/
destroy() {
if (this.rest.sweepInterval) clearInterval(this.rest.sweepInterval);
this.rest.requestManager.clearHashSweeper();
this.rest.requestManager.clearHandlerSweeper();
}
/**
@@ -81,7 +70,6 @@ class BaseClient extends EventEmitter {
module.exports = BaseClient;
/**
* Emitted for general debugging information.
* @event BaseClient#debug
* @param {string} info The debug information
* @external REST
* @see {@link https://discord.js.org/#/docs/rest/main/class/REST}
*/

View File

@@ -2,6 +2,7 @@
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const BaseClient = require('./BaseClient');
const ActionsManager = require('./actions/ActionsManager');
const ClientVoiceManager = require('./voice/ClientVoiceManager');
@@ -210,6 +211,7 @@ class Client extends BaseClient {
async login(token = this.token) {
if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
this.rest.setToken(token);
this.emit(
Events.DEBUG,
`Provided token: ${token
@@ -252,6 +254,7 @@ class Client extends BaseClient {
this.sweepers.destroy();
this.ws.destroy();
this.token = null;
this.rest.setToken(null);
}
/**
@@ -273,9 +276,14 @@ class Client extends BaseClient {
*/
async fetchInvite(invite, options) {
const code = DataResolver.resolveInviteCode(invite);
const data = await this.api.invites(code).get({
query: { with_counts: true, with_expiration: true, guild_scheduled_event_id: options?.guildScheduledEventId },
const query = new URLSearchParams({
with_counts: true,
with_expiration: true,
});
if (options?.guildScheduledEventId) {
query.set('guild_scheduled_event_id', options.guildScheduledEventId);
}
const data = await this.rest.get(Routes.invite(code), { query });
return new Invite(this, data);
}
@@ -290,7 +298,7 @@ class Client extends BaseClient {
*/
async fetchGuildTemplate(template) {
const code = DataResolver.resolveGuildTemplateCode(template);
const data = await this.api.guilds.templates(code).get();
const data = await this.rest.get(Routes.template(code));
return new GuildTemplate(this, data);
}
@@ -305,7 +313,7 @@ class Client extends BaseClient {
* .catch(console.error);
*/
async fetchWebhook(id, token) {
const data = await this.api.webhooks(id, token).get();
const data = await this.rest.get(Routes.webhook(id, token));
return new Webhook(this, { token, ...data });
}
@@ -318,7 +326,7 @@ class Client extends BaseClient {
* .catch(console.error);
*/
async fetchVoiceRegions() {
const apiRegions = await this.api.voice.regions.get();
const apiRegions = await this.rest.get(Routes.voiceRegions());
const regions = new Collection();
for (const region of apiRegions) regions.set(region.id, new VoiceRegion(region));
return regions;
@@ -334,7 +342,7 @@ class Client extends BaseClient {
* .catch(console.error);
*/
async fetchSticker(id) {
const data = await this.api.stickers(id).get();
const data = await this.rest.get(Routes.sticker(id));
return new Sticker(this, data);
}
@@ -347,7 +355,7 @@ class Client extends BaseClient {
* .catch(console.error);
*/
async fetchPremiumStickerPacks() {
const data = await this.api('sticker-packs').get();
const data = await this.rest.get(Routes.nitroStickerPacks());
return new Collection(data.sticker_packs.map(p => [p.id, new StickerPack(this, p)]));
}
@@ -359,7 +367,7 @@ class Client extends BaseClient {
async fetchGuildPreview(guild) {
const id = this.guilds.resolveId(guild);
if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
const data = await this.api.guilds(id).preview.get();
const data = await this.rest.get(Routes.guildPreview(id));
return new GuildPreview(this, data);
}
@@ -371,7 +379,7 @@ class Client extends BaseClient {
async fetchGuildWidget(guild) {
const id = this.guilds.resolveId(guild);
if (!id) throw new TypeError('INVALID_TYPE', 'guild', 'GuildResolvable');
const data = await this.api.guilds(id, 'widget.json').get();
const data = await this.rest.get(Routes.guildWidgetJSON(id));
return new Widget(this, data);
}
@@ -443,7 +451,7 @@ class Client extends BaseClient {
query.set('guild_id', guildId);
}
return `${this.options.http.api}${this.api.oauth2.authorize}?${query}`;
return `${this.options.rest.api}${Routes.oauth2Authorization()}?${query}`;
}
toJSON() {
@@ -487,39 +495,15 @@ class Client extends BaseClient {
if (typeof options.sweepers !== 'object' || options.sweepers === null) {
throw new TypeError('CLIENT_INVALID_OPTION', 'sweepers', 'an object');
}
if (typeof options.invalidRequestWarningInterval !== 'number' || isNaN(options.invalidRequestWarningInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'invalidRequestWarningInterval', 'a number');
}
if (!Array.isArray(options.partials)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'partials', 'an Array');
}
if (typeof options.waitGuildTimeout !== 'number' || isNaN(options.waitGuildTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'waitGuildTimeout', 'a number');
}
if (typeof options.restRequestTimeout !== 'number' || isNaN(options.restRequestTimeout)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'restRequestTimeout', 'a number');
}
if (typeof options.restGlobalRateLimit !== 'number' || isNaN(options.restGlobalRateLimit)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'restGlobalRateLimit', 'a number');
}
if (typeof options.restSweepInterval !== 'number' || isNaN(options.restSweepInterval)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'restSweepInterval', 'a number');
}
if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'retryLimit', 'a number');
}
if (typeof options.failIfNotExists !== 'boolean') {
throw new TypeError('CLIENT_INVALID_OPTION', 'failIfNotExists', 'a boolean');
}
if (!Array.isArray(options.userAgentSuffix)) {
throw new TypeError('CLIENT_INVALID_OPTION', 'userAgentSuffix', 'an array of strings');
}
if (
typeof options.rejectOnRateLimit !== 'undefined' &&
!(typeof options.rejectOnRateLimit === 'function' || Array.isArray(options.rejectOnRateLimit))
) {
throw new TypeError('CLIENT_INVALID_OPTION', 'rejectOnRateLimit', 'an array or a function');
}
}
}
@@ -538,6 +522,12 @@ module.exports = Client;
* @typedef {string} Snowflake
*/
/**
* Emitted for general debugging information.
* @event Client#debug
* @param {string} info The debug information
*/
/**
* Emitted for general warnings.
* @event Client#warn
@@ -548,3 +538,8 @@ module.exports = Client;
* @external Collection
* @see {@link https://discord.js.org/#/docs/collection/main/class/Collection}
*/
/**
* @external ImageURLOptions
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/ImageURLOptions}
*/

View File

@@ -4,7 +4,7 @@ const EventEmitter = require('node:events');
const { setImmediate } = require('node:timers');
const { setTimeout: sleep } = require('node:timers/promises');
const { Collection } = require('@discordjs/collection');
const { RPCErrorCodes } = require('discord-api-types/v9');
const { Routes, RPCErrorCodes } = require('discord-api-types/v9');
const WebSocketShard = require('./WebSocketShard');
const PacketHandlers = require('./handlers');
const { Error } = require('../../errors');
@@ -131,7 +131,7 @@ class WebSocketManager extends EventEmitter {
url: gatewayURL,
shards: recommendedShards,
session_start_limit: sessionStartLimit,
} = await this.client.api.gateway.bot.get().catch(error => {
} = await this.client.rest.get(Routes.gatewayBot()).catch(error => {
throw error.httpStatus === 401 ? invalidToken : error;
});

View File

@@ -16,16 +16,13 @@ exports.BitField = require('./util/BitField');
exports.Collection = require('@discordjs/collection').Collection;
exports.Constants = require('./util/Constants');
exports.DataResolver = require('./util/DataResolver');
exports.DiscordAPIError = require('./rest/DiscordAPIError');
exports.EnumResolvers = require('./util/EnumResolvers');
exports.Formatters = require('./util/Formatters');
exports.HTTPError = require('./rest/HTTPError');
exports.Intents = require('./util/Intents');
exports.LimitedCollection = require('./util/LimitedCollection');
exports.MessageFlags = require('./util/MessageFlags');
exports.Options = require('./util/Options');
exports.Permissions = require('./util/Permissions');
exports.RateLimitError = require('./rest/RateLimitError');
exports.SnowflakeUtil = require('@sapphire/snowflake').DiscordSnowflake;
exports.Sweepers = require('./util/Sweepers');
exports.SystemChannelFlags = require('./util/SystemChannelFlags');
@@ -181,3 +178,6 @@ exports.ActionRow = require('@discordjs/builders').ActionRow;
exports.ButtonComponent = require('@discordjs/builders').ButtonComponent;
exports.SelectMenuComponent = require('@discordjs/builders').SelectMenuComponent;
exports.SelectMenuOption = require('@discordjs/builders').SelectMenuOption;
exports.DiscordAPIError = require('@discordjs/rest').DiscordAPIError;
exports.HTTPError = require('@discordjs/rest').HTTPError;
exports.RateLimitError = require('@discordjs/rest').RateLimitError;

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
@@ -36,13 +37,23 @@ class ApplicationCommandManager extends CachedManager {
* @param {Snowflake} [options.id] The application command's id
* @param {Snowflake} [options.guildId] The guild's id to use in the path,
* ignored when using a {@link GuildApplicationCommandManager}
* @returns {Object}
* @returns {string}
* @private
*/
commandPath({ id, guildId } = {}) {
let path = this.client.api.applications(this.client.application.id);
if (this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId);
return id ? path.commands(id) : path.commands;
if (this.guild ?? guildId) {
if (id) {
return Routes.applicationGuildCommand(this.client.application.id, this.guild?.id ?? guildId, id);
}
return Routes.applicationGuildCommands(this.client.application.id, this.guild?.id ?? guildId);
}
if (id) {
return Routes.applicationCommand(this.client.application.id, id);
}
return Routes.applicationCommands(this.client.application.id);
}
/**
@@ -89,11 +100,11 @@ class ApplicationCommandManager extends CachedManager {
const existing = this.cache.get(id);
if (existing) return existing;
}
const command = await this.commandPath({ id, guildId }).get();
const command = await this.client.rest.get(this.commandPath({ id, guildId }));
return this._add(command, cache);
}
const data = await this.commandPath({ guildId }).get();
const data = await this.client.rest.get(this.commandPath({ guildId }));
return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection());
}
@@ -113,8 +124,8 @@ class ApplicationCommandManager extends CachedManager {
* .catch(console.error);
*/
async create(command, guildId) {
const data = await this.commandPath({ guildId }).post({
data: this.constructor.transformCommand(command),
const data = await this.client.rest.post(this.commandPath({ guildId }), {
body: this.constructor.transformCommand(command),
});
return this._add(data, true, guildId);
}
@@ -142,8 +153,8 @@ class ApplicationCommandManager extends CachedManager {
* .catch(console.error);
*/
async set(commands, guildId) {
const data = await this.commandPath({ guildId }).put({
data: commands.map(c => this.constructor.transformCommand(c)),
const data = await this.client.rest.put(this.commandPath({ guildId }), {
body: commands.map(c => this.constructor.transformCommand(c)),
});
return data.reduce((coll, command) => coll.set(command.id, this._add(command, true, guildId)), new Collection());
}
@@ -167,8 +178,8 @@ class ApplicationCommandManager extends CachedManager {
const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
const patched = await this.commandPath({ id, guildId }).patch({
data: this.constructor.transformCommand(data),
const patched = await this.client.rest.patch(this.commandPath({ id, guildId }), {
body: this.constructor.transformCommand(data),
});
return this._add(patched, true, guildId);
}
@@ -189,7 +200,7 @@ class ApplicationCommandManager extends CachedManager {
const id = this.resolveId(command);
if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable');
await this.commandPath({ id, guildId }).delete();
await this.client.rest.delete(this.commandPath({ id, guildId }));
const cached = this.cache.get(id);
this.cache.delete(id);

View File

@@ -1,7 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { RESTJSONErrorCodes } = require('discord-api-types/v9');
const { RESTJSONErrorCodes, Routes } = require('discord-api-types/v9');
const BaseManager = require('./BaseManager');
const { Error, TypeError } = require('../errors');
@@ -43,11 +43,15 @@ class ApplicationCommandPermissionsManager extends BaseManager {
* The APIRouter path to the commands
* @param {Snowflake} guildId The guild's id to use in the path,
* @param {Snowflake} [commandId] The application command's id
* @returns {Object}
* @returns {string}
* @private
*/
permissionsPath(guildId, commandId) {
return this.client.api.applications(this.client.application.id).guilds(guildId).commands(commandId).permissions;
if (commandId) {
return Routes.applicationCommandPermissions(this.client.application.id, guildId, commandId);
}
return Routes.guildApplicationCommandsPermissions(this.client.application.id, guildId);
}
/**
@@ -95,11 +99,11 @@ class ApplicationCommandPermissionsManager extends BaseManager {
async fetch({ guild, command } = {}) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (commandId) {
const data = await this.permissionsPath(guildId, commandId).get();
const data = await this.client.rest.get(this.permissionsPath(guildId, commandId));
return data.permissions;
}
const data = await this.permissionsPath(guildId).get();
const data = await this.client.rest.get(this.permissionsPath(guildId));
return data.reduce((coll, perm) => coll.set(perm.id, perm.permissions), new Collection());
}
@@ -158,7 +162,7 @@ class ApplicationCommandPermissionsManager extends BaseManager {
if (!Array.isArray(permissions)) {
throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true);
}
const data = await this.permissionsPath(guildId, commandId).put({ data: { permissions } });
const data = await this.client.rest.put(this.permissionsPath(guildId, commandId), { body: { permissions } });
return data.permissions;
}
@@ -166,7 +170,7 @@ class ApplicationCommandPermissionsManager extends BaseManager {
throw new TypeError('INVALID_TYPE', 'fullPermissions', 'Array of GuildApplicationCommandPermissionData', true);
}
const data = await this.permissionsPath(guildId).put({ data: fullPermissions });
const data = await this.client.rest.put(this.permissionsPath(guildId), { body: fullPermissions });
return data.reduce((coll, perm) => coll.set(perm.id, perm.permissions), new Collection());
}

View File

@@ -1,6 +1,7 @@
'use strict';
const process = require('node:process');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Channel } = require('../structures/Channel');
const { Events, ThreadChannelTypes } = require('../util/Constants');
@@ -112,7 +113,7 @@ class ChannelManager extends CachedManager {
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.channels(id).get();
const data = await this.client.rest.get(Routes.channel(id));
return this._add(data, null, { cache, allowUnknownGuild });
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const GuildBan = require('../structures/GuildBan');
@@ -107,12 +108,12 @@ class GuildBanManager extends CachedManager {
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.guilds(this.guild.id).bans(user).get();
const data = await this.client.rest.get(Routes.guildBan(this.guild.id, user));
return this._add(data, cache);
}
async _fetchMany(cache) {
const data = await this.client.api.guilds(this.guild.id).bans.get();
const data = await this.client.rest.get(Routes.guildBans(this.guild.id));
return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, cache)), new Collection());
}
@@ -140,13 +141,10 @@ class GuildBanManager extends CachedManager {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
const id = this.client.users.resolveId(user);
if (!id) throw new Error('BAN_RESOLVE_ID', true);
await this.client.api
.guilds(this.guild.id)
.bans(id)
.put({
data: { delete_message_days: options.days },
reason: options.reason,
});
await this.client.rest.put(Routes.guildBan(this.guild.id, id), {
body: { delete_message_days: options.days },
reason: options.reason,
});
if (user instanceof GuildMember) return user;
const _user = this.client.users.resolve(id);
if (_user) {
@@ -169,7 +167,7 @@ class GuildBanManager extends CachedManager {
async remove(user, reason) {
const id = this.client.users.resolveId(user);
if (!id) throw new Error('BAN_RESOLVE_ID');
await this.client.api.guilds(this.guild.id).bans(id).delete({ reason });
await this.client.rest.delete(Routes.guildBan(this.guild.id, id), { reason });
return this.client.users.resolve(user);
}
}

View File

@@ -2,7 +2,7 @@
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const ThreadManager = require('./ThreadManager');
const { Error } = require('../errors');
@@ -150,8 +150,8 @@ class GuildChannelManager extends CachedManager {
);
}
const data = await this.client.api.guilds(this.guild.id).channels.post({
data: {
const data = await this.client.rest.post(Routes.guildChannels(this.guild.id), {
body: {
name,
topic,
type,
@@ -192,13 +192,13 @@ class GuildChannelManager extends CachedManager {
}
if (id) {
const data = await this.client.api.channels(id).get();
const data = await this.client.rest.get(Routes.channel(id));
// Since this is the guild manager, throw if on a different guild
if (this.guild.id !== data.guild_id) throw new Error('GUILD_CHANNEL_UNOWNED');
return this.client.channels._add(data, this.guild, { cache });
}
const data = await this.client.api.guilds(this.guild.id).channels.get();
const data = await this.client.rest.get(Routes.guildChannels(this.guild.id));
const channels = new Collection();
for (const channel of data) channels.set(channel.id, this.client.channels._add(channel, this.guild, { cache }));
return channels;
@@ -238,7 +238,7 @@ class GuildChannelManager extends CachedManager {
parent_id: typeof r.parent !== 'undefined' ? this.channels.resolveId(r.parent) : undefined,
}));
await this.client.api.guilds(this.guild.id).channels.patch({ data: channelPositions });
await this.client.rest.patch(Routes.guildChannels(this.guild.id), { body: channelPositions });
return this.client.actions.GuildChannelsPositionUpdate.handle({
guild_id: this.guild.id,
channels: channelPositions,
@@ -256,7 +256,7 @@ class GuildChannelManager extends CachedManager {
* .catch(console.error);
*/
async fetchActiveThreads(cache = true) {
const raw = await this.client.api.guilds(this.guild.id).threads.active.get();
const raw = await this.client.rest.get(Routes.guildActiveThreads(this.guild.id));
return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache });
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const BaseGuildEmojiManager = require('./BaseGuildEmojiManager');
const { TypeError } = require('../errors');
const DataResolver = require('../util/DataResolver');
@@ -52,20 +53,20 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
attachment = await DataResolver.resolveImage(attachment);
if (!attachment) throw new TypeError('REQ_RESOURCE_TYPE');
const data = { image: attachment, name };
const body = { image: attachment, name };
if (roles) {
if (!Array.isArray(roles) && !(roles instanceof Collection)) {
throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true);
}
data.roles = [];
body.roles = [];
for (const role of roles.values()) {
const resolvedRole = this.guild.roles.resolveId(role);
if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role);
data.roles.push(resolvedRole);
body.roles.push(resolvedRole);
}
}
const emoji = await this.client.api.guilds(this.guild.id).emojis.post({ data, reason });
const emoji = await this.client.rest.post(Routes.guildEmojis(this.guild.id), { body, reason });
return this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji;
}
@@ -91,11 +92,11 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
const existing = this.cache.get(id);
if (existing) return existing;
}
const emoji = await this.client.api.guilds(this.guild.id).emojis(id).get();
const emoji = await this.client.rest.get(Routes.guildEmoji(this.guild.id, id));
return this._add(emoji, cache);
}
const data = await this.client.api.guilds(this.guild.id).emojis.get();
const data = await this.client.rest.get(Routes.guildEmojis(this.guild.id));
const emojis = new Collection();
for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache));
return emojis;
@@ -110,7 +111,7 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
async delete(emoji, reason) {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
await this.client.api.guilds(this.guild.id).emojis(id).delete({ reason });
await this.client.rest.delete(Routes.guildEmoji(this.guild.id, id), { reason });
}
/**
@@ -124,16 +125,13 @@ class GuildEmojiManager extends BaseGuildEmojiManager {
const id = this.resolveId(emoji);
if (!id) throw new TypeError('INVALID_TYPE', 'emoji', 'EmojiResolvable', true);
const roles = data.roles?.map(r => this.guild.roles.resolveId(r));
const newData = await this.client.api
.guilds(this.guild.id)
.emojis(id)
.patch({
data: {
name: data.name,
roles,
},
reason,
});
const newData = await this.client.rest.patch(Routes.guildEmoji(this.guild.id, id), {
body: {
name: data.name,
roles,
},
reason,
});
const existing = this.cache.get(id);
if (existing) {
const clone = existing._clone();

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Error } = require('../errors');
const Invite = require('../structures/Invite');
@@ -155,12 +156,12 @@ class GuildInviteManager extends CachedManager {
}
async _fetchMany(cache) {
const data = await this.client.api.guilds(this.guild.id).invites.get();
const data = await this.client.rest.get(Routes.guildInvites(this.guild.id));
return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection());
}
async _fetchChannelMany(channelId, cache) {
const data = await this.client.api.channels(channelId).invites.get();
const data = await this.client.rest.get(Routes.channelInvites(channelId));
return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection());
}
@@ -182,8 +183,8 @@ class GuildInviteManager extends CachedManager {
const id = this.guild.channels.resolveId(channel);
if (!id) throw new Error('GUILD_CHANNEL_RESOLVE');
const invite = await this.client.api.channels(id).invites.post({
data: {
const invite = await this.client.rest.post(Routes.channelInvites(id), {
body: {
temporary,
max_age: maxAge,
max_uses: maxUses,
@@ -206,7 +207,7 @@ class GuildInviteManager extends CachedManager {
async delete(invite, reason) {
const code = DataResolver.resolveInviteCode(invite);
await this.client.api.invites(code).delete({ reason });
await this.client.rest.delete(Routes.invite(code), { reason });
}
}

View File

@@ -3,6 +3,7 @@
const process = require('node:process');
const { setTimeout, clearTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Guild } = require('../structures/Guild');
const GuildChannel = require('../structures/GuildChannel');
@@ -199,8 +200,8 @@ class GuildManager extends CachedManager {
}
systemChannelFlags &&= SystemChannelFlags.resolve(systemChannelFlags);
const data = await this.client.api.guilds.post({
data: {
const data = await this.client.rest.post(Routes.guilds(), {
body: {
name,
icon,
verification_level: verificationLevel,
@@ -266,11 +267,13 @@ class GuildManager extends CachedManager {
if (existing) return existing;
}
const data = await this.client.api.guilds(id).get({ query: { with_counts: options.withCounts ?? true } });
const data = await this.client.rest.get(Routes.guild(id), {
query: new URLSearchParams({ with_counts: options.withCounts ?? true }),
});
return this._add(data, options.cache);
}
const data = await this.client.api.users('@me').guilds.get({ query: options });
const data = await this.client.rest.get(Routes.userGuilds(), { query: new URLSearchParams(options) });
return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection());
}
}

View File

@@ -4,6 +4,7 @@ const { Buffer } = require('node:buffer');
const { setTimeout, clearTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Error, TypeError, RangeError } = require('../errors');
const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel');
@@ -113,7 +114,7 @@ class GuildMemberManager extends CachedManager {
}
resolvedOptions.roles = resolvedRoles;
}
const data = await this.client.api.guilds(this.guild.id).members(userId).put({ data: resolvedOptions });
const data = await this.client.rest.put(Routes.guildMember(this.guild.id, userId), { body: resolvedOptions });
// Data is an empty buffer if the member is already part of the guild.
return data instanceof Buffer ? (options.fetchWhenExisting === false ? null : this.fetch(userId)) : this._add(data);
}
@@ -203,7 +204,9 @@ class GuildMemberManager extends CachedManager {
* @returns {Promise<Collection<Snowflake, GuildMember>>}
*/
async search({ query, limit = 1, cache = true } = {}) {
const data = await this.client.api.guilds(this.guild.id).members.search.get({ query: { query, limit } });
const data = await this.client.rest.get(Routes.guildMembersSearch(this.guild.id), {
query: new URLSearchParams({ query, limit }),
});
return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
}
@@ -221,7 +224,11 @@ class GuildMemberManager extends CachedManager {
* @returns {Promise<Collection<Snowflake, GuildMember>>}
*/
async list({ after, limit = 1, cache = true } = {}) {
const data = await this.client.api.guilds(this.guild.id).members.get({ query: { after, limit } });
const query = new URLSearchParams({ limit });
if (after) {
query.set('after', after);
}
const data = await this.client.rest.get(Routes.guildMembers(this.guild.id), { query });
return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
}
@@ -271,13 +278,13 @@ class GuildMemberManager extends CachedManager {
? new Date(_data.communicationDisabledUntil).toISOString()
: _data.communicationDisabledUntil;
let endpoint = this.client.api.guilds(this.guild.id);
let endpoint;
if (id === this.client.user.id) {
const keys = Object.keys(data);
if (keys.length === 1 && keys[0] === 'nick') endpoint = endpoint.members('@me');
else endpoint = endpoint.members(id);
if (keys.length === 1 && keys[0] === 'nick') endpoint = Routes.guildMember(this.guild.id);
else endpoint = Routes.guildMember(this.guild.id, id);
} else {
endpoint = endpoint.members(id);
endpoint = Routes.guildMember(this.guild.id, id);
}
const d = await endpoint.patch({ data: _data, reason });
@@ -336,11 +343,11 @@ class GuildMemberManager extends CachedManager {
query.include_roles = dry ? resolvedRoles.join(',') : resolvedRoles;
}
const endpoint = this.client.api.guilds(this.guild.id).prune;
const endpoint = Routes.guildPrune(this.guild.id);
const { pruned } = await (dry
? endpoint.get({ query, reason })
: endpoint.post({ data: { ...query, compute_prune_count }, reason }));
? this.client.rest.get(endpoint, { query: new URLSearchParams(query), reason })
: this.client.rest.post(endpoint, { body: { ...query, compute_prune_count }, reason }));
return pruned;
}
@@ -363,7 +370,7 @@ class GuildMemberManager extends CachedManager {
const id = this.client.users.resolveId(user);
if (!id) return Promise.reject(new TypeError('INVALID_TYPE', 'user', 'UserResolvable'));
await this.client.api.guilds(this.guild.id).members(id).delete({ reason });
await this.client.rest.delete(Routes.guildMember(this.guild.id, id), { reason });
return this.resolve(user) ?? this.client.users.resolve(user) ?? id;
}
@@ -407,7 +414,7 @@ class GuildMemberManager extends CachedManager {
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.guilds(this.guild.id).members(user).get();
const data = await this.client.rest.get(Routes.guildMember(this.guild.id, user));
return this._add(data, cache);
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const DataManager = require('./DataManager');
const { TypeError } = require('../errors');
const { Role } = require('../structures/Role');
@@ -121,7 +122,7 @@ class GuildMemberRoleManager extends DataManager {
throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
}
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].put({ reason });
await this.client.rest.put(Routes.guildMemberRole(this.guild.id, this.member.id, roleOrRoles), { reason });
const clone = this.member._clone();
clone._roles = [...this.cache.keys(), roleOrRoles];
@@ -152,7 +153,7 @@ class GuildMemberRoleManager extends DataManager {
throw new TypeError('INVALID_TYPE', 'roles', 'Role, Snowflake or Array or Collection of Roles or Snowflakes');
}
await this.client.api.guilds[this.guild.id].members[this.member.id].roles[roleOrRoles].delete({ reason });
await this.client.rest.delete(Routes.guildMemberRole(this.guild.id, this.member.id, roleOrRoles), { reason });
const clone = this.member._clone();
const newRoles = this.cache.filter(role => role.id !== roleOrRoles);

View File

@@ -1,7 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { GuildScheduledEventEntityType } = require('discord-api-types/v9');
const { GuildScheduledEventEntityType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent');
@@ -89,8 +89,8 @@ class GuildScheduledEventManager extends CachedManager {
entity_metadata = typeof entityMetadata === 'undefined' ? entityMetadata : null;
}
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events').post({
data: {
const data = await this.client.rest.post(Routes.guildScheduledEvents(this.guild.id), {
body: {
channel_id,
name,
privacy_level: privacyLevel,
@@ -136,15 +136,15 @@ class GuildScheduledEventManager extends CachedManager {
if (existing) return existing;
}
const data = await this.client.api
.guilds(this.guild.id, 'scheduled-events', id)
.get({ query: { with_user_count: options.withUserCount ?? true } });
const data = await this.client.rest.get(Routes.guildScheduledEvent(this.guild.id, id), {
query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
});
return this._add(data, options.cache);
}
const data = await this.client.api
.guilds(this.guild.id, 'scheduled-events')
.get({ query: { with_user_count: options.withUserCount ?? true } });
const data = await this.client.rest.get(Routes.guildScheduledEvents(this.guild.id), {
query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
});
return data.reduce(
(coll, rawGuildScheduledEventData) =>
@@ -205,8 +205,8 @@ class GuildScheduledEventManager extends CachedManager {
};
}
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).patch({
data: {
const data = await this.client.rest.patch(Routes.guildScheduledEvent(this.guild.id, guildScheduledEventId), {
body: {
channel_id: typeof channel === 'undefined' ? channel : this.guild.channels.resolveId(channel),
name,
privacy_level: privacyLevel,
@@ -232,7 +232,7 @@ class GuildScheduledEventManager extends CachedManager {
const guildScheduledEventId = this.resolveId(guildScheduledEvent);
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).delete();
await this.client.rest.delete(Routes.guildScheduledEvent(this.guild.id, guildScheduledEventId));
}
/**
@@ -265,8 +265,26 @@ class GuildScheduledEventManager extends CachedManager {
let { limit, withMember, before, after } = options;
const data = await this.client.api.guilds(this.guild.id, 'scheduled-events', guildScheduledEventId).users.get({
query: { limit, with_member: withMember, before, after },
const query = new URLSearchParams();
if (limit) {
query.set('limit', limit);
}
if (typeof withMember !== 'undefined') {
query.set('with_member', withMember);
}
if (before) {
query.set('before', before);
}
if (after) {
query.set('after', after);
}
const data = await this.client.rest.get(Routes.guildScheduledEventUsers(this.guild.id, guildScheduledEventId), {
query,
});
return data.reduce(

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const MessagePayload = require('../structures/MessagePayload');
@@ -61,11 +62,14 @@ class GuildStickerManager extends CachedManager {
if (!resolvedFile) throw new TypeError('REQ_RESOURCE_TYPE');
file = { ...resolvedFile, key: 'file' };
const data = { name, tags, description: description ?? '' };
const body = { name, tags, description: description ?? '' };
const sticker = await this.client.api
.guilds(this.guild.id)
.stickers.post({ data, files: [file], reason, dontUsePayloadJSON: true });
const sticker = await this.client.rest.post(Routes.guildStickers(this.guild.id), {
appendToFormData: true,
body,
files: [file],
reason,
});
return this.client.actions.GuildStickerCreate.handle(this.guild, sticker).sticker;
}
@@ -105,8 +109,8 @@ class GuildStickerManager extends CachedManager {
const stickerId = this.resolveId(sticker);
if (!stickerId) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
const d = await this.client.api.guilds(this.guild.id).stickers(stickerId).patch({
data,
const d = await this.client.rest.patch(Routes.guildSticker(this.guild.id, stickerId), {
body: data,
reason,
});
@@ -129,7 +133,7 @@ class GuildStickerManager extends CachedManager {
sticker = this.resolveId(sticker);
if (!sticker) throw new TypeError('INVALID_TYPE', 'sticker', 'StickerResolvable');
await this.client.api.guilds(this.guild.id).stickers(sticker).delete({ reason });
await this.client.rest.delete(Routes.guildSticker(this.guild.id, sticker), { reason });
}
/**
@@ -154,11 +158,11 @@ class GuildStickerManager extends CachedManager {
const existing = this.cache.get(id);
if (existing) return existing;
}
const sticker = await this.client.api.guilds(this.guild.id).stickers(id).get();
const sticker = await this.client.rest.get(Routes.guildSticker(this.guild.id, id));
return this._add(sticker, cache);
}
const data = await this.client.api.guilds(this.guild.id).stickers.get();
const data = await this.client.rest.get(Routes.guildStickers(this.guild.id));
return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)]));
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const { Message } = require('../structures/Message');
@@ -82,7 +83,7 @@ class MessageManager extends CachedManager {
* .catch(console.error);
*/
async fetchPinned(cache = true) {
const data = await this.client.api.channels[this.channel.id].pins.get();
const data = await this.client.rest.get(Routes.channelPins(this.channel.id));
const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache));
return messages;
@@ -123,13 +124,13 @@ class MessageManager extends CachedManager {
const messageId = this.resolveId(message);
if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
const { data, files } = await (options instanceof MessagePayload
const { body, files } = await (options instanceof MessagePayload
? options
: MessagePayload.create(message instanceof Message ? message : this, options)
)
.resolveData()
.resolveBody()
.resolveFiles();
const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data, files });
const d = await this.client.rest.patch(Routes.channelMessage(this.channel.id, messageId), { body, files });
const existing = this.cache.get(messageId);
if (existing) {
@@ -149,7 +150,7 @@ class MessageManager extends CachedManager {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post();
const data = await this.client.rest.post(Routes.channelMessageCrosspost(this.channel.id, message));
return this.cache.get(data.id) ?? this._add(data);
}
@@ -162,7 +163,7 @@ class MessageManager extends CachedManager {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).put();
await this.client.rest.put(Routes.channelPins(this.channel.id, message));
}
/**
@@ -174,7 +175,7 @@ class MessageManager extends CachedManager {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).pins(message).delete();
await this.client.rest.delete(Routes.channelPin(this.channel.id, message));
}
/**
@@ -194,8 +195,7 @@ class MessageManager extends CachedManager {
? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}`
: encodeURIComponent(emoji.name);
// eslint-disable-next-line newline-per-chained-call
await this.client.api.channels(this.channel.id).messages(message).reactions(emojiId, '@me').put();
await this.client.rest.put(Routes.channelMessageOwnReaction(this.channel.id, message, emojiId));
}
/**
@@ -207,7 +207,7 @@ class MessageManager extends CachedManager {
message = this.resolveId(message);
if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable');
await this.client.api.channels(this.channel.id).messages(message).delete();
await this.client.rest.delete(Routes.channelMessage(this.channel.id, message));
}
async _fetchId(messageId, cache, force) {
@@ -216,12 +216,14 @@ class MessageManager extends CachedManager {
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.channels[this.channel.id].messages[messageId].get();
const data = await this.client.rest.get(Routes.channelMessage(this.channel.id, messageId));
return this._add(data, cache);
}
async _fetchMany(options = {}, cache) {
const data = await this.client.api.channels[this.channel.id].messages.get({ query: options });
const data = await this.client.rest.get(Routes.channelMessages(this.channel.id), {
query: new URLSearchParams(options),
});
const messages = new Collection();
for (const message of data) messages.set(message.id, this._add(message, cache));
return messages;

View File

@@ -2,7 +2,7 @@
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { OverwriteType } = require('discord-api-types/v9');
const { OverwriteType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const PermissionOverwrites = require('../structures/PermissionOverwrites');
@@ -99,13 +99,10 @@ class PermissionOverwriteManager extends CachedManager {
const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options, existing);
await this.client.api
.channels(this.channel.id)
.permissions(userOrRoleId)
.put({
data: { id: userOrRoleId, type, allow, deny },
reason,
});
await this.client.rest.put(Routes.channelPermission(this.channel.id, userOrRoleId), {
body: { id: userOrRoleId, type, allow, deny },
reason,
});
return this.channel;
}
@@ -157,7 +154,7 @@ class PermissionOverwriteManager extends CachedManager {
const userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole);
if (!userOrRoleId) throw new TypeError('INVALID_TYPE', 'parameter', 'User nor a Role');
await this.client.api.channels(this.channel.id).permissions(userOrRoleId).delete({ reason });
await this.client.rest.delete(Routes.channelPermission(this.channel.id, userOrRoleId), { reason });
return this.channel;
}
}

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const MessageReaction = require('../structures/MessageReaction');
@@ -58,7 +59,7 @@ class ReactionManager extends CachedManager {
* @returns {Promise<Message>}
*/
async removeAll() {
await this.client.api.channels(this.message.channelId).messages(this.message.id).reactions.delete();
await this.client.rest.delete(Routes.channelMessageAllReactions(this.message.channelId, this.message.id));
return this.message;
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { Error } = require('../errors');
const User = require('../structures/User');
@@ -40,9 +41,14 @@ class ReactionUserManager extends CachedManager {
*/
async fetch({ limit = 100, after } = {}) {
const message = this.reaction.message;
const data = await this.client.api.channels[message.channelId].messages[message.id].reactions[
this.reaction.emoji.identifier
].get({ query: { limit, after } });
const query = new URLSearchParams({ limit });
if (after) {
query.set('after', after);
}
const data = await this.client.rest.get(
Routes.channelMessageReaction(message.channelId, message.id, this.reaction.emoji.identifier),
{ query },
);
const users = new Collection();
for (const rawUser of data) {
const user = this.client.users._add(rawUser);
@@ -61,9 +67,11 @@ class ReactionUserManager extends CachedManager {
const userId = this.client.users.resolveId(user);
if (!userId) throw new Error('REACTION_RESOLVE_USER');
const message = this.reaction.message;
await this.client.api.channels[message.channelId].messages[message.id].reactions[this.reaction.emoji.identifier][
userId === this.client.user.id ? '@me' : userId
].delete();
const route =
userId === this.client.user.id
? Routes.channelMessageOwnReaction(message.channelId, message.id, this.reaction.emoji.identifier)
: Routes.channelMessageUserReaction(message.channelId, message.id, this.reaction.emoji.identifier, userId);
await this.client.rest.delete(route);
return this.reaction;
}
}

View File

@@ -2,6 +2,7 @@
const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const { Role } = require('../structures/Role');
@@ -66,7 +67,7 @@ class RoleManager extends CachedManager {
}
// We cannot fetch a single role, as of this commit's date, Discord API throws with 405
const data = await this.client.api.guilds(this.guild.id).roles.get();
const data = await this.client.rest.get(Routes.guildRoles(this.guild.id));
const roles = new Collection();
for (const role of data) roles.set(role.id, this._add(role, cache));
return id ? roles.get(id) ?? null : roles;
@@ -143,8 +144,8 @@ class RoleManager extends CachedManager {
if (typeof icon !== 'string') icon = undefined;
}
const data = await this.client.api.guilds(this.guild.id).roles.post({
data: {
const data = await this.client.rest.post(Routes.guildRoles(this.guild.id), {
body: {
name,
color,
hoist,
@@ -190,7 +191,7 @@ class RoleManager extends CachedManager {
if (typeof icon !== 'string') icon = undefined;
}
const _data = {
const body = {
name: data.name,
color: typeof data.color === 'undefined' ? undefined : resolveColor(data.color),
hoist: data.hoist,
@@ -200,7 +201,7 @@ class RoleManager extends CachedManager {
unicode_emoji: data.unicodeEmoji,
};
const d = await this.client.api.guilds(this.guild.id).roles(role.id).patch({ data: _data, reason });
const d = await this.client.rest.patch(Routes.guildRole(this.guild.id, role.id), { body, reason });
const clone = role._clone();
clone._patch(d);
@@ -220,7 +221,7 @@ class RoleManager extends CachedManager {
*/
async delete(role, reason) {
const id = this.resolveId(role);
await this.client.api.guilds[this.guild.id].roles[id].delete({ reason });
await this.client.rest.delete(Routes.guildRole(this.guild.id, id), { reason });
this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id });
}
@@ -248,9 +249,7 @@ class RoleManager extends CachedManager {
}));
// Call the API to update role positions
await this.client.api.guilds(this.guild.id).roles.patch({
data: rolePositions,
});
await this.client.rest.patch(Routes.guildRoles(this.guild.id), { body: rolePositions });
return this.client.actions.GuildRolesPositionUpdate.handle({
guild_id: this.guild.id,
roles: rolePositions,

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError, Error } = require('../errors');
const { StageInstance } = require('../structures/StageInstance');
@@ -59,8 +60,8 @@ class StageInstanceManager extends CachedManager {
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
let { topic, privacyLevel } = options;
const data = await this.client.api['stage-instances'].post({
data: {
const data = await this.client.rest.post(Routes.stageInstances(), {
body: {
channel_id: channelId,
topic,
privacy_level: privacyLevel,
@@ -90,7 +91,7 @@ class StageInstanceManager extends CachedManager {
if (existing) return existing;
}
const data = await this.client.api('stage-instances', channelId).get();
const data = await this.client.rest.get(Routes.stageInstance(channelId));
return this._add(data, cache);
}
@@ -119,8 +120,8 @@ class StageInstanceManager extends CachedManager {
let { topic, privacyLevel } = options;
const data = await this.client.api('stage-instances', channelId).patch({
data: {
const data = await this.client.rest.patch(Routes.stageInstance(channelId), {
body: {
topic,
privacy_level: privacyLevel,
},
@@ -144,7 +145,7 @@ class StageInstanceManager extends CachedManager {
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('STAGE_CHANNEL_RESOLVE');
await this.client.api('stage-instances', channelId).delete();
await this.client.rest.delete(Routes.stageInstance(channelId));
}
}

View File

@@ -1,7 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ThreadChannel = require('../structures/ThreadChannel');
@@ -108,16 +108,15 @@ class ThreadManager extends CachedManager {
reason,
rateLimitPerUser,
} = {}) {
let path = this.client.api.channels(this.channel.id);
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === ChannelType.GuildNews ? ChannelType.GuildNewsThread : ChannelType.GuildPublicThread;
let startMessageId;
if (startMessage) {
const startMessageId = this.channel.messages.resolveId(startMessage);
startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
path = path.messages(startMessageId);
} else if (this.channel.type !== ChannelType.GuildNews) {
resolvedType = type ?? resolvedType;
}
@@ -130,8 +129,8 @@ class ThreadManager extends CachedManager {
}
}
const data = await path.threads.post({
data: {
const data = await this.client.rest.post(Routes.threads(this.channel.id, startMessageId), {
body: {
name,
auto_archive_duration: autoArchiveDuration,
type: resolvedType,
@@ -207,27 +206,37 @@ class ThreadManager extends CachedManager {
* @returns {Promise<FetchedThreads>}
*/
async fetchArchived({ type = 'public', fetchAll = false, before, limit } = {}, cache = true) {
let path = this.client.api.channels(this.channel.id);
let path = Routes.channelThreads(this.channel.id, type);
if (type === 'private' && !fetchAll) {
path = path.users('@me');
path = Routes.channelJoinedArchivedThreads(this.channel.id);
}
let timestamp;
let id;
const query = new URLSearchParams();
if (typeof before !== 'undefined') {
if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) {
id = this.resolveId(before);
timestamp = this.resolve(before)?.archivedAt?.toISOString();
const toUse = type === 'private' && !fetchAll ? id : timestamp;
if (toUse) {
query.set('before', toUse);
}
} else {
try {
timestamp = new Date(before).toISOString();
if (type === 'public' || fetchAll) {
query.set('before', timestamp);
}
} catch {
throw new TypeError('INVALID_TYPE', 'before', 'DateResolvable or ThreadChannelResolvable');
}
}
}
const raw = await path.threads
.archived(type)
.get({ query: { before: type === 'private' && !fetchAll ? id : timestamp, limit } });
if (limit) {
query.set('limit', limit);
}
const raw = await this.client.rest.get(path, { query });
return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
}
@@ -237,7 +246,7 @@ class ThreadManager extends CachedManager {
* @returns {Promise<FetchedThreads>}
*/
async fetchActive(cache = true) {
const raw = await this.client.api.guilds(this.channel.guild.id).threads.active.get();
const raw = await this.client.rest.get(Routes.guildActiveThreads(this.channel.guild.id));
return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ThreadMember = require('../structures/ThreadMember');
@@ -77,7 +78,7 @@ class ThreadMemberManager extends CachedManager {
async add(member, reason) {
const id = member === '@me' ? member : this.client.users.resolveId(member);
if (!id) throw new TypeError('INVALID_TYPE', 'member', 'UserResolvable');
await this.client.api.channels(this.thread.id, 'thread-members', id).put({ reason });
await this.client.rest.put(Routes.threadMembers(this.thread.id, id), { reason });
return id;
}
@@ -88,7 +89,7 @@ class ThreadMemberManager extends CachedManager {
* @returns {Promise<Snowflake>}
*/
async remove(id, reason) {
await this.client.api.channels(this.thread.id, 'thread-members', id).delete({ reason });
await this.client.rest.delete(Routes.threadMembers(this.thread.id, id), { reason });
return id;
}
@@ -98,12 +99,12 @@ class ThreadMemberManager extends CachedManager {
if (existing) return existing;
}
const data = await this.client.api.channels(this.thread.id, 'thread-members', memberId).get();
const data = await this.client.rest.get(Routes.threadMembers(this.thread.id, memberId));
return this._add(data, cache);
}
async _fetchMany(cache) {
const raw = await this.client.api.channels(this.thread.id, 'thread-members').get();
const raw = await this.client.rest.get(Routes.threadMembers(this.thread.id));
return raw.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection());
}

View File

@@ -1,6 +1,6 @@
'use strict';
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, Routes } = require('discord-api-types/v9');
const CachedManager = require('./CachedManager');
const { GuildMember } = require('../structures/GuildMember');
const { Message } = require('../structures/Message');
@@ -56,11 +56,7 @@ class UserManager extends CachedManager {
if (dmChannel && !dmChannel.partial) return dmChannel;
}
const data = await this.client.api.users(this.client.user.id).channels.post({
data: {
recipient_id: id,
},
});
const data = await this.client.rest.post(Routes.userChannels(), { body: { recipient_id: id } });
return this.client.channels._add(data, null, { cache });
}
@@ -73,7 +69,7 @@ class UserManager extends CachedManager {
const id = this.resolveId(user);
const dmChannel = this.dmChannel(id);
if (!dmChannel) throw new Error('USER_NO_DM_CHANNEL');
await this.client.api.channels(dmChannel.id).delete();
await this.client.rest.delete(Routes.channel(dmChannel.id));
this.client.channels._remove(dmChannel.id);
return dmChannel;
}
@@ -91,7 +87,7 @@ class UserManager extends CachedManager {
if (existing && !existing.partial) return existing;
}
const data = await this.client.api.users(id).get();
const data = await this.client.rest.get(Routes.user(id));
return this._add(data, cache);
}

View File

@@ -1,83 +0,0 @@
'use strict';
const https = require('node:https');
const { setTimeout, clearTimeout } = require('node:timers');
const FormData = require('form-data');
const fetch = require('node-fetch');
const { UserAgent } = require('../util/Constants');
let agent = null;
class APIRequest {
constructor(rest, method, path, options) {
this.rest = rest;
this.client = rest.client;
this.method = method;
this.route = options.route;
this.options = options;
this.retries = 0;
const { userAgentSuffix } = this.client.options;
this.fullUserAgent = `${UserAgent}${userAgentSuffix.length ? `, ${userAgentSuffix.join(', ')}` : ''}`;
let queryString = '';
if (options.query) {
const query = Object.entries(options.query)
.filter(([, value]) => value !== null && typeof value !== 'undefined')
.flatMap(([key, value]) => (Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]]));
queryString = new URLSearchParams(query).toString();
}
this.path = `${path}${queryString && `?${queryString}`}`;
}
make() {
agent ??= new https.Agent({ ...this.client.options.http.agent, keepAlive: true });
const API =
this.options.versioned === false
? this.client.options.http.api
: `${this.client.options.http.api}/v${this.client.options.http.version}`;
const url = API + this.path;
let headers = {
...this.client.options.http.headers,
'User-Agent': this.fullUserAgent,
};
if (this.options.auth !== false) headers.Authorization = this.rest.getAuth();
if (this.options.reason) headers['X-Audit-Log-Reason'] = encodeURIComponent(this.options.reason);
if (this.options.headers) headers = Object.assign(headers, this.options.headers);
let body;
if (this.options.files?.length) {
body = new FormData();
for (const [index, file] of this.options.files.entries()) {
if (file?.file) body.append(file.key ?? `files[${index}]`, file.file, file.name);
}
if (typeof this.options.data !== 'undefined') {
if (this.options.dontUsePayloadJSON) {
for (const [key, value] of Object.entries(this.options.data)) body.append(key, value);
} else {
body.append('payload_json', JSON.stringify(this.options.data));
}
}
headers = Object.assign(headers, body.getHeaders());
// eslint-disable-next-line eqeqeq
} else if (this.options.data != null) {
body = JSON.stringify(this.options.data);
headers['Content-Type'] = 'application/json';
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.client.options.restRequestTimeout).unref();
return fetch(url, {
method: this.method,
headers,
agent,
body,
signal: controller.signal,
}).finally(() => clearTimeout(timeout));
}
}
module.exports = APIRequest;

View File

@@ -1,53 +0,0 @@
'use strict';
const noop = () => {}; // eslint-disable-line no-empty-function
const methods = ['get', 'post', 'delete', 'patch', 'put'];
const reflectors = [
'toString',
'valueOf',
'inspect',
'constructor',
Symbol.toPrimitive,
Symbol.for('nodejs.util.inspect.custom'),
];
function buildRoute(manager) {
const route = [''];
const handler = {
get(target, name) {
if (reflectors.includes(name)) return () => route.join('/');
if (methods.includes(name)) {
const routeBucket = [];
for (let i = 0; i < route.length; i++) {
// Reactions routes and sub-routes all share the same bucket
if (route[i - 1] === 'reactions') break;
// Literal ids should only be taken account if they are the Major id (the Channel/Guild id)
if (/\d{16,19}/g.test(route[i]) && !/channels|guilds/.test(route[i - 1])) routeBucket.push(':id');
// All other parts of the route should be considered as part of the bucket identifier
else routeBucket.push(route[i]);
}
return options =>
manager.request(
name,
route.join('/'),
Object.assign(
{
versioned: manager.versioned,
route: routeBucket.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,82 +0,0 @@
'use strict';
/**
* Represents an error from the Discord API.
* @extends Error
*/
class DiscordAPIError extends Error {
constructor(error, status, request) {
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 HTTP method used for the request
* @type {string}
*/
this.method = request.method;
/**
* The path of the request relative to the HTTP endpoint
* @type {string}
*/
this.path = request.path;
/**
* HTTP error code returned by Discord
* @type {number}
*/
this.code = error.code;
/**
* The HTTP status code
* @type {number}
*/
this.httpStatus = status;
/**
* The data associated with the request that caused this error
* @type {HTTPErrorData}
*/
this.requestData = {
json: request.options.data,
files: request.options.files ?? [],
};
}
/**
* Flattens an errors object returned from the API into an array.
* @param {APIError} 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;
/**
* @external APIError
* @see {@link https://discord.com/developers/docs/reference#error-messages}
*/

View File

@@ -1,61 +0,0 @@
'use strict';
/**
* Represents an HTTP error from a request.
* @extends Error
*/
class HTTPError extends Error {
constructor(message, name, code, request) {
super(message);
/**
* The name of the error
* @type {string}
*/
this.name = name;
/**
* HTTP error code returned from the request
* @type {number}
*/
this.code = code ?? 500;
/**
* The HTTP method used for the request
* @type {string}
*/
this.method = request.method;
/**
* The path of the request relative to the HTTP endpoint
* @type {string}
*/
this.path = request.path;
/**
* The HTTP data that was sent to Discord
* @typedef {Object} HTTPErrorData
* @property {*} json The JSON data that was sent
* @property {HTTPAttachmentData[]} files The files that were sent with this request, if any
*/
/**
* The attachment data that is sent to Discord
* @typedef {Object} HTTPAttachmentData
* @property {string|Buffer|Stream} attachment The source of this attachment data
* @property {string} name The file name
* @property {Buffer|Stream} file The file buffer
*/
/**
* The data associated with the request that caused this error
* @type {HTTPErrorData}
*/
this.requestData = {
json: request.options.data,
files: request.options.files ?? [],
};
}
}
module.exports = HTTPError;

View File

@@ -1,62 +0,0 @@
'use strict';
const { setInterval } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const APIRequest = require('./APIRequest');
const routeBuilder = require('./APIRouter');
const RequestHandler = require('./RequestHandler');
const { Error } = require('../errors');
const { Endpoints } = require('../util/Constants');
class RESTManager {
constructor(client) {
this.client = client;
this.handlers = new Collection();
this.versioned = true;
this.globalLimit = client.options.restGlobalRateLimit > 0 ? client.options.restGlobalRateLimit : Infinity;
this.globalRemaining = this.globalLimit;
this.globalReset = null;
this.globalDelay = null;
if (client.options.restSweepInterval > 0) {
this.sweepInterval = setInterval(() => {
this.handlers.sweep(handler => handler._inactive);
}, client.options.restSweepInterval * 1_000).unref();
}
}
get api() {
return routeBuilder(this);
}
getAuth() {
const token = this.client.token ?? this.client.accessToken;
if (token) return `Bot ${token}`;
throw new Error('TOKEN_MISSING');
}
get cdn() {
return Endpoints.CDN(this.client.options.http.cdn);
}
request(method, url, options = {}) {
const apiRequest = new APIRequest(this, method, url, options);
let handler = this.handlers.get(apiRequest.route);
if (!handler) {
handler = new RequestHandler(this);
this.handlers.set(apiRequest.route, handler);
}
return handler.push(apiRequest);
}
get endpoint() {
return this.client.options.http.api;
}
set endpoint(endpoint) {
this.client.options.http.api = endpoint;
}
}
module.exports = RESTManager;

View File

@@ -1,55 +0,0 @@
'use strict';
/**
* Represents a RateLimit error from a request.
* @extends Error
*/
class RateLimitError extends Error {
constructor({ timeout, limit, method, path, route, global }) {
super(`A ${global ? 'global ' : ''}rate limit was hit on route ${route}`);
/**
* The name of the error
* @type {string}
*/
this.name = 'RateLimitError';
/**
* Time until this rate limit ends, in milliseconds
* @type {number}
*/
this.timeout = timeout;
/**
* The HTTP method used for the request
* @type {string}
*/
this.method = method;
/**
* The path of the request relative to the HTTP endpoint
* @type {string}
*/
this.path = path;
/**
* The route of the request relative to the HTTP endpoint
* @type {string}
*/
this.route = route;
/**
* Whether this rate limit is global
* @type {boolean}
*/
this.global = global;
/**
* The maximum amount of requests of this endpoint
* @type {number}
*/
this.limit = limit;
}
}
module.exports = RateLimitError;

View File

@@ -1,379 +0,0 @@
'use strict';
const { setTimeout } = require('node:timers');
const { setTimeout: sleep } = require('node:timers/promises');
const { AsyncQueue } = require('@sapphire/async-queue');
const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError');
const RateLimitError = require('./RateLimitError');
const {
Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST },
} = require('../util/Constants');
function parseResponse(res) {
if (res.headers.get('content-type').startsWith('application/json')) return res.json();
return res.buffer();
}
function getAPIOffset(serverDate) {
return Date.parse(serverDate) - Date.now();
}
function calculateReset(reset, resetAfter, serverDate) {
// Use direct reset time when available, server date becomes irrelevant in this case
if (resetAfter) {
return Date.now() + Number(resetAfter) * 1_000;
}
return Number(reset) * 1_000 - getAPIOffset(serverDate);
}
/* Invalid request limiting is done on a per-IP basis, not a per-token basis.
* The best we can do is track invalid counts process-wide (on the theory that
* users could have multiple bots run from one process) rather than per-bot.
* Therefore, store these at file scope here rather than in the client's
* RESTManager object.
*/
let invalidCount = 0;
let invalidCountResetTime = null;
class RequestHandler {
constructor(manager) {
this.manager = manager;
this.queue = new AsyncQueue();
this.reset = -1;
this.remaining = -1;
this.limit = -1;
}
async push(request) {
await this.queue.wait();
try {
return await this.execute(request);
} finally {
this.queue.shift();
}
}
get globalLimited() {
return this.manager.globalRemaining <= 0 && Date.now() < this.manager.globalReset;
}
get localLimited() {
return this.remaining <= 0 && Date.now() < this.reset;
}
get limited() {
return this.globalLimited || this.localLimited;
}
get _inactive() {
return this.queue.remaining === 0 && !this.limited;
}
globalDelayFor(ms) {
return new Promise(resolve => {
setTimeout(() => {
this.manager.globalDelay = null;
resolve();
}, ms).unref();
});
}
/*
* Determines whether the request should be queued or whether a RateLimitError should be thrown
*/
async onRateLimit(request, limit, timeout, isGlobal) {
const { options } = this.manager.client;
if (!options.rejectOnRateLimit) return;
const rateLimitData = {
timeout,
limit,
method: request.method,
path: request.path,
route: request.route,
global: isGlobal,
};
const shouldThrow =
typeof options.rejectOnRateLimit === 'function'
? await options.rejectOnRateLimit(rateLimitData)
: options.rejectOnRateLimit.some(route => rateLimitData.route.startsWith(route.toLowerCase()));
if (shouldThrow) {
throw new RateLimitError(rateLimitData);
}
}
async execute(request) {
/*
* After calculations have been done, pre-emptively stop further requests
* Potentially loop until this task can run if e.g. the global rate limit is hit twice
*/
while (this.limited) {
const isGlobal = this.globalLimited;
let limit, timeout, delayPromise;
if (isGlobal) {
// Set the variables based on the global rate limit
limit = this.manager.globalLimit;
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
} else {
// Set the variables based on the route-specific rate limit
limit = this.limit;
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
}
if (this.manager.client.listenerCount(RATE_LIMIT)) {
/**
* Emitted when the client hits a rate limit while making a request
* @event BaseClient#rateLimit
* @param {RateLimitData} rateLimitData Object containing the rate limit info
*/
this.manager.client.emit(RATE_LIMIT, {
timeout,
limit,
method: request.method,
path: request.path,
route: request.route,
global: isGlobal,
});
}
if (isGlobal) {
// If this is the first task to reach the global timeout, set the global delay
if (!this.manager.globalDelay) {
// The global delay function should clear the global delay state when it is resolved
this.manager.globalDelay = this.globalDelayFor(timeout);
}
delayPromise = this.manager.globalDelay;
} else {
delayPromise = sleep(timeout);
}
// Determine whether a RateLimitError should be thrown
await this.onRateLimit(request, limit, timeout, isGlobal); // eslint-disable-line no-await-in-loop
// Wait for the timeout to expire in order to avoid an actual 429
await delayPromise; // eslint-disable-line no-await-in-loop
}
// As the request goes out, update the global usage information
if (!this.manager.globalReset || this.manager.globalReset < Date.now()) {
this.manager.globalReset = Date.now() + 1_000;
this.manager.globalRemaining = this.manager.globalLimit;
}
this.manager.globalRemaining--;
/**
* Represents a request that will or has been made to the Discord API
* @typedef {Object} APIRequest
* @property {HTTPMethod} method The HTTP method used in this request
* @property {string} path The full path used to make the request
* @property {string} route The API route identifying the rate limit for this request
* @property {Object} options Additional options for this request
* @property {number} retries The number of times this request has been attempted
*/
if (this.manager.client.listenerCount(API_REQUEST)) {
/**
* Emitted before every API request.
* This event can emit several times for the same request, e.g. when hitting a rate limit.
* <info>This is an informational event that is emitted quite frequently,
* it is highly recommended to check `request.path` to filter the data.</info>
* @event BaseClient#apiRequest
* @param {APIRequest} request The request that is about to be sent
*/
this.manager.client.emit(API_REQUEST, {
method: request.method,
path: request.path,
route: request.route,
options: request.options,
retries: request.retries,
});
}
// Perform the request
let res;
try {
res = await request.make();
} catch (error) {
// Retry the specified number of times for request abortions
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(error.message, error.constructor.name, error.status, request);
}
request.retries++;
return this.execute(request);
}
if (this.manager.client.listenerCount(API_RESPONSE)) {
/**
* Emitted after every API request has received a response.
* This event does not necessarily correlate to completion of the request, e.g. when hitting a rate limit.
* <info>This is an informational event that is emitted quite frequently,
* it is highly recommended to check `request.path` to filter the data.</info>
* @event BaseClient#apiResponse
* @param {APIRequest} request The request that triggered this response
* @param {Response} response The response received from the Discord API
*/
this.manager.client.emit(
API_RESPONSE,
{
method: request.method,
path: request.path,
route: request.route,
options: request.options,
retries: request.retries,
},
res.clone(),
);
}
let sublimitTimeout;
if (res.headers) {
const serverDate = res.headers.get('date');
const limit = res.headers.get('x-ratelimit-limit');
const remaining = res.headers.get('x-ratelimit-remaining');
const reset = res.headers.get('x-ratelimit-reset');
const resetAfter = res.headers.get('x-ratelimit-reset-after');
this.limit = limit ? Number(limit) : Infinity;
this.remaining = remaining ? Number(remaining) : 1;
this.reset = reset || resetAfter ? calculateReset(reset, resetAfter, serverDate) : Date.now();
// https://github.com/discord/discord-api-docs/issues/182
if (!resetAfter && request.route.includes('reactions')) {
this.reset = Date.parse(serverDate) - getAPIOffset(serverDate) + 250;
}
// Handle retryAfter, which means we have actually hit a rate limit
let retryAfter = res.headers.get('retry-after');
retryAfter = retryAfter ? Number(retryAfter) * 1_000 : -1;
if (retryAfter > 0) {
// If the global rate limit header is set, that means we hit the global rate limit
if (res.headers.get('x-ratelimit-global')) {
this.manager.globalRemaining = 0;
this.manager.globalReset = Date.now() + retryAfter;
} else if (!this.localLimited) {
/*
* This is a sublimit (e.g. 2 channel name changes/10 minutes) since the headers don't indicate a
* route-wide rate limit. Don't update remaining or reset to avoid rate limiting the whole
* endpoint, just set a reset time on the request itself to avoid retrying too soon.
*/
sublimitTimeout = retryAfter;
}
}
}
// Count the invalid requests
if (res.status === 401 || res.status === 403 || res.status === 429) {
if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
invalidCountResetTime = Date.now() + 1_000 * 60 * 10;
invalidCount = 0;
}
invalidCount++;
const emitInvalid =
this.manager.client.listenerCount(INVALID_REQUEST_WARNING) &&
this.manager.client.options.invalidRequestWarningInterval > 0 &&
invalidCount % this.manager.client.options.invalidRequestWarningInterval === 0;
if (emitInvalid) {
/**
* @typedef {Object} InvalidRequestWarningData
* @property {number} count Number of invalid requests that have been made in the window
* @property {number} remainingTime Time in milliseconds remaining before the count resets
*/
/**
* Emitted periodically when the process sends invalid requests to let users avoid the
* 10k invalid requests in 10 minutes threshold that causes a ban
* @event BaseClient#invalidRequestWarning
* @param {InvalidRequestWarningData} invalidRequestWarningData Object containing the invalid request info
*/
this.manager.client.emit(INVALID_REQUEST_WARNING, {
count: invalidCount,
remainingTime: invalidCountResetTime - Date.now(),
});
}
}
// Handle 2xx and 3xx responses
if (res.ok) {
// Nothing wrong with the request, proceed with the next one
return parseResponse(res);
}
// Handle 4xx responses
if (res.status >= 400 && res.status < 500) {
// Handle ratelimited requests
if (res.status === 429) {
const isGlobal = this.globalLimited;
let limit, timeout;
if (isGlobal) {
// Set the variables based on the global rate limit
limit = this.manager.globalLimit;
timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
} else {
// Set the variables based on the route-specific rate limit
limit = this.limit;
timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
}
this.manager.client.emit(
DEBUG,
`Hit a 429 while executing a request.
Global : ${isGlobal}
Method : ${request.method}
Path : ${request.path}
Route : ${request.route}
Limit : ${limit}
Timeout : ${timeout}ms
Sublimit: ${sublimitTimeout ? `${sublimitTimeout}ms` : 'None'}`,
);
await this.onRateLimit(request, limit, timeout, isGlobal);
// If caused by a sublimit, wait it out here so other requests on the route can be handled
if (sublimitTimeout) {
await sleep(sublimitTimeout);
}
return this.execute(request);
}
// Handle possible malformed requests
let data;
try {
data = await parseResponse(res);
} catch (err) {
throw new HTTPError(err.message, err.constructor.name, err.status, request);
}
throw new DiscordAPIError(data, res.status, request);
}
// Handle 5xx responses
if (res.status >= 500 && res.status < 600) {
// Retry the specified number of times for possible serverside issues
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(res.statusText, res.constructor.name, res.status, request);
}
request.retries++;
return this.execute(request);
}
// Fallback in the rare case a status code outside the range 200..=599 is returned
return null;
}
}
module.exports = RequestHandler;
/**
* @external HTTPMethod
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods}
*/
/**
* @external Response
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response}
*/

View File

@@ -71,7 +71,7 @@ class AnonymousGuild extends BaseGuild {
* @returns {?string}
*/
bannerURL(options = {}) {
return this.banner && this.client.rest.cdn.Banner(this.id, this.banner, options);
return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options);
}
/**
@@ -80,7 +80,7 @@ class AnonymousGuild extends BaseGuild {
* @returns {?string}
*/
splashURL(options = {}) {
return this.splash && this.client.rest.cdn.Splash(this.id, this.splash, options);
return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options);
}
}

View File

@@ -1,6 +1,6 @@
'use strict';
const { InteractionResponseType } = require('discord-api-types/v9');
const { InteractionResponseType, Routes } = require('discord-api-types/v9');
const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver');
const Interaction = require('./Interaction');
@@ -70,8 +70,8 @@ class AutocompleteInteraction extends Interaction {
async respond(options) {
if (this.responded) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
data: {
choices: options,

View File

@@ -1,6 +1,7 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
/**
@@ -91,7 +92,7 @@ class BaseGuild extends Base {
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.Icon(this.id, this.icon, options);
return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options);
}
/**
@@ -99,7 +100,9 @@ class BaseGuild extends Base {
* @returns {Promise<Guild>}
*/
async fetch() {
const data = await this.client.api.guilds(this.id).get({ query: { with_counts: true } });
const data = await this.client.rest.get(Routes.guild(this.id), {
query: new URLSearchParams({ with_counts: true }),
});
return this.client.guilds._add(data);
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const GuildChannel = require('./GuildChannel');
const Webhook = require('./Webhook');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
@@ -122,7 +123,7 @@ class BaseGuildTextChannel extends GuildChannel {
* .catch(console.error);
*/
async fetchWebhooks() {
const data = await this.client.api.channels[this.id].webhooks.get();
const data = await this.client.rest.get(Routes.channelWebhooks(this.id));
const hooks = new Collection();
for (const hook of data) hooks.set(hook.id, new Webhook(this.client, hook));
return hooks;
@@ -153,8 +154,8 @@ class BaseGuildTextChannel extends GuildChannel {
if (typeof avatar === 'string' && !avatar.startsWith('data:')) {
avatar = await DataResolver.resolveImage(avatar);
}
const data = await this.client.api.channels[this.id].webhooks.post({
data: {
const data = await this.client.rest.post(Routes.channelWebhooks(this.id), {
body: {
name,
avatar,
},

View File

@@ -1,7 +1,7 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { ThreadChannelTypes } = require('../util/Constants');
let CategoryChannel;
@@ -88,7 +88,7 @@ class Channel extends Base {
* .catch(console.error);
*/
async delete() {
await this.client.api.channels(this.id).delete();
await this.client.rest.delete(Routes.channel(this.id));
return this;
}

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const Team = require('./Team');
const Application = require('./interfaces/Application');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
@@ -96,7 +97,7 @@ class ClientApplication extends Application {
* @returns {Promise<ClientApplication>}
*/
async fetch() {
const app = await this.client.api.oauth2.applications('@me').get();
const app = await this.client.rest.get(Routes.oauth2CurrentApplication());
this._patch(app);
return this;
}

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const User = require('./User');
const DataResolver = require('../util/DataResolver');
@@ -55,8 +56,9 @@ class ClientUser extends User {
*/
async edit(data) {
if (typeof data.avatar !== 'undefined') data.avatar = await DataResolver.resolveImage(data.avatar);
const newData = await this.client.api.users('@me').patch({ data });
const newData = await this.client.rest.patch(Routes.user(), { body: data });
this.client.token = newData.token;
this.client.rest.setToken(newData.token);
const { updated } = this.client.actions.UserUpdate.handle(newData);
return updated ?? this;
}

View File

@@ -53,7 +53,7 @@ class Emoji extends Base {
* @readonly
*/
get url() {
return this.id && this.client.rest.cdn.Emoji(this.id, this.animated ? 'gif' : 'png');
return this.id && this.client.rest.cdn.emoji(this.id, this.animated ? 'gif' : 'png');
}
/**

View File

@@ -1,7 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { GuildPremiumTier, ChannelType } = require('discord-api-types/v9');
const { ChannelType, GuildPremiumTier, Routes } = require('discord-api-types/v9');
const AnonymousGuild = require('./AnonymousGuild');
const GuildAuditLogs = require('./GuildAuditLogs');
const GuildPreview = require('./GuildPreview');
@@ -9,7 +9,7 @@ const GuildTemplate = require('./GuildTemplate');
const Integration = require('./Integration');
const Webhook = require('./Webhook');
const WelcomeScreen = require('./WelcomeScreen');
const { Error } = require('../errors');
const { Error, TypeError } = require('../errors');
const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager');
const GuildBanManager = require('../managers/GuildBanManager');
const GuildChannelManager = require('../managers/GuildChannelManager');
@@ -477,7 +477,7 @@ class Guild extends AnonymousGuild {
* @returns {?string}
*/
discoverySplashURL(options = {}) {
return this.discoverySplash && this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, options);
return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options);
}
/**
@@ -582,7 +582,7 @@ class Guild extends AnonymousGuild {
* .catch(console.error);
*/
async fetchIntegrations() {
const data = await this.client.api.guilds(this.id).integrations.get();
const data = await this.client.rest.get(Routes.guildIntegrations(this.id));
return data.reduce(
(collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)),
new Collection(),
@@ -595,7 +595,7 @@ class Guild extends AnonymousGuild {
* @returns {Promise<Collection<string, GuildTemplate>>}
*/
async fetchTemplates() {
const templates = await this.client.api.guilds(this.id).templates.get();
const templates = await this.client.rest.get(Routes.guildTemplate(this.id));
return templates.reduce((col, data) => col.set(data.code, new GuildTemplate(this.client, data)), new Collection());
}
@@ -604,7 +604,7 @@ class Guild extends AnonymousGuild {
* @returns {Promise<WelcomeScreen>}
*/
async fetchWelcomeScreen() {
const data = await this.client.api.guilds(this.id, 'welcome-screen').get();
const data = await this.client.rest.get(Routes.guildWelcomeScreen(this.id));
return new WelcomeScreen(this, data);
}
@@ -615,7 +615,7 @@ class Guild extends AnonymousGuild {
* @returns {Promise<GuildTemplate>}
*/
async createTemplate(name, description) {
const data = await this.client.api.guilds(this.id).templates.post({ data: { name, description } });
const data = await this.client.rest.post(Routes.guildTemplates(this.id), { body: { name, description } });
return new GuildTemplate(this.client, data);
}
@@ -624,7 +624,7 @@ class Guild extends AnonymousGuild {
* @returns {Promise<GuildPreview>}
*/
async fetchPreview() {
const data = await this.client.api.guilds(this.id).preview.get();
const data = await this.client.rest.get(Routes.guildPreview(this.id));
return new GuildPreview(this.client, data);
}
@@ -651,7 +651,7 @@ class Guild extends AnonymousGuild {
if (!this.features.includes('VANITY_URL')) {
throw new Error('VANITY_URL');
}
const data = await this.client.api.guilds(this.id, 'vanity-url').get();
const data = await this.client.rest.get(Routes.guildVanityUrl(this.id));
this.vanityURLCode = data.code;
this.vanityURLUses = data.uses;
@@ -668,7 +668,7 @@ class Guild extends AnonymousGuild {
* .catch(console.error);
*/
async fetchWebhooks() {
const apiHooks = await this.client.api.guilds(this.id).webhooks.get();
const apiHooks = await this.client.rest.get(Routes.guildWebhooks(this.id));
const hooks = new Collection();
for (const hook of apiHooks) hooks.set(hook.id, new Webhook(this.client, hook));
return hooks;
@@ -711,7 +711,7 @@ class Guild extends AnonymousGuild {
* .catch(console.error);
*/
async fetchWidgetSettings() {
const data = await this.client.api.guilds(this.id).widget.get();
const data = await this.client.rest.get(Routes.guildWidgetSettings(this.id));
this.widgetEnabled = data.enabled;
this.widgetChannelId = data.channel_id;
return {
@@ -742,14 +742,27 @@ class Guild extends AnonymousGuild {
async fetchAuditLogs(options = {}) {
if (options.before && options.before instanceof GuildAuditLogs.Entry) options.before = options.before.id;
const data = await this.client.api.guilds(this.id)['audit-logs'].get({
query: {
before: options.before,
limit: options.limit,
user_id: this.client.users.resolveId(options.user),
action_type: options.type,
},
});
const query = new URLSearchParams();
if (options.before) {
query.set('before', options.before);
}
if (options.limit) {
query.set('limit', options.limit);
}
if (options.user) {
const id = this.client.user.resolveId(options.user);
if (!id) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable');
query.set('user_id', id);
}
if (options.type) {
query.set('action_type', options.type);
}
const data = await this.client.rest.get(Routes.guildAuditLog(this.id), { query });
return GuildAuditLogs.build(this, data);
}
@@ -848,7 +861,7 @@ class Guild extends AnonymousGuild {
}
if (data.preferredLocale) _data.preferred_locale = data.preferredLocale;
if ('premiumProgressBarEnabled' in data) _data.premium_progress_bar_enabled = data.premiumProgressBarEnabled;
const newData = await this.client.api.guilds(this.id).patch({ data: _data, reason });
const newData = await this.client.rest.patch(Routes.guild(this.id), { body: _data, reason });
return this.client.actions.GuildUpdate.handle(newData).updated;
}
@@ -912,8 +925,8 @@ class Guild extends AnonymousGuild {
};
});
const patchData = await this.client.api.guilds(this.id, 'welcome-screen').patch({
data: {
const patchData = await this.client.rest.patch(Routes.guildWelcomeScreen(this.id), {
body: {
welcome_channels,
description,
enabled,
@@ -1166,8 +1179,8 @@ class Guild extends AnonymousGuild {
* @returns {Promise<Guild>}
*/
async setWidgetSettings(settings, reason) {
await this.client.api.guilds(this.id).widget.patch({
data: {
await this.client.rest.patch(Routes.guildWidgetSettings(this.id), {
body: {
enabled: settings.enabled,
channel_id: this.channels.resolveId(settings.channel),
},
@@ -1187,7 +1200,7 @@ class Guild extends AnonymousGuild {
*/
async leave() {
if (this.ownerId === this.client.user.id) throw new Error('GUILD_OWNED');
await this.client.api.users('@me').guilds(this.id).delete();
await this.client.rest.delete(Routes.userGuild(this.id));
return this;
}
@@ -1201,7 +1214,7 @@ class Guild extends AnonymousGuild {
* .catch(console.error);
*/
async delete() {
await this.client.api.guilds(this.id).delete();
await this.client.rest.delete(Routes.guild(this.id));
return this;
}

View File

@@ -1,6 +1,6 @@
'use strict';
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, Routes } = require('discord-api-types/v9');
const { Channel } = require('./Channel');
const PermissionOverwrites = require('./PermissionOverwrites');
const { Error } = require('../errors');
@@ -323,8 +323,8 @@ class GuildChannel extends Channel {
}
}
const newData = await this.client.api.channels(this.id).patch({
data: {
const newData = await this.client.rest.patch(Routes.channel(this.id), {
body: {
name: (data.name ?? this.name).trim(),
type: data.type,
topic: data.topic,
@@ -411,7 +411,8 @@ class GuildChannel extends Channel {
position,
relative,
this.guild._sortedChannels(this),
this.client.api.guilds(this.guild.id).channels,
this.client,
Routes.guildChannels(this.guild.id),
reason,
);
this.client.actions.GuildChannelsPositionUpdate.handle({
@@ -534,7 +535,7 @@ class GuildChannel extends Channel {
* .catch(console.error);
*/
async delete(reason) {
await this.client.api.channels(this.id).delete({ reason });
await this.client.rest.delete(Routes.channel(this.id), { reason });
return this;
}
}

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const BaseGuildEmoji = require('./BaseGuildEmoji');
const { Error } = require('../errors');
const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager');
@@ -81,7 +82,7 @@ class GuildEmoji extends BaseGuildEmoji {
throw new Error('MISSING_MANAGE_EMOJIS_AND_STICKERS_PERMISSION', this.guild);
}
}
const data = await this.client.api.guilds(this.guild.id).emojis(this.id).get();
const data = await this.client.rest.get(Routes.guildEmoji(this.guild.id, this.id));
this._patch(data);
return this.author;
}

View File

@@ -127,7 +127,7 @@ class GuildMember extends Base {
* @returns {?string}
*/
avatarURL(options = {}) {
return this.avatar && this.client.rest.cdn.GuildMemberAvatar(this.guild.id, this.id, this.avatar, options);
return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guild.id, this.id, this.avatar, options);
}
/**

View File

@@ -2,6 +2,7 @@
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const GuildPreviewEmoji = require('./GuildPreviewEmoji');
const { Sticker } = require('./Sticker');
@@ -138,7 +139,7 @@ class GuildPreview extends Base {
* @returns {?string}
*/
splashURL(options = {}) {
return this.splash && this.client.rest.cdn.Splash(this.id, this.splash, options);
return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options);
}
/**
@@ -147,7 +148,7 @@ class GuildPreview extends Base {
* @returns {?string}
*/
discoverySplashURL(options = {}) {
return this.discoverySplash && this.client.rest.cdn.DiscoverySplash(this.id, this.discoverySplash, options);
return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options);
}
/**
@@ -156,7 +157,7 @@ class GuildPreview extends Base {
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.Icon(this.id, this.icon, options);
return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options);
}
/**
@@ -164,7 +165,7 @@ class GuildPreview extends Base {
* @returns {Promise<GuildPreview>}
*/
async fetch() {
const data = await this.client.api.guilds(this.id).preview.get();
const data = await this.client.rest.get(Routes.guildPreview(this.id));
this._patch(data);
return this;
}

View File

@@ -1,10 +1,9 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { GuildScheduledEventStatus, GuildScheduledEventEntityType } = require('discord-api-types/v9');
const { GuildScheduledEventStatus, GuildScheduledEventEntityType, RouteBases } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error } = require('../errors');
const { Endpoints } = require('../util/Constants');
/**
* Represents a scheduled event in a {@link Guild}.
@@ -216,7 +215,7 @@ class GuildScheduledEvent extends Base {
* @readonly
*/
get url() {
return Endpoints.scheduledEvent(this.client.options.http.scheduledEvent, this.guildId, this.id);
return `${RouteBases.scheduledEvent}/${this.guildId}/${this.id}`;
}
/**
@@ -240,7 +239,7 @@ class GuildScheduledEvent extends Base {
if (!channelId) throw new Error('GUILD_CHANNEL_RESOLVE');
}
const invite = await this.guild.invites.create(channelId, options);
return Endpoints.invite(this.client.options.http.invite, invite.code, this.id);
return `${RouteBases.invite}/${invite.code}?event=${this.id}`;
}
/**

View File

@@ -1,6 +1,7 @@
'use strict';
const { setTimeout, clearTimeout } = require('node:timers');
const { RouteBases, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { Events } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
@@ -114,8 +115,8 @@ class GuildTemplate extends Base {
*/
async createGuild(name, icon) {
const { client } = this;
const data = await client.api.guilds.templates(this.code).post({
data: {
const data = await client.rest.post(Routes.template(this.code), {
body: {
name,
icon: await DataResolver.resolveImage(icon),
},
@@ -157,7 +158,9 @@ class GuildTemplate extends Base {
* @returns {Promise<GuildTemplate>}
*/
async edit({ name, description } = {}) {
const data = await this.client.api.guilds(this.guildId).templates(this.code).patch({ data: { name, description } });
const data = await this.client.rest.patch(Routes.guildTemplate(this.guildId, this.code), {
body: { name, description },
});
return this._patch(data);
}
@@ -166,7 +169,7 @@ class GuildTemplate extends Base {
* @returns {Promise<GuildTemplate>}
*/
async delete() {
await this.client.api.guilds(this.guildId).templates(this.code).delete();
await this.client.rest.delete(Routes.guildTemplate(this.guildId, this.code));
return this;
}
@@ -175,7 +178,7 @@ class GuildTemplate extends Base {
* @returns {Promise<GuildTemplate>}
*/
async sync() {
const data = await this.client.api.guilds(this.guildId).templates(this.code).put();
const data = await this.client.rest.put(Routes.guildTemplate(this.guildId, this.code));
return this._patch(data);
}
@@ -212,7 +215,7 @@ class GuildTemplate extends Base {
* @readonly
*/
get url() {
return `${this.client.options.http.template}/${this.code}`;
return `${RouteBases.template}/${this.code}`;
}
/**

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const IntegrationApplication = require('./IntegrationApplication');
@@ -191,7 +192,7 @@ class Integration extends Base {
* @param {string} [reason] Reason for deleting this integration
*/
async delete(reason) {
await this.client.api.guilds(this.guild.id).integrations(this.id).delete({ reason });
await this.client.rest.delete(Routes.guildIntegration(this.guild.id, this.id), { reason });
return this;
}

View File

@@ -1,11 +1,11 @@
'use strict';
const { RouteBases, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { GuildScheduledEvent } = require('./GuildScheduledEvent');
const IntegrationApplication = require('./IntegrationApplication');
const InviteStageInstance = require('./InviteStageInstance');
const { Error } = require('../errors');
const { Endpoints } = require('../util/Constants');
const Permissions = require('../util/Permissions');
/**
@@ -267,7 +267,7 @@ class Invite extends Base {
* @readonly
*/
get url() {
return Endpoints.invite(this.client.options.http.invite, this.code);
return `${RouteBases.invite}/${this.code}`;
}
/**
@@ -276,7 +276,7 @@ class Invite extends Base {
* @returns {Promise<Invite>}
*/
async delete(reason) {
await this.client.api.invites[this.code].delete({ reason });
await this.client.rest.delete(Routes.invite(this.code), { reason });
return this;
}

View File

@@ -29,21 +29,14 @@ class MessagePayload {
this.options = options;
/**
* Data sendable to the API
* Body sendable to the API
* @type {?APIMessage}
*/
this.data = null;
/**
* @typedef {Object} MessageFile
* @property {Buffer|string|Stream} attachment The original attachment that generated this file
* @property {string} name The name of this file
* @property {Buffer|Stream} file The file to be sent to the API
*/
this.body = null;
/**
* Files sendable to the API
* @type {?MessageFile[]}
* @type {?RawFile[]}
*/
this.files = null;
}
@@ -117,10 +110,10 @@ class MessagePayload {
}
/**
* Resolves data.
* Resolves the body.
* @returns {MessagePayload}
*/
resolveData() {
resolveBody() {
if (this.data) return this;
const isInteraction = this.isInteraction;
const isWebhook = this.isWebhook;
@@ -189,7 +182,7 @@ class MessagePayload {
this.options.attachments = attachments;
}
this.data = {
this.body = {
content,
tts,
nonce,
@@ -221,11 +214,11 @@ class MessagePayload {
/**
* Resolves a single file into an object sendable to the API.
* @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file
* @returns {Promise<MessageFile>}
* @returns {Promise<RawFile>}
*/
static async resolveFile(fileLike) {
let attachment;
let name;
let fileName;
const findName = thing => {
if (typeof thing === 'string') {
@@ -243,14 +236,14 @@ class MessagePayload {
typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function';
if (ownAttachment) {
attachment = fileLike;
name = findName(attachment);
fileName = findName(attachment);
} else {
attachment = fileLike.attachment;
name = fileLike.name ?? findName(attachment);
fileName = fileLike.name ?? findName(attachment);
}
const resource = await DataResolver.resolveFile(attachment);
return { attachment, name, file: resource };
const fileData = await DataResolver.resolveFile(attachment);
return { fileData, fileName };
}
/**
@@ -280,3 +273,8 @@ module.exports = MessagePayload;
* @external APIMessage
* @see {@link https://discord.com/developers/docs/resources/channel#message-object}
*/
/**
* @external RawFile
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/RawFile}
*/

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const GuildEmoji = require('./GuildEmoji');
const ReactionEmoji = require('./ReactionEmoji');
const ReactionUserManager = require('../managers/ReactionUserManager');
@@ -56,11 +57,9 @@ class MessageReaction {
* @returns {Promise<MessageReaction>}
*/
async remove() {
await this.client.api
.channels(this.message.channelId)
.messages(this.message.id)
.reactions(this._emoji.identifier)
.delete();
await this.client.rest.delete(
Routes.channelMessageReaction(this.message.channelId, this.message.id, this._emoji.identifier),
);
return this;
}

View File

@@ -1,5 +1,6 @@
'use strict';
const { Routes } = require('discord-api-types/v9');
const BaseGuildTextChannel = require('./BaseGuildTextChannel');
const { Error } = require('../errors');
@@ -23,7 +24,7 @@ class NewsChannel extends BaseGuildTextChannel {
async addFollower(channel, reason) {
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new Error('GUILD_CHANNEL_RESOLVE');
await this.client.api.channels(this.id).followers.post({ data: { webhook_channel_id: channelId }, reason });
await this.client.rest.post(Routes.channelFollowers(this.id), { body: { webhook_channel_id: channelId }, reason });
return this;
}
}

View File

@@ -42,7 +42,7 @@ class PartialGroupDMChannel extends Channel {
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.GDMIcon(this.id, this.icon, options);
return this.icon && this.client.rest.cdn.channelIcon(this.id, this.icon, options);
}
delete() {

View File

@@ -361,7 +361,7 @@ class RichPresenceAssets {
}
}
return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.smallImage, options);
return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.smallImage, options);
}
/**
@@ -387,7 +387,7 @@ class RichPresenceAssets {
}
}
return this.activity.presence.client.rest.cdn.AppAsset(this.activity.applicationId, this.largeImage, options);
return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.largeImage, options);
}
}

View File

@@ -1,6 +1,7 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error } = require('../errors');
const Permissions = require('../util/Permissions');
@@ -364,7 +365,8 @@ class Role extends Base {
position,
relative,
this.guild._sortedRoles(),
this.client.api.guilds(this.guild.id).roles,
this.client,
Routes.guildRoles(this.guild.id),
reason,
);
this.client.actions.GuildRolesPositionUpdate.handle({
@@ -395,7 +397,7 @@ class Role extends Base {
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.RoleIcon(this.id, this.icon, options);
return this.icon && this.client.rest.cdn.roleIcon(this.id, this.icon, options);
}
/**

View File

@@ -1,6 +1,7 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, StickerFormatType } = require('discord-api-types/v9');
const Base = require('./Base');
/**
@@ -161,7 +162,7 @@ class Sticker extends Base {
* @type {string}
*/
get url() {
return this.client.rest.cdn.Sticker(this.id, this.format);
return this.client.rest.cdn.sticker(this.id, this.format === StickerFormatType.Lottie ? 'json' : 'png');
}
/**
@@ -169,7 +170,7 @@ class Sticker extends Base {
* @returns {Promise<Sticker>}
*/
async fetch() {
const data = await this.client.api.stickers(this.id).get();
const data = await this.client.rest.get(Routes.sticker(this.id));
this._patch(data);
return this;
}
@@ -190,7 +191,7 @@ class Sticker extends Base {
if (this.partial) await this.fetch();
if (!this.guildId) throw new Error('NOT_GUILD_STICKER');
const data = await this.client.api.guilds(this.guildId).stickers(this.id).get();
const data = await this.client.rest.get(Routes.guildSticker(this.guildId, this.id));
this._patch(data);
return this.user;
}

View File

@@ -88,7 +88,7 @@ class StickerPack extends Base {
* @returns {?string}
*/
bannerURL(options = {}) {
return this.bannerId && this.client.rest.cdn.StickerPackBanner(this.bannerId, options);
return this.bannerId && this.client.rest.cdn.stickerPackBanner(this.bannerId, options);
}
}

View File

@@ -94,7 +94,7 @@ class Team extends Base {
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.TeamIcon(this.id, this.icon, options);
return this.icon && this.client.rest.cdn.teamIcon(this.id, this.icon, options);
}
/**

View File

@@ -1,6 +1,6 @@
'use strict';
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, Routes } = require('discord-api-types/v9');
const { Channel } = require('./Channel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const { RangeError } = require('../errors');
@@ -322,8 +322,8 @@ class ThreadChannel extends Channel {
autoArchiveDuration = 4320;
}
}
const newData = await this.client.api.channels(this.id).patch({
data: {
const newData = await this.client.rest.patch(Routes.channel(this.id), {
body: {
name: (data.name ?? this.name).trim(),
archived: data.archived,
auto_archive_duration: autoArchiveDuration,
@@ -540,7 +540,7 @@ class ThreadChannel extends Channel {
* .catch(console.error);
*/
async delete(reason) {
await this.client.api.channels(this.id).delete({ reason });
await this.client.rest.delete(Routes.channel(this.id), { reason });
return this;
}

View File

@@ -144,7 +144,7 @@ class User extends Base {
* @returns {?string}
*/
avatarURL(options = {}) {
return this.avatar && this.client.rest.cdn.Avatar(this.id, this.avatar, options);
return this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options);
}
/**
@@ -153,7 +153,7 @@ class User extends Base {
* @readonly
*/
get defaultAvatarURL() {
return this.client.rest.cdn.DefaultAvatar(this.discriminator % 5);
return this.client.rest.cdn.defaultAvatar(this.discriminator % 5);
}
/**
@@ -183,7 +183,7 @@ class User extends Base {
* @returns {?string}
*/
bannerURL(options = {}) {
return this.banner && this.client.rest.cdn.Banner(this.id, this.banner, options);
return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options);
}
/**

View File

@@ -1,6 +1,6 @@
'use strict';
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const { Error, TypeError } = require('../errors');
@@ -222,8 +222,8 @@ class VoiceState extends Base {
if (this.client.user.id !== this.id) throw new Error('VOICE_STATE_NOT_OWN');
await this.client.api.guilds(this.guild.id, 'voice-states', '@me').patch({
data: {
await this.client.rest.patch(Routes.guildVoiceState(this.guild.id), {
body: {
channel_id: this.channelId,
request_to_speak_timestamp: request ? new Date().toISOString() : null,
},
@@ -254,8 +254,8 @@ class VoiceState extends Base {
const target = this.client.user.id === this.id ? '@me' : this.id;
await this.client.api.guilds(this.guild.id, 'voice-states', target).patch({
data: {
await this.client.rest.patch(Routes.guildVoiceState(this.guild.id, target), {
body: {
channel_id: this.channelId,
suppress: suppressed,
},

View File

@@ -1,7 +1,7 @@
'use strict';
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { WebhookType } = require('discord-api-types/v9');
const { Routes, WebhookType } = require('discord-api-types/v9');
const MessagePayload = require('./MessagePayload');
const { Error } = require('../errors');
const DataResolver = require('../util/DataResolver');
@@ -194,18 +194,19 @@ class Webhook {
let messagePayload;
if (options instanceof MessagePayload) {
messagePayload = options.resolveData();
messagePayload = options.resolveBody();
} else {
messagePayload = MessagePayload.create(this, options).resolveData();
messagePayload = MessagePayload.create(this, options).resolveBody();
}
const { data, files } = await messagePayload.resolveFiles();
const d = await this.client.api.webhooks(this.id, this.token).post({
data,
files,
query: { thread_id: messagePayload.options.threadId, wait: true },
auth: false,
});
const query = new URLSearchParams({ wait: true });
if (messagePayload.options.threadId) {
query.set('thread_id', messagePayload.options.threadId);
}
const { body, files } = await messagePayload.resolveFiles();
const d = await this.client.rest.post(Routes.webhook(this.id, this.token), { body, files, query, auth: false });
return this.client.channels?.cache.get(d.channel_id)?.messages._add(d, false) ?? d;
}
@@ -230,10 +231,10 @@ class Webhook {
async sendSlackMessage(body) {
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
const data = await this.client.api.webhooks(this.id, this.token).slack.post({
query: { wait: true },
const data = await this.client.rest.post(Routes.webhookPlatform(this.id, this.token, 'slack'), {
query: new URLSearchParams({ wait: true }),
auth: false,
data: body,
body,
});
return data.toString() === 'ok';
}
@@ -257,8 +258,8 @@ class Webhook {
avatar = await DataResolver.resolveImage(avatar);
}
channel &&= channel.id ?? channel;
const data = await this.client.api.webhooks(this.id, channel ? undefined : this.token).patch({
data: { name, avatar, channel_id: channel },
const data = await this.client.rest.patch(Routes.webhook(this.id, channel ? undefined : this.token), {
body: { name, avatar, channel_id: channel },
reason,
auth: !this.token || Boolean(channel),
});
@@ -287,15 +288,14 @@ class Webhook {
async fetchMessage(message, { cache = true, threadId } = {}) {
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
const data = await this.client.api
.webhooks(this.id, this.token)
.messages(message)
.get({
query: {
thread_id: threadId,
},
auth: false,
});
const data = await this.client.rest.get(Routes.webhookMessage(this.id, this.token, message), {
query: threadId
? new URLSearchParams({
thread_id: threadId,
})
: undefined,
auth: false,
});
return this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cache) ?? data;
}
@@ -314,19 +314,21 @@ class Webhook {
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { data, files } = await messagePayload.resolveData().resolveFiles();
const { body, files } = await messagePayload.resolveData().resolveFiles();
const d = await this.client.api
.webhooks(this.id, this.token)
.messages(typeof message === 'string' ? message : message.id)
.patch({
data,
const d = await this.client.rest.patch(
Routes.webhookMessage(this.id, this.token, typeof message === 'string' ? message : message.id),
{
body,
files,
query: {
thread_id: messagePayload.options.threadId,
},
query: messagePayload.options.threadId
? new URLSearchParams({
thread_id: messagePayload.options.threadId,
})
: undefined,
auth: false,
});
},
);
const messageManager = this.client.channels?.cache.get(d.channel_id)?.messages;
if (!messageManager) return d;
@@ -345,7 +347,7 @@ class Webhook {
* @returns {Promise<void>}
*/
async delete(reason) {
await this.client.api.webhooks(this.id, this.token).delete({ reason, auth: !this.token });
await this.client.rest.delete(Routes.webhook(this.id, this.token), { reason, auth: !this.token });
}
/**
@@ -357,15 +359,17 @@ class Webhook {
async deleteMessage(message, threadId) {
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
await this.client.api
.webhooks(this.id, this.token)
.messages(typeof message === 'string' ? message : message.id)
.delete({
query: {
thread_id: threadId,
},
await this.client.rest.delete(
Routes.webhookMessage(this.id, this.token, typeof message === 'string' ? message : message.id),
{
query: threadId
? new URLSearchParams({
thread_id: threadId,
})
: undefined,
auth: false,
});
},
);
}
/**
@@ -392,7 +396,7 @@ class Webhook {
* @readonly
*/
get url() {
return this.client.options.http.api + this.client.api.webhooks(this.id, this.token);
return this.client.options.rest.api + Routes.webhook(this.id, this.token);
}
/**
@@ -401,7 +405,7 @@ class Webhook {
* @returns {?string}
*/
avatarURL(options = {}) {
return this.avatar && this.client.rest.cdn.Avatar(this.id, this.avatar, options);
return this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options);
}
/**

View File

@@ -1,6 +1,7 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { Routes } = require('discord-api-types/v9');
const Base = require('./Base');
const WidgetMember = require('./WidgetMember');
@@ -77,7 +78,7 @@ class Widget extends Base {
* @returns {Promise<Widget>}
*/
async fetch() {
const data = await this.client.api.guilds(this.id, 'widget.json').get();
const data = await this.client.rest.get(Routes.guildWidgetJSON(this.id));
this._patch(data);
return this;
}

View File

@@ -75,7 +75,7 @@ class Application extends Base {
* @returns {?string}
*/
iconURL(options = {}) {
return this.icon && this.client.rest.cdn.AppIcon(this.id, this.icon, options);
return this.icon && this.client.rest.cdn.appIcon(this.id, this.icon, options);
}
/**
@@ -84,7 +84,7 @@ class Application extends Base {
* @returns {?string}
*/
coverURL(options = {}) {
return this.cover && this.client.rest.cdn.AppIcon(this.id, this.cover, options);
return this.cover && this.client.rest.cdn.appIcon(this.id, this.cover, options);
}
/**

View File

@@ -1,6 +1,6 @@
'use strict';
const { InteractionResponseType } = require('discord-api-types/v9');
const { InteractionResponseType, Routes } = require('discord-api-types/v9');
const { Error } = require('../../errors');
const MessageFlags = require('../../util/MessageFlags');
const MessagePayload = require('../MessagePayload');
@@ -56,8 +56,8 @@ class InteractionResponses {
async deferReply(options = {}) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
this.ephemeral = options.ephemeral ?? false;
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
flags: options.ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined,
@@ -96,10 +96,10 @@ class InteractionResponses {
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { data, files } = await messagePayload.resolveData().resolveFiles();
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.ChannelMessageWithSource,
data,
},
@@ -180,8 +180,8 @@ class InteractionResponses {
*/
async deferUpdate(options = {}) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.DeferredMessageUpdate,
},
auth: false,
@@ -211,10 +211,10 @@ class InteractionResponses {
if (options instanceof MessagePayload) messagePayload = options;
else messagePayload = MessagePayload.create(this, options);
const { data, files } = await messagePayload.resolveData().resolveFiles();
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.UpdateMessage,
data,
},

View File

@@ -2,7 +2,7 @@
const { Collection } = require('@discordjs/collection');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { InteractionType } = require('discord-api-types/v9');
const { InteractionType, Routes } = require('discord-api-types/v9');
const { TypeError, Error } = require('../../errors');
const InteractionCollector = require('../InteractionCollector');
const MessageCollector = require('../MessageCollector');
@@ -166,13 +166,13 @@ class TextBasedChannel {
let messagePayload;
if (options instanceof MessagePayload) {
messagePayload = options.resolveData();
messagePayload = options.resolveBody();
} else {
messagePayload = MessagePayload.create(this, options).resolveData();
messagePayload = MessagePayload.create(this, options).resolveBody();
}
const { data, files } = await messagePayload.resolveFiles();
const d = await this.client.api.channels[this.id].messages.post({ data, files });
const { body, files } = await messagePayload.resolveFiles();
const d = await this.client.rest.post(Routes.channelMessages(this.id), { body, files });
return this.messages.cache.get(d.id) ?? this.messages._add(d);
}
@@ -185,7 +185,7 @@ class TextBasedChannel {
* channel.sendTyping();
*/
async sendTyping() {
await this.client.api.channels(this.id).typing.post();
await this.client.rest.post(Routes.channelTyping(this.id));
}
/**
@@ -298,7 +298,7 @@ class TextBasedChannel {
}
if (messageIds.length === 0) return new Collection();
if (messageIds.length === 1) {
await this.client.api.channels(this.id).messages(messageIds[0]).delete();
await this.client.rest.delete(Routes.channelMessage(this.id, messageIds[0]));
const message = this.client.actions.MessageDelete.getMessage(
{
message_id: messageIds[0],
@@ -307,7 +307,7 @@ class TextBasedChannel {
);
return message ? new Collection([[message.id, message]]) : new Collection();
}
await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { messages: messageIds } });
await this.client.rest.post(Routes.channelBulkDelete(this.id), { body: { messages: messageIds } });
return messageIds.reduce(
(col, id) =>
col.set(

View File

@@ -1,9 +1,8 @@
'use strict';
const process = require('node:process');
const { ChannelType, MessageType, StickerFormatType } = require('discord-api-types/v9');
const { ChannelType, MessageType } = require('discord-api-types/v9');
const Package = (exports.Package = require('../../package.json'));
const { Error, RangeError, TypeError } = require('../errors');
exports.UserAgent = `DiscordBot (${Package.homepage}, ${Package.version}) Node.js/${process.version}`;
@@ -16,84 +15,6 @@ exports.WSCodes = {
4014: 'DISALLOWED_INTENTS',
};
const AllowedImageFormats = ['webp', 'png', 'jpg', 'jpeg'];
const AllowedImageSizes = [16, 32, 56, 64, 96, 128, 256, 300, 512, 600, 1024, 2048, 4096];
function makeImageUrl(root, { hash, format = 'webp', forceStatic = false, size } = {}) {
if (!['undefined', 'number'].includes(typeof size)) throw new TypeError('INVALID_TYPE', 'size', 'number');
if (!AllowedImageFormats.includes(format)) throw new Error('IMAGE_FORMAT', format);
if (size && !AllowedImageSizes.includes(size)) throw new RangeError('IMAGE_SIZE', size);
if (!forceStatic && hash?.startsWith('a_')) format = 'gif';
return `${root}${hash ? `/${hash}` : ''}.${format}${size ? `?size=${size}` : ''}`;
}
/**
* A list of image sizes:
* * `16`
* * `32`
* * `56`
* * `64`
* * `96`
* * `128`
* * `256`
* * `300`
* * `512`
* * `600`
* * `1024`
* * `2048`
* * `4096`
* @typedef {number} ImageSize
*/
/**
* A list of image formats:
* * `webp`
* * `png`
* * `jpg`
* * `jpeg`
* @typedef {string} ImageFormat
*/
/**
* Options for image URLs.
* @typedef {Object} ImageURLOptions
* @property {ImageFormat} [format='webp'] An image format.
* @property {boolean} [forceStatic=false] If `true`, the format will be as specified.
* If `false`, `format` may be a `gif` if animated.
* @property {ImageSize} [size] An image size.
*/
// https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints
exports.Endpoints = {
CDN(root) {
return {
Emoji: (emojiId, format) => `${root}/emojis/${emojiId}.${format}`,
DefaultAvatar: discriminator => `${root}/embed/avatars/${discriminator}.png`,
Avatar: (userId, hash, options) => makeImageUrl(`${root}/avatars/${userId}`, { hash, ...options }),
GuildMemberAvatar: (guildId, memberId, hash, options) =>
makeImageUrl(`${root}/guilds/${guildId}/users/${memberId}/avatars`, { hash, ...options }),
Banner: (id, hash, options) => makeImageUrl(`${root}/banners/${id}`, { hash, ...options }),
Icon: (guildId, hash, options) => makeImageUrl(`${root}/icons/${guildId}`, { hash, ...options }),
AppIcon: (appId, hash, options) => makeImageUrl(`${root}/app-icons/${appId}`, { hash, ...options }),
AppAsset: (appId, hash, options) => makeImageUrl(`${root}/app-assets/${appId}`, { hash, ...options }),
StickerPackBanner: (bannerId, options) =>
makeImageUrl(`${root}/app-assets/710982414301790216/store/${bannerId}`, options),
GDMIcon: (channelId, hash, options) => makeImageUrl(`${root}/channel-icons/${channelId}`, { hash, ...options }),
Splash: (guildId, hash, options) => makeImageUrl(`${root}/splashes/${guildId}`, { hash, ...options }),
DiscoverySplash: (guildId, hash, options) =>
makeImageUrl(`${root}/discovery-splashes/${guildId}`, { hash, ...options }),
TeamIcon: (teamId, hash, options) => makeImageUrl(`${root}/team-icons/${teamId}`, { hash, ...options }),
Sticker: (stickerId, format) =>
`${root}/stickers/${stickerId}.${format === StickerFormatType.Lottie ? 'json' : 'png'}`,
RoleIcon: (roleId, hash, options) => makeImageUrl(`${root}/role-icons/${roleId}`, { hash, ...options }),
};
},
invite: (root, code, eventId) => (eventId ? `${root}/${code}?event=${eventId}` : `${root}/${code}`),
scheduledEvent: (root, guildId, eventId) => `${root}/${guildId}/${eventId}`,
botGateway: '/gateway/bot',
};
/**
* The current status of the client. Here are the available statuses:
* * READY: 0
@@ -135,10 +56,6 @@ exports.Opcodes = {
};
exports.Events = {
RATE_LIMIT: 'rateLimit',
INVALID_REQUEST_WARNING: 'invalidRequestWarning',
API_RESPONSE: 'apiResponse',
API_REQUEST: 'apiRequest',
CLIENT_READY: 'ready',
GUILD_CREATE: 'guildCreate',
GUILD_DELETE: 'guildDelete',

View File

@@ -66,7 +66,7 @@ class DataResolver extends null {
if (typeof image === 'string' && image.startsWith('data:')) {
return image;
}
const file = await this.resolveFileAsBuffer(image);
const file = await this.resolveFile(image);
return DataResolver.resolveBase64(file);
}
@@ -102,12 +102,19 @@ class DataResolver extends null {
*/
/**
* Resolves a BufferResolvable to a Buffer or a Stream.
* Resolves a BufferResolvable to a Buffer.
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
* @returns {Promise<Buffer|Stream>}
* @returns {Promise<Buffer>}
*/
static async resolveFile(resource) {
if (Buffer.isBuffer(resource) || resource instanceof stream.Readable) return resource;
if (Buffer.isBuffer(resource)) return resource;
if (resource instanceof stream.Readable) {
const buffers = [];
for await (const data of resource) buffers.push(data);
return Buffer.concat(buffers);
}
if (typeof resource === 'string') {
if (/^https?:\/\//.test(resource)) {
const res = await fetch(resource);
@@ -126,20 +133,6 @@ class DataResolver extends null {
throw new TypeError('REQ_RESOURCE_TYPE');
}
/**
* Resolves a BufferResolvable to a Buffer.
* @param {BufferResolvable|Stream} resource The buffer or stream resolvable to resolve
* @returns {Promise<Buffer>}
*/
static async resolveFileAsBuffer(resource) {
const file = await this.resolveFile(resource);
if (Buffer.isBuffer(file)) return file;
const buffers = [];
for await (const data of file) buffers.push(data);
return Buffer.concat(buffers);
}
}
module.exports = DataResolver;

View File

@@ -1,24 +1,7 @@
'use strict';
const process = require('node:process');
/**
* Rate limit data
* @typedef {Object} RateLimitData
* @property {number} timeout Time until this rate limit ends, in milliseconds
* @property {number} limit The maximum amount of requests of this endpoint
* @property {string} method The HTTP method of this request
* @property {string} path The path of the request relative to the HTTP endpoint
* @property {string} route The route of the request relative to the HTTP endpoint
* @property {boolean} global Whether this is a global rate limit
*/
/**
* Whether this rate limit should throw an Error
* @typedef {Function} RateLimitQueueFilter
* @param {RateLimitData} rateLimitData The data of this rate limit
* @returns {boolean|Promise<boolean>}
*/
const { DefaultRestOptions } = require('@discordjs/rest');
/**
* @typedef {Function} CacheFactory
@@ -40,36 +23,18 @@ const process = require('node:process');
* <warn>Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`,
* and `PermissionOverwriteManager` is unsupported and **will** break functionality</warn>
* @property {MessageMentionOptions} [allowedMentions] Default value for {@link MessageOptions#allowedMentions}
* @property {number} [invalidRequestWarningInterval=0] The number of invalid REST requests (those that return
* 401, 403, or 429) in a 10 minute window between emitted warnings (0 for no warnings). That is, if set to 500,
* warnings will be emitted at invalid request number 500, 1000, 1500, and so on.
* @property {PartialType[]} [partials] Structures allowed to be partial. This means events can be emitted even when
* they're missing all the data for a particular structure. See the "Partial Structures" topic on the
* [guide](https://discordjs.guide/popular-topics/partials.html) for some
* important usage information, as partials require you to put checks in place when handling data.
* @property {number} [restTimeOffset=500] Extra time in milliseconds to wait before continuing to make REST
* requests (higher values will reduce rate-limiting errors on bad connections)
* @property {number} [restRequestTimeout=15000] Time to wait before cancelling a REST request, in milliseconds
* @property {number} [restSweepInterval=60] How frequently to delete inactive request buckets, in seconds
* (or 0 for never)
* @property {number} [restGlobalRateLimit=0] How many requests to allow sending per second (0 for unlimited, 50 for
* the standard global limit used by Discord)
* @property {string[]|RateLimitQueueFilter} [rejectOnRateLimit] Decides how rate limits and pre-emptive throttles
* should be handled. If this option is an array containing the prefix of the request route (e.g. /channels to match any
* route starting with /channels, such as /channels/222197033908436994/messages) or a function returning true, a
* {@link RateLimitError} will be thrown. Otherwise the request will be queued for later
* @property {number} [retryLimit=1] How many times to retry on 5XX errors
* (Infinity for an indefinite amount of retries)
* @property {boolean} [failIfNotExists=true] Default value for {@link ReplyMessageOptions#failIfNotExists}
* @property {string[]} [userAgentSuffix] An array of additional bot info to be appended to the end of the required
* [User Agent](https://discord.com/developers/docs/reference#user-agent) header
* @property {PresenceData} [presence={}] Presence data to use upon login
* @property {IntentsResolvable} intents Intents to enable for this connection
* @property {number} [waitGuildTimeout=15_000] Time in milliseconds that Clients with the GUILDS intent should wait for
* missing guilds to be received before starting the bot. If not specified, the default is 15 seconds.
* @property {SweeperOptions} [sweepers={}] Options for cache sweeping
* @property {WebsocketOptions} [ws] Options for the WebSocket
* @property {HTTPOptions} [http] HTTP options
* @property {RESTOptions} [rest] Options for the REST manager
*/
/**
@@ -95,26 +60,6 @@ const process = require('node:process');
* sent in the initial guild member list, must be between 50 and 250
*/
/**
* HTTPS Agent options.
* @typedef {Object} AgentOptions
* @see {@link https://nodejs.org/api/https.html#https_class_https_agent}
* @see {@link https://nodejs.org/api/http.html#http_new_agent_options}
*/
/**
* HTTP options
* @typedef {Object} HTTPOptions
* @property {number} [version=9] API version to use
* @property {AgentOptions} [agent={}] HTTPS Agent options
* @property {string} [api='https://discord.com/api'] Base URL of the API
* @property {string} [cdn='https://cdn.discordapp.com'] Base URL of the CDN
* @property {string} [invite='https://discord.gg'] Base URL of invites
* @property {string} [template='https://discord.new'] Base URL of templates
* @property {Object} [headers] Additional headers to send for all API requests
* @property {string} [scheduledEvent='https://discord.com/events'] Base URL of guild scheduled events
*/
/**
* Contains various utilities for client options.
*/
@@ -128,15 +73,8 @@ class Options extends null {
waitGuildTimeout: 15_000,
shardCount: 1,
makeCache: this.cacheWithLimits(this.defaultMakeCacheSettings),
invalidRequestWarningInterval: 0,
partials: [],
restRequestTimeout: 15_000,
restGlobalRateLimit: 0,
retryLimit: 1,
restTimeOffset: 500,
restSweepInterval: 60,
failIfNotExists: true,
userAgentSuffix: [],
presence: {},
sweepers: this.defaultSweeperSettings,
ws: {
@@ -149,15 +87,7 @@ class Options extends null {
},
version: 9,
},
http: {
agent: {},
version: 9,
api: 'https://discord.com/api',
cdn: 'https://cdn.discordapp.com',
invite: 'https://discord.gg',
template: 'https://discord.new',
scheduledEvent: 'https://discord.com/events',
},
rest: DefaultRestOptions,
};
}
@@ -246,3 +176,8 @@ Options.defaultSweeperSettings = {
};
module.exports = Options;
/**
* @external RESTOptions
* @see {@link https://discord.js.org/#/docs/rest/main/typedef/RESTOptions}
*/

View File

@@ -2,10 +2,9 @@
const { parse } = require('node:path');
const { Collection } = require('@discordjs/collection');
const { ChannelType } = require('discord-api-types/v9');
const { ChannelType, RouteBases, Routes } = require('discord-api-types/v9');
const fetch = require('node-fetch');
const { Colors, Endpoints } = require('./Constants');
const Options = require('./Options');
const { Colors } = require('./Constants');
const { Error: DiscordError, RangeError, TypeError } = require('../errors');
const isObject = d => typeof d === 'object' && d !== null;
@@ -269,8 +268,7 @@ class Util extends null {
*/
static async fetchRecommendedShards(token, { guildsPerShard = 1_000, multipleOf = 1 } = {}) {
if (!token) throw new DiscordError('TOKEN_MISSING');
const defaults = Options.createDefault();
const response = await fetch(`${defaults.http.api}/v${defaults.http.version}${Endpoints.botGateway}`, {
const response = await fetch(RouteBases.api + Routes.gatewayBot(), {
method: 'GET',
headers: { Authorization: `Bot ${token.replace(/^Bot\s*/i, '')}` },
});
@@ -495,16 +493,17 @@ class Util extends null {
* @param {number} position New position for the object
* @param {boolean} relative Whether `position` is relative to its current position
* @param {Collection<string, Channel|Role>} sorted A collection of the objects sorted properly
* @param {APIRouter} route Route to call PATCH on
* @param {Client} client The client to use to patch the data
* @param {string} route Route to call PATCH on
* @param {string} [reason] Reason for the change
* @returns {Promise<Channel[]|Role[]>} Updated item list, with `id` and `position` properties
* @private
*/
static async setPosition(item, position, relative, sorted, route, reason) {
static async setPosition(item, position, relative, sorted, client, route, reason) {
let updatedItems = [...sorted.values()];
Util.moveElementInArray(updatedItems, item, position, relative);
updatedItems = updatedItems.map((r, i) => ({ id: r.id, position: i }));
await route.patch({ data: updatedItems, reason });
await client.rest.patch(route, { body: updatedItems, reason });
return updatedItems;
}

View File

@@ -26,6 +26,7 @@ import {
userMention,
} from '@discordjs/builders';
import { Collection } from '@discordjs/collection';
import { ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
import {
APIActionRowComponent,
APIApplicationCommand,
@@ -81,8 +82,6 @@ import {
} from 'discord-api-types/v9';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { AgentOptions } from 'node:https';
import { Response } from 'node-fetch';
import { Stream } from 'node:stream';
import { MessagePort, Worker } from 'node:worker_threads';
import * as WebSocket from 'ws';
@@ -270,45 +269,11 @@ export abstract class Base {
export class BaseClient extends EventEmitter {
public constructor(options?: ClientOptions | WebhookClientOptions);
private readonly api: unknown;
private rest: unknown;
private decrementMaxListeners(): void;
private incrementMaxListeners(): void;
public on<K extends keyof BaseClientEvents>(
event: K,
listener: (...args: BaseClientEvents[K]) => Awaitable<void>,
): this;
public on<S extends string | symbol>(
event: Exclude<S, keyof BaseClientEvents>,
listener: (...args: any[]) => Awaitable<void>,
): this;
public once<K extends keyof BaseClientEvents>(
event: K,
listener: (...args: BaseClientEvents[K]) => Awaitable<void>,
): this;
public once<S extends string | symbol>(
event: Exclude<S, keyof BaseClientEvents>,
listener: (...args: any[]) => Awaitable<void>,
): this;
public emit<K extends keyof BaseClientEvents>(event: K, ...args: BaseClientEvents[K]): boolean;
public emit<S extends string | symbol>(event: Exclude<S, keyof BaseClientEvents>, ...args: unknown[]): boolean;
public off<K extends keyof BaseClientEvents>(
event: K,
listener: (...args: BaseClientEvents[K]) => Awaitable<void>,
): this;
public off<S extends string | symbol>(
event: Exclude<S, keyof BaseClientEvents>,
listener: (...args: any[]) => Awaitable<void>,
): this;
public removeAllListeners<K extends keyof BaseClientEvents>(event?: K): this;
public removeAllListeners<S extends string | symbol>(event?: Exclude<S, keyof BaseClientEvents>): this;
public options: ClientOptions | WebhookClientOptions;
public rest: REST;
public destroy(): void;
public toJSON(...props: Record<string, boolean | string>[]): unknown;
}
@@ -819,8 +784,7 @@ export class DataResolver extends null {
private constructor();
public static resolveBase64(data: Base64Resolvable): string;
public static resolveCode(data: string, regx: RegExp): string;
public static resolveFile(resource: BufferResolvable | Stream): Promise<Buffer | Stream>;
public static resolveFileAsBuffer(resource: BufferResolvable | Stream): Promise<Buffer>;
public static resolveFile(resource: BufferResolvable | Stream): Promise<Buffer>;
public static resolveImage(resource: BufferResolvable | Base64Resolvable): Promise<string | null>;
public static resolveInviteCode(data: InviteResolvable): string;
public static resolveGuildTemplateCode(data: GuildTemplateResolvable): string;
@@ -871,17 +835,6 @@ export class EnumResolvers extends null {
): IntegrationExpireBehavior;
}
export class DiscordAPIError extends Error {
private constructor(error: unknown, status: number, request: unknown);
private static flattenErrors(obj: unknown, key: string): string[];
public code: number;
public method: string;
public path: string;
public httpStatus: number;
public requestData: HTTPErrorData;
}
export class DMChannel extends TextBasedChannelMixin(Channel, ['bulkDelete']) {
private constructor(client: Client, data?: RawDMChannelData);
public messages: MessageManager;
@@ -1261,22 +1214,6 @@ export class GuildPreviewEmoji extends BaseGuildEmoji {
public roles: Snowflake[];
}
export class HTTPError extends Error {
private constructor(message: string, name: string, code: number, request: unknown);
public code: number;
public method: string;
public name: string;
public path: string;
public requestData: HTTPErrorData;
}
// tslint:disable-next-line:no-empty-interface - Merge RateLimitData into RateLimitError to not have to type it again
export interface RateLimitError extends RateLimitData {}
export class RateLimitError extends Error {
private constructor(data: RateLimitData);
public name: 'RateLimitError';
}
export class Integration extends Base {
private constructor(client: Client, data: RawIntegrationData, guild: Guild);
public account: IntegrationAccount;
@@ -1680,13 +1617,13 @@ export class MessageMentions {
export class MessagePayload {
public constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions);
public data: RawMessagePayloadData | null;
public body: RawMessagePayloadData | null;
public readonly isUser: boolean;
public readonly isWebhook: boolean;
public readonly isMessage: boolean;
public readonly isMessageManager: boolean;
public readonly isInteraction: boolean;
public files: HTTPAttachmentData[] | null;
public files: RawFile[] | null;
public options: MessageOptions | WebhookMessageOptions;
public target: MessageTarget;
@@ -1695,12 +1632,10 @@ export class MessagePayload {
options: string | MessageOptions | WebhookMessageOptions,
extra?: MessageOptions | WebhookMessageOptions,
): MessagePayload;
public static resolveFile(
fileLike: BufferResolvable | Stream | FileOptions | MessageAttachment,
): Promise<HTTPAttachmentData>;
public static resolveFile(fileLike: BufferResolvable | Stream | FileOptions | MessageAttachment): Promise<RawFile>;
public makeContent(): string | undefined;
public resolveData(): this;
public resolveBody(): this;
public resolveFiles(): Promise<this>;
}
@@ -2355,7 +2290,8 @@ export class Util extends null {
position: number,
relative: boolean,
sorted: Collection<Snowflake, T>,
route: unknown,
client: Client,
route: string,
reason?: string,
): Promise<{ id: Snowflake; position: number }[]>;
public static splitMessage(text: string, options?: SplitOptions): string[];
@@ -2636,29 +2572,6 @@ export const Constants: {
[key: string]: unknown;
};
UserAgent: string;
Endpoints: {
botGateway: string;
invite: (root: string, code: string, eventId?: Snowflake) => string;
scheduledEvent: (root: string, guildId: Snowflake, eventId: Snowflake) => string;
CDN: (root: string) => {
Emoji: (emojiId: Snowflake, format: 'gif' | 'png') => string;
Asset: (name: string) => string;
DefaultAvatar: (discriminator: number) => string;
Avatar: (userId: Snowflake, hash: string, options: ImageURLOptions) => string;
Banner: (id: Snowflake, hash: string, options: ImageURLOptions) => string;
GuildMemberAvatar: (guildId: Snowflake, memberId: Snowflake, hash: string, options: ImageURLOptions) => string;
Icon: (guildId: Snowflake, hash: string, options: ImageURLOptions) => string;
AppIcon: (appId: Snowflake, hash: string, options: ImageURLOptions) => string;
AppAsset: (appId: Snowflake, hash: string, options: ImageURLOptions) => string;
StickerPackBanner: (bannerId: Snowflake, options: ImageURLOptions) => string;
GDMIcon: (channelId: Snowflake, hash: string, options: ImageURLOptions) => string;
Splash: (guildId: Snowflake, hash: string, options: ImageURLOptions) => string;
DiscoverySplash: (guildId: Snowflake, hash: string, options: ImageURLOptions) => string;
TeamIcon: (teamId: Snowflake, hash: string, options: ImageURLOptions) => string;
Sticker: (stickerId: Snowflake, format: StickerFormatType) => string;
RoleIcon: (roleId: Snowflake, hash: string, options: ImageURLOptions) => string;
};
};
WSCodes: {
1000: 'WS_CLOSE_REQUESTED';
4004: 'TOKEN_INVALID';
@@ -2726,7 +2639,7 @@ export class ApplicationCommandManager<
PermissionsGuildType,
null
>;
private commandPath({ id, guildId }: { id?: Snowflake; guildId?: Snowflake }): unknown;
private commandPath({ id, guildId }: { id?: Snowflake; guildId?: Snowflake }): string;
public create(command: ApplicationCommandDataResolvable, guildId?: Snowflake): Promise<ApplicationCommandScope>;
public delete(command: ApplicationCommandResolvable, guildId?: Snowflake): Promise<ApplicationCommandScope | null>;
public edit(
@@ -2797,7 +2710,7 @@ export class ApplicationCommandPermissionsManager<
fullPermissions: GuildApplicationCommandPermissionData[];
},
): Promise<Collection<Snowflake, ApplicationCommandPermissions[]>>;
private permissionsPath(guildId: Snowflake, commandId?: Snowflake): unknown;
private permissionsPath(guildId: Snowflake, commandId?: Snowflake): string;
}
export class BaseGuildEmojiManager extends CachedManager<Snowflake, GuildEmoji, EmojiResolvable> {
@@ -3208,24 +3121,12 @@ export interface AddGuildMemberOptions {
fetchWhenExisting?: boolean;
}
export type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'jpeg';
export type AllowedImageSize = 16 | 32 | 56 | 64 | 96 | 128 | 256 | 300 | 512 | 600 | 1024 | 2048 | 4096;
export type AllowedPartial = User | Channel | GuildMember | Message | MessageReaction;
export type AllowedThreadTypeForNewsChannel = ChannelType.GuildNewsThread;
export type AllowedThreadTypeForTextChannel = ChannelType.GuildPublicThread | ChannelType.GuildPrivateThread;
export interface APIRequest {
method: 'get' | 'post' | 'delete' | 'patch' | 'put';
options: unknown;
path: string;
retries: number;
route: string;
}
export interface BaseApplicationCommandData {
name: string;
defaultPermission?: boolean;
@@ -3554,15 +3455,7 @@ export interface ChannelWebhookCreateOptions {
reason?: string;
}
export interface BaseClientEvents {
apiResponse: [request: APIRequest, response: Response];
apiRequest: [request: APIRequest];
debug: [message: string];
rateLimit: [rateLimitData: RateLimitData];
invalidRequestWarning: [invalidRequestWarningData: InvalidRequestWarningData];
}
export interface ClientEvents extends BaseClientEvents {
export interface ClientEvents {
cacheSweep: [message: string];
channelCreate: [channel: NonThreadGuildBasedChannel];
channelDelete: [channel: DMChannel | NonThreadGuildBasedChannel];
@@ -3571,6 +3464,7 @@ export interface ClientEvents extends BaseClientEvents {
oldChannel: DMChannel | NonThreadGuildBasedChannel,
newChannel: DMChannel | NonThreadGuildBasedChannel,
];
debug: [message: string];
warn: [message: string];
emojiCreate: [emoji: GuildEmoji];
emojiDelete: [emoji: GuildEmoji];
@@ -3652,22 +3546,14 @@ export interface ClientOptions {
shardCount?: number;
makeCache?: CacheFactory;
allowedMentions?: MessageMentionOptions;
invalidRequestWarningInterval?: number;
partials?: PartialTypes[];
restTimeOffset?: number;
restRequestTimeout?: number;
restGlobalRateLimit?: number;
restSweepInterval?: number;
retryLimit?: number;
failIfNotExists?: boolean;
userAgentSuffix?: string[];
presence?: PresenceData;
intents: BitFieldResolvable<IntentsString, number>;
waitGuildTimeout?: number;
sweepers?: SweeperOptions;
ws?: WebSocketOptions;
http?: HTTPOptions;
rejectOnRateLimit?: string[] | ((data: RateLimitData) => boolean | Promise<boolean>);
rest?: RESTOptions;
}
export type ClientPresenceStatus = 'online' | 'idle' | 'dnd';
@@ -3796,10 +3682,6 @@ export interface ConstantsColors {
}
export interface ConstantsEvents {
RATE_LIMIT: 'rateLimit';
INVALID_REQUEST_WARNING: 'invalidRequestWarning';
API_RESPONSE: 'apiResponse';
API_REQUEST: 'apiRequest';
CLIENT_READY: 'ready';
GUILD_CREATE: 'guildCreate';
GUILD_DELETE: 'guildDelete';
@@ -4537,35 +4419,6 @@ export type GuildVoiceChannelResolvable = VoiceBasedChannel | Snowflake;
export type HexColorString = `#${string}`;
export interface HTTPAttachmentData {
attachment: string | Buffer | Stream;
name: string;
file: Buffer | Stream;
}
export interface HTTPErrorData {
json: unknown;
files: HTTPAttachmentData[];
}
export interface HTTPOptions {
agent?: Omit<AgentOptions, 'keepAlive'>;
api?: string;
version?: number;
host?: string;
cdn?: string;
invite?: string;
template?: string;
headers?: Record<string, string>;
scheduledEvent?: string;
}
export interface ImageURLOptions {
format?: AllowedImageFormat;
forceStatic?: boolean;
size?: AllowedImageSize;
}
export interface IntegrationAccount {
id: string | Snowflake;
name: string;
@@ -5004,20 +4857,6 @@ export type PresenceStatusData = ClientPresenceStatus | 'invisible';
export type PresenceStatus = PresenceStatusData | 'offline';
export interface RateLimitData {
timeout: number;
limit: number;
method: string;
path: string;
route: string;
global: boolean;
}
export interface InvalidRequestWarningData {
count: number;
remainingTime: number;
}
export interface ReactionCollectorOptions extends CollectorOptions<[MessageReaction, User]> {
max?: number;
maxEmojis?: number;
@@ -5270,10 +5109,7 @@ export interface WebhookClientDataURL {
url: string;
}
export type WebhookClientOptions = Pick<
ClientOptions,
'allowedMentions' | 'restTimeOffset' | 'restRequestTimeout' | 'retryLimit' | 'http'
>;
export type WebhookClientOptions = Pick<ClientOptions, 'allowedMentions' | 'rest'>;
export interface WebhookEditData {
name?: string;
@@ -5455,3 +5291,4 @@ export {
SelectMenuOption,
ActionRowComponent,
} from '@discordjs/builders';
export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';