diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index f3968344f660..40b33b98c3d9 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -32,19 +32,15 @@ class GenericAction { } getChannel(data) { + const payloadData = { recipients: data.recipients ?? [data.author ?? data.user ?? { id: data.user_id }] }; const id = data.channel_id ?? data.id; + if (id !== undefined) payloadData.id = id; + if ('guild_id' in data) payloadData.guild_id = data.guild_id; + if ('last_message_id' in data) payloadData.last_message_id = data.last_message_id; + return ( data[this.client.actions.injectedChannel] ?? - this.getPayload( - { - id, - guild_id: data.guild_id, - recipients: [data.author ?? data.user ?? { id: data.user_id }], - }, - this.client.channels, - id, - PartialTypes.CHANNEL, - ) + this.getPayload(payloadData, this.client.channels, id, PartialTypes.CHANNEL) ); } diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index acff7c28526b..5ba66a1b777c 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -8,6 +8,13 @@ module.exports = (client, { d: data }, shard) => { if (!guild.available && !data.unavailable) { // A newly available guild guild._patch(data); + + /** + * Emitted whenever a guild becomes available. + * @event Client#guildAvailable + * @param {Guild} guild The guild that became available + */ + client.emit(Events.GUILD_AVAILABLE, guild); } } else { // A new guild diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index cae8b6d704b3..88095e9f1adf 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -15,7 +15,7 @@ const Permissions = require('../util/Permissions'); */ /** - * Represents a Client OAuth2 Application. + * Represents a client application. * @extends {Application} */ class ClientApplication extends Application { @@ -69,6 +69,26 @@ class ClientApplication extends Application { this.flags = new ApplicationFlags(data.flags).freeze(); } + if ('approximate_guild_count' in data) { + /** + * An approximate amount of guilds this application is in. + * @type {?number} + */ + this.approximateGuildCount = data.approximate_guild_count; + } else { + this.approximateGuildCount ??= null; + } + + if ('guild_id' in data) { + /** + * The id of the guild associated with this application. + * @type {?Snowflake} + */ + this.guildId = data.guild_id; + } else { + this.guildId ??= null; + } + if ('cover_image' in data) { /** * The hash of the application's cover image @@ -120,6 +140,15 @@ class ClientApplication extends Application { : this.owner ?? null; } + /** + * The guild associated with this application. + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.cache.get(this.guildId) ?? null; + } + /** * Whether this application is partial * @type {boolean} @@ -134,8 +163,8 @@ class ClientApplication extends Application { * @returns {Promise} */ async fetch() { - const app = await this.client.api.oauth2.applications('@me').get(); - this._patch(app); + const data = await this.client.api.applications('@me').get(); + this._patch(data); return this; } diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index 04ccce6e4553..0a1e45c16719 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -49,11 +49,18 @@ class ClientPresence extends Presence { if (activities?.length) { for (const [i, activity] of activities.entries()) { if (typeof activity.name !== 'string') throw new TypeError('INVALID_TYPE', `activities[${i}].name`, 'string'); - activity.type ??= 0; + + activity.type ??= ActivityTypes.PLAYING; + + if (activity.type === ActivityTypes.CUSTOM && !activity.state) { + activity.state = activity.name; + activity.name = 'Custom Status'; + } data.activities.push({ type: typeof activity.type === 'number' ? activity.type : ActivityTypes[activity.type], name: activity.name, + state: activity.state, url: activity.url, }); } @@ -62,6 +69,7 @@ class ClientPresence extends Presence { ...this.activities.map(a => ({ name: a.name, type: ActivityTypes[a.type], + state: a.state ?? undefined, url: a.url ?? undefined, })), ); diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 61e2522e7def..4af22b74baad 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -96,7 +96,8 @@ class ClientUser extends User { /** * Options for setting activities * @typedef {Object} ActivitiesOptions - * @property {string} [name] Name of the activity + * @property {string} name Name of the activity + * @property {string} [state] State of the activity * @property {ActivityType|number} [type] Type of the activity * @property {string} [url] Twitch / YouTube stream URL */ @@ -147,7 +148,7 @@ class ClientUser extends User { /** * Options for setting an activity. * @typedef {Object} ActivityOptions - * @property {string} [name] Name of the activity + * @property {string} name Name of the activity * @property {string} [url] Twitch / YouTube stream URL * @property {ActivityType|number} [type] Type of the activity * @property {number|number[]} [shardId] Shard Id(s) to have the activity set on @@ -155,7 +156,7 @@ class ClientUser extends User { /** * Sets the activity the client user is playing. - * @param {string|ActivityOptions} [name] Activity being played, or options for setting the activity + * @param {string|ActivityOptions} name Activity being played, or options for setting the activity * @param {ActivityOptions} [options] Options for setting the activity * @returns {ClientPresence} * @example diff --git a/src/structures/MessageAttachment.js b/src/structures/MessageAttachment.js index 4be74c8f44c7..bf5f345ebbe6 100644 --- a/src/structures/MessageAttachment.js +++ b/src/structures/MessageAttachment.js @@ -1,5 +1,6 @@ 'use strict'; +const AttachmentFlags = require('../util/AttachmentFlags'); const Util = require('../util/Util'); /** @@ -169,6 +170,16 @@ class MessageAttachment { } else { this.waveform ??= null; } + + if ('flags' in data) { + /** + * The flags of this attachment + * @type {Readonly} + */ + this.flags = new AttachmentFlags(data.flags).freeze(); + } else { + this.flags ??= new AttachmentFlags().freeze(); + } } /** diff --git a/src/structures/Role.js b/src/structures/Role.js index 446a5947ee5c..b4e85b815f5d 100644 --- a/src/structures/Role.js +++ b/src/structures/Role.js @@ -4,6 +4,7 @@ const process = require('node:process'); const Base = require('./Base'); const { Error } = require('../errors'); const Permissions = require('../util/Permissions'); +const RoleFlags = require('../util/RoleFlags'); const SnowflakeUtil = require('../util/SnowflakeUtil'); let deprecationEmittedForComparePositions = false; @@ -142,6 +143,16 @@ class Role extends Base { this.tags.guildConnections = true; } } + + if ('flags' in data) { + /** + * The flags of this role + * @type {Readonly} + */ + this.flags = new RoleFlags(data.flags).freeze(); + } else { + this.flags ??= new RoleFlags().freeze(); + } } /** diff --git a/src/util/AttachmentFlags.js b/src/util/AttachmentFlags.js new file mode 100644 index 000000000000..fa94351566aa --- /dev/null +++ b/src/util/AttachmentFlags.js @@ -0,0 +1,38 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link GuildMember#flags} bitfield. + * @extends {BitField} + */ +class AttachmentFlags extends BitField {} + +/** + * @name AttachmentFlags + * @kind constructor + * @memberof AttachmentFlags + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/* eslint-disable max-len */ +/** + * Numeric guild member flags. All available properties: + * * `IS_REMIX` + * @type {Object} + * @see {@link https://discord.com/developers/docs/resources/channel#attachment-object-attachment-structure-attachment-flags} + */ +AttachmentFlags.FLAGS = { + IS_REMIX: 1 << 2, +}; + +/** + * Data that can be resolved to give a guild attachment bitfield. This can be: + * * A string (see {@link AttachmentFlags.FLAGS}) + * * A attachment flag + * * An instance of AttachmentFlags + * * An Array of AttachmentFlagsResolvable + * @typedef {string|number|AttachmentFlags|AttachmentFlagsResolvable[]} AttachmentFlagsResolvable + */ + +module.exports = AttachmentFlags; diff --git a/src/util/Constants.js b/src/util/Constants.js index 5a293dcfaeb9..ec94567d9603 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -183,6 +183,7 @@ exports.Opcodes = { * * AUTO_MODERATION_RULE_CREATE: autoModerationRuleCreate * * AUTO_MODERATION_RULE_DELETE: autoModerationRuleDelete * * AUTO_MODERATION_RULE_UPDATE: autoModerationRuleUpdate + * * GUILD_AVAILABLE: guildAvailable * * GUILD_CREATE: guildCreate * * GUILD_DELETE: guildDelete * * GUILD_UPDATE: guildUpdate @@ -267,6 +268,7 @@ exports.Events = { AUTO_MODERATION_RULE_CREATE: 'autoModerationRuleCreate', AUTO_MODERATION_RULE_DELETE: 'autoModerationRuleDelete', AUTO_MODERATION_RULE_UPDATE: 'autoModerationRuleUpdate', + GUILD_AVAILABLE: 'guildAvailable', GUILD_CREATE: 'guildCreate', GUILD_DELETE: 'guildDelete', GUILD_UPDATE: 'guildUpdate', diff --git a/src/util/RoleFlags.js b/src/util/RoleFlags.js new file mode 100644 index 000000000000..41e38ba3e8d3 --- /dev/null +++ b/src/util/RoleFlags.js @@ -0,0 +1,37 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with an {@link GuildMember#flags} bitfield. + * @extends {BitField} + */ +class RoleFlags extends BitField {} + +/** + * @name RoleFlags + * @kind constructor + * @memberof RoleFlags + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Numeric guild member flags. All available properties: + * * `IN_PROMPT` + * @type {Object} + * @see {@link https://discord.com/developers/docs/topics/permissions#role-object-role-flags} + */ +RoleFlags.FLAGS = { + IN_PROMPT: 1 << 0, +}; + +/** + * Data that can be resolved to give a role flag bitfield. This can be: + * * A string (see {@link RoleFlags.FLAGS}) + * * A role flag + * * An instance of RoleFlags + * * An Array of RoleFlagsResolvable + * @typedef {string|number|RoleFlags|RoleFlagsResolvable[]} RoleFlagsResolvable + */ + +module.exports = RoleFlags; diff --git a/typings/index.d.ts b/typings/index.d.ts index 810ecf5ec651..2317322e2d70 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -677,11 +677,14 @@ export class Client extends BaseClient { export class ClientApplication extends Application { private constructor(client: Client, data: RawClientApplicationData); + public approximateGuildCount: number | null; public botPublic: boolean | null; public botRequireCodeGrant: boolean | null; public commands: ApplicationCommandManager; public cover: string | null; public flags: Readonly; + public guildId: Snowflake | null; + public readonly guild: Guild | null; public tags: string[]; public installParams: ClientApplicationInstallParams | null; public customInstallURL: string | null; @@ -708,7 +711,7 @@ export class ClientUser extends User { public verified: boolean; public edit(data: ClientUserEditData): Promise; public setActivity(options?: ActivityOptions): ClientPresence; - public setActivity(name: string, options?: ActivityOptions): ClientPresence; + public setActivity(name: string, options?: Omit): ClientPresence; public setAFK(afk?: boolean, shardId?: number | number[]): ClientPresence; public setAvatar(avatar: BufferResolvable | Base64Resolvable | null): Promise; public setPresence(data: PresenceData): ClientPresence; @@ -1569,27 +1572,27 @@ export class LimitedCollection extends Collection { public static filterByLifetime(options?: LifetimeFilterOptions): SweepFilter; } -export type MessageCollectorOptionsParams = - | { - componentType?: T; - } & MessageComponentCollectorOptions[T]>; +export type MessageCollectorOptionsParams< + T extends MessageComponentTypeResolvable, + Cached extends boolean = boolean, +> = { + componentType?: T; +} & MessageComponentCollectorOptions[T]>; export type MessageChannelCollectorOptionsParams< T extends MessageComponentTypeResolvable, Cached extends boolean = boolean, -> = - | { - componentType?: T; - } & MessageChannelComponentCollectorOptions[T]>; +> = { + componentType?: T; +} & MessageChannelComponentCollectorOptions[T]>; export type AwaitMessageCollectorOptionsParams< T extends MessageComponentTypeResolvable, Cached extends boolean = boolean, -> = - | { componentType?: T } & Pick< - InteractionCollectorOptions[T]>, - keyof AwaitMessageComponentOptions - >; +> = { componentType?: T } & Pick< + InteractionCollectorOptions[T]>, + keyof AwaitMessageComponentOptions +>; export interface StringMappedInteractionTypes { BUTTON: ButtonInteraction; @@ -1712,6 +1715,7 @@ export class MessageAttachment { public description: string | null; public duration: number | null; public ephemeral: boolean; + public flags: Readonly; public height: number | null; public id: Snowflake; public name: string | null; @@ -1728,6 +1732,13 @@ export class MessageAttachment { public toJSON(): unknown; } +export class AttachmentFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; +} + +export type AttachmentFlagsString = 'IS_REMIX'; + export class MessageButton extends BaseMessageComponent { public constructor(data?: MessageButton | MessageButtonOptions | APIButtonComponent); public customId: string | null; @@ -2156,6 +2167,7 @@ export class Role extends Base { /** @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091 */ public deleted: boolean; public readonly editable: boolean; + public flags: Readonly; public guild: Guild; public readonly hexColor: HexColorString; public hoist: boolean; @@ -2191,6 +2203,13 @@ export class Role extends Base { public static comparePositions(role1: Role, role2: Role): number; } +export class RoleFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; +} + +export type RoleFlagsString = 'IN_PROMPT'; + export class SelectMenuInteraction extends MessageComponentInteraction { public constructor(client: Client, data: RawMessageSelectMenuInteractionData); public readonly component: CacheTypeReducer< @@ -3224,7 +3243,7 @@ export abstract class DataManager extends BaseManager { } export abstract class CachedManager extends DataManager { - protected constructor(client: Client, holds: Constructable); + protected constructor(client: Client, holds: Constructable, iterable?: Iterable); private readonly _cache: Collection; private _add(data: unknown, cache?: boolean, { id, extras }?: { id: K; extras: unknown[] }): Holds; } @@ -3795,9 +3814,10 @@ export type ActivityFlagsString = export type ActivitiesOptions = Omit; export interface ActivityOptions { - name?: string; + name: string; + state?: string; url?: string; - type?: ExcludeEnum; + type?: ActivityType; shardId?: number | readonly number[]; } @@ -4582,6 +4602,7 @@ export interface ClientEvents extends BaseClientEvents { emojiDelete: [emoji: GuildEmoji]; emojiUpdate: [oldEmoji: GuildEmoji, newEmoji: GuildEmoji]; error: [error: Error]; + guildAvailable: [guild: Guild]; guildBanAdd: [ban: GuildBan]; guildBanRemove: [ban: GuildBan]; guildCreate: [guild: Guild]; @@ -4848,6 +4869,7 @@ export interface ConstantsEvents { AUTO_MODERATION_RULE_CREATE: 'autoModerationRuleCreate'; AUTO_MODERATION_RULE_DELETE: 'autoModerationRuleDelete'; AUTO_MODERATION_RULE_UPDATE: 'autoModerationRuleUpdate'; + GUILD_AVAILABLE: 'guildAvailable'; GUILD_CREATE: 'guildCreate'; GUILD_DELETE: 'guildDelete'; GUILD_UPDATE: 'guildUpdate'; @@ -5786,6 +5808,7 @@ export interface MessageActivity { } export interface BaseButtonOptions extends BaseMessageComponentOptions { + type: 'BUTTON' | MessageComponentTypes.BUTTON; disabled?: boolean; emoji?: EmojiIdentifierResolvable; label?: string;