diff --git a/src/client/actions/Action.js b/src/client/actions/Action.js index 8d0b6df..223d4e7 100644 --- a/src/client/actions/Action.js +++ b/src/client/actions/Action.js @@ -32,20 +32,12 @@ class GenericAction { } getChannel(data) { + const payloadData = { recipients: data.recipients ?? [data.author ?? data.user ?? { id: data.user_id }] }; const id = data.channel_id ?? data.id; - return ( - data.channel ?? - this.getPayload( - { - id, - guild_id: data.guild_id, - recipients: [data.author ?? data.user ?? { id: data.user_id }], - }, - this.client.channels, - id, - PartialTypes.CHANNEL, - ) - ); + 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.channel ?? this.getPayload(payloadData, this.client.channels, id, PartialTypes.CHANNEL); } getMessage(data, channel, cache) { diff --git a/src/client/websocket/handlers/GUILD_CREATE.js b/src/client/websocket/handlers/GUILD_CREATE.js index 87fa249..5ac6c3c 100644 --- a/src/client/websocket/handlers/GUILD_CREATE.js +++ b/src/client/websocket/handlers/GUILD_CREATE.js @@ -28,6 +28,13 @@ module.exports = (client, { d: data }, shard) => { // A newly available guild guild._patch(data); run(client, guild); + + /** + * 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 0f03361..c54c853 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -16,7 +16,7 @@ const Permissions = require('../util/Permissions'); */ /** - * Represents a Client OAuth2 Application. + * Represents a client application. * @extends {Application} */ class ClientApplication extends Application { @@ -70,6 +70,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 @@ -132,6 +152,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} diff --git a/src/structures/ClientPresence.js b/src/structures/ClientPresence.js index e073856..2761668 100644 --- a/src/structures/ClientPresence.js +++ b/src/structures/ClientPresence.js @@ -4,6 +4,8 @@ const { Presence } = require('./Presence'); const { TypeError } = require('../errors'); const { Opcodes, ActivityTypes } = require('../util/Constants'); +const CustomStatusActivityTypes = [ActivityTypes.CUSTOM, ActivityTypes[ActivityTypes.CUSTOM]]; + /** * Represents the client's presence. * @extends {Presence} @@ -54,7 +56,13 @@ class ClientPresence extends Presence { if (![ActivityTypes.CUSTOM, 'CUSTOM'].includes(activity.type) && typeof activity.name !== 'string') { throw new TypeError('INVALID_TYPE', `activities[${i}].name`, 'string'); } - activity.type ??= 0; + activity.type ??= ActivityTypes.PLAYING; + + if (CustomStatusActivityTypes.includes(activity.type) && !activity.state) { + activity.state = activity.name; + activity.name = 'Custom Status'; + } + data.activities.push( Object.assign(activity, { type: typeof activity.type === 'number' ? activity.type : ActivityTypes[activity.type], diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js index 2ca45bb..be2239f 100644 --- a/src/structures/ClientUser.js +++ b/src/structures/ClientUser.js @@ -381,7 +381,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 */ @@ -433,7 +434,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 @@ -441,7 +442,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 4be74c8..bf5f345 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 446a594..b4e85b8 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/structures/WebEmbed.js b/src/structures/WebEmbed.js index 815e7f5..20f09d6 100644 --- a/src/structures/WebEmbed.js +++ b/src/structures/WebEmbed.js @@ -35,6 +35,7 @@ class WebEmbed { * @property {Partial} [video] The video of this embed * @property {Partial} [footer] The footer of this embed * @property {Partial} [provider] The provider of this embed + * @property {string} [redirect] Redirect URL */ // eslint-disable-next-line valid-jsdoc @@ -188,6 +189,12 @@ class WebEmbed { url: data.provider.name, } : null; + + /** + * Redirect URL + * @type {string} + */ + this.redirect = data.redirect; } /** * The options to provide for setting an author for a {@link WebEmbed}. @@ -319,6 +326,16 @@ class WebEmbed { return this; } + /** + * Sets the redirect URL of this embed. + * @param {string} url The URL + * @returns {WebEmbed} + */ + setRedirect(url) { + this.redirect = url; + return this; + } + toString() { const url = new URL(baseURL); url.searchParams.set('image_type', this.imageType); @@ -359,6 +376,9 @@ class WebEmbed { if (this.thumbnail?.url) { url.searchParams.set('image', this.thumbnail.url); } + if (this.redirect) { + url.searchParams.set('redirect', this.redirect); + } return url.toString(); } diff --git a/src/util/AttachmentFlags.js b/src/util/AttachmentFlags.js new file mode 100644 index 00000000..fa94351 --- /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 717b611..e73310b 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -360,6 +360,7 @@ exports.Opcodes = { * * CALL_CREATE: callCreate * * CALL_DELETE: callDelete * * CALL_UPDATE: callUpdate + * * GUILD_AVAILABLE: guildAvailable * * GUILD_CREATE: guildCreate * * GUILD_DELETE: guildDelete * * GUILD_UPDATE: guildUpdate @@ -451,6 +452,7 @@ exports.Events = { CALL_CREATE: 'callCreate', CALL_DELETE: 'callDelete', CALL_UPDATE: 'callUpdate', + 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 00000000..41e38ba --- /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 4b5f0e3..c0c87b9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1014,12 +1014,15 @@ 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 popularCommands: Collection | undefined; 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; @@ -1051,7 +1054,7 @@ export class ClientUser extends User { public setThemeColors(primary?: ColorResolvable, accent?: ColorResolvable): ClientUser; public edit(data: ClientUserEditData): Promise; public setActivity(options?: ActivitiesOptions): 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 setBanner(banner: BufferResolvable | Base64Resolvable | null): Promise; @@ -2003,27 +2006,23 @@ 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 - >; +export type AwaitMessageCollectorOptionsParams = { + componentType?: T; +} & Pick[T]>, keyof AwaitMessageComponentOptions>; export interface StringMappedInteractionTypes { BUTTON: ButtonInteraction; @@ -2164,6 +2163,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; @@ -2185,6 +2185,13 @@ export interface InteractionResponseBody { nonce: Snowflake; } +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; @@ -2328,6 +2335,7 @@ export class WebEmbed { public hidden: boolean; public shorten: boolean; public imageType: 'thumbnail' | 'image'; + public redirect?: string; public setAuthor(options: EmbedAuthorData | null): this; public setColor(color: ColorResolvable): this; public setDescription(description: string): this; @@ -2704,6 +2712,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; @@ -2739,6 +2748,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 BaseSelectMenuInteraction< Cached extends CacheType = CacheType, > extends MessageComponentInteraction { @@ -3802,7 +3818,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; } @@ -4526,9 +4542,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[]; } @@ -4766,6 +4783,7 @@ export interface ClientEvents extends BaseClientEvents { callCreate: [call: Call]; callDelete: [call: Call]; callUpdate: [call: Call]; + guildAvailable: [guild: Guild]; guildBanAdd: [ban: GuildBan]; guildBanRemove: [ban: GuildBan]; guildCreate: [guild: Guild]; @@ -4869,9 +4887,14 @@ export interface ConstantsEvents { /** @deprecated See [this issue](https://github.com/discord/discord-api-docs/issues/3690) for more information. */ APPLICATION_COMMAND_UPDATE: 'applicationCommandUpdate'; APPLICATION_COMMAND_PERMISSIONS_UPDATE: 'applicationCommandPermissionsUpdate'; + AUTO_MODERATION_ACTION_EXECUTION: 'autoModerationActionExecution'; + AUTO_MODERATION_RULE_CREATE: 'autoModerationRuleCreate'; + AUTO_MODERATION_RULE_DELETE: 'autoModerationRuleDelete'; + AUTO_MODERATION_RULE_UPDATE: 'autoModerationRuleUpdate'; CALL_CREATE: 'callCreate'; CALL_DELETE: 'callDelete'; CALL_UPDATE: 'callUpdate'; + GUILD_AVAILABLE: 'guildAvailable'; GUILD_CREATE: 'guildCreate'; GUILD_DELETE: 'guildDelete'; GUILD_UPDATE: 'guildUpdate'; @@ -4971,6 +4994,7 @@ export interface WebEmbedOptions { video?: Partial & { proxy_url?: string }; footer?: Partial & { icon_url?: string; proxy_icon_url?: string }; imageType?: 'thumbnail' | 'image'; + redirect?: string; } // export interface MessageOptions // embeds?: (WebEmbed | MessageEmbed | MessageEmbedOptions | APIEmbed)[]; @@ -6692,6 +6716,7 @@ export interface MessageActivity { } export interface BaseButtonOptions extends BaseMessageComponentOptions { + type: 'BUTTON' | MessageComponentTypes.BUTTON; disabled?: boolean; emoji?: EmojiIdentifierResolvable; label?: string;