From 4f26ba7c2a9eb72c3d54de0fa3bbf568894b8503 Mon Sep 17 00:00:00 2001 From: Jaw0r3k Date: Mon, 17 Apr 2023 15:59:25 +0200 Subject: [PATCH] feat v13: Support pagination for fetching thread members (#9045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support pagnation * feat: support pagnation * feat: fix tests * fix: better fetch * fix: tests * Apply suggestions from code review Co-authored-by: Aura Román * fix: bad merge --------- Co-authored-by: Aura Román --- src/managers/ThreadMemberManager.js | 70 +++++++++++++++++++++++------ src/structures/ThreadMember.js | 19 ++++++-- typings/index.d.ts | 50 ++++++++++++++++++--- typings/index.test-d.ts | 52 ++++++++++++++++++--- 4 files changed, 163 insertions(+), 28 deletions(-) diff --git a/src/managers/ThreadMemberManager.js b/src/managers/ThreadMemberManager.js index db9e0c5c2..b6d380983 100644 --- a/src/managers/ThreadMemberManager.js +++ b/src/managers/ThreadMemberManager.js @@ -1,10 +1,13 @@ 'use strict'; +const process = require('node:process'); const { Collection } = require('@discordjs/collection'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const ThreadMember = require('../structures/ThreadMember'); +let deprecationEmittedForPassingBoolean = false; + /** * Manages API methods for GuildMembers and stores their cache. * @extends {CachedManager} @@ -28,10 +31,10 @@ class ThreadMemberManager extends CachedManager { _add(data, cache = true) { const existing = this.cache.get(data.user_id); - if (cache) existing?._patch(data); + if (cache) existing?._patch(data, { cache }); if (existing) return existing; - const member = new ThreadMember(this.thread, data); + const member = new ThreadMember(this.thread, data, { cache }); if (cache) this.cache.set(data.user_id, member); return member; } @@ -110,32 +113,73 @@ class ThreadMemberManager extends CachedManager { return id; } - async _fetchOne(memberId, cache, force) { + async _fetchOne(memberId, { cache, force = false, withMember }) { if (!force) { const existing = this.cache.get(memberId); if (existing) return existing; } - const data = await this.client.api.channels(this.thread.id, 'thread-members', memberId).get(); + const data = await this.client.api.channels(this.thread.id, 'thread-members', memberId).get({ + query: { with_member: withMember }, + }); return this._add(data, cache); } - async _fetchMany(cache) { - const raw = await this.client.api.channels(this.thread.id, 'thread-members').get(); + async _fetchMany({ cache, limit, after, withMember } = {}) { + const raw = await this.client.api.channels(this.thread.id, 'thread-members').get({ + query: { with_member: withMember, limit, after }, + }); return raw.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection()); } /** - * Fetches member(s) for the thread from Discord, requires access to the `GUILD_MEMBERS` gateway intent. - * @param {UserResolvable|boolean} [member] The member to fetch. If `undefined`, all members - * in the thread are fetched, and will be cached based on `options.cache`. If boolean, this serves - * the purpose of `options.cache`. - * @param {BaseFetchOptions} [options] Additional options for this fetch + * Options used to fetch a thread member. + * @typedef {BaseFetchOptions} FetchThreadMemberOptions + * @property {boolean} [withMember] Whether to also return the guild member associated with this thread member + */ + + /** + * Options used to fetch multiple thread members with guild member data. + * With `withMember` set to `true`, pagination is enabled. + * @typedef {Object} FetchThreadMembersWithGuildMemberDataOptions + * @property {true} withMember Whether to also return the guild member data + * @property {Snowflake} [after] Consider only thread members after this id + * @property {number} [limit] The maximum number of thread members to return + * @property {boolean} [cache] Whether to cache the fetched thread members and guild members + */ + + /** + * Options used to fetch multiple thread members without guild member data. + * @typedef {Object} FetchThreadMembersWithoutGuildMemberDataOptions + * @property {false} [withMember] Whether to also return the guild member data + * @property {boolean} [cache] Whether to cache the fetched thread members + */ + + /** + * Options used to fetch multiple thread members. + * @typedef {FetchThreadMembersWithGuildMemberDataOptions| + * FetchThreadMembersWithoutGuildMemberDataOptions} FetchThreadMembersOptions + */ + + /** + * Fetches member(s) for the thread from Discord. + * @param {UserResolvable|FetchThreadMembersOptions|boolean} [member] The member to fetch. If `undefined`, all members + * in the thread are fetched, and will be cached based on `options.cache`. + * @param {FetchThreadMemberOptions|FetchThreadMembersOptions} [options] Additional options for this fetch * @returns {Promise>} */ - fetch(member, { cache = true, force = false } = {}) { + fetch(member, options = { cache: true, force: false }) { + if (typeof member === 'boolean' && !deprecationEmittedForPassingBoolean) { + process.emitWarning( + 'Passing boolean to member option is deprecated, use cache property instead.', + 'DeprecationWarning', + ); + deprecationEmittedForPassingBoolean = true; + } const id = this.resolveId(member); - return id ? this._fetchOne(id, cache, force) : this._fetchMany(member ?? cache); + return id + ? this._fetchOne(id, options) + : this._fetchMany(typeof member === 'boolean' ? { ...options, cache: member } : options); } } diff --git a/src/structures/ThreadMember.js b/src/structures/ThreadMember.js index 3dcf3dad7..abd83628f 100644 --- a/src/structures/ThreadMember.js +++ b/src/structures/ThreadMember.js @@ -8,7 +8,7 @@ const ThreadMemberFlags = require('../util/ThreadMemberFlags'); * @extends {Base} */ class ThreadMember extends Base { - constructor(thread, data) { + constructor(thread, data, extra = {}) { super(thread.client); /** @@ -29,10 +29,10 @@ class ThreadMember extends Base { */ this.id = data.user_id; - this._patch(data); + this._patch(data, extra); } - _patch(data) { + _patch(data, extra = {}) { if ('join_timestamp' in data) this.joinedTimestamp = new Date(data.join_timestamp).getTime(); if ('flags' in data) { @@ -42,6 +42,17 @@ class ThreadMember extends Base { */ this.flags = new ThreadMemberFlags(data.flags).freeze(); } + + if ('member' in data) { + /** + * The guild member associated with this thread member. + * @type {?GuildMember} + * @private + */ + this.member = this.thread.guild.members._add(data.member, extra.cache); + } else { + this.member ??= null; + } } /** @@ -50,7 +61,7 @@ class ThreadMember extends Base { * @readonly */ get guildMember() { - return this.thread.guild.members.resolve(this.id); + return this.member ?? this.thread.guild.members.resolve(this.id); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 2de47c5bd..f427c42a1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -2653,14 +2653,15 @@ export class ThreadChannel extends TextBasedChannelMixin(Channel, ['fetchWebhook public unpin(reason?: string): Promise; } -export class ThreadMember extends Base { - private constructor(thread: ThreadChannel, data?: RawThreadMemberData); +export class ThreadMember extends Base { + private constructor(thread: ThreadChannel, data?: RawThreadMemberData, extra?: unknown); public flags: ThreadMemberFlags; - public readonly guildMember: GuildMember | null; + public readonly guildMember: HasMemberData extends true ? GuildMember : GuildMember | null; public id: Snowflake; public readonly joinedAt: Date | null; public joinedTimestamp: number | null; public readonly manageable: boolean; + private member: If; public thread: ThreadChannel; public readonly user: User | null; public remove(reason?: string): Promise; @@ -3641,9 +3642,26 @@ export class ThreadMemberManager extends CachedManager; - public fetch(member?: UserResolvable, options?: BaseFetchOptions): Promise; - /** @deprecated Use `fetch(member, options)` instead. */ - public fetch(cache?: boolean): Promise>; + public fetch(options?: FetchThreadMembersWithoutGuildMemberDataOptions): Promise>; + public fetch( + member: ThreadMember, + options?: FetchMemberOptions + ): Promise>; + public fetch( + member: Snowflake, + options: (FetchThreadMemberOptions & { withMember: true }), + ): Promise>; + public fetch( + options: FetchThreadMembersWithGuildMemberDataOptions, + ): Promise>>; + public fetch(member: UserResolvable, options?: FetchThreadMemberOptions): Promise; + + /** @deprecated Use `fetch(options)` instead. */ + public fetch(cache: boolean, options?: FetchThreadMembersOptions): Promise>; + + public fetch(x: undefined, options: FetchThreadMembersWithGuildMemberDataOptions): Promise>>; + public fetch(x: undefined, options?: FetchThreadMembersWithoutGuildMemberDataOptions): Promise>; + public fetchMe(options?: BaseFetchOptions): Promise; public remove(id: Snowflake | '@me', reason?: string): Promise; } @@ -5083,6 +5101,26 @@ export interface FetchReactionUsersOptions { after?: Snowflake; } +export interface FetchThreadMemberOptions extends BaseFetchOptions { + withMember?: boolean; +} + +export interface FetchThreadMembersWithGuildMemberDataOptions { + withMember: true; + after?: Snowflake; + limit?: number; + cache?: boolean; +} + +export interface FetchThreadMembersWithoutGuildMemberDataOptions { + withMember?: false; + cache?: boolean; +} + +export type FetchThreadMembersOptions = + | FetchThreadMembersWithGuildMemberDataOptions + | FetchThreadMembersWithoutGuildMemberDataOptions; + export interface FetchThreadsOptions { archived?: FetchArchivedThreadOptions; } diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 735194535..f0e470cd7 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -98,6 +98,7 @@ import { GuildBan, GuildBanManager, ForumChannel, + ThreadMemberManager, } from '.'; import type { ApplicationCommandOptionTypes } from './enums'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; @@ -898,7 +899,14 @@ declare const categoryChannel: CategoryChannel; declare const guildChannelManager: GuildChannelManager; { - type AnyChannel = TextChannel | VoiceChannel | CategoryChannel | NewsChannel | StoreChannel | StageChannel | ForumChannel; + type AnyChannel = + | TextChannel + | VoiceChannel + | CategoryChannel + | NewsChannel + | StoreChannel + | StageChannel + | ForumChannel; expectType>(guildChannelManager.create('name')); expectType>(guildChannelManager.create('name', {})); @@ -1323,7 +1331,9 @@ declare const GuildBasedChannel: GuildBasedChannel; declare const NonThreadGuildBasedChannel: NonThreadGuildBasedChannel; declare const GuildTextBasedChannel: GuildTextBasedChannel; -expectType(TextBasedChannel); +expectType( + TextBasedChannel, +); expectType< | 'DM' | 'GUILD_NEWS' @@ -1335,10 +1345,42 @@ expectType< | 'GUILD_STAGE_VOICE' >(TextBasedChannelTypes); expectType(VoiceBasedChannel); -expectType( - GuildBasedChannel, -); +expectType< + | CategoryChannel + | NewsChannel + | StageChannel + | StoreChannel + | TextChannel + | ThreadChannel + | VoiceChannel + | ForumChannel +>(GuildBasedChannel); expectType( NonThreadGuildBasedChannel, ); + +declare const threadMemberWithGuildMember: ThreadMember; +declare const threadMemberManager: ThreadMemberManager; +{ + expectType>(threadMemberManager.fetch('12345678')); + expectType>(threadMemberManager.fetch('12345678', { cache: false })); + expectType>(threadMemberManager.fetch('12345678', { force: true })); + expectType>>(threadMemberManager.fetch(threadMemberWithGuildMember)); + expectType>>(threadMemberManager.fetch('12345678901234567', { withMember: true })); + expectType>>(threadMemberManager.fetch()); + expectType>>(threadMemberManager.fetch({})); + + expectType>>>( + threadMemberManager.fetch({ cache: true, limit: 50, withMember: true, after: '12345678901234567' }), + ); + + expectType>>(threadMemberManager.fetch(undefined, { cache: true })); + expectType>>( + threadMemberManager.fetch({ cache: true, withMember: false }), + ); + + // @ts-expect-error `withMember` needs to be `true` to receive paginated results. + threadMemberManager.fetch({ withMember: false, limit: 5, after: '12345678901234567' }); +} + expectType(GuildTextBasedChannel);