From 198a5c490d6d60316f3201ad2c84654e05d43aff Mon Sep 17 00:00:00 2001 From: ckohen Date: Sat, 31 Jul 2021 04:21:47 -0700 Subject: [PATCH] refactor: move member adding to manager (#6231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vlad Frangu Co-authored-by: Antonio Román Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> --- src/managers/GuildMemberManager.js | 50 ++++++++++++++++++++++++++++++ src/structures/Guild.js | 42 +------------------------ typings/index.d.ts | 8 ++++- typings/index.ts | 29 +++++++++++++++++ 4 files changed, 87 insertions(+), 42 deletions(-) diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index 9c63c7246..defced969 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -66,6 +66,56 @@ class GuildMemberManager extends CachedManager { return this.cache.has(userResolvable) ? userResolvable : null; } + /** + * Options used to add a user to a guild using OAuth2. + * @typedef {Object} AddGuildMemberOptions + * @property {string} accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the + * bot's application + * @property {string} [nick] The nickname to give to the member (requires `MANAGE_NICKNAMES`) + * @property {Collection|RoleResolvable[]} [roles] The roles to add to the member + * (requires `MANAGE_ROLES`) + * @property {boolean} [mute] Whether the member should be muted (requires `MUTE_MEMBERS`) + * @property {boolean} [deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`) + * @property {boolean} [force] Whehter to skip the cache check and call the API directly + * @property {boolean} [fetchWhenExisting=true] Whether to fetch the user if not cached and already a member + */ + + /** + * Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission. + * @param {UserResolvable} user The user to add to the guild + * @param {AddGuildMemberOptions} options Options for adding the user to the guild + * @returns {Promise} + */ + async add(user, options) { + const userId = this.client.users.resolveId(user); + if (!userId) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable'); + if (!options.force) { + const cachedUser = this.cache.get(userId); + if (cachedUser) return cachedUser; + } + const resolvedOptions = { + access_token: options.accessToken, + nick: options.nick, + mute: options.mute, + deaf: options.deaf, + }; + if (options.roles) { + if (!Array.isArray(options.roles) && !(options.roles instanceof Collection)) { + throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true); + } + const resolvedRoles = []; + for (const role of options.roles.values()) { + const resolvedRole = this.guild.roles.resolveId(role); + if (!resolvedRole) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role); + resolvedRoles.push(resolvedRole); + } + resolvedOptions.roles = resolvedRoles; + } + const data = await this.client.api.guilds(this.guild.id).members(userId).put({ data: 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); + } + /** * Options used to fetch a single member from a guild. * @typedef {BaseFetchOptions} FetchMemberOptions diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 758bd92e3..49989d994 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -8,7 +8,7 @@ const GuildTemplate = require('./GuildTemplate'); const Integration = require('./Integration'); const Webhook = require('./Webhook'); const WelcomeScreen = require('./WelcomeScreen'); -const { Error, TypeError } = require('../errors'); +const { Error } = require('../errors'); const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager'); const GuildBanManager = require('../managers/GuildBanManager'); const GuildChannelManager = require('../managers/GuildChannelManager'); @@ -730,46 +730,6 @@ class Guild extends AnonymousGuild { .then(data => GuildAuditLogs.build(this, data)); } - /** - * Options used to add a user to a guild using OAuth2. - * @typedef {Object} AddGuildMemberOptions - * @property {string} accessToken An OAuth2 access token for the user with the `guilds.join` scope granted to the - * bot's application - * @property {string} [nick] The nickname to give to the member (requires `MANAGE_NICKNAMES`) - * @property {Collection|RoleResolvable[]} [roles] The roles to add to the member - * (requires `MANAGE_ROLES`) - * @property {boolean} [mute] Whether the member should be muted (requires `MUTE_MEMBERS`) - * @property {boolean} [deaf] Whether the member should be deafened (requires `DEAFEN_MEMBERS`) - */ - - /** - * Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission. - * @param {UserResolvable} user The user to add to the guild - * @param {AddGuildMemberOptions} options Options for adding the user to the guild - * @returns {Promise} - */ - async addMember(user, options) { - user = this.client.users.resolveId(user); - if (!user) throw new TypeError('INVALID_TYPE', 'user', 'UserResolvable'); - if (this.members.cache.has(user)) return this.members.cache.get(user); - options.access_token = options.accessToken; - if (options.roles) { - if (!Array.isArray(options.roles) && !(options.roles instanceof Collection)) { - throw new TypeError('INVALID_TYPE', 'options.roles', 'Array or Collection of Roles or Snowflakes', true); - } - const resolvedRoles = []; - for (const role of options.roles.values()) { - const resolvedRole = this.roles.resolveId(role); - if (!role) throw new TypeError('INVALID_ELEMENT', 'Array or Collection', 'options.roles', role); - resolvedRoles.push(resolvedRole); - } - options.roles = resolvedRoles; - } - const data = await this.client.api.guilds(this.id).members(user).put({ data: options }); - // Data is an empty buffer if the member is already part of the guild. - return data instanceof Buffer ? this.members.fetch(user) : this.members._add(data); - } - /** * The data for editing a guild. * @typedef {Object} GuildEditData diff --git a/typings/index.d.ts b/typings/index.d.ts index 974c069f1..04e8a8f52 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -573,7 +573,6 @@ export class Guild extends AnonymousGuild { public readonly widgetChannel: TextChannel | null; public widgetChannelId: Snowflake | null; public widgetEnabled: boolean | null; - public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise; public createTemplate(name: string, description?: string): Promise; public delete(): Promise; public discoverySplashURL(options?: StaticImageURLOptions): string | null; @@ -2341,6 +2340,11 @@ export class GuildManager extends CachedManager { public constructor(guild: Guild, iterable?: Iterable); public guild: Guild; + public add( + user: UserResolvable, + options: AddGuildMemberOptions & { fetchWhenExisting: false }, + ): Promise; + public add(user: UserResolvable, options: AddGuildMemberOptions): Promise; public ban(user: UserResolvable, options?: BanOptions): Promise; public edit(user: UserResolvable, data: GuildMemberEditData, reason?: string): Promise; public fetch( @@ -2609,6 +2613,8 @@ export interface AddGuildMemberOptions { roles?: Collection | RoleResolvable[]; mute?: boolean; deaf?: boolean; + force?: boolean; + fetchWhenExisting?: boolean; } export type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'jpeg' | 'gif'; diff --git a/typings/index.ts b/typings/index.ts index 0e01cbf4f..51e86c3c2 100644 --- a/typings/index.ts +++ b/typings/index.ts @@ -404,6 +404,9 @@ client.on('ready', async () => { }); }); +// This is to check that stuff is the right type +declare const assertIsPromiseMember: (m: Promise) => void; + client.on('guildCreate', g => { const channel = g.channels.cache.random(); if (!channel) return; @@ -411,6 +414,32 @@ client.on('guildCreate', g => { channel.setName('foo').then(updatedChannel => { console.log(`New channel name: ${updatedChannel.name}`); }); + + // @ts-expect-error no options + assertIsPromiseMember(g.members.add(testUserId)); + + // @ts-expect-error no access token + assertIsPromiseMember(g.members.add(testUserId, {})); + + // @ts-expect-error invalid role resolvable + assertIsPromiseMember(g.members.add(testUserId, { accessToken: 'totallyRealAccessToken', roles: [g.roles.cache] })); + + assertType>( + g.members.add(testUserId, { accessToken: 'totallyRealAccessToken', fetchWhenExisting: false }), + ); + + assertIsPromiseMember(g.members.add(testUserId, { accessToken: 'totallyRealAccessToken' })); + + assertIsPromiseMember( + g.members.add(testUserId, { + accessToken: 'totallyRealAccessToken', + mute: true, + deaf: false, + roles: [g.roles.cache.first()!], + force: true, + fetchWhenExisting: true, + }), + ); }); client.on('messageReactionRemoveAll', async message => {