diff --git a/scripts/generateRequires.mjs b/scripts/generateRequires.mjs index 4b8ab236ade7..2b3eab7c9c5c 100644 --- a/scripts/generateRequires.mjs +++ b/scripts/generateRequires.mjs @@ -16,7 +16,18 @@ async function writeWebsocketHandlerImports() { } async function writeClientActionImports() { - const lines = ["'use strict';\n", 'class ActionsManager {', ' constructor(client) {', ' this.client = client;\n']; + const lines = [ + "'use strict';\n", + 'class ActionsManager {', + ' constructor(client) {', + ' this.client = client;\n', + ' // These symbols represent fully built data that we inject at times when calling actions manually.', + ' // Action#getUser for example, will return the injected data (which is assumed to be a built structure)', + ' // instead of trying to make it from provided data', + " this.injectedUser = Symbol('djs.actions.injectedUser');", + " this.injectedChannel = Symbol('djs.actions.injectedChannel');", + " this.injectedMessage = Symbol('djs.actions.injectedMessage');\n", + ]; const actionsDirectory = new URL('../src/client/actions', import.meta.url); for (const file of (await readdir(actionsDirectory)).sort()) { diff --git a/src/client/actions/ActionsManager.js b/src/client/actions/ActionsManager.js index 65415dd20af8..5594fdd84585 100644 --- a/src/client/actions/ActionsManager.js +++ b/src/client/actions/ActionsManager.js @@ -4,9 +4,9 @@ class ActionsManager { constructor(client) { this.client = client; - // These symbols represent fully built data that we inject at times when calling actions manually. Action#getUser, - // for example, will return the injected data (which is assumed to be a built structure) instead of trying to make - // it from provided data + // These symbols represent fully built data that we inject at times when calling actions manually. + // Action#getUser for example, will return the injected data (which is assumed to be a built structure) + // instead of trying to make it from provided data this.injectedUser = Symbol('djs.actions.injectedUser'); this.injectedChannel = Symbol('djs.actions.injectedChannel'); this.injectedMessage = Symbol('djs.actions.injectedMessage'); diff --git a/src/client/websocket/handlers/index.js b/src/client/websocket/handlers/index.js index df3c2ea2deca..271613878cdf 100644 --- a/src/client/websocket/handlers/index.js +++ b/src/client/websocket/handlers/index.js @@ -11,7 +11,6 @@ const handlers = Object.fromEntries([ ['AUTO_MODERATION_RULE_CREATE', require('./AUTO_MODERATION_RULE_CREATE')], ['AUTO_MODERATION_RULE_DELETE', require('./AUTO_MODERATION_RULE_DELETE')], ['AUTO_MODERATION_RULE_UPDATE', require('./AUTO_MODERATION_RULE_UPDATE')], - ['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')], ['GUILD_CREATE', require('./GUILD_CREATE')], ['GUILD_DELETE', require('./GUILD_DELETE')], ['GUILD_UPDATE', require('./GUILD_UPDATE')], @@ -62,6 +61,7 @@ const handlers = Object.fromEntries([ ['GUILD_SCHEDULED_EVENT_DELETE', require('./GUILD_SCHEDULED_EVENT_DELETE')], ['GUILD_SCHEDULED_EVENT_USER_ADD', require('./GUILD_SCHEDULED_EVENT_USER_ADD')], ['GUILD_SCHEDULED_EVENT_USER_REMOVE', require('./GUILD_SCHEDULED_EVENT_USER_REMOVE')], + ['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')], ]); module.exports = handlers; diff --git a/src/managers/CachedManager.js b/src/managers/CachedManager.js index 0f7e914077e4..85fc03d92482 100644 --- a/src/managers/CachedManager.js +++ b/src/managers/CachedManager.js @@ -12,6 +12,13 @@ class CachedManager extends DataManager { constructor(client, holds, iterable) { super(client, holds); + /** + * The private cache of items for this manager. + * @type {Collection} + * @private + * @readonly + * @name CachedManager#_cache + */ Object.defineProperty(this, '_cache', { value: this.client.options.makeCache(this.constructor, this.holds) }); let cleanup = this._cache[_cleanupSymbol]?.(); diff --git a/src/managers/ThreadManager.js b/src/managers/ThreadManager.js index fc37a7b1a74a..f56723f98be2 100644 --- a/src/managers/ThreadManager.js +++ b/src/managers/ThreadManager.js @@ -59,10 +59,9 @@ class ThreadManager extends CachedManager { */ /** - * The options for fetching multiple threads, the properties are mutually exclusive + * Options for fetching multiple threads. * @typedef {Object} FetchThreadsOptions - * @property {FetchArchivedThreadOptions} [archived] The options used to fetch archived threads - * @property {boolean} [active] When true, fetches active threads. If `archived` is set, this is ignored! + * @property {FetchArchivedThreadOptions} [archived] Options used to fetch archived threads */ /** @@ -78,10 +77,10 @@ class ThreadManager extends CachedManager { * .then(channel => console.log(channel.name)) * .catch(console.error); */ - fetch(options, { cache = true, force = false } = {}) { + fetch(options, { cache, force } = {}) { if (!options) return this.fetchActive(cache); const channel = this.client.channels.resolveId(options); - if (channel) return this.client.channels.fetch(channel, cache, force); + if (channel) return this.client.channels.fetch(channel, { cache, force }); if (options.archived) { return this.fetchArchived(options.archived, cache); } @@ -102,7 +101,7 @@ class ThreadManager extends CachedManager { * @property {string} [type='public'] The type of threads to fetch, either `public` or `private` * @property {boolean} [fetchAll=false] Whether to fetch **all** archived threads when type is `private`. * Requires `MANAGE_THREADS` if true - * @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were created before this Date + * @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were archived before this Date * or Snowflake. Must be a {@link ThreadChannelResolvable} when type is `private` and fetchAll is `false` * @property {number} [limit] Maximum number of threads to return */ @@ -128,7 +127,7 @@ class ThreadManager extends CachedManager { let timestamp; let id; if (typeof before !== 'undefined') { - if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) { + if (before instanceof ThreadChannel || /^\d{17,19}$/.test(String(before))) { id = this.resolveId(before); timestamp = this.resolve(before)?.archivedAt?.toISOString(); } else { diff --git a/src/structures/AutocompleteInteraction.js b/src/structures/AutocompleteInteraction.js index 1ebb3c5319aa..a2f4bed0e27b 100644 --- a/src/structures/AutocompleteInteraction.js +++ b/src/structures/AutocompleteInteraction.js @@ -96,9 +96,7 @@ class AutocompleteInteraction extends Interaction { await this.client.api.interactions(this.id, this.token).callback.post({ data: { type: InteractionResponseTypes.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT, - data: { - choices: options, - }, + data: { choices: { ...options, name_localizations: options.nameLocalizations } }, }, auth: false, }); diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 9136752f0b3f..e07b181df81a 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -53,11 +53,13 @@ class ClientUser extends User { * @param {ClientUserEditData} data The new data * @returns {Promise} */ - async edit(data) { - if (typeof data.avatar !== 'undefined') data.avatar = await DataResolver.resolveImage(data.avatar); - const newData = await this.client.api.users('@me').patch({ data }); - this.client.token = newData.token; - const { updated } = this.client.actions.UserUpdate.handle(newData); + async edit({ username, avatar }) { + const data = await this.client.api + .users('@me') + .patch({ username, avatar: avatar && (await DataResolver.resolveImage(avatar)) }); + + this.client.token = data.token; + const { updated } = this.client.actions.UserUpdate.handle(data); return updated ?? this; } diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 9b3517c077ec..100799d69bdd 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -716,9 +716,6 @@ class Guild extends AnonymousGuild { * .catch(console.error); */ async fetchVanityData() { - if (!this.features.includes('VANITY_URL')) { - throw new Error('VANITY_URL'); - } const data = await this.client.api.guilds(this.id, 'vanity-url').get(); this.vanityURLCode = data.code; this.vanityURLUses = data.uses; @@ -1341,7 +1338,7 @@ class Guild extends AnonymousGuild { * @example * // Leave a guild * guild.leave() - * .then(g => console.log(`Left the guild ${g}`)) + * .then(guild => console.log(`Left the guild: ${guild.name}`)) * .catch(console.error); */ async leave() { diff --git a/src/structures/GuildMember.js b/src/structures/GuildMember.js index 28e7770cdba3..2fff24990d37 100644 --- a/src/structures/GuildMember.js +++ b/src/structures/GuildMember.js @@ -62,6 +62,11 @@ class GuildMember extends Base { */ this.communicationDisabledUntilTimestamp = null; + /** + * The role ids of the member + * @type {Snowflake[]} + * @private + */ this._roles = []; if (data) this._patch(data); } @@ -352,6 +357,16 @@ class GuildMember extends Base { * @param {?string} nick The nickname for the guild member, or `null` if you want to reset their nickname * @param {string} [reason] Reason for setting the nickname * @returns {Promise} + * @example + * // Set a nickname for a guild member + * guildMember.setNickname('cool nickname', 'Needed a new nickname') + * .then(member => console.log(`Set nickname of ${member.user.username}`)) + * .catch(console.error); + * @example + * // Remove a nickname for a guild member + * guildMember.setNickname(null, 'No nicknames allowed!') + * .then(member => console.log(`Removed nickname for ${member.user.username}`)) + * .catch(console.error); */ setNickname(nick, reason) { return this.edit({ nick }, reason); @@ -404,7 +419,7 @@ class GuildMember extends Base { * .catch(console.error); */ ban(options) { - return this.guild.members.ban(this, options); + return this.guild.bans.create(this, options); } /** @@ -418,6 +433,11 @@ class GuildMember extends Base { * guildMember.disableCommunicationUntil(Date.now() + (5 * 60 * 1000), 'They deserved it') * .then(console.log) * .catch(console.error); + * @example + * // Remove the timeout of a guild member + * guildMember.disableCommunicationUntil(null) + * .then(member => console.log(`Removed timeout for ${member.displayName}`)) + * .catch(console.error); */ disableCommunicationUntil(communicationDisabledUntil, reason) { return this.edit({ communicationDisabledUntil }, reason); @@ -494,12 +514,22 @@ class GuildMember extends Base { json.displayAvatarURL = this.displayAvatarURL(); return json; } - - // These are here only for documentation purposes - they are implemented by TextBasedChannel - /* eslint-disable no-empty-function */ - send() {} } +/** + * Sends a message to this user. + * @method send + * @memberof GuildMember + * @instance + * @param {string|MessagePayload|MessageOptions} 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/src/structures/Message.js b/src/structures/Message.js index 30752424aee3..38714bea79bf 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -599,11 +599,17 @@ class Message extends Base { const precheck = Boolean( this.author.id === this.client.user.id && !deletedMessages.has(this) && (!this.guild || this.channel?.viewable), ); + // Regardless of permissions thread messages cannot be edited if - // the thread is locked. + // the thread is archived or the thread is locked and the bot does not have permission to manage threads. if (this.channel?.isThread()) { - return precheck && !this.channel.locked; + if (this.channel.archived) return false; + if (this.channel.locked) { + const permissions = this.permissionsFor(this.client.user); + if (!permissions?.has(Permissions.FLAGS.MANAGE_THREADS, true)) return false; + } } + return precheck; } @@ -645,12 +651,11 @@ class Message extends Base { * channel.bulkDelete(messages.filter(message => message.bulkDeletable)); */ get bulkDeletable() { - const permissions = this.channel?.permissionsFor(this.client.user); return ( (this.inGuild() && Date.now() - this.createdTimestamp < MaxBulkDeletableMessageAge && this.deletable && - permissions?.has(Permissions.FLAGS.MANAGE_MESSAGES, false)) ?? + this.channel?.permissionsFor(this.client.user).has(Permissions.FLAGS.MANAGE_MESSAGES, false)) ?? false ); } diff --git a/src/structures/Presence.js b/src/structures/Presence.js index b9cf29ed4943..345e4f186eff 100644 --- a/src/structures/Presence.js +++ b/src/structures/Presence.js @@ -155,6 +155,12 @@ class Presence extends Base { */ class Activity { constructor(presence, data) { + /** + * The presence of the Activity + * @type {Presence} + * @readonly + * @name Activity#presence + */ Object.defineProperty(this, 'presence', { value: presence }); /** @@ -325,6 +331,12 @@ class Activity { */ class RichPresenceAssets { constructor(activity, assets) { + /** + * The activity of the RichPresenceAssets + * @type {Activity} + * @readonly + * @name RichPresenceAssets#activity + */ Object.defineProperty(this, 'activity', { value: activity }); /** diff --git a/src/structures/Role.js b/src/structures/Role.js index cf59017bdf76..c35a42491d1a 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -233,6 +233,10 @@ class Role extends Base { * @param {RoleResolvable} role Role to compare to this one * @returns {number} Negative number if this role's position is lower (other role's is higher), * positive number if this one is higher (other's is lower), 0 if equal + * @example + * // Compare the position of a role to another + * const roleCompare = role.comparePositionTo(otherRole); + * if (roleCompare >= 1) console.log(`${role.name} is higher than ${otherRole.name}`); */ comparePositionTo(role) { return this.guild.roles.comparePositions(this, role); diff --git a/src/structures/User.js b/src/structures/User.js index 588519053db3..dd1880edec3d 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -310,12 +310,22 @@ class User extends Base { json.bannerURL = this.banner ? this.bannerURL() : this.banner; return json; } - - // These are here only for documentation purposes - they are implemented by TextBasedChannel - /* eslint-disable no-empty-function */ - send() {} } +/** + * Sends a message to this user. + * @method send + * @memberof User + * @instance + * @param {string|MessagePayload|MessageOptions} options The options to provide + * @returns {Promise} + * @example + * // Send a direct message + * user.send('Hello!') + * .then(message => console.log(`Sent message: ${message.content} to ${user.tag}`)) + * .catch(console.error); + */ + TextBasedChannel.applyToClass(User); module.exports = User; diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index e44cceb79490..a05a1281c118 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -136,25 +136,6 @@ class TextBasedChannel { * }) * .then(console.log) * .catch(console.error); - * @example - * // Send an embed with a local image inside - * channel.send({ - * content: 'This is an embed', - * embeds: [ - * { - * thumbnail: { - * url: 'attachment://file.jpg' - * } - * } - * ], - * files: [{ - * attachment: 'entire/path/to/file.jpg', - * name: 'file.jpg', - * description: 'A description of the file' - * }] - * }) - * .then(console.log) - * .catch(console.error); */ async send(options) { const User = require('../User'); diff --git a/src/util/BitField.js b/src/util/BitField.js index c34f36246c3c..fd920a3c501d 100644 --- a/src/util/BitField.js +++ b/src/util/BitField.js @@ -146,8 +146,8 @@ class BitField { if (bit instanceof BitField) return bit.bitfield; if (Array.isArray(bit)) return bit.map(p => this.resolve(p)).reduce((prev, p) => prev | p, defaultBit); if (typeof bit === 'string') { - if (typeof this.FLAGS[bit] !== 'undefined') return this.FLAGS[bit]; if (!isNaN(bit)) return typeof defaultBit === 'bigint' ? BigInt(bit) : Number(bit); + if (this.FLAGS[bit] !== undefined) return this.FLAGS[bit]; } throw new RangeError('BITFIELD_INVALID', bit); } diff --git a/typings/index.d.ts b/typings/index.d.ts index d1eff3bfaf95..01b53deb742d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -166,6 +166,7 @@ import { export class Activity { private constructor(presence: Presence, data?: RawActivityData); + public readonly presence: Presence; public applicationId: Snowflake | null; public assets: RichPresenceAssets | null; public buttons: string[]; @@ -1189,6 +1190,7 @@ export class GuildEmoji extends BaseGuildEmoji { export class GuildMember extends PartialTextBasedChannel(Base) { private constructor(client: Client, data: RawGuildMemberData, guild: Guild); + private _roles: Snowflake[]; public avatar: string | null; public readonly bannable: boolean; /** @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 */ @@ -2124,6 +2126,7 @@ export class ReactionEmoji extends Emoji { export class RichPresenceAssets { private constructor(activity: Activity, assets: RawRichPresenceAssets); + public readonly activity: Activity; public largeImage: Snowflake | null; public largeText: string | null; public smallImage: Snowflake | null; @@ -3203,6 +3206,7 @@ export abstract class DataManager extends BaseManager { export abstract class CachedManager extends DataManager { protected constructor(client: Client, holds: Constructable); + private readonly _cache: Collection; private _add(data: unknown, cache?: boolean, { id, extras }?: { id: K; extras: unknown[] }): Holds; } @@ -5080,7 +5084,6 @@ export interface FetchReactionUsersOptions { export interface FetchThreadsOptions { archived?: FetchArchivedThreadOptions; - active?: boolean; } export interface FileOptions {