diff --git a/packages/discord.js/src/client/websocket/handlers/RATE_LIMITED.js b/packages/discord.js/src/client/websocket/handlers/RATE_LIMITED.js new file mode 100644 index 000000000..22249fdde --- /dev/null +++ b/packages/discord.js/src/client/websocket/handlers/RATE_LIMITED.js @@ -0,0 +1,25 @@ +'use strict'; + +const process = require('node:process'); +const { GatewayOpcodes } = require('discord-api-types/v10'); + +const emittedFor = new Set(); + +module.exports = (_, { d: data }) => { + switch (data.opcode) { + case GatewayOpcodes.RequestGuildMembers: { + break; + } + + default: { + if (!emittedFor.has(data.opcode)) { + process.emitWarning( + // eslint-disable-next-line max-len + `Hit a gateway rate limit on opcode ${data.opcode} (${GatewayOpcodes[data.opcode]}). If the discord.js version you're using is up-to-date, please open an issue on GitHub.`, + ); + + emittedFor.add(data.opcode); + } + } + } +}; diff --git a/packages/discord.js/src/client/websocket/handlers/index.js b/packages/discord.js/src/client/websocket/handlers/index.js index 62315be2d..80866c714 100644 --- a/packages/discord.js/src/client/websocket/handlers/index.js +++ b/packages/discord.js/src/client/websocket/handlers/index.js @@ -52,6 +52,7 @@ const handlers = Object.fromEntries([ ['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI')], ['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')], ['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')], + ['RATE_LIMITED', require('./RATE_LIMITED')], ['READY', require('./READY')], ['RESUMED', require('./RESUMED')], ['SOUNDBOARD_SOUNDS', require('./SOUNDBOARD_SOUNDS')], diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index e37a874d7..82929d08a 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -4,8 +4,9 @@ const { process } = require('node:process'); const { setTimeout, clearTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); +const { GatewayRateLimitError } = require('@discordjs/util'); const { DiscordSnowflake } = require('@sapphire/snowflake'); -const { Routes, GatewayOpcodes } = require('discord-api-types/v10'); +const { Routes, GatewayOpcodes, GatewayDispatchEvents } = require('discord-api-types/v10'); const CachedManager = require('./CachedManager'); const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel'); @@ -239,19 +240,25 @@ class GuildMemberManager extends CachedManager { return new Promise((resolve, reject) => { if (!query && !users) query = ''; - this.guild.shard.send({ - op: GatewayOpcodes.RequestGuildMembers, - d: { - guild_id: this.guild.id, - presences, - user_ids: users, - query, - nonce, - limit, - }, - }); const fetchedMembers = new Collection(); let i = 0; + + const cleanup = () => { + /* eslint-disable no-use-before-define */ + clearTimeout(timeout); + + this.client.removeListener(Events.Raw, rateLimitHandler); + this.client.decrementMaxListeners(); + this.client.removeListener(Events.GuildMembersChunk, handler); + this.client.decrementMaxListeners(); + /* eslint-enable no-use-before-define */ + }; + + const timeout = setTimeout(() => { + cleanup(); + reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout)); + }, time).unref(); + const handler = (members, _, chunk) => { if (chunk.nonce !== nonce) return; timeout.refresh(); @@ -260,19 +267,37 @@ class GuildMemberManager extends CachedManager { fetchedMembers.set(member.id, member); } if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) { - clearTimeout(timeout); - this.client.removeListener(Events.GuildMembersChunk, handler); - this.client.decrementMaxListeners(); + cleanup(); resolve(users && !Array.isArray(users) && fetchedMembers.size ? fetchedMembers.first() : fetchedMembers); } }; - const timeout = setTimeout(() => { - this.client.removeListener(Events.GuildMembersChunk, handler); - this.client.decrementMaxListeners(); - reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout)); - }, time).unref(); + + const requestData = { + guild_id: this.guild.id, + presences, + user_ids: users, + query, + nonce, + limit, + }; + + const rateLimitHandler = payload => { + if (payload.t === GatewayDispatchEvents.RateLimited && payload.d.meta.nonce === nonce) { + cleanup(); + reject(new GatewayRateLimitError(payload.d, requestData)); + } + }; + + this.client.incrementMaxListeners(); + this.client.on(Events.Raw, rateLimitHandler); + this.client.incrementMaxListeners(); this.client.on(Events.GuildMembersChunk, handler); + + this.guild.shard.send({ + op: GatewayOpcodes.RequestGuildMembers, + d: requestData, + }); }); }