Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict';

const process = require('node:process');
const { GatewayOpcodes } = require('discord-api-types/v10');
const { Events } = require('../../../util/Events.js');

let emittedWarning = false;

module.exports = (client, { d: data }) => {
/**
* Represents metadata for a gateway request guild member rate limit event
*
* @typedef {Object} GatewayRequestGuildMemberRateLimitMetaData
* @property {Guild} guild The guild related to the rate limited request
* @property {string} nonce The nonce used in the request that was rate limited
*/

/**
* Represents additional metadata for a gateway rate limited event
*
* @typedef {GatewayRequestGuildMemberRateLimitMetaData} GatewayRateLimitMetaData
*/

/**
* Represents the information provided by Discord when a gateway ratelimit is hit.
*
* @typedef {Object} GatewayRateLimitData
* @property {GatewayOpCodes} opcode The opcode of the event that triggered the rate limit
* @property {number} retryAfter The number of seconds to wait before submitting another request
* @property {GatewayRateLimitMetaData} [meta] Additional metadata for the event that was rate limited. Missing for unknown opcodes.
*/

let meta;
switch (data.opcode) {
case GatewayOpcodes.RequestGuildMembers: {
const guild = client.guilds.cache.get(data.meta.guild_id);

meta = {
guild,
nonce: data.nonce,
};

break;
}

default: {
if (!emittedWarning && !client.listenerCount(Events.RateLimited)) {
process.emitWarning(
`Received a rate limit for an unknown opcode: ${data.opcode}. For additional information, listen to the Client#rateLimited event.`,
);
emittedWarning = true;
}
}
}

/**
* Emitted whenever a gateway rate limit is hit.
*
* @event Client#rateLimited
* @param {GatewayRateLimitData} rateLimitData The data related to the rate limit
*/
client.emit(Events.RateLimited, {
opcode: data.opcode,
retryAfter: data.retry_after,
meta,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const PacketHandlers = Object.fromEntries([
['MESSAGE_REACTION_REMOVE_EMOJI', require('./MESSAGE_REACTION_REMOVE_EMOJI.js')],
['MESSAGE_UPDATE', require('./MESSAGE_UPDATE.js')],
['PRESENCE_UPDATE', require('./PRESENCE_UPDATE.js')],
['RATE_LIMITED', require('./RATE_LIMITED.js')],
['READY', require('./READY.js')],
['SOUNDBOARD_SOUNDS', require('./SOUNDBOARD_SOUNDS.js')],
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE.js')],
Expand Down
67 changes: 44 additions & 23 deletions packages/discord.js/src/managers/GuildMemberManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
const { setTimeout, clearTimeout } = require('node:timers');
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { GatewayRateLimitError } = require('@discordjs/util');
const { WebSocketShardEvents } = require('@discordjs/ws');
const { DiscordSnowflake } = require('@sapphire/snowflake');
const { Routes, GatewayOpcodes } = require('discord-api-types/v10');
const { Routes, GatewayOpcodes, GatewayDispatchEvents } = require('discord-api-types/v10');
const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors/index.js');
const { BaseGuildVoiceChannel } = require('../structures/BaseGuildVoiceChannel.js');
const { GuildMember } = require('../structures/GuildMember.js');
Expand Down Expand Up @@ -246,46 +248,65 @@ class GuildMemberManager extends CachedManager {
const query = initialQuery ?? (users ? undefined : '');

return new Promise((resolve, reject) => {
this.guild.client.ws.send(this.guild.shardId, {
op: GatewayOpcodes.RequestGuildMembers,
// eslint-disable-next-line id-length
d: {
guild_id: this.guild.id,
presences,
user_ids: users,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
let index = 0;

const cleanup = () => {
/* eslint-disable no-use-before-define */
clearTimeout(timeout);

this.client.ws.removeListener(WebSocketShardEvents.Dispatch, rateLimitHandler);
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;

// eslint-disable-next-line no-use-before-define
timeout.refresh();
index++;
for (const member of members.values()) {
fetchedMembers.set(member.id, member);
}

if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || index === chunk.count) {
// eslint-disable-next-line no-use-before-define
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.ws.on(WebSocketShardEvents.Dispatch, rateLimitHandler);

this.client.incrementMaxListeners();
this.client.on(Events.GuildMembersChunk, handler);

this.guild.client.ws.send(this.guild.shardId, {
op: GatewayOpcodes.RequestGuildMembers,
// eslint-disable-next-line id-length
d: requestData,
});
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/discord.js/src/util/Events.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
* @property {string} MessageReactionRemoveEmoji messageReactionRemoveEmoji
* @property {string} MessageUpdate messageUpdate
* @property {string} PresenceUpdate presenceUpdate
* @property {string} RateLimited rateLimited
* @property {string} SoundboardSounds soundboardSounds
* @property {string} StageInstanceCreate stageInstanceCreate
* @property {string} StageInstanceDelete stageInstanceDelete
Expand Down Expand Up @@ -156,6 +157,7 @@ exports.Events = {
MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji',
MessageUpdate: 'messageUpdate',
PresenceUpdate: 'presenceUpdate',
RateLimited: 'rateLimited',
SoundboardSounds: 'soundboardSounds',
StageInstanceCreate: 'stageInstanceCreate',
StageInstanceDelete: 'stageInstanceDelete',
Expand Down
14 changes: 14 additions & 0 deletions packages/discord.js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ import {
GatewayIntentBits,
GatewayInteractionCreateDispatchData,
GatewayMessageUpdateDispatchData,
GatewayOpcodeRateLimitMetadataMap,
GatewayPresenceUpdate,
GatewaySendPayload,
GatewayTypingStartDispatchData,
Expand Down Expand Up @@ -5439,6 +5440,17 @@ export interface WebhookCreateOptions extends ChannelWebhookCreateOptions {
channel: AnnouncementChannel | ForumChannel | MediaChannel | Snowflake | StageChannel | TextChannel | VoiceChannel;
}

export interface GatewayRequestGuildMemberRateLimitMetaData {
guild: Guild;
nonce: string;
}

export interface GatewayRateLimitData {
meta?: GatewayRequestGuildMemberRateLimitMetaData;
opcode: keyof GatewayOpcodeRateLimitMetadataMap;
retryAfter: number;
}

export interface GuildMembersChunk {
count: number;
index: number;
Expand Down Expand Up @@ -5537,6 +5549,7 @@ export interface ClientEventTypes {
newMessage: OmitPartialGroupDMChannel<Message>,
];
presenceUpdate: [oldPresence: Presence | null, newPresence: Presence];
rateLimit: [rateLimitData: GatewayRateLimitData];
roleCreate: [role: Role];
roleDelete: [role: Role];
roleUpdate: [oldRole: Role, newRole: Role];
Expand Down Expand Up @@ -5757,6 +5770,7 @@ export enum Events {
MessageReactionRemoveEmoji = 'messageReactionRemoveEmoji',
MessageUpdate = 'messageUpdate',
PresenceUpdate = 'presenceUpdate',
RateLimited = 'rateLimited',
SoundboardSounds = 'soundboardSounds',
StageInstanceCreate = 'stageInstanceCreate',
StageInstanceDelete = 'stageInstanceDelete',
Expand Down