Skip to content

Commit

Permalink
refactor(GuildMemberManager): Tidy up fetching guild members (#8972)
Browse files Browse the repository at this point in the history
* refactor(GuildMemberManager): tidy up fetching guild members

* refactor: no destructure

* fix: throw `nonce` error correctly

* refactor: simplify `resolve()` with ternary

* refactor: prioritise `nonce` check

* fix: allow single user

* refactor: do not use `null`

This is not documented to request over the gateway.

* refactor: better name

* fix: extract correct property
  • Loading branch information
Jiralite committed Feb 21, 2023
1 parent 6e481f0 commit 4e0e125
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 81 deletions.
150 changes: 70 additions & 80 deletions packages/discord.js/src/managers/GuildMemberManager.js
Expand Up @@ -159,20 +159,18 @@ class GuildMemberManager extends CachedManager {
/**
* Options used to fetch multiple members from a guild.
* @typedef {Object} FetchMembersOptions
* @property {UserResolvable|UserResolvable[]} user The user(s) to fetch
* @property {?string} query Limit fetch to members with similar usernames
* @property {UserResolvable|UserResolvable[]} [user] The user(s) to fetch
* @property {?string} [query] Limit fetch to members with similar usernames
* @property {number} [limit=0] Maximum number of members to request
* @property {boolean} [withPresences=false] Whether or not to include the presences
* @property {boolean} [withPresences=false] Whether to include the presences
* @property {number} [time=120e3] Timeout for receipt of members
* @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
* @property {boolean} [force=false] Whether to skip the cache check and request the API
* @property {?string} [nonce] Nonce for this request (32 characters max - default to base 16 now timestamp)
*/

/**
* Fetches member(s) from Discord, even if they're offline.
* @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] If a UserResolvable, the user to fetch.
* If undefined, fetches all members.
* If a query, it limits the results to users with similar usernames.
* Fetches member(s) from a guild.
* @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] Options for fetching member(s).
* Omitting the parameter or providing `undefined` will fetch all members.
* @returns {Promise<GuildMember|Collection<Snowflake, GuildMember>>}
* @example
* // Fetch all members from a guild
Expand Down Expand Up @@ -207,18 +205,70 @@ class GuildMemberManager extends CachedManager {
*/
fetch(options) {
if (!options) return this._fetchMany();
const user = this.client.users.resolveId(options);
if (user) return this._fetchSingle({ user, cache: true });
if (options.user) {
if (Array.isArray(options.user)) {
options.user = options.user.map(u => this.client.users.resolveId(u));
return this._fetchMany(options);
} else {
options.user = this.client.users.resolveId(options.user);
}
if (!options.limit && !options.withPresences) return this._fetchSingle(options);
const { user: users, limit, withPresences, cache, force } = options;
const resolvedUser = this.client.users.resolveId(users ?? options);
if (resolvedUser && !limit && !withPresences) return this._fetchSingle({ user: resolvedUser, cache, force });
const resolvedUsers = users?.map?.(user => this.client.users.resolveId(user)) ?? resolvedUser ?? undefined;
return this._fetchMany({ ...options, users: resolvedUsers });
}

async _fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return existing;
}
return this._fetchMany(options);

const data = await this.client.rest.get(Routes.guildMember(this.guild.id, user));
return this._add(data, cache);
}

_fetchMany({
limit = 0,
withPresences: presences,
users,
query,
time = 120e3,
nonce = DiscordSnowflake.generate().toString(),
} = {}) {
if (nonce.length > 32) return Promise.reject(new DiscordjsRangeError(ErrorCodes.MemberFetchNonceLength));

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 handler = (members, _, chunk) => {
if (chunk.nonce !== nonce) return;
timeout.refresh();
i++;
for (const member of members.values()) {
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();
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();
this.client.incrementMaxListeners();
this.client.on(Events.GuildMembersChunk, handler);
});
}

/**
Expand Down Expand Up @@ -485,66 +535,6 @@ class GuildMemberManager extends CachedManager {

return this.resolve(user) ?? this.client.users.resolve(user) ?? userId;
}

async _fetchSingle({ user, cache, force = false }) {
if (!force) {
const existing = this.cache.get(user);
if (existing && !existing.partial) return existing;
}

const data = await this.client.rest.get(Routes.guildMember(this.guild.id, user));
return this._add(data, cache);
}

_fetchMany({
limit = 0,
withPresences: presences = false,
user: user_ids,
query,
time = 120e3,
nonce = DiscordSnowflake.generate().toString(),
} = {}) {
return new Promise((resolve, reject) => {
if (!query && !user_ids) query = '';
if (nonce.length > 32) throw new DiscordjsRangeError(ErrorCodes.MemberFetchNonceLength);
this.guild.shard.send({
op: GatewayOpcodes.RequestGuildMembers,
d: {
guild_id: this.guild.id,
presences,
user_ids,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
let i = 0;
const handler = (members, _, chunk) => {
timeout.refresh();
if (chunk.nonce !== nonce) return;
i++;
for (const member of members.values()) {
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();
let fetched = fetchedMembers;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
resolve(fetched);
}
};
const timeout = setTimeout(() => {
this.client.removeListener(Events.GuildMembersChunk, handler);
this.client.decrementMaxListeners();
reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout));
}, time).unref();
this.client.incrementMaxListeners();
this.client.on(Events.GuildMembersChunk, handler);
});
}
}

module.exports = GuildMemberManager;
1 change: 0 additions & 1 deletion packages/discord.js/typings/index.d.ts
Expand Up @@ -5167,7 +5167,6 @@ export interface FetchMembersOptions {
withPresences?: boolean;
time?: number;
nonce?: string;
force?: boolean;
}

export interface FetchMessageOptions extends BaseFetchOptions {
Expand Down
23 changes: 23 additions & 0 deletions packages/discord.js/typings/index.test-d.ts
Expand Up @@ -158,6 +158,7 @@ import {
AutoModerationRuleManager,
PrivateThreadChannel,
PublicThreadChannel,
GuildMemberManager,
GuildMemberFlagsBitField,
} from '.';
import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd';
Expand Down Expand Up @@ -1486,6 +1487,28 @@ declare const guildTextThreadManager: GuildTextThreadManager<
>;
expectType<TextChannel | NewsChannel>(guildTextThreadManager.channel);

declare const guildMemberManager: GuildMemberManager;
{
expectType<Promise<GuildMember>>(guildMemberManager.fetch('12345678901234567'));
expectType<Promise<GuildMember>>(guildMemberManager.fetch({ user: '12345678901234567' }));
expectType<Promise<GuildMember>>(guildMemberManager.fetch({ user: '12345678901234567', cache: true, force: false }));
expectType<Promise<GuildMember>>(guildMemberManager.fetch({ user: '12345678901234567', cache: true, force: false }));
expectType<Promise<Collection<Snowflake, GuildMember>>>(guildMemberManager.fetch());
expectType<Promise<Collection<Snowflake, GuildMember>>>(guildMemberManager.fetch({}));
expectType<Promise<Collection<Snowflake, GuildMember>>>(guildMemberManager.fetch({ user: ['12345678901234567'] }));
expectType<Promise<Collection<Snowflake, GuildMember>>>(guildMemberManager.fetch({ withPresences: false }));
expectType<Promise<GuildMember>>(guildMemberManager.fetch({ user: '12345678901234567', withPresences: true }));

expectType<Promise<Collection<Snowflake, GuildMember>>>(
guildMemberManager.fetch({ query: 'test', user: ['12345678901234567'], nonce: 'test' }),
);

// @ts-expect-error The cache & force options have no effect here.
guildMemberManager.fetch({ cache: true, force: false });
// @ts-expect-error The force option has no effect here.
guildMemberManager.fetch({ user: ['12345678901234567'], cache: true, force: false });
}

declare const messageManager: MessageManager;
{
expectType<Promise<Message>>(messageManager.fetch('1234567890'));
Expand Down

2 comments on commit 4e0e125

@vercel
Copy link

@vercel vercel bot commented on 4e0e125 Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 4e0e125 Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.