diff --git a/packages/discord.js/src/managers/GuildMemberRoleManager.js b/packages/discord.js/src/managers/GuildMemberRoleManager.js index 7d19bf778b64..cefdbc3f0c12 100644 --- a/packages/discord.js/src/managers/GuildMemberRoleManager.js +++ b/packages/discord.js/src/managers/GuildMemberRoleManager.js @@ -34,7 +34,7 @@ class GuildMemberRoleManager extends DataManager { */ get cache() { const everyone = this.guild.roles.everyone; - return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); + return this.guild.roles.cache.filter(role => this.member.roleIds.includes(role.id)).set(everyone.id, everyone); } /** @@ -133,7 +133,7 @@ class GuildMemberRoleManager extends DataManager { await this.client.rest.put(Routes.guildMemberRole(this.guild.id, this.member.id, roleOrRoles), { reason }); const clone = this.member._clone(); - clone._roles = [...this.cache.keys(), roleOrRoles]; + clone.roleIds = [...this.cache.keys(), roleOrRoles]; return clone; } } @@ -173,7 +173,7 @@ class GuildMemberRoleManager extends DataManager { const clone = this.member._clone(); const newRoles = this.cache.filter(role => role.id !== roleOrRoles); - clone._roles = [...newRoles.keys()]; + clone.roleIds = [...newRoles.keys()]; return clone; } } @@ -200,7 +200,7 @@ class GuildMemberRoleManager extends DataManager { clone() { const clone = new this.constructor(this.member); - clone.member._roles = [...this.cache.keys()]; + clone.member.roleIds = [...this.cache.keys()]; return clone; } } diff --git a/packages/discord.js/src/structures/BaseInteraction.js b/packages/discord.js/src/structures/BaseInteraction.js index 5c79b7e471f6..d184251e97e2 100644 --- a/packages/discord.js/src/structures/BaseInteraction.js +++ b/packages/discord.js/src/structures/BaseInteraction.js @@ -4,6 +4,7 @@ const { Collection } = require('@discordjs/collection'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v10'); const Base = require('./Base'); +const MinimalGuildMember = require('./MinimalGuildMember'); const { SelectMenuTypes } = require('../util/Constants'); const PermissionsBitField = require('../util/PermissionsBitField'); @@ -62,9 +63,11 @@ class BaseInteraction extends Base { /** * If this interaction was sent in a guild, the member which sent it - * @type {?(GuildMember|APIInteractionGuildMember)} + * @type {?(GuildMember|MinimalGuildMember)} */ - this.member = data.member ? (this.guild?.members._add(data.member) ?? data.member) : null; + this.member = data.member + ? (this.guild?.members._add(data.member) ?? new MinimalGuildMember(this.client, data.member, this.guildId)) + : null; /** * The version diff --git a/packages/discord.js/src/structures/CommandInteraction.js b/packages/discord.js/src/structures/CommandInteraction.js index 88086f9605b2..e5714a0da8ea 100644 --- a/packages/discord.js/src/structures/CommandInteraction.js +++ b/packages/discord.js/src/structures/CommandInteraction.js @@ -3,6 +3,7 @@ const Attachment = require('./Attachment'); const BaseInteraction = require('./BaseInteraction'); const InteractionWebhook = require('./InteractionWebhook'); +const MinimalGuildMember = require('./MinimalGuildMember'); const InteractionResponses = require('./interfaces/InteractionResponses'); /** @@ -83,7 +84,7 @@ class CommandInteraction extends BaseInteraction { * Represents the resolved data of a received command interaction. * @typedef {Object} CommandInteractionResolvedData * @property {Collection} [users] The resolved users - * @property {Collection} [members] The resolved guild members + * @property {Collection} [members] The resolved guild members * @property {Collection} [roles] The resolved roles * @property {Collection} [channels] The resolved channels * @property {Collection} [messages] The resolved messages @@ -102,7 +103,7 @@ class CommandInteraction extends BaseInteraction { * @property {CommandInteractionOption[]} [options] Additional options if this option is a * subcommand (group) * @property {User} [user] The resolved user - * @property {GuildMember|APIGuildMember} [member] The resolved member + * @property {GuildMember|MinimalGuildMember} [member] The resolved member * @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel * @property {Role|APIRole} [role] The resolved role * @property {Attachment} [attachment] The resolved attachment @@ -129,7 +130,11 @@ class CommandInteraction extends BaseInteraction { if (user) result.user = this.client.users._add(user); const member = resolved.members?.[option.value]; - if (member) result.member = this.guild?.members._add({ user, ...member }) ?? member; + if (member) { + result.member = + this.guild?.members._add({ user, ...member }) ?? + new MinimalGuildMember(this.client, { user, ...member }, this.guildId); + } const channel = resolved.channels?.[option.value]; if (channel) result.channel = this.client.channels._add(channel, this.guild) ?? channel; diff --git a/packages/discord.js/src/structures/CommandInteractionOptionResolver.js b/packages/discord.js/src/structures/CommandInteractionOptionResolver.js index b59f6328e40c..dadaaafb9859 100644 --- a/packages/discord.js/src/structures/CommandInteractionOptionResolver.js +++ b/packages/discord.js/src/structures/CommandInteractionOptionResolver.js @@ -214,7 +214,7 @@ class CommandInteractionOptionResolver { /** * Gets a member option. * @param {string} name The name of the option. - * @returns {?(GuildMember|APIGuildMember)} + * @returns {?(GuildMember|MinimalGuildMember)} * The value of the option, or null if the user is not present in the guild or the option is not set. */ getMember(name) { @@ -258,7 +258,7 @@ class CommandInteractionOptionResolver { * Gets a mentionable option. * @param {string} name The name of the option. * @param {boolean} [required=false] Whether to throw an error if the option is not found. - * @returns {?(User|GuildMember|APIGuildMember|Role|APIRole)} + * @returns {?(User|GuildMember|MinimalGuildMember|Role|APIRole)} * The value of the option, or null if not set and not required. */ getMentionable(name, required = false) { diff --git a/packages/discord.js/src/structures/GuildMember.js b/packages/discord.js/src/structures/GuildMember.js index b1f1b5283460..be893cb2930a 100644 --- a/packages/discord.js/src/structures/GuildMember.js +++ b/packages/discord.js/src/structures/GuildMember.js @@ -1,133 +1,29 @@ 'use strict'; const { PermissionFlagsBits } = require('discord-api-types/v10'); -const Base = require('./Base'); +const MinimalGuildMember = require('./MinimalGuildMember'); const VoiceState = require('./VoiceState'); -const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { DiscordjsError, ErrorCodes } = require('../errors'); const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); -const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField'); const PermissionsBitField = require('../util/PermissionsBitField'); /** * Represents a member of a guild on Discord. - * @implements {TextBasedChannel} - * @extends {Base} + * @extends {MinimalGuildMember} */ -class GuildMember extends Base { +class GuildMember extends MinimalGuildMember { constructor(client, data, guild) { - super(client); + super(client, data, guild.id); /** * The guild that this member is part of * @type {Guild} */ this.guild = guild; - - /** - * The timestamp the member joined the guild at - * @type {?number} - */ - this.joinedTimestamp = null; - - /** - * The last timestamp this member started boosting the guild - * @type {?number} - */ - this.premiumSinceTimestamp = null; - - /** - * The nickname of this member, if they have one - * @type {?string} - */ - this.nickname = null; - - /** - * Whether this member has yet to pass the guild's membership gate - * @type {?boolean} - */ - this.pending = null; - - /** - * The timestamp this member's timeout will be removed - * @type {?number} - */ - this.communicationDisabledUntilTimestamp = null; - - /** - * The role ids of the member - * @name GuildMember#_roles - * @type {Snowflake[]} - * @private - */ - Object.defineProperty(this, '_roles', { value: [], writable: true }); - - if (data) this._patch(data); } - _patch(data) { - if ('user' in data) { - /** - * The user that this guild member instance represents - * @type {?User} - */ - this.user = this.client.users._add(data.user, true); - } - - if ('nick' in data) this.nickname = data.nick; - if ('avatar' in data) { - /** - * The guild member's avatar hash - * @type {?string} - */ - this.avatar = data.avatar; - } else if (typeof this.avatar !== 'string') { - this.avatar = null; - } - - if ('banner' in data) { - /** - * The guild member's banner hash. - * @type {?string} - */ - this.banner = data.banner; - } else { - this.banner ??= null; - } - - if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at); - if ('premium_since' in data) { - this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null; - } - if ('roles' in data) this._roles = data.roles; - - if ('pending' in data) { - this.pending = data.pending; - } else if (!this.partial) { - // See https://github.com/discordjs/discord.js/issues/6546 for more info. - this.pending ??= false; - } - - if ('communication_disabled_until' in data) { - this.communicationDisabledUntilTimestamp = - data.communication_disabled_until && Date.parse(data.communication_disabled_until); - } - - if ('flags' in data) { - /** - * The flags of this member - * @type {Readonly} - */ - this.flags = new GuildMemberFlagsBitField(data.flags).freeze(); - } else { - this.flags ??= new GuildMemberFlagsBitField().freeze(); - } - } - - _clone() { - const clone = super._clone(); - clone._roles = this._roles.slice(); - return clone; + isInCachedGuild() { + return true; } /** @@ -156,35 +52,6 @@ class GuildMember extends Base { get voice() { return this.guild.voiceStates.cache.get(this.id) ?? new VoiceState(this.guild, { user_id: this.id }); } - - /** - * A link to the member's guild avatar. - * @param {ImageURLOptions} [options={}] Options for the image URL - * @returns {?string} - */ - avatarURL(options = {}) { - return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guild.id, this.id, this.avatar, options); - } - - /** - * A link to the member's banner. - * @param {ImageURLOptions} [options={}] Options for the banner URL - * @returns {?string} - */ - bannerURL(options = {}) { - return this.banner && this.client.rest.cdn.guildMemberBanner(this.guild.id, this.id, this.banner, options); - } - - /** - * A link to the member's guild avatar if they have one. - * Otherwise, a link to their {@link User#displayAvatarURL} will be returned. - * @param {ImageURLOptions} [options={}] Options for the image URL - * @returns {string} - */ - displayAvatarURL(options) { - return this.avatarURL(options) ?? this.user.displayAvatarURL(options); - } - /** * A link to the member's guild banner if they have one. * Otherwise, a link to their {@link User#bannerURL} will be returned. @@ -195,33 +62,6 @@ class GuildMember extends Base { return this.bannerURL(options) ?? this.user.bannerURL(options); } - /** - * The time this member joined the guild - * @type {?Date} - * @readonly - */ - get joinedAt() { - return this.joinedTimestamp && new Date(this.joinedTimestamp); - } - - /** - * The time this member's timeout will be removed - * @type {?Date} - * @readonly - */ - get communicationDisabledUntil() { - return this.communicationDisabledUntilTimestamp && new Date(this.communicationDisabledUntilTimestamp); - } - - /** - * The last time this member started boosting the guild - * @type {?Date} - * @readonly - */ - get premiumSince() { - return this.premiumSinceTimestamp && new Date(this.premiumSinceTimestamp); - } - /** * The presence of this guild member * @type {?Presence} @@ -249,33 +89,6 @@ class GuildMember extends Base { return this.roles.color?.hexColor ?? '#000000'; } - /** - * The member's id - * @type {Snowflake} - * @readonly - */ - get id() { - return this.user.id; - } - - /** - * The DM between the client's user and this member - * @type {?DMChannel} - * @readonly - */ - get dmChannel() { - return this.client.users.dmChannel(this.id); - } - - /** - * The nickname of this member, or their user display name if they don't have one - * @type {?string} - * @readonly - */ - get displayName() { - return this.nickname ?? this.user.displayName; - } - /** * The overall set of permissions for this member, taking only roles and owner status into account * @type {Readonly} @@ -333,14 +146,6 @@ class GuildMember extends Base { ); } - /** - * Whether this member is currently timed out - * @returns {boolean} - */ - isCommunicationDisabled() { - return this.communicationDisabledUntilTimestamp > Date.now(); - } - /** * Returns `channel.permissionsFor(guildMember)`. Returns permissions for a member in a guild channel, * taking into account roles and permission overwrites. @@ -392,23 +197,6 @@ class GuildMember extends Base { return this.edit({ nick, reason }); } - /** - * Creates a DM channel between the client and this member. - * @param {boolean} [force=false] Whether to skip the cache check and request the API - * @returns {Promise} - */ - createDM(force = false) { - return this.user.createDM(force); - } - - /** - * Deletes any DMs with this member. - * @returns {Promise} - */ - deleteDM() { - return this.user.deleteDM(); - } - /** * Kicks this member from the guild. * @param {string} [reason] Reason for kicking user @@ -498,51 +286,10 @@ class GuildMember extends Base { this.pending === member.pending && this.communicationDisabledUntilTimestamp === member.communicationDisabledUntilTimestamp && this.flags.bitfield === member.flags.bitfield && - (this._roles === member._roles || - (this._roles.length === member._roles.length && this._roles.every((role, i) => role === member._roles[i]))) + (this.roleIds === member.roleIds || + (this.roleIds.length === member.roleIds.length && this.roleIds.every((role, i) => role === member.roleIds[i]))) ); } - - /** - * When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object. - * @returns {string} - * @example - * // Logs: Hello from <@123456789012345678>! - * console.log(`Hello from ${member}!`); - */ - toString() { - return this.user.toString(); - } - - toJSON() { - const json = super.toJSON({ - guild: 'guildId', - user: 'userId', - displayName: true, - roles: true, - }); - json.avatarURL = this.avatarURL(); - json.bannerURL = this.bannerURL(); - json.displayAvatarURL = this.displayAvatarURL(); - json.displayBannerURL = this.displayBannerURL(); - return json; - } } -/** - * Sends a message to this user. - * @method send - * @memberof GuildMember - * @instance - * @param {string|MessagePayload|MessageCreateOptions} options The options to provide - * @returns {Promise} - * @example - * // Send a direct message - * guildMember.send('Hello!') - * .then(message => console.log(`Sent message: ${message.content} to ${guildMember.displayName}`)) - * .catch(console.error); - */ - -TextBasedChannel.applyToClass(GuildMember); - exports.GuildMember = GuildMember; diff --git a/packages/discord.js/src/structures/MentionableSelectMenuInteraction.js b/packages/discord.js/src/structures/MentionableSelectMenuInteraction.js index 416d5ceb5429..285cb45a7569 100644 --- a/packages/discord.js/src/structures/MentionableSelectMenuInteraction.js +++ b/packages/discord.js/src/structures/MentionableSelectMenuInteraction.js @@ -2,6 +2,7 @@ const { Collection } = require('@discordjs/collection'); const MessageComponentInteraction = require('./MessageComponentInteraction'); +const MinimalGuildMember = require('./MinimalGuildMember'); const Events = require('../util/Events'); /** @@ -28,7 +29,7 @@ class MentionableSelectMenuInteraction extends MessageComponentInteraction { /** * Collection of the selected users - * @type {Collection} + * @type {Collection} */ this.members = new Collection(); @@ -50,7 +51,11 @@ class MentionableSelectMenuInteraction extends MessageComponentInteraction { continue; } - this.members.set(id, this.guild?.members._add({ user, ...member }) ?? { user, ...member }); + this.members.set( + id, + this.guild?.members._add({ user, ...member }) ?? + new MinimalGuildMember(this.client, { user, ...member }, this.guildId), + ); } } diff --git a/packages/discord.js/src/structures/MinimalGuildMember.js b/packages/discord.js/src/structures/MinimalGuildMember.js new file mode 100644 index 000000000000..d31828c7e179 --- /dev/null +++ b/packages/discord.js/src/structures/MinimalGuildMember.js @@ -0,0 +1,321 @@ +'use strict'; + +const Base = require('./Base'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField'); + +/** + * Represents a member of a guild on Discord. Used in interactions from guilds that aren't cached. + * @implements {TextBasedChannel} + * @extends {Base} + */ +class MinimalGuildMember extends Base { + constructor(client, data, guildId) { + super(client); + + /** + * The ID of the guild that this member is part of + * @type {string} + */ + this.guildId = guildId; + + /** + * The nickname of this member, if they have one + * @type {?string} + */ + this.nickname = null; + + /** + * The guild member's avatar hash + * @type {?string} + */ + this.avatar = null; + + /** + * The guild member's banner hash + * @type {?string} + */ + this.banner = null; + + /** + * The role ids of the member + * @type {Snowflake[]} + */ + this.roleIds = []; + + /** + * The timestamp the member joined the guild at + * @type {?number} + */ + this.joinedTimestamp = null; + + /** + * The last timestamp this member started boosting the guild + * @type {?number} + */ + this.premiumSinceTimestamp = null; + + /** + * The flags of this member + * @type {Readonly} + */ + this.flags = new GuildMemberFlagsBitField().freeze(); + + /** + * Whether this member has yet to pass the guild's membership gate + * @type {?boolean} + */ + this.pending = null; + + /** + * The timestamp this member's timeout will be removed + * @type {?number} + */ + this.communicationDisabledUntilTimestamp = null; + + if (data) this._patch(data); + } + + _patch(data) { + if ('user' in data) { + /** + * The user that this guild member instance represents + * @type {User} + */ + this.user = this.client.users._add(data.user, true); + } + + if ('nick' in data) this.nickname = data.nick; + + if ('avatar' in data) this.avatar = data.avatar; + + if ('banner' in data) this.banner = data.banner; + + if ('roles' in data) this.roleIds = data.roles; + + if ('joined_at' in data) this.joinedTimestamp = data.joined_at && Date.parse(data.joined_at); + + if ('premium_since' in data) this.premiumSinceTimestamp = data.premium_since && Date.parse(data.premium_since); + + if ('flags' in data) this.flags = new GuildMemberFlagsBitField(data.flags).freeze(); + + if ('pending' in data) { + this.pending = data.pending; + } else if (!this.partial) { + // See https://github.com/discordjs/discord.js/issues/6546 for more info. + this.pending ??= false; + } + + if ('communication_disabled_until' in data) { + this.communicationDisabledUntilTimestamp = + data.communication_disabled_until && Date.parse(data.communication_disabled_until); + } + } + + _clone() { + const clone = super._clone(); + clone.roleIds = this.roleIds.slice(); + return clone; + } + + /** + * Whether this member is in a cached guild (true for GuildMembers, false for MinimalGuildMembers) + * @returns {boolean} + */ + isInCachedGuild() { + return false; + } + + /** + * Whether this member is a partial (always true for MinimalGuildMembers, as they are partial GuildMembers) + * @type {boolean} + * @readonly + */ + get partial() { + return true; + } + + /** + * A link to the member's guild avatar. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + avatarURL(options = {}) { + return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guildId, this.id, this.avatar, options); + } + + /** + * A link to the member's banner. + * @param {ImageURLOptions} [options={}] Options for the banner URL + * @returns {?string} + */ + bannerURL(options = {}) { + return this.banner && this.client.rest.cdn.guildMemberBanner(this.guildId, this.id, this.banner, options); + } + + /** + * A link to the member's guild avatar if they have one. + * Otherwise, a link to their {@link User#displayAvatarURL} will be returned. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {string} + */ + displayAvatarURL(options) { + return this.avatarURL(options) ?? this.user.displayAvatarURL(options); + } + + /** + * A link to the member's guild banner if they have one. + * Otherwise, a link to their {@link User#bannerURL} will be returned. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + displayBannerURL(options) { + return this.bannerURL(options) ?? this.user.bannerURL(options); + } + + /** + * The time this member joined the guild + * @type {?Date} + * @readonly + */ + get joinedAt() { + return this.joinedTimestamp && new Date(this.joinedTimestamp); + } + + /** + * The time this member's timeout will be removed + * @type {?Date} + * @readonly + */ + get communicationDisabledUntil() { + return this.communicationDisabledUntilTimestamp && new Date(this.communicationDisabledUntilTimestamp); + } + + /** + * The last time this member started boosting the guild + * @type {?Date} + * @readonly + */ + get premiumSince() { + return this.premiumSinceTimestamp && new Date(this.premiumSinceTimestamp); + } + + /** + * The member's id + * @type {Snowflake} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * The DM between the client's user and this member + * @type {?DMChannel} + * @readonly + */ + get dmChannel() { + return this.client.users.dmChannel(this.id); + } + + /** + * The nickname of this member, or their user display name if they don't have one + * @type {string} + * @readonly + */ + get displayName() { + return this.nickname ?? this.user.displayName; + } + + /** + * Whether this member is currently timed out + * @returns {boolean} + */ + isCommunicationDisabled() { + return this.communicationDisabledUntilTimestamp > Date.now(); + } + + /** + * Creates a DM channel between the client and this member. + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise} + */ + createDM(force = false) { + return this.user.createDM(force); + } + + /** + * Deletes a DM channel (if one exists) between the client and the member. Resolves with the channel if successful. + * @returns {Promise} + */ + deleteDM() { + return this.user.deleteDM(); + } + + /** + * Whether this guild member equals another guild member. It compares all properties, so for most + * comparison it is advisable to just compare `member.id === member2.id` as it is significantly faster + * and is often what most users need. + * @param {MinimalGuildMember} member The member to compare with + * @returns {boolean} + */ + equals(member) { + return ( + member instanceof this.constructor && + this.id === member.id && + this.partial === member.partial && + this.guildId === member.guildId && + this.joinedTimestamp === member.joinedTimestamp && + this.nickname === member.nickname && + this.avatar === member.avatar && + this.pending === member.pending && + this.communicationDisabledUntilTimestamp === member.communicationDisabledUntilTimestamp && + this.flags.bitfield === member.flags.bitfield && + (this.roleIds === member.roleIds || + (this.roleIds.length === member.roleIds.length && this.roleIds.every((role, i) => role === member.roleIds[i]))) + ); + } + + /** + * When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${member}!`); + */ + toString() { + return this.user.toString(); + } + + toJSON() { + const json = super.toJSON({ + guildId: true, + user: 'userId', + displayName: true, + roles: true, + }); + json.avatarURL = this.avatarURL(); + json.bannerURL = this.bannerURL(); + json.displayAvatarURL = this.displayAvatarURL(); + json.displayBannerURL = this.displayBannerURL(); + return json; + } +} + +/** + * Sends a message to this user. + * @method send + * @memberof MinimalGuildMember + * @instance + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a direct message + * guildMember.send('Hello!') + * .then(message => console.log(`Sent message: ${message.content} to ${guildMember.displayName}`)) + * .catch(console.error); + */ + +TextBasedChannel.applyToClass(MinimalGuildMember); + +module.exports = MinimalGuildMember; diff --git a/packages/discord.js/src/structures/Role.js b/packages/discord.js/src/structures/Role.js index 06cbac60bade..aa62a924ef6f 100644 --- a/packages/discord.js/src/structures/Role.js +++ b/packages/discord.js/src/structures/Role.js @@ -181,7 +181,7 @@ class Role extends Base { get members() { return this.id === this.guild.id ? this.guild.members.cache.clone() - : this.guild.members.cache.filter(member => member._roles.includes(this.id)); + : this.guild.members.cache.filter(member => member.roleIds.includes(this.id)); } /** diff --git a/packages/discord.js/src/structures/UserContextMenuCommandInteraction.js b/packages/discord.js/src/structures/UserContextMenuCommandInteraction.js index 2e9dc7c62877..db5dc65a4bf0 100644 --- a/packages/discord.js/src/structures/UserContextMenuCommandInteraction.js +++ b/packages/discord.js/src/structures/UserContextMenuCommandInteraction.js @@ -18,7 +18,7 @@ class UserContextMenuCommandInteraction extends ContextMenuCommandInteraction { /** * The target member from this interaction - * @type {?(GuildMember|APIGuildMember)} + * @type {?(GuildMember|MinimalGuildMember)} * @readonly */ get targetMember() { diff --git a/packages/discord.js/src/structures/UserSelectMenuInteraction.js b/packages/discord.js/src/structures/UserSelectMenuInteraction.js index 5e232399708e..49c6337ed559 100644 --- a/packages/discord.js/src/structures/UserSelectMenuInteraction.js +++ b/packages/discord.js/src/structures/UserSelectMenuInteraction.js @@ -2,6 +2,7 @@ const { Collection } = require('@discordjs/collection'); const MessageComponentInteraction = require('./MessageComponentInteraction'); +const MinimalGuildMember = require('./MinimalGuildMember'); const Events = require('../util/Events'); /** @@ -27,7 +28,7 @@ class UserSelectMenuInteraction extends MessageComponentInteraction { /** * Collection of the selected members - * @type {Collection} + * @type {Collection} */ this.members = new Collection(); @@ -43,7 +44,11 @@ class UserSelectMenuInteraction extends MessageComponentInteraction { continue; } - this.members.set(id, this.guild?.members._add({ user, ...member }) ?? { user, ...member }); + this.members.set( + id, + this.guild?.members._add({ user, ...member }) ?? + new MinimalGuildMember(this.client, { user, ...member }, this.guildId), + ); } } } diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index d2e408580253..7883d8c74a23 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -164,8 +164,9 @@ class TextBasedChannel { async send(options) { const User = require('../User'); const { GuildMember } = require('../GuildMember'); + const MinimalGuildMember = require('../MinimalGuildMember'); - if (this instanceof User || this instanceof GuildMember) { + if (this instanceof User || this instanceof GuildMember || this instanceof MinimalGuildMember) { const dm = await this.createDM(); return dm.send(options); } diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index b9e4fb4e20e0..b9dade9c5b3a 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -30,8 +30,6 @@ import { APIEmbed, APIEmoji, APIInteractionDataResolvedChannel, - APIInteractionDataResolvedGuildMember, - APIInteractionGuildMember, APIMessage, APIMessageComponent, APIOverwrite, @@ -118,7 +116,6 @@ import { APIRoleSelectComponent, APIMentionableSelectComponent, APIChannelSelectComponent, - APIGuildMember, APIMessageRoleSelectInteractionData, APIMessageMentionableSelectInteractionData, APIMessageChannelSelectInteractionData, @@ -1652,18 +1649,14 @@ export class GuildMemberFlagsBitField extends BitField { public static resolve(bit?: BitFieldResolvable): number; } -export interface GuildMember extends PartialTextBasedChannelFields {} -export class GuildMember extends Base { - private constructor(client: Client, data: RawGuildMemberData, guild: Guild); - private _roles: Snowflake[]; +export interface MinimalGuildMember extends PartialTextBasedChannelFields {} +export class MinimalGuildMember extends Base { + protected constructor(client: Client, data: RawGuildMemberData, guildId: Snowflake); public avatar: string | null; public banner: string | null; - public get bannable(): boolean; public get dmChannel(): DMChannel | null; - public get displayColor(): number; - public get displayHexColor(): HexColorString; public get displayName(): string; - public guild: Guild; + public guildId: string; public get id(): Snowflake; public pending: boolean; public get communicationDisabledUntil(): Date | null; @@ -1671,40 +1664,52 @@ export class GuildMember extends Base { public flags: Readonly; public get joinedAt(): Date | null; public joinedTimestamp: number | null; - public get kickable(): boolean; - public get manageable(): boolean; - public get moderatable(): boolean; public nickname: string | null; - public get partial(): false; - public get permissions(): Readonly; + public get partial(): boolean; public get premiumSince(): Date | null; public premiumSinceTimestamp: number | null; - public get presence(): Presence | null; - public get roles(): GuildMemberRoleManager; + public roleIds: Snowflake[]; public user: User; - public get voice(): VoiceState; public avatarURL(options?: ImageURLOptions): string | null; public bannerURL(options?: ImageURLOptions): string | null; - public ban(options?: BanOptions): Promise; - public disableCommunicationUntil(timeout: DateResolvable | null, reason?: string): Promise; - public timeout(timeout: number | null, reason?: string): Promise; - public fetch(force?: boolean): Promise; public createDM(force?: boolean): Promise; public deleteDM(): Promise; public displayAvatarURL(options?: ImageURLOptions): string; public displayBannerURL(options?: ImageURLOptions): string | null; - public edit(options: GuildMemberEditOptions): Promise; - public isCommunicationDisabled(): this is GuildMember & { + public isCommunicationDisabled(): this is MinimalGuildMember & { communicationDisabledUntilTimestamp: number; readonly communicationDisabledUntil: Date; }; + public isInCachedGuild(): this is GuildMember; + public toJSON(): unknown; + public toString(): UserMention; + public valueOf(): string; +} + +export interface GuildMember extends PartialTextBasedChannelFields {} +export class GuildMember extends MinimalGuildMember { + private constructor(client: Client, data: RawGuildMemberData, guild: Guild); + public get bannable(): boolean; + public get displayColor(): number; + public get displayHexColor(): HexColorString; + public guild: Guild; + public get kickable(): boolean; + public get manageable(): boolean; + public get moderatable(): boolean; + public get partial(): false; + public get permissions(): Readonly; + public get presence(): Presence | null; + public get roles(): GuildMemberRoleManager; + public get voice(): VoiceState; + public ban(options?: BanOptions): Promise; + public disableCommunicationUntil(timeout: DateResolvable | null, reason?: string): Promise; + public timeout(timeout: number | null, reason?: string): Promise; + public fetch(force?: boolean): Promise; + public edit(options: GuildMemberEditOptions): Promise; public kick(reason?: string): Promise; public permissionsIn(channel: GuildChannelResolvable): Readonly; public setFlags(flags: GuildMemberFlagsResolvable, reason?: string): Promise; public setNickname(nickname: string | null, reason?: string): Promise; - public toJSON(): unknown; - public toString(): UserMention; - public valueOf(): string; } export class GuildOnboarding extends Base { @@ -1965,7 +1970,7 @@ export class BaseInteraction extends Base public get guild(): CacheTypeReducer; public guildId: CacheTypeReducer; public id: Snowflake; - public member: CacheTypeReducer; + public member: CacheTypeReducer; public readonly token: string; public type: InteractionType; public user: User; @@ -2892,7 +2897,13 @@ export class UserSelectMenuInteraction< public users: Collection; public members: Collection< Snowflake, - CacheTypeReducer + CacheTypeReducer< + Cached, + GuildMember, + MinimalGuildMember, + GuildMember | MinimalGuildMember, + GuildMember | MinimalGuildMember + > >; public inGuild(): this is UserSelectMenuInteraction<'raw' | 'cached'>; public inCachedGuild(): this is UserSelectMenuInteraction<'cached'>; @@ -2934,7 +2945,13 @@ export class MentionableSelectMenuInteraction< public users: Collection; public members: Collection< Snowflake, - CacheTypeReducer + CacheTypeReducer< + Cached, + GuildMember, + MinimalGuildMember, + GuildMember | MinimalGuildMember, + GuildMember | MinimalGuildMember + > >; public roles: Collection>; public inGuild(): this is MentionableSelectMenuInteraction<'raw' | 'cached'>; @@ -3501,7 +3518,7 @@ export class UserContextMenuCommandInteraction< | 'getSubcommand' >; public get targetUser(): User; - public get targetMember(): CacheTypeReducer | null; + public get targetMember(): CacheTypeReducer | null; public inGuild(): this is UserContextMenuCommandInteraction<'raw' | 'cached'>; public inCachedGuild(): this is UserContextMenuCommandInteraction<'cached'>; public inRawGuild(): this is UserContextMenuCommandInteraction<'raw'>; @@ -5359,7 +5376,7 @@ export interface CommandInteractionOption autocomplete?: boolean; options?: readonly CommandInteractionOption[]; user?: User; - member?: CacheTypeReducer; + member?: CacheTypeReducer; channel?: CacheTypeReducer; role?: CacheTypeReducer; attachment?: Attachment; @@ -5368,7 +5385,7 @@ export interface CommandInteractionOption export interface CommandInteractionResolvedData { users?: ReadonlyCollection; - members?: ReadonlyCollection>; + members?: ReadonlyCollection>; roles?: ReadonlyCollection>; channels?: ReadonlyCollection>; messages?: ReadonlyCollection>; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index b587e3bf25bd..3b90ad56930f 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -1,10 +1,8 @@ import type { ChildProcess } from 'node:child_process'; import type { Worker } from 'node:worker_threads'; import { - APIInteractionGuildMember, APIPartialChannel, APIPartialGuild, - APIInteractionDataResolvedGuildMember, APIInteractionDataResolvedChannel, APIRole, APIButtonComponent, @@ -209,6 +207,7 @@ import { SendableChannels, PollData, InteractionCallbackResponse, + MinimalGuildMember, } from '.'; import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; @@ -330,7 +329,7 @@ client.on('interactionCreate', async interaction => { expectType>(interaction.client); expectType(interaction.guildId); expectType(interaction.channelId); - expectType(interaction.member); + expectType(interaction.member); if (interaction.type === InteractionType.MessageComponent) { expectType(interaction.channelId); @@ -1825,13 +1824,13 @@ client.on('interactionCreate', async interaction => { expectAssignable(interaction); expectType(interaction.guildLocale); } else if (interaction.inRawGuild()) { - expectAssignable(interaction.member); + expectAssignable(interaction.member); expectNotAssignable>(interaction); expectType(interaction.guildLocale); } else if (interaction.inGuild()) { expectType(interaction.guildLocale); } else { - expectType(interaction.member); + expectType(interaction.member); expectNotAssignable>(interaction); expectType(interaction.guildId); } @@ -1895,18 +1894,18 @@ client.on('interactionCreate', async interaction => { interaction.commandType === ApplicationCommandType.User ) { expectType(interaction.targetUser); - expectType(interaction.targetMember); + expectType(interaction.targetMember); expectType(interaction.options.getUser('user')); - expectType(interaction.options.getMember('user')); + expectType(interaction.options.getMember('user')); if (interaction.inCachedGuild()) { expectType(interaction.targetMember); expectType(interaction.options.getMember('user')); } else if (interaction.inRawGuild()) { - expectType(interaction.targetMember); - expectType(interaction.options.getMember('user')); + expectType(interaction.targetMember); + expectType(interaction.options.getMember('user')); } else if (interaction.inGuild()) { - expectType(interaction.targetMember); - expectType(interaction.options.getMember('user')); + expectType(interaction.targetMember); + expectType(interaction.options.getMember('user')); } } @@ -1971,7 +1970,7 @@ client.on('interactionCreate', async interaction => { expectNotAssignable>(interaction); expectAssignable(interaction); expectType>(interaction.reply({ withResponse: true })); - expectType(interaction.options.getMember('test')); + expectType(interaction.options.getMember('test')); expectType(interaction.options.getChannel('test', true)); expectType(interaction.options.getRole('test', true)); @@ -2003,7 +2002,7 @@ client.on('interactionCreate', async interaction => { } else { expectType(interaction); expectType>(interaction.reply({ withResponse: true })); - expectType(interaction.options.getMember('test')); + expectType(interaction.options.getMember('test')); expectType(interaction.options.getChannel('test', true)); expectType(interaction.options.getRole('test', true));