diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 854d81350..4c38a4ea5 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -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" }, diff --git a/packages/discord.js/src/client/BaseClient.js b/packages/discord.js/src/client/BaseClient.js index 65149ff46..86ec6bc57 100644 --- a/packages/discord.js/src/client/BaseClient.js +++ b/packages/discord.js/src/client/BaseClient.js @@ -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} */ diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 00aa765fb..fae1d932a 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -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} + */ diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js index e66696744..bb68a4701 100644 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ b/packages/discord.js/src/client/websocket/WebSocketManager.js @@ -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; }); diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index ec43ca603..b1fd156a8 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -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; diff --git a/packages/discord.js/src/managers/ApplicationCommandManager.js b/packages/discord.js/src/managers/ApplicationCommandManager.js index 406c274ea..d9fbf5fa2 100644 --- a/packages/discord.js/src/managers/ApplicationCommandManager.js +++ b/packages/discord.js/src/managers/ApplicationCommandManager.js @@ -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); diff --git a/packages/discord.js/src/managers/ApplicationCommandPermissionsManager.js b/packages/discord.js/src/managers/ApplicationCommandPermissionsManager.js index af078be73..1ed521738 100644 --- a/packages/discord.js/src/managers/ApplicationCommandPermissionsManager.js +++ b/packages/discord.js/src/managers/ApplicationCommandPermissionsManager.js @@ -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()); } diff --git a/packages/discord.js/src/managers/ChannelManager.js b/packages/discord.js/src/managers/ChannelManager.js index 95795cca4..05fb42bad 100644 --- a/packages/discord.js/src/managers/ChannelManager.js +++ b/packages/discord.js/src/managers/ChannelManager.js @@ -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 }); } } diff --git a/packages/discord.js/src/managers/GuildBanManager.js b/packages/discord.js/src/managers/GuildBanManager.js index e7039b33e..1213f320f 100644 --- a/packages/discord.js/src/managers/GuildBanManager.js +++ b/packages/discord.js/src/managers/GuildBanManager.js @@ -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); } } diff --git a/packages/discord.js/src/managers/GuildChannelManager.js b/packages/discord.js/src/managers/GuildChannelManager.js index 8b2221b96..db8a34430 100644 --- a/packages/discord.js/src/managers/GuildChannelManager.js +++ b/packages/discord.js/src/managers/GuildChannelManager.js @@ -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 }); } } diff --git a/packages/discord.js/src/managers/GuildEmojiManager.js b/packages/discord.js/src/managers/GuildEmojiManager.js index b96b10ba3..d08b8bdcd 100644 --- a/packages/discord.js/src/managers/GuildEmojiManager.js +++ b/packages/discord.js/src/managers/GuildEmojiManager.js @@ -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(); diff --git a/packages/discord.js/src/managers/GuildInviteManager.js b/packages/discord.js/src/managers/GuildInviteManager.js index c229a5810..592fdd597 100644 --- a/packages/discord.js/src/managers/GuildInviteManager.js +++ b/packages/discord.js/src/managers/GuildInviteManager.js @@ -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 }); } } diff --git a/packages/discord.js/src/managers/GuildManager.js b/packages/discord.js/src/managers/GuildManager.js index 51131404d..8ff5ced69 100644 --- a/packages/discord.js/src/managers/GuildManager.js +++ b/packages/discord.js/src/managers/GuildManager.js @@ -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()); } } diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index d69004fac..88b7761aa 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -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>} */ 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>} */ 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); } diff --git a/packages/discord.js/src/managers/GuildMemberRoleManager.js b/packages/discord.js/src/managers/GuildMemberRoleManager.js index bdb60ae00..5e39c2fc2 100644 --- a/packages/discord.js/src/managers/GuildMemberRoleManager.js +++ b/packages/discord.js/src/managers/GuildMemberRoleManager.js @@ -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); diff --git a/packages/discord.js/src/managers/GuildScheduledEventManager.js b/packages/discord.js/src/managers/GuildScheduledEventManager.js index b6ac54302..dd04f259c 100644 --- a/packages/discord.js/src/managers/GuildScheduledEventManager.js +++ b/packages/discord.js/src/managers/GuildScheduledEventManager.js @@ -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( diff --git a/packages/discord.js/src/managers/GuildStickerManager.js b/packages/discord.js/src/managers/GuildStickerManager.js index 68abfb03b..184b8d278 100644 --- a/packages/discord.js/src/managers/GuildStickerManager.js +++ b/packages/discord.js/src/managers/GuildStickerManager.js @@ -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)])); } } diff --git a/packages/discord.js/src/managers/MessageManager.js b/packages/discord.js/src/managers/MessageManager.js index a1e9a8b86..293cc14bf 100644 --- a/packages/discord.js/src/managers/MessageManager.js +++ b/packages/discord.js/src/managers/MessageManager.js @@ -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; diff --git a/packages/discord.js/src/managers/PermissionOverwriteManager.js b/packages/discord.js/src/managers/PermissionOverwriteManager.js index 6e4b6e00c..2fdbc29ad 100644 --- a/packages/discord.js/src/managers/PermissionOverwriteManager.js +++ b/packages/discord.js/src/managers/PermissionOverwriteManager.js @@ -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; } } diff --git a/packages/discord.js/src/managers/ReactionManager.js b/packages/discord.js/src/managers/ReactionManager.js index b587a0e94..cd104f650 100644 --- a/packages/discord.js/src/managers/ReactionManager.js +++ b/packages/discord.js/src/managers/ReactionManager.js @@ -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} */ 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; } } diff --git a/packages/discord.js/src/managers/ReactionUserManager.js b/packages/discord.js/src/managers/ReactionUserManager.js index cc86187e7..d8ac3c566 100644 --- a/packages/discord.js/src/managers/ReactionUserManager.js +++ b/packages/discord.js/src/managers/ReactionUserManager.js @@ -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; } } diff --git a/packages/discord.js/src/managers/RoleManager.js b/packages/discord.js/src/managers/RoleManager.js index d0e0094c2..0ba477a67 100644 --- a/packages/discord.js/src/managers/RoleManager.js +++ b/packages/discord.js/src/managers/RoleManager.js @@ -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, diff --git a/packages/discord.js/src/managers/StageInstanceManager.js b/packages/discord.js/src/managers/StageInstanceManager.js index da58fe135..f2df1f69d 100644 --- a/packages/discord.js/src/managers/StageInstanceManager.js +++ b/packages/discord.js/src/managers/StageInstanceManager.js @@ -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)); } } diff --git a/packages/discord.js/src/managers/ThreadManager.js b/packages/discord.js/src/managers/ThreadManager.js index 38620a100..fbb839905 100644 --- a/packages/discord.js/src/managers/ThreadManager.js +++ b/packages/discord.js/src/managers/ThreadManager.js @@ -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} */ 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} */ 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 }); } diff --git a/packages/discord.js/src/managers/ThreadMemberManager.js b/packages/discord.js/src/managers/ThreadMemberManager.js index 1178fe761..1521f9252 100644 --- a/packages/discord.js/src/managers/ThreadMemberManager.js +++ b/packages/discord.js/src/managers/ThreadMemberManager.js @@ -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} */ 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()); } diff --git a/packages/discord.js/src/managers/UserManager.js b/packages/discord.js/src/managers/UserManager.js index a5d9ba2fe..dbffece4d 100644 --- a/packages/discord.js/src/managers/UserManager.js +++ b/packages/discord.js/src/managers/UserManager.js @@ -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); } diff --git a/packages/discord.js/src/rest/APIRequest.js b/packages/discord.js/src/rest/APIRequest.js deleted file mode 100644 index c1e06081f..000000000 --- a/packages/discord.js/src/rest/APIRequest.js +++ /dev/null @@ -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; diff --git a/packages/discord.js/src/rest/APIRouter.js b/packages/discord.js/src/rest/APIRouter.js deleted file mode 100644 index b22b37f76..000000000 --- a/packages/discord.js/src/rest/APIRouter.js +++ /dev/null @@ -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; diff --git a/packages/discord.js/src/rest/DiscordAPIError.js b/packages/discord.js/src/rest/DiscordAPIError.js deleted file mode 100644 index 3137b7b30..000000000 --- a/packages/discord.js/src/rest/DiscordAPIError.js +++ /dev/null @@ -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} - */ diff --git a/packages/discord.js/src/rest/HTTPError.js b/packages/discord.js/src/rest/HTTPError.js deleted file mode 100644 index 0e9ab9f1c..000000000 --- a/packages/discord.js/src/rest/HTTPError.js +++ /dev/null @@ -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; diff --git a/packages/discord.js/src/rest/RESTManager.js b/packages/discord.js/src/rest/RESTManager.js deleted file mode 100644 index d553c9fe0..000000000 --- a/packages/discord.js/src/rest/RESTManager.js +++ /dev/null @@ -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; diff --git a/packages/discord.js/src/rest/RateLimitError.js b/packages/discord.js/src/rest/RateLimitError.js deleted file mode 100644 index 2b3f3a870..000000000 --- a/packages/discord.js/src/rest/RateLimitError.js +++ /dev/null @@ -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; diff --git a/packages/discord.js/src/rest/RequestHandler.js b/packages/discord.js/src/rest/RequestHandler.js deleted file mode 100644 index d1a49d6df..000000000 --- a/packages/discord.js/src/rest/RequestHandler.js +++ /dev/null @@ -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. - * This is an informational event that is emitted quite frequently, - * it is highly recommended to check `request.path` to filter the data. - * @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. - * This is an informational event that is emitted quite frequently, - * it is highly recommended to check `request.path` to filter the data. - * @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} - */ diff --git a/packages/discord.js/src/structures/AnonymousGuild.js b/packages/discord.js/src/structures/AnonymousGuild.js index 8490d95f4..fbcfd7d00 100644 --- a/packages/discord.js/src/structures/AnonymousGuild.js +++ b/packages/discord.js/src/structures/AnonymousGuild.js @@ -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); } } diff --git a/packages/discord.js/src/structures/AutocompleteInteraction.js b/packages/discord.js/src/structures/AutocompleteInteraction.js index 63abf07ca..f9d32712f 100644 --- a/packages/discord.js/src/structures/AutocompleteInteraction.js +++ b/packages/discord.js/src/structures/AutocompleteInteraction.js @@ -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, diff --git a/packages/discord.js/src/structures/BaseGuild.js b/packages/discord.js/src/structures/BaseGuild.js index 12dce1fd1..580a28170 100644 --- a/packages/discord.js/src/structures/BaseGuild.js +++ b/packages/discord.js/src/structures/BaseGuild.js @@ -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} */ 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); } diff --git a/packages/discord.js/src/structures/BaseGuildTextChannel.js b/packages/discord.js/src/structures/BaseGuildTextChannel.js index 28387ac06..07b0ac890 100644 --- a/packages/discord.js/src/structures/BaseGuildTextChannel.js +++ b/packages/discord.js/src/structures/BaseGuildTextChannel.js @@ -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, }, diff --git a/packages/discord.js/src/structures/Channel.js b/packages/discord.js/src/structures/Channel.js index 6f2a1bb7f..bba09cdf6 100644 --- a/packages/discord.js/src/structures/Channel.js +++ b/packages/discord.js/src/structures/Channel.js @@ -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; } diff --git a/packages/discord.js/src/structures/ClientApplication.js b/packages/discord.js/src/structures/ClientApplication.js index 6ad83464b..10f36456d 100644 --- a/packages/discord.js/src/structures/ClientApplication.js +++ b/packages/discord.js/src/structures/ClientApplication.js @@ -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} */ 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; } diff --git a/packages/discord.js/src/structures/ClientUser.js b/packages/discord.js/src/structures/ClientUser.js index 8399b0951..a3090bd51 100644 --- a/packages/discord.js/src/structures/ClientUser.js +++ b/packages/discord.js/src/structures/ClientUser.js @@ -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; } diff --git a/packages/discord.js/src/structures/Emoji.js b/packages/discord.js/src/structures/Emoji.js index 0df860caa..409d29258 100644 --- a/packages/discord.js/src/structures/Emoji.js +++ b/packages/discord.js/src/structures/Emoji.js @@ -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'); } /** diff --git a/packages/discord.js/src/structures/Guild.js b/packages/discord.js/src/structures/Guild.js index a0f655871..4da9b1ba6 100644 --- a/packages/discord.js/src/structures/Guild.js +++ b/packages/discord.js/src/structures/Guild.js @@ -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>} */ 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} */ 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} */ 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} */ 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} */ 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; } diff --git a/packages/discord.js/src/structures/GuildChannel.js b/packages/discord.js/src/structures/GuildChannel.js index 50a251b1f..1d40a4698 100644 --- a/packages/discord.js/src/structures/GuildChannel.js +++ b/packages/discord.js/src/structures/GuildChannel.js @@ -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; } } diff --git a/packages/discord.js/src/structures/GuildEmoji.js b/packages/discord.js/src/structures/GuildEmoji.js index c38f632fc..004cdf11d 100644 --- a/packages/discord.js/src/structures/GuildEmoji.js +++ b/packages/discord.js/src/structures/GuildEmoji.js @@ -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; } diff --git a/packages/discord.js/src/structures/GuildMember.js b/packages/discord.js/src/structures/GuildMember.js index 51c232779..cd8e24337 100644 --- a/packages/discord.js/src/structures/GuildMember.js +++ b/packages/discord.js/src/structures/GuildMember.js @@ -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); } /** diff --git a/packages/discord.js/src/structures/GuildPreview.js b/packages/discord.js/src/structures/GuildPreview.js index fc2a1bf6a..cef18a31c 100644 --- a/packages/discord.js/src/structures/GuildPreview.js +++ b/packages/discord.js/src/structures/GuildPreview.js @@ -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} */ 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; } diff --git a/packages/discord.js/src/structures/GuildScheduledEvent.js b/packages/discord.js/src/structures/GuildScheduledEvent.js index 675c041b5..769a0a531 100644 --- a/packages/discord.js/src/structures/GuildScheduledEvent.js +++ b/packages/discord.js/src/structures/GuildScheduledEvent.js @@ -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}`; } /** diff --git a/packages/discord.js/src/structures/GuildTemplate.js b/packages/discord.js/src/structures/GuildTemplate.js index f1e37f3a4..a33b66a6e 100644 --- a/packages/discord.js/src/structures/GuildTemplate.js +++ b/packages/discord.js/src/structures/GuildTemplate.js @@ -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} */ 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} */ 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} */ 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}`; } /** diff --git a/packages/discord.js/src/structures/Integration.js b/packages/discord.js/src/structures/Integration.js index 0e69c5f75..d08effdd2 100644 --- a/packages/discord.js/src/structures/Integration.js +++ b/packages/discord.js/src/structures/Integration.js @@ -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; } diff --git a/packages/discord.js/src/structures/Invite.js b/packages/discord.js/src/structures/Invite.js index 7f4726304..802ae6f9f 100644 --- a/packages/discord.js/src/structures/Invite.js +++ b/packages/discord.js/src/structures/Invite.js @@ -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} */ async delete(reason) { - await this.client.api.invites[this.code].delete({ reason }); + await this.client.rest.delete(Routes.invite(this.code), { reason }); return this; } diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 83dc1e971..2652af624 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -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} + * @returns {Promise} */ 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} + */ diff --git a/packages/discord.js/src/structures/MessageReaction.js b/packages/discord.js/src/structures/MessageReaction.js index 8a294be09..f5ea3144d 100644 --- a/packages/discord.js/src/structures/MessageReaction.js +++ b/packages/discord.js/src/structures/MessageReaction.js @@ -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} */ 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; } diff --git a/packages/discord.js/src/structures/NewsChannel.js b/packages/discord.js/src/structures/NewsChannel.js index a39fc2a73..bc8cd6f0b 100644 --- a/packages/discord.js/src/structures/NewsChannel.js +++ b/packages/discord.js/src/structures/NewsChannel.js @@ -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; } } diff --git a/packages/discord.js/src/structures/PartialGroupDMChannel.js b/packages/discord.js/src/structures/PartialGroupDMChannel.js index 6ef75fd08..f604e72bb 100644 --- a/packages/discord.js/src/structures/PartialGroupDMChannel.js +++ b/packages/discord.js/src/structures/PartialGroupDMChannel.js @@ -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() { diff --git a/packages/discord.js/src/structures/Presence.js b/packages/discord.js/src/structures/Presence.js index 2d4e17737..71be96fa4 100644 --- a/packages/discord.js/src/structures/Presence.js +++ b/packages/discord.js/src/structures/Presence.js @@ -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); } } diff --git a/packages/discord.js/src/structures/Role.js b/packages/discord.js/src/structures/Role.js index 90a1f6faa..f0f2ab716 100644 --- a/packages/discord.js/src/structures/Role.js +++ b/packages/discord.js/src/structures/Role.js @@ -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); } /** diff --git a/packages/discord.js/src/structures/Sticker.js b/packages/discord.js/src/structures/Sticker.js index 78f523e88..f9398481e 100644 --- a/packages/discord.js/src/structures/Sticker.js +++ b/packages/discord.js/src/structures/Sticker.js @@ -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} */ 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; } diff --git a/packages/discord.js/src/structures/StickerPack.js b/packages/discord.js/src/structures/StickerPack.js index d5458bbb4..7e599b764 100644 --- a/packages/discord.js/src/structures/StickerPack.js +++ b/packages/discord.js/src/structures/StickerPack.js @@ -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); } } diff --git a/packages/discord.js/src/structures/Team.js b/packages/discord.js/src/structures/Team.js index 773b661ca..98eb199c2 100644 --- a/packages/discord.js/src/structures/Team.js +++ b/packages/discord.js/src/structures/Team.js @@ -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); } /** diff --git a/packages/discord.js/src/structures/ThreadChannel.js b/packages/discord.js/src/structures/ThreadChannel.js index fb69bf1e5..ef295942c 100644 --- a/packages/discord.js/src/structures/ThreadChannel.js +++ b/packages/discord.js/src/structures/ThreadChannel.js @@ -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; } diff --git a/packages/discord.js/src/structures/User.js b/packages/discord.js/src/structures/User.js index cc0ebe1d3..b18b33846 100644 --- a/packages/discord.js/src/structures/User.js +++ b/packages/discord.js/src/structures/User.js @@ -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); } /** diff --git a/packages/discord.js/src/structures/VoiceState.js b/packages/discord.js/src/structures/VoiceState.js index bf8577338..be77df020 100644 --- a/packages/discord.js/src/structures/VoiceState.js +++ b/packages/discord.js/src/structures/VoiceState.js @@ -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, }, diff --git a/packages/discord.js/src/structures/Webhook.js b/packages/discord.js/src/structures/Webhook.js index 1ea3a05f6..7af9662f5 100644 --- a/packages/discord.js/src/structures/Webhook.js +++ b/packages/discord.js/src/structures/Webhook.js @@ -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} */ 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); } /** diff --git a/packages/discord.js/src/structures/Widget.js b/packages/discord.js/src/structures/Widget.js index 7373d0a4d..43c7c84f5 100644 --- a/packages/discord.js/src/structures/Widget.js +++ b/packages/discord.js/src/structures/Widget.js @@ -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} */ 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; } diff --git a/packages/discord.js/src/structures/interfaces/Application.js b/packages/discord.js/src/structures/interfaces/Application.js index feba1b1bc..25cc20193 100644 --- a/packages/discord.js/src/structures/interfaces/Application.js +++ b/packages/discord.js/src/structures/interfaces/Application.js @@ -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); } /** diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index fa30d5e2d..9b0f4dc43 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -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, }, diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index 5d3e790b4..ddc02ff91 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -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( diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index d0d354ff9..c17b1835d 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -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', diff --git a/packages/discord.js/src/util/DataResolver.js b/packages/discord.js/src/util/DataResolver.js index d7d9eb7ac..a96f67dee 100644 --- a/packages/discord.js/src/util/DataResolver.js +++ b/packages/discord.js/src/util/DataResolver.js @@ -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} + * @returns {Promise} */ 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} - */ - 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; diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 774dd40c0..8ac5e8dae 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -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} - */ +const { DefaultRestOptions } = require('@discordjs/rest'); /** * @typedef {Function} CacheFactory @@ -40,36 +23,18 @@ const process = require('node:process'); * Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`, * and `PermissionOverwriteManager` is unsupported and **will** break functionality * @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} + */ diff --git a/packages/discord.js/src/util/Util.js b/packages/discord.js/src/util/Util.js index 48999e6c6..481bf0f9a 100644 --- a/packages/discord.js/src/util/Util.js +++ b/packages/discord.js/src/util/Util.js @@ -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} 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} 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; } diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 43e878a7d..6b72490eb 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -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( - event: K, - listener: (...args: BaseClientEvents[K]) => Awaitable, - ): this; - public on( - event: Exclude, - listener: (...args: any[]) => Awaitable, - ): this; - - public once( - event: K, - listener: (...args: BaseClientEvents[K]) => Awaitable, - ): this; - public once( - event: Exclude, - listener: (...args: any[]) => Awaitable, - ): this; - - public emit(event: K, ...args: BaseClientEvents[K]): boolean; - public emit(event: Exclude, ...args: unknown[]): boolean; - - public off( - event: K, - listener: (...args: BaseClientEvents[K]) => Awaitable, - ): this; - public off( - event: Exclude, - listener: (...args: any[]) => Awaitable, - ): this; - - public removeAllListeners(event?: K): this; - public removeAllListeners(event?: Exclude): this; - public options: ClientOptions | WebhookClientOptions; + public rest: REST; public destroy(): void; public toJSON(...props: Record[]): 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; - public static resolveFileAsBuffer(resource: BufferResolvable | Stream): Promise; + public static resolveFile(resource: BufferResolvable | Stream): Promise; public static resolveImage(resource: BufferResolvable | Base64Resolvable): Promise; 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; + public static resolveFile(fileLike: BufferResolvable | Stream | FileOptions | MessageAttachment): Promise; public makeContent(): string | undefined; - public resolveData(): this; + public resolveBody(): this; public resolveFiles(): Promise; } @@ -2355,7 +2290,8 @@ export class Util extends null { position: number, relative: boolean, sorted: Collection, - 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; public delete(command: ApplicationCommandResolvable, guildId?: Snowflake): Promise; public edit( @@ -2797,7 +2710,7 @@ export class ApplicationCommandPermissionsManager< fullPermissions: GuildApplicationCommandPermissionData[]; }, ): Promise>; - private permissionsPath(guildId: Snowflake, commandId?: Snowflake): unknown; + private permissionsPath(guildId: Snowflake, commandId?: Snowflake): string; } export class BaseGuildEmojiManager extends CachedManager { @@ -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; waitGuildTimeout?: number; sweepers?: SweeperOptions; ws?: WebSocketOptions; - http?: HTTPOptions; - rejectOnRateLimit?: string[] | ((data: RateLimitData) => boolean | Promise); + 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; - api?: string; - version?: number; - host?: string; - cdn?: string; - invite?: string; - template?: string; - headers?: Record; - 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; export interface WebhookEditData { name?: string; @@ -5455,3 +5291,4 @@ export { SelectMenuOption, ActionRowComponent, } from '@discordjs/builders'; +export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';