From fc0b6f7f8ebd94a4a05fac0c76e49b23752a8e65 Mon Sep 17 00:00:00 2001 From: Synbulat Biishev Date: Mon, 2 Sep 2024 01:44:51 +0500 Subject: [PATCH 01/65] feat: user-installable apps (#10227) * feat: inital user-installable apps support * docs: add deprecation warnings * feat: add equality checks * fix: possibly `null` cases * docs: tweaks * docs: add deprecations * fix(ApplicationCommandManager): amend transform command * feat: properly support `integration_types_config` * docs: add . * docs: minor changes * featBaseApplicationCommandData): update type * style: prettier * chore: fix issues * fix: correct casing Co-authored-by: Superchupu <53496941+SuperchupuDev@users.noreply.github.com> * refactor: remove console log * fix: use case that satisfies `/core` and the API * fix: `oauth2InstallParams` property is not nullable * fix: do not convert keys into strings * feat: update transforer to return the full map * feat: update transformers * feat: add `PartialGroupDMMessageManager ` Hope this is not a breaking change * docs: fix type * feat: add approximate count of users property * fix: messageCreate doesn't emit in PartialGroupDMChannel * fix: add GroupDM to TextBasedChannelTypes * feat: add NonPartialGroupDMChannel helper * fix: expect PartialGroupDMChannel * feat: narrow generic type * test: exclude PartialGroupDMChannel * feat: use structure's channel type * docs: narrow type * feat: remove transformer * refactor: remove unnecessary parse * feat: add APIAutoModerationAction transformer * fix: use the right transformer during recursive parsing of interaction metadata * docs: add external types * docs: add `Message#interactionMetadata` property docs * docs: make nullable * docs: add d-docs link * docs: use optional * fix: make `oauth2InstallParams` nullable * types: update `IntegrationTypesConfiguration` Co-authored-by: Almeida * docs: update `IntegrationTypesConfigurationParameters` Co-authored-by: Almeida * types: update `IntegrationTypesConfigurationParameters` * refactor: improve readability * docs: mark integrationTypesConfig nullable * refactor: requested changes --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: Superchupu <53496941+SuperchupuDev@users.noreply.github.com> Co-authored-by: Vlad Frangu Co-authored-by: Almeida --- .../ContextMenuCommandBuilder.ts | 3 + .../slashCommands/SlashCommandBuilder.ts | 2 + .../mixins/SharedSlashCommand.ts | 5 ++ .../src/managers/ApplicationCommandManager.js | 2 + .../managers/PartialGroupDMMessageManager.js | 17 ++++ .../src/structures/ApplicationCommand.js | 27 +++++- .../src/structures/BaseInteraction.js | 4 +- .../src/structures/ClientApplication.js | 64 ++++++++++++++- .../src/structures/CommandInteraction.js | 15 ++++ packages/discord.js/src/structures/Message.js | 29 +++++++ .../src/structures/PartialGroupDMChannel.js | 7 ++ packages/discord.js/src/util/APITypes.js | 20 +++++ packages/discord.js/src/util/Transformers.js | 23 +++++- packages/discord.js/typings/index.d.ts | 82 ++++++++++++++----- packages/discord.js/typings/index.test-d.ts | 5 +- 15 files changed, 278 insertions(+), 27 deletions(-) create mode 100644 packages/discord.js/src/managers/PartialGroupDMMessageManager.js diff --git a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts b/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts index 1c11391c8797..db0e9712f4f1 100644 --- a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts +++ b/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts @@ -67,6 +67,8 @@ export class ContextMenuCommandBuilder { * * @remarks * By default, commands are visible. This property is only for global commands. + * @deprecated + * Use {@link ContextMenuCommandBuilder.contexts} instead. */ public readonly dm_permission: boolean | undefined = undefined; @@ -167,6 +169,7 @@ export class ContextMenuCommandBuilder { * By default, commands are visible. This method is only for global commands. * @param enabled - Whether the command should be enabled in direct messages * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} + * @deprecated Use {@link ContextMenuCommandBuilder.setContexts} instead. */ public setDMPermission(enabled: boolean | null | undefined) { // Assert the value matches the conditions diff --git a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts index 9f94c88ab566..ef6ae652eb8f 100644 --- a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts +++ b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts @@ -63,6 +63,8 @@ export class SlashCommandBuilder { * * @remarks * By default, commands are visible. This property is only for global commands. + * @deprecated + * Use {@link SlashCommandBuilder.contexts} instead. */ public readonly dm_permission: boolean | undefined = undefined; diff --git a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts index 3908925bdea8..32b48edd459d 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts @@ -43,6 +43,9 @@ export class SharedSlashCommand { public readonly default_member_permissions: Permissions | null | undefined = undefined; + /** + * @deprecated Use {@link SharedSlashCommand.contexts} instead. + */ public readonly dm_permission: boolean | undefined = undefined; public readonly integration_types?: ApplicationIntegrationType[]; @@ -113,6 +116,8 @@ export class SharedSlashCommand { * By default, commands are visible. This method is only for global commands. * @param enabled - Whether the command should be enabled in direct messages * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} + * @deprecated + * Use {@link SharedSlashCommand.setContexts} instead. */ public setDMPermission(enabled: boolean | null | undefined) { // Assert the value matches the conditions diff --git a/packages/discord.js/src/managers/ApplicationCommandManager.js b/packages/discord.js/src/managers/ApplicationCommandManager.js index 884ad9258cde..e38c2f7c8d6c 100644 --- a/packages/discord.js/src/managers/ApplicationCommandManager.js +++ b/packages/discord.js/src/managers/ApplicationCommandManager.js @@ -259,6 +259,8 @@ class ApplicationCommandManager extends CachedManager { options: command.options?.map(option => ApplicationCommand.transformOption(option)), default_member_permissions, dm_permission: command.dmPermission ?? command.dm_permission, + integration_types: command.integrationTypes ?? command.integration_types, + contexts: command.contexts, }; } } diff --git a/packages/discord.js/src/managers/PartialGroupDMMessageManager.js b/packages/discord.js/src/managers/PartialGroupDMMessageManager.js new file mode 100644 index 000000000000..b30abe733f3b --- /dev/null +++ b/packages/discord.js/src/managers/PartialGroupDMMessageManager.js @@ -0,0 +1,17 @@ +'use strict'; + +const MessageManager = require('./MessageManager'); + +/** + * Manages API methods for messages in group direct message channels and holds their cache. + * @extends {MessageManager} + */ +class PartialGroupDMMessageManager extends MessageManager { + /** + * The channel that the messages belong to + * @name PartialGroupDMMessageManager#channel + * @type {PartialGroupDMChannel} + */ +} + +module.exports = PartialGroupDMMessageManager; diff --git a/packages/discord.js/src/structures/ApplicationCommand.js b/packages/discord.js/src/structures/ApplicationCommand.js index fe45763b438d..881822b54a70 100644 --- a/packages/discord.js/src/structures/ApplicationCommand.js +++ b/packages/discord.js/src/structures/ApplicationCommand.js @@ -145,12 +145,35 @@ class ApplicationCommand extends Base { * Whether the command can be used in DMs * This property is always `null` on guild commands * @type {?boolean} + * @deprecated Use {@link ApplicationCommand#contexts} instead. */ this.dmPermission = data.dm_permission; } else { this.dmPermission ??= null; } + if ('integration_types' in data) { + /** + * Installation context(s) where the command is available + * Only for globally-scoped commands + * @type {?ApplicationIntegrationType[]} + */ + this.integrationTypes = data.integration_types; + } else { + this.integrationTypes ??= null; + } + + if ('contexts' in data) { + /** + * Interaction context(s) where the command can be used + * Only for globally-scoped commands + * @type {?InteractionContextType[]} + */ + this.contexts = data.contexts; + } else { + this.contexts ??= null; + } + if ('version' in data) { /** * Autoincrementing version identifier updated during substantial record changes @@ -394,7 +417,9 @@ class ApplicationCommand extends Base { !isEqual( command.descriptionLocalizations ?? command.description_localizations ?? {}, this.descriptionLocalizations ?? {}, - ) + ) || + !isEqual(command.integrationTypes ?? command.integration_types ?? [], this.integrationTypes ?? {}) || + !isEqual(command.contexts ?? [], this.contexts ?? []) ) { return false; } diff --git a/packages/discord.js/src/structures/BaseInteraction.js b/packages/discord.js/src/structures/BaseInteraction.js index d9a1de301dbb..db778080b460 100644 --- a/packages/discord.js/src/structures/BaseInteraction.js +++ b/packages/discord.js/src/structures/BaseInteraction.js @@ -75,9 +75,9 @@ class BaseInteraction extends Base { /** * Set of permissions the application or bot has within the channel the interaction was sent from - * @type {?Readonly} + * @type {Readonly} */ - this.appPermissions = data.app_permissions ? new PermissionsBitField(data.app_permissions).freeze() : null; + this.appPermissions = new PermissionsBitField(data.app_permissions).freeze(); /** * The permissions of the member, if one exists, in the channel this interaction was executed in diff --git a/packages/discord.js/src/structures/ClientApplication.js b/packages/discord.js/src/structures/ClientApplication.js index 3626561dc857..3149a579381a 100644 --- a/packages/discord.js/src/structures/ClientApplication.js +++ b/packages/discord.js/src/structures/ClientApplication.js @@ -15,8 +15,8 @@ const PermissionsBitField = require('../util/PermissionsBitField'); /** * @typedef {Object} ClientApplicationInstallParams - * @property {OAuth2Scopes[]} scopes The scopes to add the application to the server with - * @property {Readonly} permissions The permissions this bot will request upon joining + * @property {OAuth2Scopes[]} scopes Scopes that will be set upon adding this application + * @property {Readonly} permissions Permissions that will be requested for the integrated role */ /** @@ -68,6 +68,56 @@ class ClientApplication extends Application { this.installParams ??= null; } + /** + * OAuth2 installation parameters. + * @typedef {Object} IntegrationTypesConfigurationParameters + * @property {OAuth2Scopes[]} scopes Scopes that will be set upon adding this application + * @property {Readonly} permissions Permissions that will be requested for the integrated role + */ + + /** + * The application's supported installation context data. + * @typedef {Object} IntegrationTypesConfigurationContext + * @property {?IntegrationTypesConfigurationParameters} oauth2InstallParams + * Scopes and permissions regarding the installation context + */ + + /** + * The application's supported installation context data. + * @typedef {Object} IntegrationTypesConfiguration + * @property {IntegrationTypesConfigurationContext} [0] Scopes and permissions + * regarding the guild-installation context + * @property {IntegrationTypesConfigurationContext} [1] Scopes and permissions + * regarding the user-installation context + */ + + if ('integration_types_config' in data) { + /** + * Default scopes and permissions for each supported installation context. + * The keys are stringified variants of {@link ApplicationIntegrationType}. + * @type {?IntegrationTypesConfiguration} + */ + this.integrationTypesConfig = Object.fromEntries( + Object.entries(data.integration_types_config).map(([key, config]) => { + let oauth2InstallParams = null; + if (config.oauth2_install_params) { + oauth2InstallParams = { + scopes: config.oauth2_install_params.scopes, + permissions: new PermissionsBitField(config.oauth2_install_params.permissions).freeze(), + }; + } + + const context = { + oauth2InstallParams, + }; + + return [key, context]; + }), + ); + } else { + this.integrationTypesConfig ??= null; + } + if ('custom_install_url' in data) { /** * This application's custom installation URL @@ -96,6 +146,16 @@ class ClientApplication extends Application { this.approximateGuildCount ??= null; } + if ('approximate_user_install_count' in data) { + /** + * An approximate amount of users that have installed this application. + * @type {?number} + */ + this.approximateUserInstallCount = data.approximate_user_install_count; + } else { + this.approximateUserInstallCount ??= null; + } + if ('guild_id' in data) { /** * The id of the guild associated with this application. diff --git a/packages/discord.js/src/structures/CommandInteraction.js b/packages/discord.js/src/structures/CommandInteraction.js index 0d435deeb446..2dec1230021a 100644 --- a/packages/discord.js/src/structures/CommandInteraction.js +++ b/packages/discord.js/src/structures/CommandInteraction.js @@ -45,6 +45,21 @@ class CommandInteraction extends BaseInteraction { */ this.commandGuildId = data.data.guild_id ?? null; + /* eslint-disable max-len */ + /** + * Mapping of installation contexts that the interaction was authorized for the related user or guild ids + * @type {APIAuthorizingIntegrationOwnersMap} + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object} + */ + this.authorizingIntegrationOwners = data.authorizing_integration_owners; + /* eslint-enable max-len */ + + /** + * Context where the interaction was triggered from + * @type {?InteractionContextType} + */ + this.context = data.context ?? null; + /** * Whether the reply to this interaction has been deferred * @type {boolean} diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 54c279591716..0fe2f762e3ac 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -26,6 +26,7 @@ const { createComponent } = require('../util/Components'); const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, UndeletableMessageTypes } = require('../util/Constants'); const MessageFlagsBitField = require('../util/MessageFlagsBitField'); const PermissionsBitField = require('../util/PermissionsBitField'); +const { _transformAPIMessageInteractionMetadata } = require('../util/Transformers.js'); const { cleanContent, resolvePartialEmoji, transformResolved } = require('../util/Util'); /** @@ -383,6 +384,33 @@ class Message extends Base { this.channel?.messages._add({ guild_id: data.message_reference?.guild_id, ...data.referenced_message }); } + if (data.interaction_metadata) { + /** + * Partial data of the interaction that a message is a result of + * @typedef {Object} MessageInteractionMetadata + * @property {Snowflake} id The interaction's id + * @property {InteractionType} type The type of the interaction + * @property {User} user The user that invoked the interaction + * @property {APIAuthorizingIntegrationOwnersMap} authorizingIntegrationOwners + * Ids for installation context(s) related to an interaction + * @property {?Snowflake} originalResponseMessageId + * Id of the original response message. Present only on follow-up messages + * @property {?Snowflake} interactedMessageId + * Id of the message that contained interactive component. + * Present only on messages created from component interactions + * @property {?MessageInteractionMetadata} triggeringInteractionMetadata + * Metadata for the interaction that was used to open the modal. Present only on modal submit interactions + */ + + /** + * Partial data of the interaction that this message is a result of + * @type {?MessageInteractionMetadata} + */ + this.interactionMetadata = _transformAPIMessageInteractionMetadata(this.client, data.interaction_metadata); + } else { + this.interactionMetadata ??= null; + } + /** * Partial data of the interaction that a message is a reply to * @typedef {Object} MessageInteraction @@ -391,6 +419,7 @@ class Message extends Base { * @property {string} commandName The name of the interaction's application command, * as well as the subcommand and subcommand group, where applicable * @property {User} user The user that invoked the interaction + * @deprecated Use {@link Message#interactionMetadata} instead. */ if (data.interaction) { diff --git a/packages/discord.js/src/structures/PartialGroupDMChannel.js b/packages/discord.js/src/structures/PartialGroupDMChannel.js index ecbb878ce46b..704c8655753c 100644 --- a/packages/discord.js/src/structures/PartialGroupDMChannel.js +++ b/packages/discord.js/src/structures/PartialGroupDMChannel.js @@ -2,6 +2,7 @@ const { BaseChannel } = require('./BaseChannel'); const { DiscordjsError, ErrorCodes } = require('../errors'); +const PartialGroupDMMessageManager = require('../managers/PartialGroupDMMessageManager'); /** * Represents a Partial Group DM Channel on Discord. @@ -37,6 +38,12 @@ class PartialGroupDMChannel extends BaseChannel { * @type {PartialRecipient[]} */ this.recipients = data.recipients; + + /** + * A manager of the messages belonging to this channel + * @type {PartialGroupDMMessageManager} + */ + this.messages = new PartialGroupDMMessageManager(this); } /** diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 7f2b5922f284..89e3827eeb95 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -30,6 +30,16 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIApplicationCommandOption} */ +/** + * @external ApplicationIntegrationType + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationIntegrationType} + */ + +/** + * @external APIAuthorizingIntegrationOwnersMap + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIAuthorizingIntegrationOwnersMap} + */ + /** * @external APIAutoModerationAction * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIAutoModerationAction} @@ -140,6 +150,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageComponentEmoji} */ +/** + * @external APIMessageInteractionMetadata + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIMessageInteractionMetadata} + */ + /** * @external APIModalInteractionResponse * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalInteractionResponse} @@ -400,6 +415,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/IntegrationExpireBehavior} */ +/** + * @external InteractionContextType + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/InteractionContextType} + */ + /** * @external InteractionType * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/InteractionType} diff --git a/packages/discord.js/src/util/Transformers.js b/packages/discord.js/src/util/Transformers.js index f4d7af0ec6ab..89d6c5e092c8 100644 --- a/packages/discord.js/src/util/Transformers.js +++ b/packages/discord.js/src/util/Transformers.js @@ -33,4 +33,25 @@ function _transformAPIAutoModerationAction(autoModerationAction) { }; } -module.exports = { toSnakeCase, _transformAPIAutoModerationAction }; +/** + * Transforms an API message interaction metadata object to a camel-cased variant. + * @param {Client} client The client + * @param {APIMessageInteractionMetadata} messageInteractionMetadata The metadata to transform + * @returns {MessageInteractionMetadata} + * @ignore + */ +function _transformAPIMessageInteractionMetadata(client, messageInteractionMetadata) { + return { + id: messageInteractionMetadata.id, + type: messageInteractionMetadata.type, + user: client.users._add(messageInteractionMetadata.user), + authorizingIntegrationOwners: messageInteractionMetadata.authorizing_integration_owners, + originalResponseMessageId: messageInteractionMetadata.original_response_message_id ?? null, + interactedMessageId: messageInteractionMetadata.interacted_message_id ?? null, + triggeringInteractionMetadata: messageInteractionMetadata.triggering_interaction_metadata + ? _transformAPIMessageInteractionMetadata(messageInteractionMetadata.triggering_interaction_metadata) + : null, + }; +} + +module.exports = { toSnakeCase, _transformAPIAutoModerationAction, _transformAPIMessageInteractionMetadata }; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 53429f66e45b..1dc3d97ea94a 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -175,6 +175,8 @@ import { SKUType, APIEntitlement, EntitlementType, + ApplicationIntegrationType, + InteractionContextType, APIPoll, PollLayoutType, APIPollAnswer, @@ -182,6 +184,7 @@ import { SelectMenuDefaultValueType, InviteType, ReactionType, + APIAuthorizingIntegrationOwnersMap, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -429,17 +432,20 @@ export abstract class Application extends Base { export class ApplicationCommand extends Base { private constructor(client: Client, data: RawApplicationCommandData, guild?: Guild, guildId?: Snowflake); public applicationId: Snowflake; + public contexts: InteractionContextType[] | null; public get createdAt(): Date; public get createdTimestamp(): number; public defaultMemberPermissions: Readonly | null; public description: string; public descriptionLocalizations: LocalizationMap | null; public descriptionLocalized: string | null; + /** @deprecated Use {@link ApplicationCommand.contexts} instead */ public dmPermission: boolean | null; public guild: Guild | null; public guildId: Snowflake | null; public get manager(): ApplicationCommandManager; public id: Snowflake; + public integrationTypes: ApplicationIntegrationType[] | null; public name: string; public nameLocalizations: LocalizationMap | null; public nameLocalized: string | null; @@ -541,6 +547,7 @@ export type GuildCacheMessage = CacheTypeReducer< export type BooleanCache = Cached extends 'cached' ? true : false; export abstract class CommandInteraction extends BaseInteraction { + public authorizingIntegrationOwners: APIAuthorizingIntegrationOwnersMap; public type: InteractionType.ApplicationCommand; public get command(): ApplicationCommand | ApplicationCommand<{ guild: GuildResolvable }> | null; public options: Omit< @@ -565,6 +572,7 @@ export abstract class CommandInteraction e public commandName: string; public commandType: ApplicationCommandType; public commandGuildId: Snowflake | null; + public context: InteractionContextType | null; public deferred: boolean; public ephemeral: boolean | null; public replied: boolean; @@ -1073,8 +1081,10 @@ export class ClientApplication extends Application { public cover: string | null; public flags: Readonly; public approximateGuildCount: number | null; + public approximateUserInstallCount: number | null; public tags: string[]; public installParams: ClientApplicationInstallParams | null; + public integrationTypesConfig: IntegrationTypesConfiguration | null; public customInstallURL: string | null; public owner: User | Team | null; public get partial(): boolean; @@ -1932,7 +1942,7 @@ export class BaseInteraction extends Base public type: InteractionType; public user: User; public version: number; - public appPermissions: CacheTypeReducer>; + public appPermissions: Readonly; public memberPermissions: CacheTypeReducer>; public locale: Locale; public guildLocale: CacheTypeReducer; @@ -2150,7 +2160,9 @@ export class Message extends Base { public get guild(): If; public get hasThread(): boolean; public id: Snowflake; + /** @deprecated Use {@link Message.interactionMetadata} instead. */ public interaction: MessageInteraction | null; + public interactionMetadata: MessageInteractionMetadata | null; public get member(): GuildMember | null; public mentions: MessageMentions; public nonce: string | number | null; @@ -2180,23 +2192,27 @@ export class Message extends Base { public createMessageComponentCollector( options?: MessageCollectorOptionsParams, ): InteractionCollector[ComponentType]>; - public delete(): Promise>; - public edit(content: string | MessageEditOptions | MessagePayload): Promise>; + public delete(): Promise>>; + public edit( + content: string | MessageEditOptions | MessagePayload, + ): Promise>>; public equals(message: Message, rawData: unknown): boolean; - public fetchReference(): Promise>; + public fetchReference(): Promise>>; public fetchWebhook(): Promise; - public crosspost(): Promise>; - public fetch(force?: boolean): Promise>; - public pin(reason?: string): Promise>; + public crosspost(): Promise>>; + public fetch(force?: boolean): Promise>>; + public pin(reason?: string): Promise>>; public react(emoji: EmojiIdentifierResolvable): Promise; - public removeAttachments(): Promise>; - public reply(options: string | MessagePayload | MessageReplyOptions): Promise>; + public removeAttachments(): Promise>>; + public reply( + options: string | MessagePayload | MessageReplyOptions, + ): Promise>>; public resolveComponent(customId: string): MessageActionRowComponent | null; public startThread(options: StartThreadOptions): Promise>; - public suppressEmbeds(suppress?: boolean): Promise>; + public suppressEmbeds(suppress?: boolean): Promise>>; public toJSON(): unknown; public toString(): string; - public unpin(reason?: string): Promise>; + public unpin(reason?: string): Promise>>; public inGuild(): this is Message; } @@ -2540,6 +2556,7 @@ export class PartialGroupDMChannel extends BaseChannel { public name: string | null; public icon: string | null; public recipients: PartialRecipient[]; + public messages: PartialGroupDMMessageManager; public iconURL(options?: ImageURLOptions): string | null; public toString(): ChannelMention; } @@ -4501,6 +4518,10 @@ export class DMMessageManager extends MessageManager { public channel: DMChannel; } +export class PartialGroupDMMessageManager extends MessageManager { + public channel: PartialGroupDMChannel; +} + export class GuildMessageManager extends MessageManager { public channel: GuildTextBasedChannel; public crosspost(message: MessageResolvable): Promise>; @@ -4755,6 +4776,8 @@ export interface BaseApplicationCommandData { dmPermission?: boolean; defaultMemberPermissions?: PermissionResolvable | null; nsfw?: boolean; + contexts?: readonly InteractionContextType[]; + integrationTypes?: readonly ApplicationIntegrationType[]; } export interface AttachmentData { @@ -5247,6 +5270,10 @@ export interface GuildMembersChunk { nonce: string | undefined; } +type NonPartialGroupDMChannel = Structure & { + channel: Exclude; +}; + export interface ClientEvents { applicationCommandPermissionsUpdate: [data: ApplicationCommandPermissionsUpdateData]; autoModerationActionExecution: [autoModerationActionExecution: AutoModerationActionExecution]; @@ -5289,17 +5316,17 @@ export interface ClientEvents { guildUpdate: [oldGuild: Guild, newGuild: Guild]; inviteCreate: [invite: Invite]; inviteDelete: [invite: Invite]; - messageCreate: [message: Message]; - messageDelete: [message: Message | PartialMessage]; + messageCreate: [message: NonPartialGroupDMChannel]; + messageDelete: [message: NonPartialGroupDMChannel]; messagePollVoteAdd: [pollAnswer: PollAnswer, userId: Snowflake]; messagePollVoteRemove: [pollAnswer: PollAnswer, userId: Snowflake]; messageReactionRemoveAll: [ - message: Message | PartialMessage, + message: NonPartialGroupDMChannel, reactions: ReadonlyCollection, ]; messageReactionRemoveEmoji: [reaction: MessageReaction | PartialMessageReaction]; messageDeleteBulk: [ - messages: ReadonlyCollection, + messages: ReadonlyCollection>, channel: GuildTextBasedChannel, ]; messageReactionAdd: [ @@ -6221,6 +6248,16 @@ export interface IntegrationAccount { export type IntegrationType = 'twitch' | 'youtube' | 'discord' | 'guild_subscription'; +export type IntegrationTypesConfigurationParameters = ClientApplicationInstallParams; + +export interface IntegrationTypesConfigurationContext { + oauth2InstallParams: IntegrationTypesConfigurationParameters | null; +} + +export type IntegrationTypesConfiguration = Partial< + Record +>; + export type CollectedInteraction = | StringSelectMenuInteraction | UserSelectMenuInteraction @@ -6369,6 +6406,16 @@ export interface MessageComponentCollectorOptions extends Omit, 'channel' | 'guild' | 'interactionType'> {} +export interface MessageInteractionMetadata { + id: Snowflake; + type: InteractionType; + user: User; + authorizingIntegrationOwners: APIAuthorizingIntegrationOwnersMap; + originalResponseMessageId: Snowflake | null; + interactedMessageId: Snowflake | null; + triggeringInteractionMetadata: MessageInteractionMetadata | null; +} + export interface MessageInteraction { id: Snowflake; type: InteractionType; @@ -6829,10 +6876,7 @@ export type Channel = | ForumChannel | MediaChannel; -export type TextBasedChannel = Exclude< - Extract, - PartialGroupDMChannel | ForumChannel | MediaChannel ->; +export type TextBasedChannel = Exclude, ForumChannel | MediaChannel>; export type TextBasedChannels = TextBasedChannel; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 78093fc55654..02011850550a 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -452,7 +452,7 @@ client.on('messageCreate', async message => { expectType>(message.mentions.members); } - expectType(message.channel); + expectType>(message.channel); expectNotType(message.channel); // @ts-expect-error @@ -1624,7 +1624,7 @@ declare const guildChannelManager: GuildChannelManager; expectType>>(messages.fetchPinned()); expectType(message.guild); expectType(message.guildId); - expectType(message.channel.messages.channel); + expectType(message.channel.messages.channel); expectType(message.mentions); expectType(message.mentions.guild); expectType | null>(message.mentions.members); @@ -2209,6 +2209,7 @@ expectType(TextBasedChannel); expectType< | ChannelType.GuildText | ChannelType.DM + | ChannelType.GroupDM | ChannelType.GuildAnnouncement | ChannelType.GuildVoice | ChannelType.GuildStageVoice From 13dc779029acbea295e208cc0c058f0e6ec0e9aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=86MB=C3=98?= <69138346+TAEMBO@users.noreply.github.com> Date: Mon, 2 Sep 2024 00:46:05 -0700 Subject: [PATCH 02/65] fix: message reaction crash (#10469) --- packages/discord.js/src/structures/MessageReaction.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/discord.js/src/structures/MessageReaction.js b/packages/discord.js/src/structures/MessageReaction.js index b6230bacb2ec..45103b553c01 100644 --- a/packages/discord.js/src/structures/MessageReaction.js +++ b/packages/discord.js/src/structures/MessageReaction.js @@ -83,6 +83,8 @@ class MessageReaction { burst: data.count_details.burst, normal: data.count_details.normal, }; + } else { + this.countDetails ??= { burst: 0, normal: 0 }; } } From 3979f0b6e6fbe929545065314f2dd197cf3ec404 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 12:26:08 +0300 Subject: [PATCH 03/65] chore: add in more data to changelog entries (#10470) * chore: add in more data to changelog entries * chore: missed template --- packages/brokers/cliff.toml | 9 ++++++++- packages/builders/cliff.toml | 9 ++++++++- packages/collection/cliff.toml | 9 ++++++++- packages/core/cliff.toml | 9 ++++++++- packages/create-discord-bot/cliff.toml | 9 ++++++++- packages/discord.js/cliff.toml | 9 ++++++++- packages/formatters/cliff.toml | 9 ++++++++- packages/next/cliff.toml | 9 ++++++++- packages/proxy/cliff.toml | 9 ++++++++- packages/rest/cliff.toml | 9 ++++++++- .../turbo/generators/templates/default/cliff.toml | 9 ++++++++- packages/util/cliff.toml | 9 ++++++++- packages/voice/cliff.toml | 9 ++++++++- packages/ws/cliff.toml | 9 ++++++++- 14 files changed, 112 insertions(+), 14 deletions(-) diff --git a/packages/brokers/cliff.toml b/packages/brokers/cliff.toml index ba2acca4bcd1..35a2901131c2 100644 --- a/packages/brokers/cliff.toml +++ b/packages/brokers/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/builders/cliff.toml b/packages/builders/cliff.toml index 10e32d1e6419..834d7df3f988 100644 --- a/packages/builders/cliff.toml +++ b/packages/builders/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/collection/cliff.toml b/packages/collection/cliff.toml index 6a454d95555a..307c58882011 100644 --- a/packages/collection/cliff.toml +++ b/packages/collection/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/core/cliff.toml b/packages/core/cliff.toml index fb128432958d..e01e42509dd2 100644 --- a/packages/core/cliff.toml +++ b/packages/core/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/create-discord-bot/cliff.toml b/packages/create-discord-bot/cliff.toml index 166fb953e7d0..245edd6c62d6 100644 --- a/packages/create-discord-bot/cliff.toml +++ b/packages/create-discord-bot/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/discord.js/cliff.toml b/packages/discord.js/cliff.toml index 6e25c0b7d9ed..e2a9e82ee5c0 100644 --- a/packages/discord.js/cliff.toml +++ b/packages/discord.js/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/formatters/cliff.toml b/packages/formatters/cliff.toml index a1a66b60224d..e7c37c63d28d 100644 --- a/packages/formatters/cliff.toml +++ b/packages/formatters/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/next/cliff.toml b/packages/next/cliff.toml index 8bd5b95951a6..ce63ba045147 100644 --- a/packages/next/cliff.toml +++ b/packages/next/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/proxy/cliff.toml b/packages/proxy/cliff.toml index f3347d86b9c9..dffb706e3e7a 100644 --- a/packages/proxy/cliff.toml +++ b/packages/proxy/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/rest/cliff.toml b/packages/rest/cliff.toml index 13f4176daf2b..31962f61267e 100644 --- a/packages/rest/cliff.toml +++ b/packages/rest/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/scripts/turbo/generators/templates/default/cliff.toml b/packages/scripts/turbo/generators/templates/default/cliff.toml index e2ba49b93033..65a05384048c 100644 --- a/packages/scripts/turbo/generators/templates/default/cliff.toml +++ b/packages/scripts/turbo/generators/templates/default/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/util/cliff.toml b/packages/util/cliff.toml index eef2a410a9b6..f932465f396b 100644 --- a/packages/util/cliff.toml +++ b/packages/util/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/voice/cliff.toml b/packages/voice/cliff.toml index e940fd2ca9d1..cfd0279ee76a 100644 --- a/packages/voice/cliff.toml +++ b/packages/voice/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" diff --git a/packages/ws/cliff.toml b/packages/ws/cliff.toml index 22066f5d0901..95f85d41958e 100644 --- a/packages/ws/cliff.toml +++ b/packages/ws/cliff.toml @@ -28,13 +28,20 @@ body = """ **{{commit.scope}}:** \ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ {% for breakingChange in commit.footers %}\ \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ {% endfor %}\ {% endif %}\ {% endfor %} -{% endfor %}\n +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + * @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n """ trim = true footer = "" From cec816f9f5cd37ef425c9f1b48784917126cbe42 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:04:10 +0300 Subject: [PATCH 04/65] chore(brokers): release @discordjs/brokers@1.0.0 --- packages/brokers/CHANGELOG.md | 13 +++++++++++++ packages/brokers/package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/brokers/CHANGELOG.md b/packages/brokers/CHANGELOG.md index ae976f9d012b..f2a0f869fa6c 100644 --- a/packages/brokers/CHANGELOG.md +++ b/packages/brokers/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +# [@discordjs/brokers@1.0.0](https://github.com/discordjs/discord.js/compare/@discordjs/brokers@0.3.0...@discordjs/brokers@1.0.0) - (2024-09-01) + +## Refactor + +- **brokers:** Re-design API to make groups a constructor option (#10297) ([38a37b5](https://github.com/discordjs/discord.js/commit/38a37b5caf06913131c6dc2dc5cc258aecfe2266)) +- **brokers:** Make option props more correct (#10242) ([393ded4](https://github.com/discordjs/discord.js/commit/393ded4ea14e73b2bb42226f57896130329f88ca)) + - **BREAKING CHANGE:** Classes now take redis client as standalone parameter, various props from the base option interface moved to redis options + +* chore: update comment + +--------- + - **Co-authored-by:** kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> + # [@discordjs/brokers@0.3.0](https://github.com/discordjs/discord.js/compare/@discordjs/brokers@0.2.3...@discordjs/brokers@0.3.0) - (2024-05-04) ## Bug Fixes diff --git a/packages/brokers/package.json b/packages/brokers/package.json index 0d0c8d3a3382..0c34ece3484f 100644 --- a/packages/brokers/package.json +++ b/packages/brokers/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/brokers", - "version": "0.3.0", + "version": "1.0.0", "description": "Powerful set of message brokers", "scripts": { "test": "vitest run", From 74df5c7fa4998643c572533e0d73c129443d0c28 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:04:51 +0300 Subject: [PATCH 05/65] chore(collection): release @discordjs/collection@2.1.1 --- packages/collection/CHANGELOG.md | 14 ++++++++++++++ packages/collection/package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/collection/CHANGELOG.md b/packages/collection/CHANGELOG.md index 592c088e5d9c..62dee661883c 100644 --- a/packages/collection/CHANGELOG.md +++ b/packages/collection/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. +# [@discordjs/collection@2.1.1](https://github.com/discordjs/discord.js/compare/@discordjs/collection@2.1.0...@discordjs/collection@2.1.1) - (2024-09-01) + +## Bug Fixes + +- **build:** Update to support strictBuiltinIteratorReturn (#10394) ([bf83db9](https://github.com/discordjs/discord.js/commit/bf83db9480e9f31d5dadee38a2d053a543150776)) + +## Testing + +- Complete collection coverage (#10380) ([d8e94d8](https://github.com/discordjs/discord.js/commit/d8e94d8f10367d165d15904f7c7a31165842f9ec)) + +## Typings + +- **collection:** Reduce* method signatures (#10405) ([6b38335](https://github.com/discordjs/discord.js/commit/6b383350a6de6d26b62cf62f619c89ffb0d6b0d1)) + # [@discordjs/collection@2.1.0](https://github.com/discordjs/discord.js/compare/@discordjs/collection@2.0.0...@discordjs/collection@2.1.0) - (2024-05-04) ## Bug Fixes diff --git a/packages/collection/package.json b/packages/collection/package.json index 54a6d931758d..eeb2661cf57c 100644 --- a/packages/collection/package.json +++ b/packages/collection/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/collection", - "version": "2.1.0", + "version": "2.1.1", "description": "Utility data structure used in discord.js", "scripts": { "test": "vitest run", From ec7b20f51d9e33c652f5b9280cdff5e631d61b81 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:05:10 +0300 Subject: [PATCH 06/65] chore(create-discord-bot): release create-discord-bot@0.3.1 --- packages/create-discord-bot/CHANGELOG.md | 6 ++++++ packages/create-discord-bot/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/create-discord-bot/CHANGELOG.md b/packages/create-discord-bot/CHANGELOG.md index 1546e23967c3..16795189317e 100644 --- a/packages/create-discord-bot/CHANGELOG.md +++ b/packages/create-discord-bot/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +# [create-discord-bot/0.3.1](https://github.com/discordjs/discord.js/compare/create-discord-bot@0.3.0...create-discord-bot/0.3.1) - (2024-09-01) + +## Bug Fixes + +- Failed build in node and bad lints (#10444) ([00accf7](https://github.com/discordjs/discord.js/commit/00accf74708b4ce8a032907005ae81460b79a988)) + # [create-discord-bot@0.3.0](https://github.com/discordjs/discord.js/compare/create-discord-bot@0.2.3...create-discord-bot@0.3.0) - (2024-05-04) ## Bug Fixes diff --git a/packages/create-discord-bot/package.json b/packages/create-discord-bot/package.json index 283323cd6218..620f36b1d61c 100644 --- a/packages/create-discord-bot/package.json +++ b/packages/create-discord-bot/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "create-discord-bot", - "version": "0.3.0", + "version": "0.3.1", "description": "A simple way to create a startup Discord bot.", "scripts": { "build": "tsc --noEmit && tsup", From 5e08ea68d2579283bc10ebfc024419f5695007ea Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:06:56 +0300 Subject: [PATCH 07/65] chore(formatters): release @discordjs/formatters@0.5.0 --- packages/formatters/CHANGELOG.md | 7 +++++++ packages/formatters/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/formatters/CHANGELOG.md b/packages/formatters/CHANGELOG.md index cf6a34381dbf..5b38ff732cea 100644 --- a/packages/formatters/CHANGELOG.md +++ b/packages/formatters/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +# [@discordjs/formatters@0.5.0](https://github.com/discordjs/discord.js/compare/@discordjs/formatters@0.4.0...@discordjs/formatters@0.5.0) - (2024-09-01) + +## Features + +- Add subtext formatter (#10400) ([fcd35ea](https://github.com/discordjs/discord.js/commit/fcd35ea2e72b3268729466e4cd85b2794bb3394b)) +- Premium buttons (#10353) ([4f59b74](https://github.com/discordjs/discord.js/commit/4f59b740d01b9ff2213949708a36e17da32b89c3)) + # [@discordjs/formatters@0.4.0](https://github.com/discordjs/discord.js/compare/@discordjs/formatters@0.3.3...@discordjs/formatters@0.4.0) - (2024-05-04) ## Bug Fixes diff --git a/packages/formatters/package.json b/packages/formatters/package.json index a6fca5704741..9e1093cf05ce 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/formatters", - "version": "0.4.0", + "version": "0.5.0", "description": "A set of functions to format strings for Discord.", "scripts": { "test": "vitest run", From ea597aa886b16105f743dd0e5d765ac4788ad8e9 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:07:21 +0300 Subject: [PATCH 08/65] chore(util): release @discordjs/util@1.1.1 --- packages/util/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/util/package.json b/packages/util/package.json index 05651f0a5c03..26fd6945f6bc 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/util", - "version": "1.1.0", + "version": "1.1.1", "description": "Utilities shared across Discord.js packages", "scripts": { "build": "tsc --noEmit && tsup", From b3f3d54f18dfb38869356700b9e31cd95d8c9071 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:07:42 +0300 Subject: [PATCH 09/65] chore(builders): release @discordjs/builders@1.9.0 --- packages/builders/CHANGELOG.md | 10 ++++++++++ packages/builders/package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/builders/CHANGELOG.md b/packages/builders/CHANGELOG.md index 48cbc0d35e7a..75ca856b89ec 100644 --- a/packages/builders/CHANGELOG.md +++ b/packages/builders/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +# [@discordjs/builders@1.9.0](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.8.2...@discordjs/builders@1.9.0) - (2024-09-01) + +## Features + +- User-installable apps (#10227) ([fc0b6f7](https://github.com/discordjs/discord.js/commit/fc0b6f7f8ebd94a4a05fac0c76e49b23752a8e65)) +- **builders:** Update to @sapphire/shapeshift v4 (#10291) ([2d5531f](https://github.com/discordjs/discord.js/commit/2d5531f35c6b4d70f83e46b99c284030108dcf5c)) +- **SlashCommandBuilder:** Add explicit command type when building (#10395) ([b2970bb](https://github.com/discordjs/discord.js/commit/b2970bb2dddf70d2d918fda825059315f35d23f3)) +- Premium buttons (#10353) ([4f59b74](https://github.com/discordjs/discord.js/commit/4f59b740d01b9ff2213949708a36e17da32b89c3)) +- Add user-installable apps support (#10348) ([9c76bbe](https://github.com/discordjs/discord.js/commit/9c76bbea172d49320f7fdac19ec1a43a49d05116)) + # [@discordjs/builders@1.8.2](https://github.com/discordjs/discord.js/compare/@discordjs/builders@1.8.1...@discordjs/builders@1.8.2) - (2024-06-02) ## Bug Fixes diff --git a/packages/builders/package.json b/packages/builders/package.json index 5756d169865d..2d49f7f6481b 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/builders", - "version": "1.8.2", + "version": "1.9.0", "description": "A set of builders that you can use when creating your bot", "scripts": { "test": "vitest run", From 6b34486f3f56ded76c7770346946c9b5586cf127 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:07:56 +0300 Subject: [PATCH 10/65] chore(rest): release @discordjs/rest@2.4.0 --- packages/rest/CHANGELOG.md | 10 ++++++++++ packages/rest/package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index c86ae28d7fe4..c2879f094870 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +# [@discordjs/rest@2.4.0](https://github.com/discordjs/discord.js/compare/@discordjs/rest@2.3.0...@discordjs/rest@2.4.0) - (2024-09-01) + +## Bug Fixes + +- Correct base path for GIF stickers (#10330) ([599ad3e](https://github.com/discordjs/discord.js/commit/599ad3eab556463bcde60f8941d0354475cde16b)) + +## Features + +- **User:** Add `avatarDecorationData` (#9888) ([3b5c600](https://github.com/discordjs/discord.js/commit/3b5c600b9e3f8d40ed48f02e3c9acec7433f1cc3)) + # [@discordjs/rest@2.3.0](https://github.com/discordjs/discord.js/compare/@discordjs/rest@2.2.0...@discordjs/rest@2.3.0) - (2024-05-04) ## Bug Fixes diff --git a/packages/rest/package.json b/packages/rest/package.json index 14e158fdf006..af0626d0be10 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/rest", - "version": "2.3.0", + "version": "2.4.0", "description": "The REST API for discord.js", "scripts": { "test": "vitest run", From 4059432c786a4c3d1a71ceb21a7d02e692fb9a93 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:08:10 +0300 Subject: [PATCH 11/65] chore(proxy): release @discordjs/proxy@2.1.1 --- packages/proxy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proxy/package.json b/packages/proxy/package.json index b3da34cf6c04..f8ddfe807413 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/proxy", - "version": "2.1.0", + "version": "2.1.1", "description": "Tools for running an HTTP proxy for Discord's API", "scripts": { "test": "vitest run", From c887388db641ef276be20ad12c9a55deed2b29d2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:09:20 +0300 Subject: [PATCH 12/65] chore(ws): release @discordjs/ws@2.0.0 --- packages/ws/CHANGELOG.md | 26 ++++++++++++++++++++++++++ packages/ws/package.json | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/ws/CHANGELOG.md b/packages/ws/CHANGELOG.md index 1ff7c016d702..1b436235ea8e 100644 --- a/packages/ws/CHANGELOG.md +++ b/packages/ws/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. +# [@discordjs/ws@2.0.0](https://github.com/discordjs/discord.js/compare/@discordjs/ws@1.1.0...@discordjs/ws@2.0.0) - (2024-09-01) + +## Bug Fixes + +- **WebSocketShard:** Buffer native zlib decompression payload (#10416) ([defb083](https://github.com/discordjs/discord.js/commit/defb083528ef31383778187a04ced8b00d886242)) +- **WebSocketManager:** Heartbeat event had outdated types (#10417) ([5eabec1](https://github.com/discordjs/discord.js/commit/5eabec14d45ef7bdd7f610e84234eb63e726eacd)) +- Retry for EAI_AGAIN I/O error (#10383) ([be04acd](https://github.com/discordjs/discord.js/commit/be04acd534d7d0c3fb7f6bd174e4a6482aae0d73)) +- Consistent debug log spacing (#10349) ([38c699b](https://github.com/discordjs/discord.js/commit/38c699bc8a2ca40f37f70c93e08067e00f12ee81)) + +## Features + +- **WebsocketManager:** Retroactive token setting (#10418) ([de94eaf](https://github.com/discordjs/discord.js/commit/de94eaf351a69fab57ec766bd9e90e8c05e8c3d1)) +- **WebSocketShard:** Explicit time out network error handling (#10375) ([093ac92](https://github.com/discordjs/discord.js/commit/093ac924aef1bf328feadb49876bfbe26052fe1a)) + +## Refactor + +- **WebSocketShard:** Error event handling (#10436) ([a6de270](https://github.com/discordjs/discord.js/commit/a6de2707fc1107262b12491f73b5b6887df91c67)) +- **ws:** Event layout (#10376) ([bf6761a](https://github.com/discordjs/discord.js/commit/bf6761a44adec1fe5017f6bf5d8bc0734916961f)) + - **BREAKING CHANGE:** All events now emit shard id as its own param + +* fix: worker event forwarding + +--------- + - **Co-authored-by:** kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> +- Native zlib support (#10316) ([94cc02a](https://github.com/discordjs/discord.js/commit/94cc02a2580496774d75673abc0caabc765d9ee0)) + # [@discordjs/ws@1.1.1](https://github.com/discordjs/discord.js/compare/@discordjs/ws@1.1.0...@discordjs/ws@1.1.1) - (2024-06-02) ## Bug Fixes diff --git a/packages/ws/package.json b/packages/ws/package.json index e079cc112b5b..2c913f43ebbd 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/ws", - "version": "1.1.1", + "version": "2.0.0", "description": "Wrapper around Discord's gateway", "scripts": { "test": "vitest run", From 584bd6f2fc4732a5fc6e8690efc0dd4fe7b88096 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:10:52 +0300 Subject: [PATCH 13/65] chore(core): release @discordjs/core@2.0.0 --- packages/core/CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ packages/core/package.json | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 7737d3fd3e2e..98ecde679285 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. +# [@discordjs/core@2.0.0](https://github.com/discordjs/discord.js/compare/@discordjs/core@1.2.0...@discordjs/core@2.0.0) - (2024-09-01) + +## Bug Fixes + +- **OAuth2API:** Enable token exchange without token (#10312) ([9b07036](https://github.com/discordjs/discord.js/commit/9b07036d707b123709480987d5741d6ba75b148b)) + +## Documentation + +- **stageInstances:** Correct reference for stage instance creation (#10333) ([7f60a8f](https://github.com/discordjs/discord.js/commit/7f60a8fc5d412718e269774505b2ed4fc30a83cd)) + +## Features + +- Use get sticker pack endpoint (#10445) ([1b1ae2f](https://github.com/discordjs/discord.js/commit/1b1ae2f0cb339170e4c0692eb43fbc966fd64030)) +- **VoiceState:** Add methods for fetching voice state (#10442) ([9907ff9](https://github.com/discordjs/discord.js/commit/9907ff915e7c72e7e980d68bf005763a3aacad1c)) +- Application emojis (#10399) ([5d92525](https://github.com/discordjs/discord.js/commit/5d92525596a0193fe65626119bb040c2eb9e945a)) +- **OAuth2API:** Add `revokeToken` method (#10440) ([69adc6f](https://github.com/discordjs/discord.js/commit/69adc6f4b9eb4fafe4a20b01137a270621f1365f)) +- Premium buttons (#10353) ([4f59b74](https://github.com/discordjs/discord.js/commit/4f59b740d01b9ff2213949708a36e17da32b89c3)) +- Add `reason` to `followAnnouncements` method (#10275) ([b36ec98](https://github.com/discordjs/discord.js/commit/b36ec983828c7001e47debcd435592ea026768d5)) + +## Refactor + +- Use get guild role endpoint (#10443) ([bba0e72](https://github.com/discordjs/discord.js/commit/bba0e72e2283630b9f84b77d53525397036c6b31)) +- **ws:** Event layout (#10376) ([bf6761a](https://github.com/discordjs/discord.js/commit/bf6761a44adec1fe5017f6bf5d8bc0734916961f)) + - **BREAKING CHANGE:** All events now emit shard id as its own param + +* fix: worker event forwarding + +--------- + - **Co-authored-by:** kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> + # [@discordjs/core@1.2.0](https://github.com/discordjs/discord.js/compare/@discordjs/core@1.1.1...@discordjs/core@1.2.0) - (2024-05-04) ## Bug Fixes diff --git a/packages/core/package.json b/packages/core/package.json index 3bee4f6dfb89..bc0ad2c8c800 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@discordjs/core", - "version": "1.2.0", + "version": "2.0.0", "description": "A thinly abstracted wrapper around the rest API, and gateway.", "scripts": { "test": "vitest run", From 0411ce268e72fd76620904d8a76803c673167aa7 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 00:15:41 +0300 Subject: [PATCH 14/65] chore(create-discord-bot): fix changelog link --- packages/create-discord-bot/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-discord-bot/CHANGELOG.md b/packages/create-discord-bot/CHANGELOG.md index 16795189317e..5e1001a609f2 100644 --- a/packages/create-discord-bot/CHANGELOG.md +++ b/packages/create-discord-bot/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -# [create-discord-bot/0.3.1](https://github.com/discordjs/discord.js/compare/create-discord-bot@0.3.0...create-discord-bot/0.3.1) - (2024-09-01) +# [create-discord-bot@0.3.1](https://github.com/discordjs/discord.js/compare/create-discord-bot@0.3.0...create-discord-bot@0.3.1) - (2024-09-01) ## Bug Fixes From 2cb2d81b82d9e75fb6d4f1cd89f1d532af671fac Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 01:15:38 +0300 Subject: [PATCH 15/65] chore: cleanup changelogs --- packages/core/CHANGELOG.md | 6 +----- packages/ws/CHANGELOG.md | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 98ecde679285..1142861af73b 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -26,11 +26,7 @@ All notable changes to this project will be documented in this file. - Use get guild role endpoint (#10443) ([bba0e72](https://github.com/discordjs/discord.js/commit/bba0e72e2283630b9f84b77d53525397036c6b31)) - **ws:** Event layout (#10376) ([bf6761a](https://github.com/discordjs/discord.js/commit/bf6761a44adec1fe5017f6bf5d8bc0734916961f)) - **BREAKING CHANGE:** All events now emit shard id as its own param - -* fix: worker event forwarding - ---------- - - **Co-authored-by:** kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> + - fix: worker event forwarding # [@discordjs/core@1.2.0](https://github.com/discordjs/discord.js/compare/@discordjs/core@1.1.1...@discordjs/core@1.2.0) - (2024-05-04) diff --git a/packages/ws/CHANGELOG.md b/packages/ws/CHANGELOG.md index 1b436235ea8e..9f9e838e5e11 100644 --- a/packages/ws/CHANGELOG.md +++ b/packages/ws/CHANGELOG.md @@ -21,11 +21,7 @@ All notable changes to this project will be documented in this file. - **WebSocketShard:** Error event handling (#10436) ([a6de270](https://github.com/discordjs/discord.js/commit/a6de2707fc1107262b12491f73b5b6887df91c67)) - **ws:** Event layout (#10376) ([bf6761a](https://github.com/discordjs/discord.js/commit/bf6761a44adec1fe5017f6bf5d8bc0734916961f)) - **BREAKING CHANGE:** All events now emit shard id as its own param - -* fix: worker event forwarding - ---------- - - **Co-authored-by:** kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> + - fix: worker event forwarding - Native zlib support (#10316) ([94cc02a](https://github.com/discordjs/discord.js/commit/94cc02a2580496774d75673abc0caabc765d9ee0)) # [@discordjs/ws@1.1.1](https://github.com/discordjs/discord.js/compare/@discordjs/ws@1.1.0...@discordjs/ws@1.1.1) - (2024-06-02) From b715b7d6537c0d6ea8136baa0e20c8927b76f023 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 01:17:19 +0300 Subject: [PATCH 16/65] chore: cleanup 2 --- packages/brokers/CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/brokers/CHANGELOG.md b/packages/brokers/CHANGELOG.md index f2a0f869fa6c..365a4a4a2f0e 100644 --- a/packages/brokers/CHANGELOG.md +++ b/packages/brokers/CHANGELOG.md @@ -10,11 +10,6 @@ All notable changes to this project will be documented in this file. - **brokers:** Make option props more correct (#10242) ([393ded4](https://github.com/discordjs/discord.js/commit/393ded4ea14e73b2bb42226f57896130329f88ca)) - **BREAKING CHANGE:** Classes now take redis client as standalone parameter, various props from the base option interface moved to redis options -* chore: update comment - ---------- - - **Co-authored-by:** kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> - # [@discordjs/brokers@0.3.0](https://github.com/discordjs/discord.js/compare/@discordjs/brokers@0.2.3...@discordjs/brokers@0.3.0) - (2024-05-04) ## Bug Fixes From 6a6bc6397323a6ea2f07eea38a2ab2be1bafe8dc Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 01:24:33 +0300 Subject: [PATCH 17/65] chore: requested cleanup --- packages/core/CHANGELOG.md | 1 - packages/ws/CHANGELOG.md | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 1142861af73b..e9c58de6c8f6 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -26,7 +26,6 @@ All notable changes to this project will be documented in this file. - Use get guild role endpoint (#10443) ([bba0e72](https://github.com/discordjs/discord.js/commit/bba0e72e2283630b9f84b77d53525397036c6b31)) - **ws:** Event layout (#10376) ([bf6761a](https://github.com/discordjs/discord.js/commit/bf6761a44adec1fe5017f6bf5d8bc0734916961f)) - **BREAKING CHANGE:** All events now emit shard id as its own param - - fix: worker event forwarding # [@discordjs/core@1.2.0](https://github.com/discordjs/discord.js/compare/@discordjs/core@1.1.1...@discordjs/core@1.2.0) - (2024-05-04) diff --git a/packages/ws/CHANGELOG.md b/packages/ws/CHANGELOG.md index 9f9e838e5e11..7bd2563b601a 100644 --- a/packages/ws/CHANGELOG.md +++ b/packages/ws/CHANGELOG.md @@ -21,7 +21,6 @@ All notable changes to this project will be documented in this file. - **WebSocketShard:** Error event handling (#10436) ([a6de270](https://github.com/discordjs/discord.js/commit/a6de2707fc1107262b12491f73b5b6887df91c67)) - **ws:** Event layout (#10376) ([bf6761a](https://github.com/discordjs/discord.js/commit/bf6761a44adec1fe5017f6bf5d8bc0734916961f)) - **BREAKING CHANGE:** All events now emit shard id as its own param - - fix: worker event forwarding - Native zlib support (#10316) ([94cc02a](https://github.com/discordjs/discord.js/commit/94cc02a2580496774d75673abc0caabc765d9ee0)) # [@discordjs/ws@1.1.1](https://github.com/discordjs/discord.js/compare/@discordjs/ws@1.1.0...@discordjs/ws@1.1.1) - (2024-06-02) From 23636a9a2fd756c16eaecf9b3e6fc7f2e6536c00 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 01:28:00 +0300 Subject: [PATCH 18/65] chore: add versions mentions for versions with meta changes only --- packages/proxy/CHANGELOG.md | 2 ++ packages/util/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/proxy/CHANGELOG.md b/packages/proxy/CHANGELOG.md index 7aa02d54d1f3..3f39bb498f56 100644 --- a/packages/proxy/CHANGELOG.md +++ b/packages/proxy/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. +# [@discordjs/proxy@2.1.1](https://github.com/discordjs/discord.js/compare/@discordjs/proxy@2.1.0...@discordjs/proxy@2.1.1) - (2024-09-01) + # [@discordjs/proxy@2.1.0](https://github.com/discordjs/discord.js/compare/@discordjs/proxy@2.0.2...@discordjs/proxy@2.1.0) - (2024-05-04) ## Bug Fixes diff --git a/packages/util/CHANGELOG.md b/packages/util/CHANGELOG.md index 323cc363cd80..e23014ca5eb5 100644 --- a/packages/util/CHANGELOG.md +++ b/packages/util/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. +# [@discordjs/util@1.1.1](https://github.com/discordjs/discord.js/compare/@discordjs/util@1.1.0...@discordjs/util@1.1.1) - (2024-09-01) + # [@discordjs/util@1.1.0](https://github.com/discordjs/discord.js/compare/@discordjs/util@1.0.2...@discordjs/util@1.1.0) - (2024-05-04) ## Bug Fixes From 1f2047ff90babb7de25fd045dfdad827ec53d29e Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 01:30:57 +0300 Subject: [PATCH 19/65] chore(create-discord-app): update discord.js version for templates --- .../create-discord-bot/template/Bun/JavaScript/package.json | 2 +- .../create-discord-bot/template/Bun/TypeScript/package.json | 2 +- packages/create-discord-bot/template/Deno/src/commands/index.ts | 2 +- packages/create-discord-bot/template/Deno/src/events/index.ts | 2 +- packages/create-discord-bot/template/Deno/src/events/ready.ts | 2 +- packages/create-discord-bot/template/Deno/src/index.ts | 2 +- packages/create-discord-bot/template/Deno/src/util/deploy.ts | 2 +- .../create-discord-bot/template/Deno/src/util/registerEvents.ts | 2 +- packages/create-discord-bot/template/JavaScript/package.json | 2 +- packages/create-discord-bot/template/TypeScript/package.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/create-discord-bot/template/Bun/JavaScript/package.json b/packages/create-discord-bot/template/Bun/JavaScript/package.json index 75f6b646ea9d..8447a3277b13 100644 --- a/packages/create-discord-bot/template/Bun/JavaScript/package.json +++ b/packages/create-discord-bot/template/Bun/JavaScript/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@discordjs/core": "^1.2.0", - "discord.js": "^14.15.3" + "discord.js": "^14.16.0" }, "devDependencies": { "eslint": "^8.57.0", diff --git a/packages/create-discord-bot/template/Bun/TypeScript/package.json b/packages/create-discord-bot/template/Bun/TypeScript/package.json index 331f9eec8104..5ca5753ca72f 100644 --- a/packages/create-discord-bot/template/Bun/TypeScript/package.json +++ b/packages/create-discord-bot/template/Bun/TypeScript/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@discordjs/core": "^1.2.0", - "discord.js": "^14.15.3" + "discord.js": "^14.16.0" }, "devDependencies": { "@sapphire/ts-config": "^5.0.1", diff --git a/packages/create-discord-bot/template/Deno/src/commands/index.ts b/packages/create-discord-bot/template/Deno/src/commands/index.ts index 3088b9fedbc7..3fddae88a812 100644 --- a/packages/create-discord-bot/template/Deno/src/commands/index.ts +++ b/packages/create-discord-bot/template/Deno/src/commands/index.ts @@ -1,4 +1,4 @@ -import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'npm:discord.js@^14.15.3'; +import type { RESTPostAPIApplicationCommandsJSONBody, CommandInteraction } from 'npm:discord.js@^14.16.0'; import { z } from 'npm:zod@^3.23.8'; import type { StructurePredicate } from '../util/loaders.ts'; diff --git a/packages/create-discord-bot/template/Deno/src/events/index.ts b/packages/create-discord-bot/template/Deno/src/events/index.ts index a02e55773620..2e1b61690354 100644 --- a/packages/create-discord-bot/template/Deno/src/events/index.ts +++ b/packages/create-discord-bot/template/Deno/src/events/index.ts @@ -1,4 +1,4 @@ -import type { ClientEvents } from 'npm:discord.js@^14.15.3'; +import type { ClientEvents } from 'npm:discord.js@^14.16.0'; import { z } from 'npm:zod@^3.23.8'; import type { StructurePredicate } from '../util/loaders.ts'; diff --git a/packages/create-discord-bot/template/Deno/src/events/ready.ts b/packages/create-discord-bot/template/Deno/src/events/ready.ts index a29759f158a8..bd0c412e7aa9 100644 --- a/packages/create-discord-bot/template/Deno/src/events/ready.ts +++ b/packages/create-discord-bot/template/Deno/src/events/ready.ts @@ -1,4 +1,4 @@ -import { Events } from 'npm:discord.js@^14.14.1'; +import { Events } from 'npm:discord.js@^14.16.0'; import type { Event } from './index.ts'; export default { diff --git a/packages/create-discord-bot/template/Deno/src/index.ts b/packages/create-discord-bot/template/Deno/src/index.ts index 99b475e08b23..6552757e99de 100644 --- a/packages/create-discord-bot/template/Deno/src/index.ts +++ b/packages/create-discord-bot/template/Deno/src/index.ts @@ -1,6 +1,6 @@ import 'https://deno.land/std@0.199.0/dotenv/load.ts'; import { URL } from 'node:url'; -import { Client, GatewayIntentBits } from 'npm:discord.js@^14.15.3'; +import { Client, GatewayIntentBits } from 'npm:discord.js@^14.16.0'; import { loadCommands, loadEvents } from './util/loaders.ts'; import { registerEvents } from './util/registerEvents.ts'; diff --git a/packages/create-discord-bot/template/Deno/src/util/deploy.ts b/packages/create-discord-bot/template/Deno/src/util/deploy.ts index 5ab15276c3b6..64289fd97a23 100644 --- a/packages/create-discord-bot/template/Deno/src/util/deploy.ts +++ b/packages/create-discord-bot/template/Deno/src/util/deploy.ts @@ -1,7 +1,7 @@ import 'https://deno.land/std@0.223.0/dotenv/load.ts'; import { URL } from 'node:url'; import { API } from 'npm:@discordjs/core@^1.2.0/http-only'; -import { REST } from 'npm:discord.js@^14.15.3'; +import { REST } from 'npm:discord.js@^14.16.0'; import { loadCommands } from './loaders.ts'; const commands = await loadCommands(new URL('../commands/', import.meta.url)); diff --git a/packages/create-discord-bot/template/Deno/src/util/registerEvents.ts b/packages/create-discord-bot/template/Deno/src/util/registerEvents.ts index 0def42a067d9..5f89519ae022 100644 --- a/packages/create-discord-bot/template/Deno/src/util/registerEvents.ts +++ b/packages/create-discord-bot/template/Deno/src/util/registerEvents.ts @@ -1,4 +1,4 @@ -import { Events, type Client } from 'npm:discord.js@^14.14.1'; +import { Events, type Client } from 'npm:discord.js@^14.16.0'; import type { Command } from '../commands/index.ts'; import type { Event } from '../events/index.ts'; diff --git a/packages/create-discord-bot/template/JavaScript/package.json b/packages/create-discord-bot/template/JavaScript/package.json index 42b4405e359f..07e485592b18 100644 --- a/packages/create-discord-bot/template/JavaScript/package.json +++ b/packages/create-discord-bot/template/JavaScript/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@discordjs/core": "^1.2.0", - "discord.js": "^14.15.3", + "discord.js": "^14.16.0", "dotenv": "^16.4.5" }, "devDependencies": { diff --git a/packages/create-discord-bot/template/TypeScript/package.json b/packages/create-discord-bot/template/TypeScript/package.json index 5cea9ca151d5..12b2c99ace44 100644 --- a/packages/create-discord-bot/template/TypeScript/package.json +++ b/packages/create-discord-bot/template/TypeScript/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@discordjs/core": "^1.2.0", - "discord.js": "^14.15.3", + "discord.js": "^14.16.0", "dotenv": "^16.4.5" }, "devDependencies": { From 641a980b6037897dd9e5a7277418d362057b7cb6 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 11:58:30 +0300 Subject: [PATCH 20/65] chore(discord.js): release discord.js@14.16.0 --- packages/discord.js/CHANGELOG.md | 45 ++++++++++++++++++++++++++++++++ packages/discord.js/package.json | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/CHANGELOG.md b/packages/discord.js/CHANGELOG.md index e9264232a803..807d291c5599 100644 --- a/packages/discord.js/CHANGELOG.md +++ b/packages/discord.js/CHANGELOG.md @@ -2,6 +2,51 @@ All notable changes to this project will be documented in this file. +# [14.16.0](https://github.com/discordjs/discord.js/compare/@discordjs/ws@1.1.1...14.16.0) - (2024-09-02) + +## Bug Fixes + +- Message reaction crash (#10469) ([13dc779](https://github.com/discordjs/discord.js/commit/13dc779029acbea295e208cc0c058f0e6ec0e9aa)) +- **MessagePayload:** Crash when resolving body (#10454) ([dd795da](https://github.com/discordjs/discord.js/commit/dd795da790ac4107bc9d8d55aa7bc119367ee8c6)) +- **Shard:** Add env, execArgv, and argv for worker-based shards (#10429) ([b0f8df0](https://github.com/discordjs/discord.js/commit/b0f8df0f6c7d2a89838132c886294428ddf090d9)) +- **GuildAuditLogsEntry:** Correct mapped `AuditLogChange` objects (#10438) ([45f7e1a](https://github.com/discordjs/discord.js/commit/45f7e1a2e85da760f548765b768bd1b378bdedb9)) +- **GuildMemberManager:** Fix data type check for `add()` method (#10338) ([ab8bf0f](https://github.com/discordjs/discord.js/commit/ab8bf0f4d2a50cd85cf8b2aa1d4e2ea93872807b)) +- Consistent debug log spacing (#10349) ([38c699b](https://github.com/discordjs/discord.js/commit/38c699bc8a2ca40f37f70c93e08067e00f12ee81)) + +## Documentation + +- Correct documentation for BaseInteraction#inCachedGuild (#10456) ([bddf018](https://github.com/discordjs/discord.js/commit/bddf018f266f7050d64f414aa60dd01b1568a1ef)) +- Lowercase "image" URL (#10386) ([785ec8f](https://github.com/discordjs/discord.js/commit/785ec8fd757da1d8cf7963e3cec231a6d5fe4a24)) +- Update rule trigger types (#9708) ([757bed0](https://github.com/discordjs/discord.js/commit/757bed0b1f345a8963bc4eb680bed4462531fb49)) + +## Features + +- User-installable apps (#10227) ([fc0b6f7](https://github.com/discordjs/discord.js/commit/fc0b6f7f8ebd94a4a05fac0c76e49b23752a8e65)) +- Super reactions (#9336) ([a5afc40](https://github.com/discordjs/discord.js/commit/a5afc406b965b39a9cc90ef9e0e7a4b460c4e04c)) +- Use get sticker pack endpoint (#10445) ([1b1ae2f](https://github.com/discordjs/discord.js/commit/1b1ae2f0cb339170e4c0692eb43fbc966fd64030)) +- **VoiceState:** Add methods for fetching voice state (#10442) ([9907ff9](https://github.com/discordjs/discord.js/commit/9907ff915e7c72e7e980d68bf005763a3aacad1c)) +- Application emojis (#10399) ([5d92525](https://github.com/discordjs/discord.js/commit/5d92525596a0193fe65626119bb040c2eb9e945a)) +- **Attachment:** Add `title` (#10423) ([c63bde9](https://github.com/discordjs/discord.js/commit/c63bde9479359a863be4ffa4916d683a88eb46f1)) +- Add support for Automated Message nonce handling (#10381) ([2ca187b](https://github.com/discordjs/discord.js/commit/2ca187bd34a8cf2ac4ac7f2bdaecd0506c5b40bd)) +- **GuildAuditLogsEntry:** Onboarding events (#9726) ([3654efe](https://github.com/discordjs/discord.js/commit/3654efede26e28f572313cc9f3556ae59db61ba3)) +- Premium buttons (#10353) ([4f59b74](https://github.com/discordjs/discord.js/commit/4f59b740d01b9ff2213949708a36e17da32b89c3)) +- **Message:** Add `call` (#10283) ([6803121](https://github.com/discordjs/discord.js/commit/68031210f52f25dff80558e0a12d1eceb785b47b)) +- **Invite:** Add `type` (#10280) ([17d4c78](https://github.com/discordjs/discord.js/commit/17d4c78fdecff62f616546e69ef9d8ddaea3986c)) +- **User:** Add `avatarDecorationData` (#9888) ([3b5c600](https://github.com/discordjs/discord.js/commit/3b5c600b9e3f8d40ed48f02e3c9acec7433f1cc3)) + +## Refactor + +- Use get guild role endpoint (#10443) ([bba0e72](https://github.com/discordjs/discord.js/commit/bba0e72e2283630b9f84b77d53525397036c6b31)) +- **actions:** Safer getChannel calls (#10434) ([87776bb](https://github.com/discordjs/discord.js/commit/87776bb0e8de0e04043ff61fdaf5e71cfbb69aef)) +- **GuildChannelManager:** Remove redundant edit code (#10370) ([9461045](https://github.com/discordjs/discord.js/commit/9461045e5a8b832778e7e8637f540ee51e6d1eef)) + +## Typings + +- Use `ThreadChannel` and `AnyThreadChannel` consistently (#10181) ([1f7d1f8](https://github.com/discordjs/discord.js/commit/1f7d1f8094da8d9ee797b72711a4453b29589f8b)) +- **Client:** `EventEmitter` static method overrides (#10360) ([9b707f2](https://github.com/discordjs/discord.js/commit/9b707f2b832a57d5768757fad09cf8982f64d03b)) +- Fix wrong auto moderation target type (#10391) ([bbef68d](https://github.com/discordjs/discord.js/commit/bbef68d27116a2e0aa8c545a2043c46774c97887)) +- **ApplicationCommandManager:** `Snowflake` fetch (#10366) ([b8397b2](https://github.com/discordjs/discord.js/commit/b8397b24e5a3b27639a5a0bf495c2c47b7954dad)) + # [14.15.3](https://github.com/discordjs/discord.js/compare/14.15.2...14.15.3) - (2024-06-02) ## Bug Fixes diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 89f511b8e526..2a8a369b4f17 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "discord.js", - "version": "14.15.3", + "version": "14.16.0", "description": "A powerful library for interacting with the Discord API", "scripts": { "test": "pnpm run docs:test && pnpm run test:typescript", From 90ed51e06ee5196c5e62ac1a4ab993680260736b Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 12:00:06 +0300 Subject: [PATCH 21/65] chore: url fixing --- packages/discord.js/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discord.js/CHANGELOG.md b/packages/discord.js/CHANGELOG.md index 807d291c5599..06ada5e8d4cc 100644 --- a/packages/discord.js/CHANGELOG.md +++ b/packages/discord.js/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -# [14.16.0](https://github.com/discordjs/discord.js/compare/@discordjs/ws@1.1.1...14.16.0) - (2024-09-02) +# [14.16.0](https://github.com/discordjs/discord.js/compare/14.15.3...14.16.0) - (2024-09-01) ## Bug Fixes From ed1c1737df58350b6ca10c644344066170b8f334 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 15:12:31 +0300 Subject: [PATCH 22/65] chore: everyone goes to node 18+ --- packages/builders/README.md | 2 +- packages/builders/package.json | 2 +- packages/discord.js/README.md | 2 +- packages/discord.js/package.json | 2 +- packages/formatters/README.md | 2 +- packages/formatters/package.json | 2 +- packages/rest/README.md | 4 +--- packages/rest/package.json | 2 +- packages/util/README.md | 2 +- packages/util/package.json | 2 +- packages/voice/README.md | 2 +- packages/voice/package.json | 2 +- packages/ws/README.md | 2 +- packages/ws/package.json | 2 +- 14 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/builders/README.md b/packages/builders/README.md index dc88b7fdfb35..08d2509eda90 100644 --- a/packages/builders/README.md +++ b/packages/builders/README.md @@ -23,7 +23,7 @@ ## Installation -**Node.js 16.11.0 or newer is required.** +**Node.js 18 or newer is required.** ```sh npm install @discordjs/builders diff --git a/packages/builders/package.json b/packages/builders/package.json index 2d49f7f6481b..9e5d39f20122 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -91,7 +91,7 @@ "vitest": "^2.0.5" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "publishConfig": { "access": "public", diff --git a/packages/discord.js/README.md b/packages/discord.js/README.md index d7eb0726c9e1..d334efbb4bf6 100644 --- a/packages/discord.js/README.md +++ b/packages/discord.js/README.md @@ -29,7 +29,7 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to ## Installation -**Node.js 16.11.0 or newer is required.** +**Node.js 18 or newer is required.** ```sh npm install discord.js diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 2a8a369b4f17..fdf5c9045756 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -98,7 +98,7 @@ "typescript": "~5.5.4" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "publishConfig": { "provenance": true diff --git a/packages/formatters/README.md b/packages/formatters/README.md index 65ad4b0d0e99..b1e9fe57939d 100644 --- a/packages/formatters/README.md +++ b/packages/formatters/README.md @@ -23,7 +23,7 @@ ## Installation -**Node.js 16.11.0 or newer is required.** +**Node.js 18 or newer is required.** ```sh npm install @discordjs/formatters diff --git a/packages/formatters/package.json b/packages/formatters/package.json index 9e1093cf05ce..ae0ee90b051e 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -75,7 +75,7 @@ "vitest": "^2.0.5" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "publishConfig": { "access": "public", diff --git a/packages/rest/README.md b/packages/rest/README.md index 6cc1e758cd6d..c7f21e812cba 100644 --- a/packages/rest/README.md +++ b/packages/rest/README.md @@ -23,9 +23,7 @@ ## Installation -**Node.js 16.11.0 or newer is required.** - -Note: native fetch (not recommended) is unavailable in this node version, either use a newer node version or use the more performant `undiciRequest` strategy (default) +**Node.js 18 or newer is required.** ```sh npm install @discordjs/rest diff --git a/packages/rest/package.json b/packages/rest/package.json index af0626d0be10..0eec7df15376 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -111,7 +111,7 @@ "vitest": "^2.0.5" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "publishConfig": { "access": "public", diff --git a/packages/util/README.md b/packages/util/README.md index bcdbc4ce19b7..42b8eafd0087 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -20,7 +20,7 @@ ## Installation -**Node.js 16.11.0 or newer is required.** +**Node.js 18 or newer is required.** ```sh npm install @discordjs/util diff --git a/packages/util/package.json b/packages/util/package.json index 26fd6945f6bc..437fa7270b3b 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -80,7 +80,7 @@ "vitest": "^2.0.5" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "publishConfig": { "access": "public", diff --git a/packages/voice/README.md b/packages/voice/README.md index 3a2c4e3a2f13..ea682f9107a3 100644 --- a/packages/voice/README.md +++ b/packages/voice/README.md @@ -32,7 +32,7 @@ ## Installation -**Node.js 16.11.0 or newer is required.** +**Node.js 18 or newer is required.** ```sh npm install @discordjs/voice diff --git a/packages/voice/package.json b/packages/voice/package.json index 389af180083f..cefe7ffbbf2e 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -93,7 +93,7 @@ "typescript": "~5.5.4" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "publishConfig": { "access": "public", diff --git a/packages/ws/README.md b/packages/ws/README.md index 61be4e93573b..c3a9dc92931c 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -23,7 +23,7 @@ ## Installation -**Node.js 16.11.0 or newer is required.** +**Node.js 18 or newer is required.** ```sh npm install @discordjs/ws diff --git a/packages/ws/package.json b/packages/ws/package.json index 2c913f43ebbd..29481ea43222 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -105,7 +105,7 @@ "zlib-sync": "^0.1.9" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" }, "publishConfig": { "access": "public", From 18ce10a9afeadd4cbd0cd4f42cf42a54ec0c1690 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 2 Sep 2024 15:17:19 +0300 Subject: [PATCH 23/65] chore: bump major releases to node 20 --- packages/brokers/README.md | 2 +- packages/brokers/package.json | 2 +- packages/core/README.md | 2 +- packages/core/package.json | 2 +- packages/ws/README.md | 2 +- packages/ws/package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/brokers/README.md b/packages/brokers/README.md index 061e642243e2..7789c86f4d00 100644 --- a/packages/brokers/README.md +++ b/packages/brokers/README.md @@ -23,7 +23,7 @@ ## Installation -**Node.js 18 or newer is required.** +**Node.js 20 or newer is required.** ```sh npm install @discordjs/brokers diff --git a/packages/brokers/package.json b/packages/brokers/package.json index 0c34ece3484f..bed670f549a9 100644 --- a/packages/brokers/package.json +++ b/packages/brokers/package.json @@ -89,7 +89,7 @@ "vitest": "^2.0.5" }, "engines": { - "node": ">=18" + "node": ">=20" }, "publishConfig": { "access": "public", diff --git a/packages/core/README.md b/packages/core/README.md index 2fe9b334a3c4..e5363e0343af 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -23,7 +23,7 @@ ## Installation -**Node.js 18 or newer is required.** +**Node.js 20 or newer is required.** ```sh npm install @discordjs/core diff --git a/packages/core/package.json b/packages/core/package.json index bc0ad2c8c800..5a4e8449b711 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -90,7 +90,7 @@ "vitest": "^2.0.5" }, "engines": { - "node": ">=18" + "node": ">=20" }, "publishConfig": { "access": "public", diff --git a/packages/ws/README.md b/packages/ws/README.md index c3a9dc92931c..9da53cd4f250 100644 --- a/packages/ws/README.md +++ b/packages/ws/README.md @@ -23,7 +23,7 @@ ## Installation -**Node.js 18 or newer is required.** +**Node.js 20 or newer is required.** ```sh npm install @discordjs/ws diff --git a/packages/ws/package.json b/packages/ws/package.json index 29481ea43222..0957e203c1e9 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -105,7 +105,7 @@ "zlib-sync": "^0.1.9" }, "engines": { - "node": ">=18" + "node": ">=20" }, "publishConfig": { "access": "public", From 4810f7c8637dacf77d0442bd84e0d579e1f1d3bd Mon Sep 17 00:00:00 2001 From: space Date: Mon, 2 Sep 2024 23:12:28 +0200 Subject: [PATCH 24/65] fix(Transformers): pass client to recursive call (#10474) --- packages/discord.js/src/util/Transformers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discord.js/src/util/Transformers.js b/packages/discord.js/src/util/Transformers.js index 89d6c5e092c8..0e6148a10034 100644 --- a/packages/discord.js/src/util/Transformers.js +++ b/packages/discord.js/src/util/Transformers.js @@ -49,7 +49,7 @@ function _transformAPIMessageInteractionMetadata(client, messageInteractionMetad originalResponseMessageId: messageInteractionMetadata.original_response_message_id ?? null, interactedMessageId: messageInteractionMetadata.interacted_message_id ?? null, triggeringInteractionMetadata: messageInteractionMetadata.triggering_interaction_metadata - ? _transformAPIMessageInteractionMetadata(messageInteractionMetadata.triggering_interaction_metadata) + ? _transformAPIMessageInteractionMetadata(client, messageInteractionMetadata.triggering_interaction_metadata) : null, }; } From 9257a09abbf80558ed2d5d209a2f6bd2a4b3d799 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 3 Sep 2024 00:20:16 +0300 Subject: [PATCH 25/65] fix(Message): reacting returning undefined (#10475) --- packages/discord.js/src/client/actions/Action.js | 4 ++++ .../discord.js/src/client/actions/MessageReactionAdd.js | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/src/client/actions/Action.js b/packages/discord.js/src/client/actions/Action.js index 96170ee87d7e..b5f1f756b4a2 100644 --- a/packages/discord.js/src/client/actions/Action.js +++ b/packages/discord.js/src/client/actions/Action.js @@ -111,6 +111,10 @@ class GenericAction { getThreadMember(id, manager) { return this.getPayload({ user_id: id }, manager, id, Partials.ThreadMember, false); } + + spreadInjectedData(data) { + return Object.fromEntries(Object.getOwnPropertySymbols(data).map(symbol => [symbol, data[symbol]])); + } } module.exports = GenericAction; diff --git a/packages/discord.js/src/client/actions/MessageReactionAdd.js b/packages/discord.js/src/client/actions/MessageReactionAdd.js index 9c9ffbc874a0..b32f7154f102 100644 --- a/packages/discord.js/src/client/actions/MessageReactionAdd.js +++ b/packages/discord.js/src/client/actions/MessageReactionAdd.js @@ -23,7 +23,13 @@ class MessageReactionAdd extends Action { if (!user) return false; // Verify channel - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, user_id: data.user_id }); + const channel = this.getChannel({ + id: data.channel_id, + guild_id: data.guild_id, + user_id: data.user_id, + ...this.spreadInjectedData(data), + }); + if (!channel?.isTextBased()) return false; // Verify message From a11ff75631190dbb9c3f29cd01b51658050df20d Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 3 Sep 2024 00:24:53 +0300 Subject: [PATCH 26/65] chore(discord.js): release discord.js@14.16.1 (#10476) --- packages/discord.js/CHANGELOG.md | 7 +++++++ packages/discord.js/package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/CHANGELOG.md b/packages/discord.js/CHANGELOG.md index 06ada5e8d4cc..324e4ca9b79f 100644 --- a/packages/discord.js/CHANGELOG.md +++ b/packages/discord.js/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +# [14.16.1](https://github.com/discordjs/discord.js/compare/14.16.0...14.16.1) - (2024-09-02) + +## Bug Fixes + +- **Message:** Reacting returning undefined (#10475) ([9257a09](https://github.com/discordjs/discord.js/commit/9257a09abbf80558ed2d5d209a2f6bd2a4b3d799)) by @vladfrangu +- **Transformers:** Pass client to recursive call (#10474) ([4810f7c](https://github.com/discordjs/discord.js/commit/4810f7c8637dacf77d0442bd84e0d579e1f1d3bd)) by @SpaceEEC + # [14.16.0](https://github.com/discordjs/discord.js/compare/14.15.3...14.16.0) - (2024-09-01) ## Bug Fixes diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index fdf5c9045756..2cad1d851e0e 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "discord.js", - "version": "14.16.0", + "version": "14.16.1", "description": "A powerful library for interacting with the Discord API", "scripts": { "test": "pnpm run docs:test && pnpm run test:typescript", From 4594896b5404c6a34e07544951c59ff8f3657184 Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Wed, 4 Sep 2024 00:20:01 +0200 Subject: [PATCH 27/65] docs(ApplicationEmojiManager): fix fetch example (#10480) * docs(ApplicationEmojiManager): fix fetch example * docs: requested changes --- packages/discord.js/src/managers/ApplicationEmojiManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/discord.js/src/managers/ApplicationEmojiManager.js b/packages/discord.js/src/managers/ApplicationEmojiManager.js index 22e1d7c1a7cb..d53ee2fb1abc 100644 --- a/packages/discord.js/src/managers/ApplicationEmojiManager.js +++ b/packages/discord.js/src/managers/ApplicationEmojiManager.js @@ -65,12 +65,12 @@ class ApplicationEmojiManager extends CachedManager { * @returns {Promise>} * @example * // Fetch all emojis from the application - * message.application.emojis.fetch() + * application.emojis.fetch() * .then(emojis => console.log(`There are ${emojis.size} emojis.`)) * .catch(console.error); * @example * // Fetch a single emoji - * message.application.emojis.fetch('222078108977594368') + * application.emojis.fetch('222078108977594368') * .then(emoji => console.log(`The emoji name is: ${emoji.name}`)) * .catch(console.error); */ From aff772c7aa3b3de58780a94588d1f3576a434f32 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Thu, 5 Sep 2024 00:16:54 +0200 Subject: [PATCH 28/65] types: export GroupDM helper type (#10478) * types: export GroupDM helper type * refactor: rename type --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/discord.js/typings/index.d.ts | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 1dc3d97ea94a..a0c3b64900b1 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2192,27 +2192,27 @@ export class Message extends Base { public createMessageComponentCollector( options?: MessageCollectorOptionsParams, ): InteractionCollector[ComponentType]>; - public delete(): Promise>>; + public delete(): Promise>>; public edit( content: string | MessageEditOptions | MessagePayload, - ): Promise>>; + ): Promise>>; public equals(message: Message, rawData: unknown): boolean; - public fetchReference(): Promise>>; + public fetchReference(): Promise>>; public fetchWebhook(): Promise; - public crosspost(): Promise>>; - public fetch(force?: boolean): Promise>>; - public pin(reason?: string): Promise>>; + public crosspost(): Promise>>; + public fetch(force?: boolean): Promise>>; + public pin(reason?: string): Promise>>; public react(emoji: EmojiIdentifierResolvable): Promise; - public removeAttachments(): Promise>>; + public removeAttachments(): Promise>>; public reply( options: string | MessagePayload | MessageReplyOptions, - ): Promise>>; + ): Promise>>; public resolveComponent(customId: string): MessageActionRowComponent | null; public startThread(options: StartThreadOptions): Promise>; - public suppressEmbeds(suppress?: boolean): Promise>>; + public suppressEmbeds(suppress?: boolean): Promise>>; public toJSON(): unknown; public toString(): string; - public unpin(reason?: string): Promise>>; + public unpin(reason?: string): Promise>>; public inGuild(): this is Message; } @@ -5270,7 +5270,7 @@ export interface GuildMembersChunk { nonce: string | undefined; } -type NonPartialGroupDMChannel = Structure & { +export type OmitPartialGroupDMChannel = Structure & { channel: Exclude; }; @@ -5316,17 +5316,17 @@ export interface ClientEvents { guildUpdate: [oldGuild: Guild, newGuild: Guild]; inviteCreate: [invite: Invite]; inviteDelete: [invite: Invite]; - messageCreate: [message: NonPartialGroupDMChannel]; - messageDelete: [message: NonPartialGroupDMChannel]; + messageCreate: [message: OmitPartialGroupDMChannel]; + messageDelete: [message: OmitPartialGroupDMChannel]; messagePollVoteAdd: [pollAnswer: PollAnswer, userId: Snowflake]; messagePollVoteRemove: [pollAnswer: PollAnswer, userId: Snowflake]; messageReactionRemoveAll: [ - message: NonPartialGroupDMChannel, + message: OmitPartialGroupDMChannel, reactions: ReadonlyCollection, ]; messageReactionRemoveEmoji: [reaction: MessageReaction | PartialMessageReaction]; messageDeleteBulk: [ - messages: ReadonlyCollection>, + messages: ReadonlyCollection>, channel: GuildTextBasedChannel, ]; messageReactionAdd: [ From c13f18e90eb6eb315397c095e948993856428757 Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Thu, 5 Sep 2024 00:22:10 +0200 Subject: [PATCH 29/65] docs(Message): mark `interaction` as deprecated (#10481) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/discord.js/src/structures/Message.js | 1 + packages/discord.js/typings/index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 0fe2f762e3ac..276a2cb340a8 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -426,6 +426,7 @@ class Message extends Base { /** * Partial data of the interaction that this message is a reply to * @type {?MessageInteraction} + * @deprecated Use {@link Message#interactionMetadata} instead. */ this.interaction = { id: data.interaction.id, diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index a0c3b64900b1..cc3f0e454fc6 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -6416,6 +6416,7 @@ export interface MessageInteractionMetadata { triggeringInteractionMetadata: MessageInteractionMetadata | null; } +/** @deprecated Use {@link MessageInteractionMetadata} instead. */ export interface MessageInteraction { id: Snowflake; type: InteractionType; From dea68400a38edb90b8b4242d64be14968943130d Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Fri, 6 Sep 2024 10:16:38 +0300 Subject: [PATCH 30/65] fix: type guard for sendable text-based channels (#10482) * fix: type-guard for sendable text-based channels * chore: suggested change * Update packages/discord.js/typings/index.test-d.ts Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> * fix: make isSendable strictly check for `.send` * fix: tests --------- Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- .../discord.js/src/structures/BaseChannel.js | 8 ++++++++ packages/discord.js/src/util/Constants.js | 17 ++++++++++++++++- packages/discord.js/typings/index.d.ts | 8 +++++++- packages/discord.js/typings/index.test-d.ts | 15 +++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/discord.js/src/structures/BaseChannel.js b/packages/discord.js/src/structures/BaseChannel.js index 2b179d3574bc..ebb95682819c 100644 --- a/packages/discord.js/src/structures/BaseChannel.js +++ b/packages/discord.js/src/structures/BaseChannel.js @@ -155,6 +155,14 @@ class BaseChannel extends Base { return 'availableTags' in this; } + /** + * Indicates whether this channel is sendable. + * @returns {boolean} + */ + isSendable() { + return 'send' in this; + } + toJSON(...props) { return super.toJSON({ createdTimestamp: true }, ...props); } diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index e64d807e4300..8babdfdbea68 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -117,9 +117,24 @@ exports.GuildTextBasedChannelTypes = [ * * {@link ChannelType.PrivateThread} * * {@link ChannelType.GuildVoice} * * {@link ChannelType.GuildStageVoice} + * * {@link ChannelType.GroupDM} * @typedef {ChannelType[]} TextBasedChannelTypes */ -exports.TextBasedChannelTypes = [...exports.GuildTextBasedChannelTypes, ChannelType.DM]; +exports.TextBasedChannelTypes = [...exports.GuildTextBasedChannelTypes, ChannelType.DM, ChannelType.GroupDM]; + +/** + * The types of channels that are text-based and can have messages sent into. The available types are: + * * {@link ChannelType.DM} + * * {@link ChannelType.GuildText} + * * {@link ChannelType.GuildAnnouncement} + * * {@link ChannelType.AnnouncementThread} + * * {@link ChannelType.PublicThread} + * * {@link ChannelType.PrivateThread} + * * {@link ChannelType.GuildVoice} + * * {@link ChannelType.GuildStageVoice} + * @typedef {ChannelType[]} SendableChannels + */ +exports.SendableChannels = [...exports.GuildTextBasedChannelTypes, ChannelType.DM]; /** * The types of channels that are threads. The available types are: diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index cc3f0e454fc6..2ac27e78a990 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -975,6 +975,7 @@ export abstract class BaseChannel extends Base { public isDMBased(): this is PartialGroupDMChannel | DMChannel | PartialDMChannel; public isVoiceBased(): this is VoiceBasedChannel; public isThreadOnly(): this is ThreadOnlyChannel; + public isSendable(): this is SendableChannels; public toString(): ChannelMention | UserMention; } @@ -3867,6 +3868,7 @@ export const Constants: { SweeperKeys: SweeperKey[]; NonSystemMessageTypes: NonSystemMessageType[]; TextBasedChannelTypes: TextBasedChannelTypes[]; + SendableChannels: SendableChannelTypes[]; GuildTextBasedChannelTypes: GuildTextBasedChannelTypes[]; ThreadChannelTypes: ThreadChannelType[]; VoiceBasedChannelTypes: VoiceBasedChannelTypes[]; @@ -6879,11 +6881,15 @@ export type Channel = export type TextBasedChannel = Exclude, ForumChannel | MediaChannel>; +export type SendableChannels = Extract any }>; + export type TextBasedChannels = TextBasedChannel; export type TextBasedChannelTypes = TextBasedChannel['type']; -export type GuildTextBasedChannelTypes = Exclude; +export type GuildTextBasedChannelTypes = Exclude; + +export type SendableChannelTypes = SendableChannels['type']; export type VoiceBasedChannel = Extract; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 02011850550a..1f499be832d6 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -209,6 +209,7 @@ import { ApplicationEmoji, ApplicationEmojiManager, StickerPack, + SendableChannels, } from '.'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; @@ -2593,3 +2594,17 @@ declare const poll: Poll; expectType>(await client.fetchStickerPacks()); expectType>(await client.fetchStickerPacks({})); expectType(await client.fetchStickerPacks({ packId: snowflake })); + +client.on('interactionCreate', interaction => { + if (!interaction.channel) { + return; + } + + // @ts-expect-error + interaction.channel.send(); + + if (interaction.channel.isSendable()) { + expectType(interaction.channel); + interaction.channel.send({ embeds: [] }); + } +}); From 8a74f144ac9e33e2bbd4dca279f186908901d2d9 Mon Sep 17 00:00:00 2001 From: Denis Cristea Date: Fri, 6 Sep 2024 16:12:19 +0300 Subject: [PATCH 31/65] chore: pin builders in discord.js (#10490) --- packages/discord.js/package.json | 2 +- pnpm-lock.yaml | 224 ++++++++++++++++--------------- 2 files changed, 116 insertions(+), 110 deletions(-) diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 2cad1d851e0e..1301e18e02b8 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -65,7 +65,7 @@ "homepage": "https://discord.js.org", "funding": "https://github.com/discordjs/discord.js?sponsor", "dependencies": { - "@discordjs/builders": "workspace:^", + "@discordjs/builders": "^1.9.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "workspace:^", "@discordjs/rest": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52c2a55ea0bf..d809a09d8581 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -920,8 +920,8 @@ importers: packages/discord.js: dependencies: '@discordjs/builders': - specifier: workspace:^ - version: link:../builders + specifier: ^1.9.0 + version: 1.9.0 '@discordjs/collection': specifier: 1.5.3 version: 1.5.3 @@ -1461,25 +1461,25 @@ importers: version: 4.1.0 '@storybook/addon-essentials': specifier: ^8.1.5 - version: 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-interactions': specifier: ^8.1.5 - version: 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) + version: 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) '@storybook/addon-links': specifier: ^8.1.5 - version: 8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-styling': specifier: ^1.3.7 version: 1.3.7(@types/react-dom@18.3.0)(@types/react@18.3.4)(encoding@0.1.13)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4) '@storybook/blocks': specifier: ^8.1.5 - version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/react': specifier: ^8.1.5 - version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) + version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) '@storybook/react-vite': specifier: ^8.1.5 - version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) + version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) '@storybook/testing-library': specifier: ^0.2.2 version: 0.2.2 @@ -1527,7 +1527,7 @@ importers: version: 15.8.1 storybook: specifier: ^8.1.5 - version: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + version: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) turbo: specifier: ^2.0.14 version: 2.0.14 @@ -2613,6 +2613,10 @@ packages: resolution: {integrity: sha512-t58AeNg6+mvyMnBHyPC6JQqWMW0Iwyb+vlpBz4V0d0iDY9H8gGCnLFg9vtN1nC+JXfTXBlf9efu9unMUeaPCiA==} engines: {node: '>=18.18.0'} + '@discordjs/builders@1.9.0': + resolution: {integrity: sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==} + engines: {node: '>=18'} + '@discordjs/collection@1.5.3': resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} engines: {node: '>=16.11.0'} @@ -2621,6 +2625,10 @@ packages: resolution: {integrity: sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==} engines: {node: '>=18'} + '@discordjs/formatters@0.5.0': + resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==} + engines: {node: '>=18'} + '@discordjs/rest@2.3.0': resolution: {integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==} engines: {node: '>=16.11.0'} @@ -2629,6 +2637,10 @@ packages: resolution: {integrity: sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==} engines: {node: '>=16.11.0'} + '@discordjs/util@1.1.1': + resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} + engines: {node: '>=18'} + '@discordjs/ws@1.1.1': resolution: {integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==} engines: {node: '>=16.11.0'} @@ -3146,11 +3158,6 @@ packages: engines: {node: '>=v18'} hasBin: true - '@favware/cliff-jumper@4.0.3': - resolution: {integrity: sha512-vRFP87hW/UM4ryVxKFlknV7ZeYwFCzMizjBBpIJ51WSLsmxehOKV365n79IY2r6lVmxMgDbOZ0AZkB8AfBzHPw==} - engines: {node: '>=v18'} - hasBin: true - '@favware/colorette-spinner@1.0.1': resolution: {integrity: sha512-PPYtcLzhSafdylp8NBOxMCYIcLqTUMNiQc7ciBoAIvxNG2egM+P7e2nNPui5+Svyk89Q+Tnbrp139ZRIIBw3IA==} engines: {node: '>=v16'} @@ -14828,10 +14835,24 @@ snapshots: tar-stream: 3.1.7 which: 4.0.0 + '@discordjs/builders@1.9.0': + dependencies: + '@discordjs/formatters': 0.5.0 + '@discordjs/util': 1.1.1 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.37.97 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.6.3 + '@discordjs/collection@1.5.3': {} '@discordjs/collection@2.1.0': {} + '@discordjs/formatters@0.5.0': + dependencies: + discord-api-types: 0.37.97 + '@discordjs/rest@2.3.0': dependencies: '@discordjs/collection': 2.1.0 @@ -14846,6 +14867,8 @@ snapshots: '@discordjs/util@1.1.0': {} + '@discordjs/util@1.1.1': {} + '@discordjs/ws@1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@discordjs/collection': 2.1.0 @@ -15180,23 +15203,6 @@ snapshots: semver: 7.6.3 smol-toml: 1.3.0 - '@favware/cliff-jumper@4.0.3': - dependencies: - '@favware/colorette-spinner': 1.0.1 - '@octokit/auth-token': 5.1.1 - '@octokit/core': 6.1.2 - '@octokit/plugin-retry': 7.1.1(@octokit/core@6.1.2) - '@sapphire/result': 2.6.6 - '@sapphire/utilities': 3.16.2 - colorette: 2.0.20 - commander: 12.1.0 - conventional-recommended-bump: 10.0.0 - execa: 9.3.1 - git-cliff: 2.4.0 - js-yaml: 4.1.0 - semver: 7.6.3 - smol-toml: 1.3.0 - '@favware/colorette-spinner@1.0.1': dependencies: colorette: 2.0.20 @@ -18174,76 +18180,76 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@storybook/addon-actions@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-actions@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 polished: 4.3.1 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-backgrounds@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-controls@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: dequal: 2.0.3 lodash: 4.17.21 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-docs@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@babel/core': 7.25.2 '@mdx-js/react': 3.0.1(@types/react@18.3.4)(react@18.3.1) - '@storybook/blocks': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/blocks': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/react': 18.3.4 fs-extra: 11.2.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) rehype-external-links: 3.0.0 rehype-slug: 6.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-essentials@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': - dependencies: - '@storybook/addon-actions': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-backgrounds': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-controls': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-docs': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-highlight': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-measure': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-outline': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-toolbars': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-viewport': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@storybook/addon-essentials@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + dependencies: + '@storybook/addon-actions': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-backgrounds': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-controls': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-docs': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-highlight': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-measure': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-outline': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-toolbars': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-viewport': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-highlight@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-highlight@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/addon-interactions@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': + '@storybook/addon-interactions@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/test': 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) + '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/test': 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) polished: 4.3.1 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 transitivePeerDependencies: - '@jest/globals' @@ -18252,25 +18258,25 @@ snapshots: - jest - vitest - '@storybook/addon-links@8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-links@8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 - '@storybook/addon-measure@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-measure@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-outline@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 '@storybook/addon-styling@1.3.7(@types/react-dom@18.3.0)(@types/react@18.3.4)(encoding@0.1.13)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)': @@ -18309,14 +18315,14 @@ snapshots: - supports-color - typescript - '@storybook/addon-toolbars@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-toolbars@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/addon-viewport@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-viewport@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: memoizerific: 1.11.3 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -18326,7 +18332,7 @@ snapshots: - react - react-dom - '@storybook/blocks@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/blocks@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 @@ -18339,7 +18345,7 @@ snapshots: memoizerific: 1.11.3 polished: 4.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) telejson: 7.2.0 ts-dedent: 2.2.0 util-deprecate: 1.0.2 @@ -18347,9 +18353,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': + '@storybook/builder-vite@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': dependencies: - '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 1.5.4 @@ -18357,7 +18363,7 @@ snapshots: find-cache-dir: 3.3.2 fs-extra: 11.2.0 magic-string: 0.30.11 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 vite: 5.4.2(@types/node@18.19.45)(terser@5.31.6) optionalDependencies: @@ -18401,7 +18407,7 @@ snapshots: '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.3 globby: 14.0.2 - jscodeshift: 0.15.2(@babel/preset-env@7.25.4(@babel/core@7.25.2)) + jscodeshift: 0.15.2(@babel/preset-env@7.25.4) lodash: 4.17.21 prettier: 3.3.3 recast: 0.23.9 @@ -18429,9 +18435,9 @@ snapshots: - '@types/react' - '@types/react-dom' - '@storybook/components@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/components@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/core-common@7.6.20(encoding@0.1.13)': dependencies: @@ -18488,9 +18494,9 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/csf-plugin@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) unplugin: 1.12.2 '@storybook/csf@0.1.11': @@ -18504,11 +18510,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/instrumenter@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 '@vitest/utils': 1.6.0 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) util: 0.12.5 '@storybook/manager-api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -18551,9 +18557,9 @@ snapshots: - react - react-dom - '@storybook/manager-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/manager-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/node-logger@7.6.20': {} @@ -18574,29 +18580,29 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/preview-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/preview-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/react-dom-shim@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/react-dom-shim@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/react-vite@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': + '@storybook/react-vite@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) '@rollup/pluginutils': 5.1.0(rollup@4.21.0) - '@storybook/builder-vite': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) - '@storybook/react': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) + '@storybook/builder-vite': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) + '@storybook/react': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) find-up: 5.0.0 magic-string: 0.30.11 react: 18.3.1 react-docgen: 7.0.3 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) tsconfig-paths: 4.2.0 vite: 5.4.2(@types/node@18.19.45)(terser@5.31.6) transitivePeerDependencies: @@ -18606,14 +18612,14 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)': + '@storybook/react@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)': dependencies: - '@storybook/components': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/components': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/preview-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/theming': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/manager-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/preview-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/theming': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 '@types/node': 18.19.45 @@ -18628,7 +18634,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: 7.5.4 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 type-fest: 2.19.0 util-deprecate: 1.0.2 @@ -18647,16 +18653,16 @@ snapshots: memoizerific: 1.11.3 qs: 6.13.0 - '@storybook/test@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': + '@storybook/test@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': dependencies: '@storybook/csf': 0.1.11 - '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) + '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) '@vitest/expect': 1.6.0 '@vitest/spy': 1.6.0 - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) util: 0.12.5 transitivePeerDependencies: - '@jest/globals' @@ -18689,9 +18695,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/theming@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/theming@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/types@7.6.17': dependencies: @@ -18763,7 +18769,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': + '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.25.4 @@ -24296,7 +24302,7 @@ snapshots: jsbn@1.1.0: {} - jscodeshift@0.15.2(@babel/preset-env@7.25.4(@babel/core@7.25.2)): + jscodeshift@0.15.2(@babel/preset-env@7.25.4): dependencies: '@babel/core': 7.25.2 '@babel/parser': 7.25.4 @@ -27807,7 +27813,7 @@ snapshots: store2@2.14.3: {} - storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4): + storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4): dependencies: '@babel/core': 7.25.2 '@babel/types': 7.25.4 @@ -27827,7 +27833,7 @@ snapshots: fs-extra: 11.2.0 giget: 1.2.3 globby: 14.0.2 - jscodeshift: 0.15.2(@babel/preset-env@7.25.4(@babel/core@7.25.2)) + jscodeshift: 0.15.2(@babel/preset-env@7.25.4) leven: 3.1.0 ora: 5.4.1 prettier: 3.3.3 From 799fa54fa4434144855be2f7a0bbac6ff8ce9d0b Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Tue, 10 Sep 2024 21:23:53 +0200 Subject: [PATCH 32/65] docs: update discord documentation links (#10484) --- packages/core/src/api/channel.ts | 28 +++++++++---------- packages/discord.js/src/structures/Message.js | 2 +- .../structures/interfaces/TextBasedChannel.js | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core/src/api/channel.ts b/packages/core/src/api/channel.ts index f97a242be591..fe8aa20c68b9 100644 --- a/packages/core/src/api/channel.ts +++ b/packages/core/src/api/channel.ts @@ -45,7 +45,7 @@ export class ChannelsAPI { /** * Sends a message in a channel * - * @see {@link https://discord.com/developers/docs/resources/channel#create-message} + * @see {@link https://discord.com/developers/docs/resources/message#create-message} * @param channelId - The id of the channel to send the message in * @param body - The data for sending the message * @param options - The options for sending the message @@ -65,7 +65,7 @@ export class ChannelsAPI { /** * Edits a message * - * @see {@link https://discord.com/developers/docs/resources/channel#edit-message} + * @see {@link https://discord.com/developers/docs/resources/message#edit-message} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to edit * @param body - The data for editing the message @@ -87,7 +87,7 @@ export class ChannelsAPI { /** * Fetches the reactions for a message * - * @see {@link https://discord.com/developers/docs/resources/channel#get-reactions} + * @see {@link https://discord.com/developers/docs/resources/message#get-reactions} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to get the reactions for * @param emoji - The emoji to get the reactions for @@ -110,7 +110,7 @@ export class ChannelsAPI { /** * Deletes a reaction for the current user * - * @see {@link https://discord.com/developers/docs/resources/channel#delete-own-reaction} + * @see {@link https://discord.com/developers/docs/resources/message#delete-own-reaction} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to delete the reaction for * @param emoji - The emoji to delete the reaction for @@ -130,7 +130,7 @@ export class ChannelsAPI { /** * Deletes a reaction for a user * - * @see {@link https://discord.com/developers/docs/resources/channel#delete-user-reaction} + * @see {@link https://discord.com/developers/docs/resources/message#delete-user-reaction} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to delete the reaction for * @param emoji - The emoji to delete the reaction for @@ -152,7 +152,7 @@ export class ChannelsAPI { /** * Deletes all reactions for a message * - * @see {@link https://discord.com/developers/docs/resources/channel#delete-all-reactions} + * @see {@link https://discord.com/developers/docs/resources/message#delete-all-reactions} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to delete the reactions for * @param options - The options for deleting the reactions @@ -168,7 +168,7 @@ export class ChannelsAPI { /** * Deletes all reactions of an emoji for a message * - * @see {@link https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji} + * @see {@link https://discord.com/developers/docs/resources/message#delete-all-reactions-for-emoji} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to delete the reactions for * @param emoji - The emoji to delete the reactions for @@ -186,7 +186,7 @@ export class ChannelsAPI { /** * Adds a reaction to a message * - * @see {@link https://discord.com/developers/docs/resources/channel#create-reaction} + * @see {@link https://discord.com/developers/docs/resources/message#create-reaction} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to add the reaction to * @param emoji - The emoji to add the reaction with @@ -242,7 +242,7 @@ export class ChannelsAPI { /** * Fetches the messages of a channel * - * @see {@link https://discord.com/developers/docs/resources/channel#get-channel-messages} + * @see {@link https://discord.com/developers/docs/resources/message#get-channel-messages} * @param channelId - The id of the channel to fetch messages from * @param query - The query options for fetching messages * @param options - The options for fetching the messages @@ -299,7 +299,7 @@ export class ChannelsAPI { /** * Deletes a message * - * @see {@link https://discord.com/developers/docs/resources/channel#delete-message} + * @see {@link https://discord.com/developers/docs/resources/message#delete-message} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to delete * @param options - The options for deleting the message @@ -315,7 +315,7 @@ export class ChannelsAPI { /** * Bulk deletes messages * - * @see {@link https://discord.com/developers/docs/resources/channel#bulk-delete-messages} + * @see {@link https://discord.com/developers/docs/resources/message#bulk-delete-messages} * @param channelId - The id of the channel the messages are in * @param messageIds - The ids of the messages to delete * @param options - The options for deleting the messages @@ -331,7 +331,7 @@ export class ChannelsAPI { /** * Fetches a message * - * @see {@link https://discord.com/developers/docs/resources/channel#get-channel-message} + * @see {@link https://discord.com/developers/docs/resources/message#get-channel-message} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to fetch * @param options - The options for fetching the message @@ -345,7 +345,7 @@ export class ChannelsAPI { /** * Crossposts a message * - * @see {@link https://discord.com/developers/docs/resources/channel#crosspost-message} + * @see {@link https://discord.com/developers/docs/resources/message#crosspost-message} * @param channelId - The id of the channel the message is in * @param messageId - The id of the message to crosspost * @param options - The options for crossposting the message @@ -452,7 +452,7 @@ export class ChannelsAPI { /** * Creates a new forum post * - * @see {@link https://discord.com/developers/docs/resources/channel#start-thread-in-forum-channel} + * @see {@link https://discord.com/developers/docs/resources/channel#start-thread-in-forum-or-media-channel} * @param channelId - The id of the forum channel to start the thread in * @param body - The data for starting the thread * @param options - The options for starting the thread diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 276a2cb340a8..8e7ee42f79fb 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -359,7 +359,7 @@ class Message extends Base { * * {@link MessageType.ChannelFollowAdd} * * {@link MessageType.Reply} * * {@link MessageType.ThreadStarterMessage} - * @see {@link https://discord.com/developers/docs/resources/channel#message-types} + * @see {@link https://discord.com/developers/docs/resources/message#message-object-message-types} * @typedef {Object} MessageReference * @property {Snowflake} channelId The channel id that was referenced * @property {Snowflake|undefined} guildId The guild id that was referenced diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index f3f2bf8d6a0b..c3f5a9e6c573 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -75,7 +75,7 @@ class TextBasedChannel { * @property {?string} [content=''] The content for the message. This can only be `null` when editing a message. * @property {Array<(EmbedBuilder|Embed|APIEmbed)>} [embeds] The embeds for the message * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content - * (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details) + * (see [here](https://discord.com/developers/docs/resources/message#allowed-mentions-object) for more details) * @property {Array<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files] * The files to send with the message. * @property {Array<(ActionRowBuilder|ActionRow|APIActionRowComponent)>} [components] From 3c74aa204909323ff6d05991438bee2c583e838b Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Wed, 11 Sep 2024 17:40:54 +1000 Subject: [PATCH 33/65] fix(ApplicationCommand): incorrect comparison in equals method (#10497) --- packages/discord.js/src/structures/ApplicationCommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discord.js/src/structures/ApplicationCommand.js b/packages/discord.js/src/structures/ApplicationCommand.js index 881822b54a70..37eff9e46d5c 100644 --- a/packages/discord.js/src/structures/ApplicationCommand.js +++ b/packages/discord.js/src/structures/ApplicationCommand.js @@ -418,7 +418,7 @@ class ApplicationCommand extends Base { command.descriptionLocalizations ?? command.description_localizations ?? {}, this.descriptionLocalizations ?? {}, ) || - !isEqual(command.integrationTypes ?? command.integration_types ?? [], this.integrationTypes ?? {}) || + !isEqual(command.integrationTypes ?? command.integration_types ?? [], this.integrationTypes ?? []) || !isEqual(command.contexts ?? [], this.contexts ?? []) ) { return false; From d9d578391ab06c1f0060e6a16aaa198f2dcf7bf2 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Thu, 12 Sep 2024 11:18:05 +0300 Subject: [PATCH 34/65] chore(discord.js): release discord.js@14.16.2 (#10500) --- packages/discord.js/CHANGELOG.md | 17 +++++++++++++++++ packages/discord.js/package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/CHANGELOG.md b/packages/discord.js/CHANGELOG.md index 324e4ca9b79f..f435d2fee4c9 100644 --- a/packages/discord.js/CHANGELOG.md +++ b/packages/discord.js/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. +# [14.16.2](https://github.com/discordjs/discord.js/compare/14.16.1...14.16.2) - (2024-09-12) + +## Bug Fixes + +- **ApplicationCommand:** Incorrect comparison in equals method (#10497) ([3c74aa2](https://github.com/discordjs/discord.js/commit/3c74aa204909323ff6d05991438bee2c583e838b)) by @monbrey +- Type guard for sendable text-based channels (#10482) ([dea6840](https://github.com/discordjs/discord.js/commit/dea68400a38edb90b8b4242d64be14968943130d)) by @vladfrangu + +## Documentation + +- Update discord documentation links (#10484) ([799fa54](https://github.com/discordjs/discord.js/commit/799fa54fa4434144855be2f7a0bbac6ff8ce9d0b)) by @sdanialraza +- **Message:** Mark `interaction` as deprecated (#10481) ([c13f18e](https://github.com/discordjs/discord.js/commit/c13f18e90eb6eb315397c095e948993856428757)) by @sdanialraza +- **ApplicationEmojiManager:** Fix fetch example (#10480) ([4594896](https://github.com/discordjs/discord.js/commit/4594896b5404c6a34e07544951c59ff8f3657184)) by @sdanialraza + +## Typings + +- Export GroupDM helper type (#10478) ([aff772c](https://github.com/discordjs/discord.js/commit/aff772c7aa3b3de58780a94588d1f3576a434f32)) by @Qjuh + # [14.16.1](https://github.com/discordjs/discord.js/compare/14.16.0...14.16.1) - (2024-09-02) ## Bug Fixes diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 1301e18e02b8..a9dc7fd671ba 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "discord.js", - "version": "14.16.1", + "version": "14.16.2", "description": "A powerful library for interacting with the Discord API", "scripts": { "test": "pnpm run docs:test && pnpm run test:typescript", From 495bc6034574ae1247a5c80bd94054cd5d6d76e7 Mon Sep 17 00:00:00 2001 From: Almeida Date: Thu, 12 Sep 2024 22:24:07 +0100 Subject: [PATCH 35/65] fix: docs search (#10501) --- apps/website/src/util/fetchDependencies.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/website/src/util/fetchDependencies.ts b/apps/website/src/util/fetchDependencies.ts index 4b91ebe64059..551baaf6a181 100644 --- a/apps/website/src/util/fetchDependencies.ts +++ b/apps/website/src/util/fetchDependencies.ts @@ -20,7 +20,7 @@ export async function fetchDependencies({ return Object.entries(parsedDependencies) .filter(([key]) => key.startsWith('@discordjs/') && !key.includes('api-extractor')) - .map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${value.replaceAll('.', '-')}`); + .map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${sanitizeVersion(value)}`); } catch { return []; } @@ -36,8 +36,12 @@ export async function fetchDependencies({ return Object.entries(parsedDependencies) .filter(([key]) => key.startsWith('@discordjs/') && !key.includes('api-extractor')) - .map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${value.replaceAll('.', '-')}`); + .map(([key, value]) => `${key.replace('@discordjs/', '').replaceAll('.', '-')}-${sanitizeVersion(value)}`); } catch { return []; } } + +function sanitizeVersion(version: string) { + return version.replaceAll('.', '-').replace(/^[\^~]/, ''); +} From 2adee06b6e92b7854ebb1c2bfd04940aab68dd10 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:14:03 +0200 Subject: [PATCH 36/65] fix: `GuildChannel#guildId` not being patched to `undefined` (#10505) * fix: `GuildChannel#guildId` not being patched to `undefined` * fix: guildId to guild_id check --- packages/discord.js/src/client/actions/MessageCreate.js | 6 +++++- packages/discord.js/src/client/actions/MessageDelete.js | 2 +- .../discord.js/src/client/actions/MessagePollVoteAdd.js | 2 +- .../discord.js/src/client/actions/MessagePollVoteRemove.js | 2 +- .../discord.js/src/client/actions/MessageReactionAdd.js | 2 +- .../discord.js/src/client/actions/MessageReactionRemove.js | 6 +++++- .../src/client/actions/MessageReactionRemoveAll.js | 2 +- .../src/client/actions/MessageReactionRemoveEmoji.js | 2 +- packages/discord.js/src/client/actions/MessageUpdate.js | 2 +- packages/discord.js/src/client/actions/TypingStart.js | 2 +- 10 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/discord.js/src/client/actions/MessageCreate.js b/packages/discord.js/src/client/actions/MessageCreate.js index 2babdaf3b287..cba5ab7bf0e0 100644 --- a/packages/discord.js/src/client/actions/MessageCreate.js +++ b/packages/discord.js/src/client/actions/MessageCreate.js @@ -6,7 +6,11 @@ const Events = require('../../util/Events'); class MessageCreateAction extends Action { handle(data) { const client = this.client; - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, author: data.author }); + const channel = this.getChannel({ + id: data.channel_id, + author: data.author, + ...('guild_id' in data && { guild_id: data.guild_id }), + }); if (channel) { if (!channel.isTextBased()) return {}; diff --git a/packages/discord.js/src/client/actions/MessageDelete.js b/packages/discord.js/src/client/actions/MessageDelete.js index 34acb42b3ba4..c67c5abf0ecf 100644 --- a/packages/discord.js/src/client/actions/MessageDelete.js +++ b/packages/discord.js/src/client/actions/MessageDelete.js @@ -6,7 +6,7 @@ const Events = require('../../util/Events'); class MessageDeleteAction extends Action { handle(data) { const client = this.client; - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); + const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); let message; if (channel) { if (!channel.isTextBased()) return {}; diff --git a/packages/discord.js/src/client/actions/MessagePollVoteAdd.js b/packages/discord.js/src/client/actions/MessagePollVoteAdd.js index 2a2bdc649ee1..411467ca3d1f 100644 --- a/packages/discord.js/src/client/actions/MessagePollVoteAdd.js +++ b/packages/discord.js/src/client/actions/MessagePollVoteAdd.js @@ -5,7 +5,7 @@ const Events = require('../../util/Events'); class MessagePollVoteAddAction extends Action { handle(data) { - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); + const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); if (!channel?.isTextBased()) return false; const message = this.getMessage(data, channel); diff --git a/packages/discord.js/src/client/actions/MessagePollVoteRemove.js b/packages/discord.js/src/client/actions/MessagePollVoteRemove.js index c3eab3bd6742..afae556a4b94 100644 --- a/packages/discord.js/src/client/actions/MessagePollVoteRemove.js +++ b/packages/discord.js/src/client/actions/MessagePollVoteRemove.js @@ -5,7 +5,7 @@ const Events = require('../../util/Events'); class MessagePollVoteRemoveAction extends Action { handle(data) { - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); + const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); if (!channel?.isTextBased()) return false; const message = this.getMessage(data, channel); diff --git a/packages/discord.js/src/client/actions/MessageReactionAdd.js b/packages/discord.js/src/client/actions/MessageReactionAdd.js index b32f7154f102..de026a82bb73 100644 --- a/packages/discord.js/src/client/actions/MessageReactionAdd.js +++ b/packages/discord.js/src/client/actions/MessageReactionAdd.js @@ -25,7 +25,7 @@ class MessageReactionAdd extends Action { // Verify channel const channel = this.getChannel({ id: data.channel_id, - guild_id: data.guild_id, + ...('guild_id' in data && { guild_id: data.guild_id }), user_id: data.user_id, ...this.spreadInjectedData(data), }); diff --git a/packages/discord.js/src/client/actions/MessageReactionRemove.js b/packages/discord.js/src/client/actions/MessageReactionRemove.js index 5430a37c461e..888f354f110a 100644 --- a/packages/discord.js/src/client/actions/MessageReactionRemove.js +++ b/packages/discord.js/src/client/actions/MessageReactionRemove.js @@ -19,7 +19,11 @@ class MessageReactionRemove extends Action { if (!user) return false; // Verify channel - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id, user_id: data.user_id }); + const channel = this.getChannel({ + id: data.channel_id, + ...('guild_id' in data && { guild_id: data.guild_id }), + user_id: data.user_id, + }); if (!channel?.isTextBased()) return false; // Verify message diff --git a/packages/discord.js/src/client/actions/MessageReactionRemoveAll.js b/packages/discord.js/src/client/actions/MessageReactionRemoveAll.js index 5816341732f6..df3d5001cb6c 100644 --- a/packages/discord.js/src/client/actions/MessageReactionRemoveAll.js +++ b/packages/discord.js/src/client/actions/MessageReactionRemoveAll.js @@ -6,7 +6,7 @@ const Events = require('../../util/Events'); class MessageReactionRemoveAll extends Action { handle(data) { // Verify channel - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); + const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); if (!channel?.isTextBased()) return false; // Verify message diff --git a/packages/discord.js/src/client/actions/MessageReactionRemoveEmoji.js b/packages/discord.js/src/client/actions/MessageReactionRemoveEmoji.js index 2916f061d290..4f8706317ee6 100644 --- a/packages/discord.js/src/client/actions/MessageReactionRemoveEmoji.js +++ b/packages/discord.js/src/client/actions/MessageReactionRemoveEmoji.js @@ -5,7 +5,7 @@ const Events = require('../../util/Events'); class MessageReactionRemoveEmoji extends Action { handle(data) { - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); + const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); if (!channel?.isTextBased()) return false; const message = this.getMessage(data, channel); diff --git a/packages/discord.js/src/client/actions/MessageUpdate.js b/packages/discord.js/src/client/actions/MessageUpdate.js index 181c17448c75..4aa4f84def5c 100644 --- a/packages/discord.js/src/client/actions/MessageUpdate.js +++ b/packages/discord.js/src/client/actions/MessageUpdate.js @@ -4,7 +4,7 @@ const Action = require('./Action'); class MessageUpdateAction extends Action { handle(data) { - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); + const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); if (channel) { if (!channel.isTextBased()) return {}; diff --git a/packages/discord.js/src/client/actions/TypingStart.js b/packages/discord.js/src/client/actions/TypingStart.js index 8e217eccc909..637edaa3e960 100644 --- a/packages/discord.js/src/client/actions/TypingStart.js +++ b/packages/discord.js/src/client/actions/TypingStart.js @@ -6,7 +6,7 @@ const Events = require('../../util/Events'); class TypingStart extends Action { handle(data) { - const channel = this.getChannel({ id: data.channel_id, guild_id: data.guild_id }); + const channel = this.getChannel({ id: data.channel_id, ...('guild_id' in data && { guild_id: data.guild_id }) }); if (!channel) return; if (!channel.isTextBased()) { From 651f2d036abecafce00bac3c3e8eb15540fafc49 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:49:31 +0200 Subject: [PATCH 37/65] feat: show default values in docs (#10465) --- .../src/generators/ApiModelGenerator.ts | 18 +++++++++++++----- .../scripts/src/generateSplitDocumentation.ts | 6 ++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/api-extractor/src/generators/ApiModelGenerator.ts b/packages/api-extractor/src/generators/ApiModelGenerator.ts index 039a21f28eb0..d4b56ce037ec 100644 --- a/packages/api-extractor/src/generators/ApiModelGenerator.ts +++ b/packages/api-extractor/src/generators/ApiModelGenerator.ts @@ -114,7 +114,7 @@ interface DocgenEventJson { } interface DocgenParamJson { - default?: string; + default?: boolean | number | string; description: string; name: string; nullable?: boolean; @@ -155,7 +155,7 @@ interface DocgenMethodJson { interface DocgenPropertyJson { abstract?: boolean; access?: DocgenAccess; - default?: string; + default?: boolean | number | string; deprecated?: DocgenDeprecated; description: string; meta: DocgenMetaJson; @@ -1264,7 +1264,7 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = jsDoc ? this._tsDocParser.parseString( - `/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}\n${ + `/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}${jsDoc.default ? ` (default: ${this._escapeSpecialChars(jsDoc.default)})` : ''}\n${ 'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : '' }${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${ 'deprecated' in jsDoc && jsDoc.deprecated @@ -1342,7 +1342,7 @@ export class ApiModelGenerator { const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration); const docComment: tsdoc.DocComment | undefined = jsDoc ? this._tsDocParser.parseString( - `/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}\n${ + `/**\n * ${this._fixLinkTags(jsDoc.description) ?? ''}${jsDoc.default ? ` (default: ${this._escapeSpecialChars(jsDoc.default)})` : ''}\n${ 'see' in jsDoc ? jsDoc.see.map((see) => ` * @see ${see}\n`).join('') : '' }${'readonly' in jsDoc && jsDoc.readonly ? ' * @readonly\n' : ''}${ 'deprecated' in jsDoc && jsDoc.deprecated @@ -1746,6 +1746,14 @@ export class ApiModelGenerator { return sourceLocation; } + private _escapeSpecialChars(input: boolean | number | string) { + if (typeof input !== 'string') { + return input; + } + + return input.replaceAll(/(?[{}])/g, '\\$'); + } + private _fixLinkTags(input?: string): string | undefined { return input ?.replaceAll(linkRegEx, (_match, _p1, _p2, _p3, _p4, _p5, _offset, _string, groups) => { @@ -1823,7 +1831,7 @@ export class ApiModelGenerator { isOptional: Boolean(prop.nullable), isReadonly: Boolean(prop.readonly), docComment: this._tsDocParser.parseString( - `/**\n * ${this._fixLinkTags(prop.description) ?? ''}\n${ + `/**\n * ${this._fixLinkTags(prop.description) ?? ''}${prop.default ? ` (default: ${this._escapeSpecialChars(prop.default)})` : ''}\n${ prop.see?.map((see) => ` * @see ${see}\n`).join('') ?? '' }${prop.readonly ? ' * @readonly\n' : ''} */`, ).docComment, diff --git a/packages/scripts/src/generateSplitDocumentation.ts b/packages/scripts/src/generateSplitDocumentation.ts index 41019ba8706a..6c523a34ef47 100644 --- a/packages/scripts/src/generateSplitDocumentation.ts +++ b/packages/scripts/src/generateSplitDocumentation.ts @@ -39,6 +39,7 @@ import { } from '@discordjs/api-extractor-model'; import { DocNodeKind, SelectorKind, StandardTags } from '@microsoft/tsdoc'; import type { + DocEscapedText, DocNode, DocNodeContainer, DocDeclarationReference, @@ -307,6 +308,11 @@ function itemTsDoc(item: DocNode, apiItem: ApiItem) { kind: DocNodeKind.PlainText, text: (node as DocPlainText).text, }; + case DocNodeKind.EscapedText: + return { + kind: DocNodeKind.PlainText, + text: (node as DocEscapedText).decodedText, + }; case DocNodeKind.Section: case DocNodeKind.Paragraph: return (node as DocNodeContainer).nodes.map((node) => createNode(node)); From 896dc8b21ec038916901ef4fb38e4449b452af5d Mon Sep 17 00:00:00 2001 From: ckohen Date: Sun, 15 Sep 2024 10:58:21 -0700 Subject: [PATCH 38/65] chore: update cliff configs (#10471) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/brokers/cliff.toml | 6 ++++-- packages/builders/cliff.toml | 6 ++++-- packages/collection/cliff.toml | 6 ++++-- packages/core/cliff.toml | 6 ++++-- packages/create-discord-bot/cliff.toml | 6 ++++-- packages/discord.js/cliff.toml | 9 +++++---- packages/formatters/cliff.toml | 6 ++++-- packages/next/cliff.toml | 6 ++++-- packages/proxy/cliff.toml | 6 ++++-- packages/rest/cliff.toml | 6 ++++-- .../turbo/generators/templates/default/cliff.toml | 6 ++++-- packages/ui/cliff.toml | 6 ++++-- packages/util/cliff.toml | 6 ++++-- packages/voice/cliff.toml | 6 ++++-- packages/ws/cliff.toml | 6 ++++-- 15 files changed, 61 insertions(+), 32 deletions(-) diff --git a/packages/brokers/cliff.toml b/packages/brokers/cliff.toml index 35a2901131c2..62d797a75f8c 100644 --- a/packages/brokers/cliff.toml +++ b/packages/brokers/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/builders/cliff.toml b/packages/builders/cliff.toml index 834d7df3f988..192d1843ba14 100644 --- a/packages/builders/cliff.toml +++ b/packages/builders/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/collection/cliff.toml b/packages/collection/cliff.toml index 307c58882011..007cd8392082 100644 --- a/packages/collection/cliff.toml +++ b/packages/collection/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/core/cliff.toml b/packages/core/cliff.toml index e01e42509dd2..ee854b5567dd 100644 --- a/packages/core/cliff.toml +++ b/packages/core/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/create-discord-bot/cliff.toml b/packages/create-discord-bot/cliff.toml index 245edd6c62d6..83efccf45828 100644 --- a/packages/create-discord-bot/cliff.toml +++ b/packages/create-discord-bot/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/discord.js/cliff.toml b/packages/discord.js/cliff.toml index e2a9e82ee5c0..fb83cc52c3ec 100644 --- a/packages/discord.js/cliff.toml +++ b/packages/discord.js/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} @@ -67,8 +69,7 @@ commit_parsers = [ { body = ".*security", group = "Security"}, ] filter_commits = true -tag_pattern = "[0-9]*" -skip_tags = "v[0-9]*|@discordjs*" +tag_pattern = "^[0-9]+" ignore_tags = "" topo_order = false sort_commits = "newest" diff --git a/packages/formatters/cliff.toml b/packages/formatters/cliff.toml index e7c37c63d28d..cb58b2509a1d 100644 --- a/packages/formatters/cliff.toml +++ b/packages/formatters/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/next/cliff.toml b/packages/next/cliff.toml index ce63ba045147..fcc874411301 100644 --- a/packages/next/cliff.toml +++ b/packages/next/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/proxy/cliff.toml b/packages/proxy/cliff.toml index dffb706e3e7a..f09afa82f5eb 100644 --- a/packages/proxy/cliff.toml +++ b/packages/proxy/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/rest/cliff.toml b/packages/rest/cliff.toml index 31962f61267e..642c5a79be8c 100644 --- a/packages/rest/cliff.toml +++ b/packages/rest/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/scripts/turbo/generators/templates/default/cliff.toml b/packages/scripts/turbo/generators/templates/default/cliff.toml index 65a05384048c..09c581a7c723 100644 --- a/packages/scripts/turbo/generators/templates/default/cliff.toml +++ b/packages/scripts/turbo/generators/templates/default/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/ui/cliff.toml b/packages/ui/cliff.toml index 7039aa4a2042..1cc24a080685 100644 --- a/packages/ui/cliff.toml +++ b/packages/ui/cliff.toml @@ -29,8 +29,10 @@ body = """ {% endif %}\ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/util/cliff.toml b/packages/util/cliff.toml index f932465f396b..8cd0e16f6015 100644 --- a/packages/util/cliff.toml +++ b/packages/util/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/voice/cliff.toml b/packages/voice/cliff.toml index cfd0279ee76a..7f8d477f1fda 100644 --- a/packages/voice/cliff.toml +++ b/packages/voice/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} diff --git a/packages/ws/cliff.toml b/packages/ws/cliff.toml index 95f85d41958e..b3fa611dad6a 100644 --- a/packages/ws/cliff.toml +++ b/packages/ws/cliff.toml @@ -30,8 +30,10 @@ body = """ {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ {% if commit.breaking %}\ - {% for breakingChange in commit.footers %}\ - \n{% raw %} {% endraw %}- **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ {% endfor %}\ {% endif %}\ {% endfor %} From 99136d6be8bf84a9295854aaaec1caa8d04a9bdf Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Sun, 15 Sep 2024 21:27:43 +0200 Subject: [PATCH 39/65] fix(website): nullable parameters on events (#10510) --- .../src/generators/ApiModelGenerator.ts | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/api-extractor/src/generators/ApiModelGenerator.ts b/packages/api-extractor/src/generators/ApiModelGenerator.ts index d4b56ce037ec..c55268815004 100644 --- a/packages/api-extractor/src/generators/ApiModelGenerator.ts +++ b/packages/api-extractor/src/generators/ApiModelGenerator.ts @@ -1510,15 +1510,17 @@ export class ApiModelGenerator { const excerptTokens: IExcerptToken[] = [ { kind: ExcerptTokenKind.Content, - text: `on('${name}', (${ - jsDoc.params?.length ? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.nullable ? '?' : ''}: ` : ') => {})' + text: `public on(eventName: '${name}', listener: (${ + jsDoc.params?.length + ? `${jsDoc.params[0]?.name}${jsDoc.params[0]?.optional ? '?' : ''}: ` + : ') => void): this;' }`, }, ]; const parameters: IApiParameterOptions[] = []; for (let index = 0; index < (jsDoc.params?.length ?? 0) - 1; index++) { const parameter = jsDoc.params![index]!; - const newTokens = this._mapVarType(parameter.type); + const newTokens = this._mapVarType(parameter.type, parameter.nullable); parameters.push({ parameterName: parameter.name, parameterTypeTokenRange: { @@ -1537,7 +1539,7 @@ export class ApiModelGenerator { if (jsDoc.params?.length) { const parameter = jsDoc.params![jsDoc.params.length - 1]!; - const newTokens = this._mapVarType(parameter.type); + const newTokens = this._mapVarType(parameter.type, parameter.nullable); parameters.push({ parameterName: parameter.name, parameterTypeTokenRange: { @@ -1550,7 +1552,7 @@ export class ApiModelGenerator { excerptTokens.push(...newTokens); excerptTokens.push({ kind: ExcerptTokenKind.Content, - text: `) => {})`, + text: `) => void): this;`, }); } @@ -1773,7 +1775,7 @@ export class ApiModelGenerator { .replaceAll('* ', '\n * * '); } - private _mapVarType(typey: DocgenVarTypeJson): IExcerptToken[] { + private _mapVarType(typey: DocgenVarTypeJson, nullable?: boolean): IExcerptToken[] { const mapper = Array.isArray(typey) ? typey : (typey.types ?? []); const lookup: { [K in ts.SyntaxKind]?: string } = { [ts.SyntaxKind.ClassDeclaration]: 'class', @@ -1816,7 +1818,22 @@ export class ApiModelGenerator { { kind: ExcerptTokenKind.Content, text: symbol ?? '' }, ]; }, []); - return index === 0 ? result : [{ kind: ExcerptTokenKind.Content, text: ' | ' }, ...result]; + return index === 0 + ? mapper.length === 1 && (nullable || ('nullable' in typey && typey.nullable)) + ? [ + ...result, + { kind: ExcerptTokenKind.Content, text: ' | ' }, + { kind: ExcerptTokenKind.Reference, text: 'null' }, + ] + : result + : index === mapper.length - 1 && (nullable || ('nullable' in typey && typey.nullable)) + ? [ + { kind: ExcerptTokenKind.Content, text: ' | ' }, + ...result, + { kind: ExcerptTokenKind.Content, text: ' | ' }, + { kind: ExcerptTokenKind.Reference, text: 'null' }, + ] + : [{ kind: ExcerptTokenKind.Content, text: ' | ' }, ...result]; }) .filter((excerpt) => excerpt.text.length); } From 665bf1486aec62e9528f5f7b5a6910ae6b5a6c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=86MB=C3=98?= <69138346+TAEMBO@users.noreply.github.com> Date: Tue, 17 Sep 2024 01:18:08 -0700 Subject: [PATCH 40/65] types(MessageEditOptions): Omit `poll` (#10509) fix: creating poll from message edit Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- packages/discord.js/src/structures/Webhook.js | 2 +- .../structures/interfaces/InteractionResponses.js | 2 +- .../src/structures/interfaces/TextBasedChannel.js | 7 ++++++- packages/discord.js/typings/index.d.ts | 13 +++++++++---- packages/discord.js/typings/index.test-d.ts | 10 ++++++++++ 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/discord.js/src/structures/Webhook.js b/packages/discord.js/src/structures/Webhook.js index 9b2bad0d5467..e52ec14040ae 100644 --- a/packages/discord.js/src/structures/Webhook.js +++ b/packages/discord.js/src/structures/Webhook.js @@ -126,7 +126,7 @@ class Webhook { /** * Options that can be passed into send. - * @typedef {BaseMessageOptions} WebhookMessageCreateOptions + * @typedef {BaseMessageOptionsWithPoll} WebhookMessageCreateOptions * @property {boolean} [tts=false] Whether the message should be spoken aloud * @property {MessageFlags} [flags] Which flags to set for the message. * Only the {@link MessageFlags.SuppressEmbeds} flag can be set. diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index 9f711b517b49..440242a34e56 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -36,7 +36,7 @@ class InteractionResponses { /** * Options for a reply to a {@link BaseInteraction}. - * @typedef {BaseMessageOptions} InteractionReplyOptions + * @typedef {BaseMessageOptionsWithPoll} InteractionReplyOptions * @property {boolean} [tts=false] Whether the message should be spoken aloud * @property {boolean} [ephemeral] Whether the reply should be ephemeral * @property {boolean} [fetchReply] Whether to fetch the reply diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index c3f5a9e6c573..d2e408580253 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -80,6 +80,11 @@ class TextBasedChannel { * The files to send with the message. * @property {Array<(ActionRowBuilder|ActionRow|APIActionRowComponent)>} [components] * Action rows containing interactive components for the message (buttons, select menus) + */ + + /** + * The base message options for messages including a poll. + * @typedef {BaseMessageOptions} BaseMessageOptionsWithPoll * @property {PollData} [poll] The poll to send with the message */ @@ -93,7 +98,7 @@ class TextBasedChannel { /** * The options for sending a message. - * @typedef {BaseMessageOptions} BaseMessageCreateOptions + * @typedef {BaseMessageOptionsWithPoll} BaseMessageCreateOptions * @property {boolean} [tts=false] Whether the message should be spoken aloud * @property {string} [nonce] The nonce for the message * This property is required if `enforceNonce` set to `true`. diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 2ac27e78a990..771623c5cff5 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -6291,7 +6291,7 @@ export interface InteractionDeferReplyOptions { export interface InteractionDeferUpdateOptions extends Omit {} -export interface InteractionReplyOptions extends BaseMessageOptions { +export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll { tts?: boolean; ephemeral?: boolean; fetchReply?: boolean; @@ -6459,10 +6459,13 @@ export interface BaseMessageOptions { | ActionRowData | APIActionRowComponent )[]; +} + +export interface BaseMessageOptionsWithPoll extends BaseMessageOptions { poll?: PollData; } -export interface MessageCreateOptions extends BaseMessageOptions { +export interface MessageCreateOptions extends BaseMessageOptionsWithPoll { tts?: boolean; nonce?: string | number; enforceNonce?: boolean; @@ -6475,7 +6478,7 @@ export interface MessageCreateOptions extends BaseMessageOptions { } export interface GuildForumThreadMessageCreateOptions - extends Omit, + extends BaseMessageOptions, Pick {} export interface MessageEditAttachmentData { @@ -6981,7 +6984,9 @@ export interface WebhookMessageEditOptions extends Omit { message?: MessageResolvable | '@original'; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 1f499be832d6..f23023f1acca 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -210,6 +210,7 @@ import { ApplicationEmojiManager, StickerPack, SendableChannels, + PollData, } from '.'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; @@ -2576,6 +2577,8 @@ await textChannel.send({ }); declare const poll: Poll; +declare const message: Message; +declare const pollData: PollData; { expectType(await poll.end()); @@ -2589,6 +2592,13 @@ declare const poll: Poll; messageId: snowflake, answerId: 1, }); + + await message.edit({ + // @ts-expect-error + poll: pollData, + }); + + await chatInputInteraction.editReply({ poll: pollData }); } expectType>(await client.fetchStickerPacks()); From cda8d88ad5be01e6bcec0d36c0ef56fe9ce48e64 Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Tue, 17 Sep 2024 11:15:00 +0200 Subject: [PATCH 41/65] build: bump discord-api-types to 0.37.100 (#10488) * build: bump discord-api-types to 0.37.100 * build: fix lockfile --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- packages/builders/package.json | 2 +- packages/core/package.json | 2 +- packages/discord.js/package.json | 2 +- packages/formatters/package.json | 2 +- packages/next/package.json | 2 +- packages/rest/package.json | 2 +- packages/voice/package.json | 2 +- packages/ws/package.json | 2 +- pnpm-lock.yaml | 39 ++++++++++++++++++-------------- 9 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/builders/package.json b/packages/builders/package.json index 9e5d39f20122..ebc3263a889f 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -68,7 +68,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/util": "workspace:^", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.97", + "discord-api-types": "0.37.100", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" diff --git a/packages/core/package.json b/packages/core/package.json index 5a4e8449b711..d1356c00b0ac 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,7 @@ "@discordjs/ws": "workspace:^", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97" + "discord-api-types": "0.37.100" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index a9dc7fd671ba..699547837525 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -72,7 +72,7 @@ "@discordjs/util": "workspace:^", "@discordjs/ws": "1.1.1", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.97", + "discord-api-types": "0.37.100", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", diff --git a/packages/formatters/package.json b/packages/formatters/package.json index ae0ee90b051e..dbd063df78fc 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -55,7 +55,7 @@ "homepage": "https://discord.js.org", "funding": "https://github.com/discordjs/discord.js?sponsor", "dependencies": { - "discord-api-types": "0.37.97" + "discord-api-types": "0.37.100" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", diff --git a/packages/next/package.json b/packages/next/package.json index a16e321e6ddb..513c02949be2 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -72,7 +72,7 @@ "@discordjs/rest": "workspace:^", "@discordjs/util": "workspace:^", "@discordjs/ws": "workspace:^", - "discord-api-types": "0.37.97" + "discord-api-types": "0.37.100" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", diff --git a/packages/rest/package.json b/packages/rest/package.json index 0eec7df15376..eb2e8ae5f481 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -88,7 +88,7 @@ "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", + "discord-api-types": "0.37.100", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.19.8" diff --git a/packages/voice/package.json b/packages/voice/package.json index cefe7ffbbf2e..5c11a768cfe7 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -64,7 +64,7 @@ "funding": "https://github.com/discordjs/discord.js?sponsor", "dependencies": { "@types/ws": "^8.5.12", - "discord-api-types": "0.37.97", + "discord-api-types": "0.37.100", "prism-media": "^1.3.5", "tslib": "^2.6.3", "ws": "^8.18.0" diff --git a/packages/ws/package.json b/packages/ws/package.json index 0957e203c1e9..4f669a5a49d6 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -79,7 +79,7 @@ "@sapphire/async-queue": "^1.5.3", "@types/ws": "^8.5.12", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", + "discord-api-types": "0.37.100", "tslib": "^2.6.3", "ws": "^8.18.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d809a09d8581..4b67fad1ec60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -680,8 +680,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -804,8 +804,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -941,8 +941,8 @@ importers: specifier: 3.5.3 version: 3.5.3 discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -1060,8 +1060,8 @@ importers: packages/formatters: dependencies: discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -1133,8 +1133,8 @@ importers: specifier: workspace:^ version: link:../ws discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -1307,8 +1307,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 magic-bytes.js: specifier: ^1.10.0 version: 1.10.0 @@ -1604,8 +1604,8 @@ importers: specifier: ^8.5.12 version: 8.5.12 discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 prism-media: specifier: ^1.3.5 version: 1.3.5 @@ -1701,8 +1701,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: 0.37.97 - version: 0.37.97 + specifier: 0.37.100 + version: 0.37.100 tslib: specifier: ^2.6.3 version: 2.6.3 @@ -7632,6 +7632,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + discord-api-types@0.37.100: + resolution: {integrity: sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==} + discord-api-types@0.37.83: resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} @@ -19154,7 +19157,7 @@ snapshots: '@types/ws@8.5.12': dependencies: - '@types/node': 16.18.105 + '@types/node': 18.19.45 '@types/yargs-parser@21.0.3': {} @@ -21483,6 +21486,8 @@ snapshots: dependencies: path-type: 4.0.0 + discord-api-types@0.37.100: {} + discord-api-types@0.37.83: {} discord-api-types@0.37.97: {} From 6c77fee41b1aabc243bff623debd157a4c7fad6a Mon Sep 17 00:00:00 2001 From: Ryan Munro Date: Tue, 24 Sep 2024 00:13:14 +1000 Subject: [PATCH 42/65] fix(BaseInteraction): add missing props (#10517) * fix(AutocompleteInteraction): add missing authorizingIntegrationOwners * fix(AutocompleteInteraction): add missing context * fix(AutocompleteInteraction): types * fix: move to BaseInteraction * fix: remove props from CommandInteraction * Update packages/discord.js/typings/index.d.ts Co-authored-by: Danial Raza --------- Co-authored-by: Vlad Frangu Co-authored-by: Danial Raza --- .../discord.js/src/structures/BaseInteraction.js | 15 +++++++++++++++ .../src/structures/CommandInteraction.js | 15 --------------- packages/discord.js/typings/index.d.ts | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/discord.js/src/structures/BaseInteraction.js b/packages/discord.js/src/structures/BaseInteraction.js index db778080b460..28d1e4b35ad3 100644 --- a/packages/discord.js/src/structures/BaseInteraction.js +++ b/packages/discord.js/src/structures/BaseInteraction.js @@ -107,6 +107,21 @@ class BaseInteraction extends Base { (coll, entitlement) => coll.set(entitlement.id, this.client.application.entitlements._add(entitlement)), new Collection(), ); + + /* eslint-disable max-len */ + /** + * Mapping of installation contexts that the interaction was authorized for the related user or guild ids + * @type {APIAuthorizingIntegrationOwnersMap} + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object} + */ + this.authorizingIntegrationOwners = data.authorizing_integration_owners; + /* eslint-enable max-len */ + + /** + * Context where the interaction was triggered from + * @type {?InteractionContextType} + */ + this.context = data.context ?? null; } /** diff --git a/packages/discord.js/src/structures/CommandInteraction.js b/packages/discord.js/src/structures/CommandInteraction.js index 2dec1230021a..0d435deeb446 100644 --- a/packages/discord.js/src/structures/CommandInteraction.js +++ b/packages/discord.js/src/structures/CommandInteraction.js @@ -45,21 +45,6 @@ class CommandInteraction extends BaseInteraction { */ this.commandGuildId = data.data.guild_id ?? null; - /* eslint-disable max-len */ - /** - * Mapping of installation contexts that the interaction was authorized for the related user or guild ids - * @type {APIAuthorizingIntegrationOwnersMap} - * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-authorizing-integration-owners-object} - */ - this.authorizingIntegrationOwners = data.authorizing_integration_owners; - /* eslint-enable max-len */ - - /** - * Context where the interaction was triggered from - * @type {?InteractionContextType} - */ - this.context = data.context ?? null; - /** * Whether the reply to this interaction has been deferred * @type {boolean} diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 771623c5cff5..25eaab5839ed 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -547,7 +547,6 @@ export type GuildCacheMessage = CacheTypeReducer< export type BooleanCache = Cached extends 'cached' ? true : false; export abstract class CommandInteraction extends BaseInteraction { - public authorizingIntegrationOwners: APIAuthorizingIntegrationOwnersMap; public type: InteractionType.ApplicationCommand; public get command(): ApplicationCommand | ApplicationCommand<{ guild: GuildResolvable }> | null; public options: Omit< @@ -572,7 +571,6 @@ export abstract class CommandInteraction e public commandName: string; public commandType: ApplicationCommandType; public commandGuildId: Snowflake | null; - public context: InteractionContextType | null; public deferred: boolean; public ephemeral: boolean | null; public replied: boolean; @@ -1925,6 +1923,7 @@ export class BaseInteraction extends Base private readonly _cacheType: Cached; protected constructor(client: Client, data: RawInteractionData); public applicationId: Snowflake; + public authorizingIntegrationOwners: APIAuthorizingIntegrationOwnersMap; public get channel(): CacheTypeReducer< Cached, GuildTextBasedChannel | null, @@ -1933,6 +1932,7 @@ export class BaseInteraction extends Base TextBasedChannel | null >; public channelId: Snowflake | null; + public context: InteractionContextType | null; public get createdAt(): Date; public get createdTimestamp(): number; public get guild(): CacheTypeReducer; From 0873f9a4c3a817ce9cf3821f713a871bc3902eb3 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Sun, 29 Sep 2024 14:20:02 +0300 Subject: [PATCH 43/65] chore(discord.js): release discord.js@14.16.3 (#10522) --- packages/discord.js/CHANGELOG.md | 11 +++++++++++ packages/discord.js/package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/CHANGELOG.md b/packages/discord.js/CHANGELOG.md index f435d2fee4c9..9225e4601fc8 100644 --- a/packages/discord.js/CHANGELOG.md +++ b/packages/discord.js/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +# [14.16.3](https://github.com/discordjs/discord.js/compare/14.16.2...14.16.3) - (2024-09-29) + +## Bug Fixes + +- **BaseInteraction:** Add missing props (#10517) ([6c77fee](https://github.com/discordjs/discord.js/commit/6c77fee41b1aabc243bff623debd157a4c7fad6a)) by @monbrey +- `GuildChannel#guildId` not being patched to `undefined` (#10505) ([2adee06](https://github.com/discordjs/discord.js/commit/2adee06b6e92b7854ebb1c2bfd04940aab68dd10)) by @Qjuh + +## Typings + +- **MessageEditOptions:** Omit `poll` (#10509) ([665bf14](https://github.com/discordjs/discord.js/commit/665bf1486aec62e9528f5f7b5a6910ae6b5a6c9c)) by @TAEMBO + # [14.16.2](https://github.com/discordjs/discord.js/compare/14.16.1...14.16.2) - (2024-09-12) ## Bug Fixes diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 699547837525..22e27f87dd42 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "discord.js", - "version": "14.16.2", + "version": "14.16.3", "description": "A powerful library for interacting with the Discord API", "scripts": { "test": "pnpm run docs:test && pnpm run test:typescript", From e1012cc54a7b28a99c10d11d5ae899bdc7f6b9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=86MB=C3=98?= <69138346+TAEMBO@users.noreply.github.com> Date: Sun, 29 Sep 2024 04:35:40 -0700 Subject: [PATCH 44/65] feat: message forwarding (#10464) * feat: message forwarding * fix: redundant usage * feat: add additional snapshot fields * refactor: use collection to store snapshots --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/discord.js/src/structures/Message.js | 25 +++++++++++++++++++ packages/discord.js/src/util/APITypes.js | 5 ++++ packages/discord.js/typings/index.d.ts | 23 +++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 8e7ee42f79fb..642524f0b6b7 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -364,6 +364,7 @@ class Message extends Base { * @property {Snowflake} channelId The channel id that was referenced * @property {Snowflake|undefined} guildId The guild id that was referenced * @property {Snowflake|undefined} messageId The message id that was referenced + * @property {MessageReferenceType} type The type of message reference */ if ('message_reference' in data) { @@ -375,6 +376,7 @@ class Message extends Base { channelId: data.message_reference.channel_id, guildId: data.message_reference.guild_id, messageId: data.message_reference.message_id, + type: data.message_reference.type, }; } else { this.reference ??= null; @@ -448,6 +450,29 @@ class Message extends Base { this.poll ??= null; } + if (data.message_snapshots) { + /** + * The message associated with the message reference + * @type {Collection} + */ + this.messageSnapshots = data.message_snapshots.reduce((coll, snapshot) => { + const channel = this.client.channels.resolve(this.reference.channelId); + const snapshotData = { + ...snapshot.message, + id: this.reference.messageId, + channel_id: this.reference.channelId, + guild_id: this.reference.guildId, + }; + + return coll.set( + this.reference.messageId, + channel ? channel.messages._add(snapshotData) : new this.constructor(this.client, snapshotData), + ); + }, new Collection()); + } else { + this.messageSnapshots ??= new Collection(); + } + /** * A call associated with a message * @typedef {Object} MessageCall diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 89e3827eeb95..42b2ddae0943 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -455,6 +455,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageActivityType} */ +/** + * @external MessageReferenceType + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageReferenceType} + */ + /** * @external MessageType * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/MessageType} diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 25eaab5839ed..f29e646f7c94 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -185,6 +185,7 @@ import { InviteType, ReactionType, APIAuthorizingIntegrationOwnersMap, + MessageReferenceType, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -2185,6 +2186,7 @@ export class Message extends Base { public webhookId: Snowflake | null; public flags: Readonly; public reference: MessageReference | null; + public messageSnapshots: Collection; public awaitMessageComponent( options?: AwaitMessageCollectorOptionsParams, ): Promise[ComponentType]>; @@ -6442,6 +6444,26 @@ export interface MessageMentionOptions { export type MessageMentionTypes = 'roles' | 'users' | 'everyone'; +export interface MessageSnapshot + extends Partialize< + Message, + null, + Exclude< + keyof Message, + | 'attachments' + | 'client' + | 'components' + | 'content' + | 'createdTimestamp' + | 'editedTimestamp' + | 'embeds' + | 'flags' + | 'mentions' + | 'stickers' + | 'type' + > + > {} + export interface BaseMessageOptions { content?: string; embeds?: readonly (JSONEncodable | APIEmbed)[]; @@ -6497,6 +6519,7 @@ export interface MessageReference { channelId: Snowflake; guildId: Snowflake | undefined; messageId: Snowflake | undefined; + type: MessageReferenceType; } export type MessageResolvable = Message | Snowflake; From 9aa3b635ef2ed6d6bb17af2422ef1cf7bbe16ab5 Mon Sep 17 00:00:00 2001 From: Almeida Date: Sun, 29 Sep 2024 19:41:57 +0100 Subject: [PATCH 45/65] feat: recurring scheduled events (#10447) * feat: recurring scheduled events * fix: nullable on patch * docs: remove unnecessary parenthesis Co-authored-by: Vlad Frangu --------- Co-authored-by: Vlad Frangu --- .../managers/GuildScheduledEventManager.js | 24 +++++++++ .../src/structures/GuildScheduledEvent.js | 50 +++++++++++++++++++ packages/discord.js/src/util/APITypes.js | 20 ++++++++ packages/discord.js/src/util/Transformers.js | 29 ++++++++++- packages/discord.js/test/random.js | 5 +- packages/discord.js/typings/index.d.ts | 41 ++++++++++++++- packages/discord.js/typings/index.test-d.ts | 4 ++ 7 files changed, 169 insertions(+), 4 deletions(-) diff --git a/packages/discord.js/src/managers/GuildScheduledEventManager.js b/packages/discord.js/src/managers/GuildScheduledEventManager.js index a0d0c8375e72..383875d70002 100644 --- a/packages/discord.js/src/managers/GuildScheduledEventManager.js +++ b/packages/discord.js/src/managers/GuildScheduledEventManager.js @@ -7,6 +7,7 @@ const CachedManager = require('./CachedManager'); const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../errors'); const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent'); const { resolveImage } = require('../util/DataResolver'); +const { _transformGuildScheduledEventRecurrenceRule } = require('../util/Transformers'); /** * Manages API methods for GuildScheduledEvents and stores their cache. @@ -36,6 +37,21 @@ class GuildScheduledEventManager extends CachedManager { * @typedef {Snowflake|GuildScheduledEvent} GuildScheduledEventResolvable */ + /** + * Options for setting a recurrence rule for a guild scheduled event. + * @typedef {Object} GuildScheduledEventRecurrenceRuleOptions + * @property {DateResolvable} startAt The time the recurrence rule interval starts at + * @property {?DateResolvable} endAt The time the recurrence rule interval ends at + * @property {GuildScheduledEventRecurrenceRuleFrequency} frequency How often the event occurs + * @property {number} interval The spacing between the events + * @property {?GuildScheduledEventRecurrenceRuleWeekday[]} byWeekday The days within a week to recur on + * @property {?GuildScheduledEventRecurrenceRuleNWeekday[]} byNWeekday The days within a week to recur on + * @property {?GuildScheduledEventRecurrenceRuleMonth[]} byMonth The months to recur on + * @property {?number[]} byMonthDay The days within a month to recur on + * @property {?number[]} byYearDay The days within a year to recur on + * @property {?number} count The total amount of times the event is allowed to recur before stopping + */ + /** * Options used to create a guild scheduled event. * @typedef {Object} GuildScheduledEventCreateOptions @@ -54,6 +70,8 @@ class GuildScheduledEventManager extends CachedManager { * This is required if `entityType` is {@link GuildScheduledEventEntityType.External} * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event * @property {string} [reason] The reason for creating the guild scheduled event + * @property {GuildScheduledEventRecurrenceRuleOptions} [recurrenceRule] + * The recurrence rule of the guild scheduled event */ /** @@ -81,6 +99,7 @@ class GuildScheduledEventManager extends CachedManager { entityMetadata, reason, image, + recurrenceRule, } = options; let entity_metadata, channel_id; @@ -104,6 +123,7 @@ class GuildScheduledEventManager extends CachedManager { entity_type: entityType, entity_metadata, image: image && (await resolveImage(image)), + recurrence_rule: recurrenceRule && _transformGuildScheduledEventRecurrenceRule(recurrenceRule), }, reason, }); @@ -178,6 +198,8 @@ class GuildScheduledEventManager extends CachedManager { * {@link GuildScheduledEventEntityType.External} * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event * @property {string} [reason] The reason for editing the guild scheduled event + * @property {?GuildScheduledEventRecurrenceRuleOptions} [recurrenceRule] + * The recurrence rule of the guild scheduled event */ /** @@ -203,6 +225,7 @@ class GuildScheduledEventManager extends CachedManager { entityMetadata, reason, image, + recurrenceRule, } = options; let entity_metadata; @@ -224,6 +247,7 @@ class GuildScheduledEventManager extends CachedManager { status, image: image && (await resolveImage(image)), entity_metadata, + recurrence_rule: recurrenceRule && _transformGuildScheduledEventRecurrenceRule(recurrenceRule), }, reason, }); diff --git a/packages/discord.js/src/structures/GuildScheduledEvent.js b/packages/discord.js/src/structures/GuildScheduledEvent.js index f5a5291232e5..9f6124f0198c 100644 --- a/packages/discord.js/src/structures/GuildScheduledEvent.js +++ b/packages/discord.js/src/structures/GuildScheduledEvent.js @@ -189,6 +189,56 @@ class GuildScheduledEvent extends Base { } else { this.image ??= null; } + + /** + * Represents the recurrence rule for a {@link GuildScheduledEvent}. + * @typedef {Object} GuildScheduledEventRecurrenceRule + * @property {number} startTimestamp The timestamp the recurrence rule interval starts at + * @property {Date} startAt The time the recurrence rule interval starts at + * @property {?number} endTimestamp The timestamp the recurrence rule interval ends at + * @property {?Date} endAt The time the recurrence rule interval ends at + * @property {GuildScheduledEventRecurrenceRuleFrequency} frequency How often the event occurs + * @property {number} interval The spacing between the events + * @property {?GuildScheduledEventRecurrenceRuleWeekday[]} byWeekday The days within a week to recur on + * @property {?GuildScheduledEventRecurrenceRuleNWeekday[]} byNWeekday The days within a week to recur on + * @property {?GuildScheduledEventRecurrenceRuleMonth[]} byMonth The months to recur on + * @property {?number[]} byMonthDay The days within a month to recur on + * @property {?number[]} byYearDay The days within a year to recur on + * @property {?number} count The total amount of times the event is allowed to recur before stopping + */ + + /** + * @typedef {Object} GuildScheduledEventRecurrenceRuleNWeekday + * @property {number} n The week to recur on + * @property {GuildScheduledEventRecurrenceRuleWeekday} day The day within the week to recur on + */ + + if ('recurrence_rule' in data) { + /** + * The recurrence rule for this scheduled event + * @type {?GuildScheduledEventRecurrenceRule} + */ + this.recurrenceRule = { + startTimestamp: Date.parse(data.recurrence_rule.start), + get startAt() { + return new Date(this.startTimestamp); + }, + endTimestamp: data.recurrence_rule.end && Date.parse(data.recurrence_rule.end), + get endAt() { + return this.endTimestamp && new Date(this.endTimestamp); + }, + frequency: data.recurrence_rule.frequency, + interval: data.recurrence_rule.interval, + byWeekday: data.recurrence_rule.by_weekday, + byNWeekday: data.recurrence_rule.by_n_weekday, + byMonth: data.recurrence_rule.by_month, + byMonthDay: data.recurrence_rule.by_month_day, + byYearDay: data.recurrence_rule.by_year_day, + count: data.recurrence_rule.count, + }; + } else { + this.recurrenceRule ??= null; + } } /** diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 42b2ddae0943..43a8bcf81ada 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -100,6 +100,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuildMember} */ +/** + * @external APIGuildScheduledEventRecurrenceRule + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuildScheduledEventRecurrenceRule} + */ + /** * @external APIInteraction * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIInteraction} @@ -390,6 +395,21 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventPrivacyLevel} */ +/** + * @external GuildScheduledEventRecurrenceRuleFrequency + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventRecurrenceRuleFrequency} + */ + +/** + * @external GuildScheduledEventRecurrenceRuleMonth + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventRecurrenceRuleMonth} + */ + +/** + * @external GuildScheduledEventRecurrenceRuleWeekday + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventRecurrenceRuleWeekday} + */ + /** * @external GuildScheduledEventStatus * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildScheduledEventStatus} diff --git a/packages/discord.js/src/util/Transformers.js b/packages/discord.js/src/util/Transformers.js index 0e6148a10034..e37fb456c65a 100644 --- a/packages/discord.js/src/util/Transformers.js +++ b/packages/discord.js/src/util/Transformers.js @@ -54,4 +54,31 @@ function _transformAPIMessageInteractionMetadata(client, messageInteractionMetad }; } -module.exports = { toSnakeCase, _transformAPIAutoModerationAction, _transformAPIMessageInteractionMetadata }; +/** + * Transforms a guild scheduled event recurrence rule object to a snake-cased variant. + * @param {GuildScheduledEventRecurrenceRuleOptions} recurrenceRule The recurrence rule to transform + * @returns {APIGuildScheduledEventRecurrenceRule} + * @ignore + */ +function _transformGuildScheduledEventRecurrenceRule(recurrenceRule) { + return { + start: new Date(recurrenceRule.startAt).toISOString(), + // eslint-disable-next-line eqeqeq + end: recurrenceRule.endAt != null ? new Date(recurrenceRule.endAt).toISOString() : recurrenceRule.endAt, + frequency: recurrenceRule.frequency, + interval: recurrenceRule.interval, + by_weekday: recurrenceRule.byWeekday, + by_n_weekday: recurrenceRule.byNWeekday, + by_month: recurrenceRule.byMonth, + by_month_day: recurrenceRule.byMonthDay, + by_year_day: recurrenceRule.byYearDay, + count: recurrenceRule.count, + }; +} + +module.exports = { + toSnakeCase, + _transformAPIAutoModerationAction, + _transformAPIMessageInteractionMetadata, + _transformGuildScheduledEventRecurrenceRule, +}; diff --git a/packages/discord.js/test/random.js b/packages/discord.js/test/random.js index e33af44d7675..48d5ab847b77 100644 --- a/packages/discord.js/test/random.js +++ b/packages/discord.js/test/random.js @@ -2,7 +2,7 @@ 'use strict'; -const { token } = require('./auth.js'); +const { token, owner } = require('./auth.js'); const { Client } = require('../src'); const { ChannelType, GatewayIntentBits } = require('discord-api-types/v10'); @@ -14,6 +14,7 @@ const client = new Client({ GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildMembers, + GatewayIntentBits.MessageContent, ], }); @@ -186,7 +187,7 @@ client.on('messageCreate', msg => { msg.channel.send(`\`\`\`${msg.content}\`\`\``); } - if (msg.content.startsWith('#eval') && msg.author.id === '66564597481480192') { + if (msg.content.startsWith('#eval') && msg.author.id === owner) { try { const com = eval(msg.content.split(' ').slice(1).join(' ')); msg.channel.send(`\`\`\`\n${com}\`\`\``); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index f29e646f7c94..57eed8b24e48 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -186,6 +186,9 @@ import { ReactionType, APIAuthorizingIntegrationOwnersMap, MessageReferenceType, + GuildScheduledEventRecurrenceRuleWeekday, + GuildScheduledEventRecurrenceRuleMonth, + GuildScheduledEventRecurrenceRuleFrequency, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -1779,6 +1782,7 @@ export class GuildScheduledEvent; } +export interface GuildScheduledEventRecurrenceRule { + startTimestamp: number; + get startAt(): Date; + endTimestamp: number | null; + get endAt(): Date | null; + frequency: GuildScheduledEventRecurrenceRuleFrequency; + interval: number; + byWeekday: readonly GuildScheduledEventRecurrenceRuleWeekday[] | null; + byNWeekday: readonly GuildScheduledEventRecurrenceRuleNWeekday[] | null; + byMonth: readonly GuildScheduledEventRecurrenceRuleMonth[] | null; + byMonthDay: readonly number[] | null; + byYearDay: readonly number[] | null; + count: number | null; +} + +export interface GuildScheduledEventRecurrenceRuleNWeekday { + n: number; + day: GuildScheduledEventRecurrenceRuleWeekday; +} + export class GuildTemplate extends Base { private constructor(client: Client, data: RawGuildTemplateData); public createdTimestamp: number; @@ -6167,14 +6191,29 @@ export interface GuildScheduledEventCreateOptions { entityMetadata?: GuildScheduledEventEntityMetadataOptions; image?: BufferResolvable | Base64Resolvable | null; reason?: string; + recurrenceRule?: GuildScheduledEventRecurrenceRuleOptions; +} + +export interface GuildScheduledEventRecurrenceRuleOptions { + startAt: DateResolvable; + endAt: DateResolvable; + frequency: GuildScheduledEventRecurrenceRuleFrequency; + interval: number; + byWeekday: readonly GuildScheduledEventRecurrenceRuleWeekday[]; + byNWeekday: readonly GuildScheduledEventRecurrenceRuleNWeekday[]; + byMonth: readonly GuildScheduledEventRecurrenceRuleMonth[]; + byMonthDay: readonly number[]; + byYearDay: readonly number[]; + count: number; } export interface GuildScheduledEventEditOptions< Status extends GuildScheduledEventStatus, AcceptableStatus extends GuildScheduledEventSetStatusArg, -> extends Omit, 'channel'> { +> extends Omit, 'channel' | 'recurrenceRule'> { channel?: GuildVoiceChannelResolvable | null; status?: AcceptableStatus; + recurrenceRule?: GuildScheduledEventRecurrenceRuleOptions | null; } export interface GuildScheduledEventEntityMetadata { diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index f23023f1acca..1f1b34f3a0b6 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -209,6 +209,7 @@ import { ApplicationEmoji, ApplicationEmojiManager, StickerPack, + GuildScheduledEventManager, SendableChannels, PollData, } from '.'; @@ -2618,3 +2619,6 @@ client.on('interactionCreate', interaction => { interaction.channel.send({ embeds: [] }); } }); + +declare const guildScheduledEventManager: GuildScheduledEventManager; +await guildScheduledEventManager.edit(snowflake, { recurrenceRule: null }); From b20346f430fa835dcd543d36d6f258bb5a1e8178 Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:07:58 +0200 Subject: [PATCH 46/65] chore: unpin discord-api-types (#10524) * chore: unpin discord-api-types * chore: bump discord-api-types --- packages/builders/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/client.ts | 1 - packages/discord.js/package.json | 2 +- packages/discord.js/typings/index.d.ts | 18 ++- packages/formatters/package.json | 2 +- packages/next/package.json | 2 +- packages/rest/package.json | 2 +- packages/voice/package.json | 2 +- packages/ws/package.json | 2 +- pnpm-lock.yaml | 208 ++++++++++++------------- 11 files changed, 124 insertions(+), 119 deletions(-) diff --git a/packages/builders/package.json b/packages/builders/package.json index ebc3263a889f..426c15a2dc51 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -68,7 +68,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/util": "workspace:^", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.100", + "discord-api-types": "^0.37.101", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" diff --git a/packages/core/package.json b/packages/core/package.json index d1356c00b0ac..b4e74637898e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,7 @@ "@discordjs/ws": "workspace:^", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.100" + "discord-api-types": "^0.37.101" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 93eae08510ff..90eea1e2d8e9 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -196,7 +196,6 @@ export class Client extends AsyncEventEmitter { this.api = new API(rest); this.gateway.on(WebSocketShardEvents.Dispatch, (dispatch, shardId) => { - // @ts-expect-error event props can't be resolved properly, but they are correct this.emit(dispatch.t, this.toEventProps(dispatch.d, shardId)); }); } diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 22e27f87dd42..5aac93e7735f 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -72,7 +72,7 @@ "@discordjs/util": "workspace:^", "@discordjs/ws": "1.1.1", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.100", + "discord-api-types": "^0.37.101", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 57eed8b24e48..94b76ff48ef2 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -1610,7 +1610,7 @@ export abstract class GuildChannel extends BaseChannel { public get permissionsLocked(): boolean | null; public get position(): number; public rawPosition: number; - public type: Exclude; + public type: GuildChannelTypes; public get viewable(): boolean; public clone(options?: GuildChannelCloneOptions): Promise; public delete(reason?: string): Promise; @@ -2111,7 +2111,13 @@ export interface MessageCall { participants: readonly Snowflake[]; } -export type MessageComponentType = Exclude; +export type MessageComponentType = + | ComponentType.Button + | ComponentType.ChannelSelect + | ComponentType.MentionableSelect + | ComponentType.RoleSelect + | ComponentType.StringSelect + | ComponentType.UserSelect; export interface MessageCollectorOptionsParams< ComponentType extends MessageComponentType, @@ -2304,11 +2310,11 @@ export class MessageComponentInteraction e public get component(): CacheTypeReducer< Cached, MessageActionRowComponent, - Exclude>, - MessageActionRowComponent | Exclude>, - MessageActionRowComponent | Exclude> + APIMessageActionRowComponent, + MessageActionRowComponent | APIMessageActionRowComponent, + MessageActionRowComponent | APIMessageActionRowComponent >; - public componentType: Exclude; + public componentType: MessageComponentType; public customId: string; public channelId: Snowflake; public deferred: boolean; diff --git a/packages/formatters/package.json b/packages/formatters/package.json index dbd063df78fc..5e5f348e5db6 100644 --- a/packages/formatters/package.json +++ b/packages/formatters/package.json @@ -55,7 +55,7 @@ "homepage": "https://discord.js.org", "funding": "https://github.com/discordjs/discord.js?sponsor", "dependencies": { - "discord-api-types": "0.37.100" + "discord-api-types": "^0.37.101" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", diff --git a/packages/next/package.json b/packages/next/package.json index 513c02949be2..2ac8450050f9 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -72,7 +72,7 @@ "@discordjs/rest": "workspace:^", "@discordjs/util": "workspace:^", "@discordjs/ws": "workspace:^", - "discord-api-types": "0.37.100" + "discord-api-types": "^0.37.101" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", diff --git a/packages/rest/package.json b/packages/rest/package.json index eb2e8ae5f481..4288337d3280 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -88,7 +88,7 @@ "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.100", + "discord-api-types": "^0.37.101", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.19.8" diff --git a/packages/voice/package.json b/packages/voice/package.json index 5c11a768cfe7..101961ae7f6f 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -64,7 +64,7 @@ "funding": "https://github.com/discordjs/discord.js?sponsor", "dependencies": { "@types/ws": "^8.5.12", - "discord-api-types": "0.37.100", + "discord-api-types": "^0.37.101", "prism-media": "^1.3.5", "tslib": "^2.6.3", "ws": "^8.18.0" diff --git a/packages/ws/package.json b/packages/ws/package.json index 4f669a5a49d6..d49a1006d9b2 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -79,7 +79,7 @@ "@sapphire/async-queue": "^1.5.3", "@types/ws": "^8.5.12", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.100", + "discord-api-types": "^0.37.101", "tslib": "^2.6.3", "ws": "^8.18.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b67fad1ec60..2d28d3f5baf4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -680,8 +680,8 @@ importers: specifier: ^4.0.0 version: 4.0.0 discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -804,8 +804,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -941,8 +941,8 @@ importers: specifier: 3.5.3 version: 3.5.3 discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 @@ -1060,8 +1060,8 @@ importers: packages/formatters: dependencies: discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -1133,8 +1133,8 @@ importers: specifier: workspace:^ version: link:../ws discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -1307,8 +1307,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 magic-bytes.js: specifier: ^1.10.0 version: 1.10.0 @@ -1461,25 +1461,25 @@ importers: version: 4.1.0 '@storybook/addon-essentials': specifier: ^8.1.5 - version: 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-interactions': specifier: ^8.1.5 - version: 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) + version: 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) '@storybook/addon-links': specifier: ^8.1.5 - version: 8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-styling': specifier: ^1.3.7 version: 1.3.7(@types/react-dom@18.3.0)(@types/react@18.3.4)(encoding@0.1.13)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4) '@storybook/blocks': specifier: ^8.1.5 - version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/react': specifier: ^8.1.5 - version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) + version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) '@storybook/react-vite': specifier: ^8.1.5 - version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) + version: 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) '@storybook/testing-library': specifier: ^0.2.2 version: 0.2.2 @@ -1527,7 +1527,7 @@ importers: version: 15.8.1 storybook: specifier: ^8.1.5 - version: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + version: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) turbo: specifier: ^2.0.14 version: 2.0.14 @@ -1604,8 +1604,8 @@ importers: specifier: ^8.5.12 version: 8.5.12 discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 prism-media: specifier: ^1.3.5 version: 1.3.5 @@ -1701,8 +1701,8 @@ importers: specifier: ^2.4.6 version: 2.4.6 discord-api-types: - specifier: 0.37.100 - version: 0.37.100 + specifier: ^0.37.101 + version: 0.37.101 tslib: specifier: ^2.6.3 version: 2.6.3 @@ -7632,8 +7632,8 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - discord-api-types@0.37.100: - resolution: {integrity: sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==} + discord-api-types@0.37.101: + resolution: {integrity: sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==} discord-api-types@0.37.83: resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} @@ -18183,76 +18183,76 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@storybook/addon-actions@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-actions@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 polished: 4.3.1 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-backgrounds@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-controls@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: dequal: 2.0.3 lodash: 4.17.21 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-docs@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@babel/core': 7.25.2 '@mdx-js/react': 3.0.1(@types/react@18.3.4)(react@18.3.1) - '@storybook/blocks': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/blocks': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/react': 18.3.4 fs-extra: 11.2.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) rehype-external-links: 3.0.0 rehype-slug: 6.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-essentials@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': - dependencies: - '@storybook/addon-actions': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-backgrounds': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-controls': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-docs': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-highlight': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-measure': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-outline': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-toolbars': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-viewport': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@storybook/addon-essentials@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + dependencies: + '@storybook/addon-actions': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-backgrounds': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-controls': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-docs': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-highlight': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-measure': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-outline': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-toolbars': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-viewport': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-highlight@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-highlight@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/addon-interactions@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': + '@storybook/addon-interactions@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/test': 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) + '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/test': 8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) polished: 4.3.1 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 transitivePeerDependencies: - '@jest/globals' @@ -18261,25 +18261,25 @@ snapshots: - jest - vitest - '@storybook/addon-links@8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-links@8.2.9(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 - '@storybook/addon-measure@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-measure@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-outline@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 '@storybook/addon-styling@1.3.7(@types/react-dom@18.3.0)(@types/react@18.3.4)(encoding@0.1.13)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4)': @@ -18318,14 +18318,14 @@ snapshots: - supports-color - typescript - '@storybook/addon-toolbars@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-toolbars@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/addon-viewport@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-viewport@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: memoizerific: 1.11.3 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -18335,7 +18335,7 @@ snapshots: - react - react-dom - '@storybook/blocks@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/blocks@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 @@ -18348,7 +18348,7 @@ snapshots: memoizerific: 1.11.3 polished: 4.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) telejson: 7.2.0 ts-dedent: 2.2.0 util-deprecate: 1.0.2 @@ -18356,9 +18356,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': + '@storybook/builder-vite@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': dependencies: - '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/csf-plugin': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 1.5.4 @@ -18366,7 +18366,7 @@ snapshots: find-cache-dir: 3.3.2 fs-extra: 11.2.0 magic-string: 0.30.11 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 vite: 5.4.2(@types/node@18.19.45)(terser@5.31.6) optionalDependencies: @@ -18410,7 +18410,7 @@ snapshots: '@types/cross-spawn': 6.0.6 cross-spawn: 7.0.3 globby: 14.0.2 - jscodeshift: 0.15.2(@babel/preset-env@7.25.4) + jscodeshift: 0.15.2(@babel/preset-env@7.25.4(@babel/core@7.25.2)) lodash: 4.17.21 prettier: 3.3.3 recast: 0.23.9 @@ -18438,9 +18438,9 @@ snapshots: - '@types/react' - '@types/react-dom' - '@storybook/components@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/components@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/core-common@7.6.20(encoding@0.1.13)': dependencies: @@ -18497,9 +18497,9 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/csf-plugin@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) unplugin: 1.12.2 '@storybook/csf@0.1.11': @@ -18513,11 +18513,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/instrumenter@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 '@vitest/utils': 1.6.0 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) util: 0.12.5 '@storybook/manager-api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -18560,9 +18560,9 @@ snapshots: - react - react-dom - '@storybook/manager-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/manager-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/node-logger@7.6.20': {} @@ -18583,29 +18583,29 @@ snapshots: ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/preview-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/preview-api@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/react-dom-shim@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/react-dom-shim@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/react-vite@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': + '@storybook/react-vite@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.21.0)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) '@rollup/pluginutils': 5.1.0(rollup@4.21.0) - '@storybook/builder-vite': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) - '@storybook/react': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) + '@storybook/builder-vite': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.4.2(@types/node@18.19.45)(terser@5.31.6)) + '@storybook/react': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) find-up: 5.0.0 magic-string: 0.30.11 react: 18.3.1 react-docgen: 7.0.3 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) tsconfig-paths: 4.2.0 vite: 5.4.2(@types/node@18.19.45)(terser@5.31.6) transitivePeerDependencies: @@ -18615,14 +18615,14 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)': + '@storybook/react@8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)': dependencies: - '@storybook/components': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/components': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/preview-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/theming': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/manager-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/preview-api': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/react-dom-shim': 8.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/theming': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 '@types/node': 18.19.45 @@ -18637,7 +18637,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: 7.5.4 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 type-fest: 2.19.0 util-deprecate: 1.0.2 @@ -18656,16 +18656,16 @@ snapshots: memoizerific: 1.11.3 qs: 6.13.0 - '@storybook/test@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': + '@storybook/test@8.2.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': dependencies: '@storybook/csf': 0.1.11 - '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/instrumenter': 8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) + '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) '@vitest/expect': 1.6.0 '@vitest/spy': 1.6.0 - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) util: 0.12.5 transitivePeerDependencies: - '@jest/globals' @@ -18698,9 +18698,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/theming@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/theming@8.2.9(storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4) '@storybook/types@7.6.17': dependencies: @@ -18772,7 +18772,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': + '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@18.19.45)(ts-node@10.9.2(@types/node@18.19.45)(typescript@5.5.4)))(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.25.4 @@ -21486,7 +21486,7 @@ snapshots: dependencies: path-type: 4.0.0 - discord-api-types@0.37.100: {} + discord-api-types@0.37.101: {} discord-api-types@0.37.83: {} @@ -24307,7 +24307,7 @@ snapshots: jsbn@1.1.0: {} - jscodeshift@0.15.2(@babel/preset-env@7.25.4): + jscodeshift@0.15.2(@babel/preset-env@7.25.4(@babel/core@7.25.2)): dependencies: '@babel/core': 7.25.2 '@babel/parser': 7.25.4 @@ -27818,7 +27818,7 @@ snapshots: store2@2.14.3: {} - storybook@8.2.9(@babel/preset-env@7.25.4)(bufferutil@4.0.8)(utf-8-validate@6.0.4): + storybook@8.2.9(@babel/preset-env@7.25.4(@babel/core@7.25.2))(bufferutil@4.0.8)(utf-8-validate@6.0.4): dependencies: '@babel/core': 7.25.2 '@babel/types': 7.25.4 @@ -27838,7 +27838,7 @@ snapshots: fs-extra: 11.2.0 giget: 1.2.3 globby: 14.0.2 - jscodeshift: 0.15.2(@babel/preset-env@7.25.4) + jscodeshift: 0.15.2(@babel/preset-env@7.25.4(@babel/core@7.25.2)) leven: 3.1.0 ora: 5.4.1 prettier: 3.3.3 From c633d5c7f69a4c4cff98669c596d64bef7a74900 Mon Sep 17 00:00:00 2001 From: Moebits <37512637+Moebits@users.noreply.github.com> Date: Tue, 1 Oct 2024 08:33:40 -0400 Subject: [PATCH 47/65] feat: Add ApplicationEmoji to EmojiResolvable and MessageReaction#emoji (#10477) * types: add ApplicationEmoji to EmojiResolvable * typings: add ApplicationEmoji to MessageReaction#emoji * removed ApplicationEmoji from MessageReaction * update BaseGuildEmojiManager * chore: lint error * feat: add ApplicationEmoji to MessageReaction#emoji getter * refactor: check application emojis first --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/managers/BaseGuildEmojiManager.js | 7 ++++++- .../discord.js/src/structures/MessageReaction.js | 15 ++++++++++++--- packages/discord.js/typings/index.d.ts | 6 +++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/discord.js/src/managers/BaseGuildEmojiManager.js b/packages/discord.js/src/managers/BaseGuildEmojiManager.js index 89eee4c95a32..305c1aaa9594 100644 --- a/packages/discord.js/src/managers/BaseGuildEmojiManager.js +++ b/packages/discord.js/src/managers/BaseGuildEmojiManager.js @@ -1,6 +1,7 @@ 'use strict'; const CachedManager = require('./CachedManager'); +const ApplicationEmoji = require('../structures/ApplicationEmoji'); const GuildEmoji = require('../structures/GuildEmoji'); const ReactionEmoji = require('../structures/ReactionEmoji'); const { parseEmoji } = require('../util/Util'); @@ -25,7 +26,8 @@ class BaseGuildEmojiManager extends CachedManager { * * A Snowflake * * A GuildEmoji object * * A ReactionEmoji object - * @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable + * * An ApplicationEmoji object + * @typedef {Snowflake|GuildEmoji|ReactionEmoji|ApplicationEmoji} EmojiResolvable */ /** @@ -35,6 +37,7 @@ class BaseGuildEmojiManager extends CachedManager { */ resolve(emoji) { if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id); + if (emoji instanceof ApplicationEmoji) return super.resolve(emoji.id); return super.resolve(emoji); } @@ -45,6 +48,7 @@ class BaseGuildEmojiManager extends CachedManager { */ resolveId(emoji) { if (emoji instanceof ReactionEmoji) return emoji.id; + if (emoji instanceof ApplicationEmoji) return emoji.id; return super.resolveId(emoji); } @@ -65,6 +69,7 @@ class BaseGuildEmojiManager extends CachedManager { const emojiResolvable = this.resolve(emoji); if (emojiResolvable) return emojiResolvable.identifier; if (emoji instanceof ReactionEmoji) return emoji.identifier; + if (emoji instanceof ApplicationEmoji) return emoji.identifier; if (typeof emoji === 'string') { const res = parseEmoji(emoji); if (res?.name.length) { diff --git a/packages/discord.js/src/structures/MessageReaction.js b/packages/discord.js/src/structures/MessageReaction.js index 45103b553c01..283156465df3 100644 --- a/packages/discord.js/src/structures/MessageReaction.js +++ b/packages/discord.js/src/structures/MessageReaction.js @@ -1,6 +1,7 @@ 'use strict'; const { Routes } = require('discord-api-types/v10'); +const ApplicationEmoji = require('./ApplicationEmoji'); const GuildEmoji = require('./GuildEmoji'); const ReactionEmoji = require('./ReactionEmoji'); const ReactionUserManager = require('../managers/ReactionUserManager'); @@ -108,16 +109,24 @@ class MessageReaction { } /** - * The emoji of this reaction. Either a {@link GuildEmoji} object for known custom emojis, or a {@link ReactionEmoji} - * object which has fewer properties. Whatever the prototype of the emoji, it will still have + * The emoji of this reaction. Either a {@link GuildEmoji} object for known custom emojis, + * {@link ApplicationEmoji} for application emojis, or a {@link ReactionEmoji} object + * which has fewer properties. Whatever the prototype of the emoji, it will still have * `name`, `id`, `identifier` and `toString()` - * @type {GuildEmoji|ReactionEmoji} + * @type {GuildEmoji|ReactionEmoji|ApplicationEmoji} * @readonly */ get emoji() { if (this._emoji instanceof GuildEmoji) return this._emoji; + if (this._emoji instanceof ApplicationEmoji) return this._emoji; // Check to see if the emoji has become known to the client if (this._emoji.id) { + const applicationEmojis = this.message.client.application.emojis.cache; + if (applicationEmojis.has(this._emoji.id)) { + const emoji = applicationEmojis.get(this._emoji.id); + this._emoji = emoji; + return emoji; + } const emojis = this.message.client.emojis.cache; if (emojis.has(this._emoji.id)) { const emoji = emojis.get(this._emoji.id); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 94b76ff48ef2..c28be62901c2 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2462,13 +2462,13 @@ export class MessagePayload { export class MessageReaction { private constructor(client: Client, data: RawMessageReactionData, message: Message); - private _emoji: GuildEmoji | ReactionEmoji; + private _emoji: GuildEmoji | ReactionEmoji | ApplicationEmoji; public burstColors: string[] | null; public readonly client: Client; public count: number; public countDetails: ReactionCountDetailsData; - public get emoji(): GuildEmoji | ReactionEmoji; + public get emoji(): GuildEmoji | ReactionEmoji | ApplicationEmoji; public me: boolean; public meBurst: boolean; public message: Message | PartialMessage; @@ -5709,7 +5709,7 @@ export type EmojiIdentifierResolvable = | `<${'' | 'a'}:${string}:${Snowflake}>` | string; -export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji; +export type EmojiResolvable = Snowflake | GuildEmoji | ReactionEmoji | ApplicationEmoji; export interface FetchApplicationCommandOptions extends BaseFetchOptions { guildId?: Snowflake; From ab32f26cbbcf50e7635a1acefb9fc650d1b0b572 Mon Sep 17 00:00:00 2001 From: Denis Cristea Date: Tue, 1 Oct 2024 19:11:56 +0300 Subject: [PATCH 48/65] refactor: builders (#10448) BREAKING CHANGE: formatters export removed (prev. deprecated) BREAKING CHANGE: `SelectMenuBuilder` and `SelectMenuOptionBuilder` have been removed (prev. deprecated) BREAKING CHANGE: `EmbedBuilder` no longer takes camalCase options BREAKING CHANGE: `ActionRowBuilder` now has specialized `[add/set]X` methods as opposed to the current `[add/set]Components` BREAKING CHANGE: Removed `equals` methods BREAKING CHANGE: Sapphire -> zod for validation BREAKING CHANGE: Removed the ability to pass `null`/`undefined` to clear fields, use `clearX()` instead BREAKING CHANGE: Renamed all "slash command" symbols to instead use "chat input command" BREAKING CHANGE: Removed `ContextMenuCommandBuilder` in favor of `MessageCommandBuilder` and `UserCommandBuilder` BREAKING CHANGE: Removed support for passing the "string key"s of enums BREAKING CHANGE: Removed `Button` class in favor for specialized classes depending on the style BREAKING CHANGE: Removed nested `addX` styled-methods in favor of plural `addXs` Co-authored-by: Vlad Frangu Co-authored-by: Almeida --- .../__tests__/components/actionRow.test.ts | 73 ++- .../__tests__/components/button.test.ts | 131 +--- .../__tests__/components/components.test.ts | 6 +- .../__tests__/components/selectMenu.test.ts | 115 ++-- .../__tests__/components/textInput.test.ts | 62 +- .../ChatInputCommands.test.ts | 516 +++++++++++++++ .../Options.test.ts | 39 +- .../interactions/ContextMenuCommands.test.ts | 116 +--- .../SlashCommands/SlashCommands.test.ts | 593 ------------------ .../__tests__/interactions/modal.test.ts | 125 +--- .../builders/__tests__/messages/embed.test.ts | 220 ++++--- packages/builders/__tests__/types.test.ts | 14 +- packages/builders/package.json | 8 +- packages/builders/src/Assertions.ts | 20 + packages/builders/src/components/ActionRow.ts | 340 ++++++++-- .../builders/src/components/Assertions.ts | 277 ++++---- packages/builders/src/components/Component.ts | 34 +- .../builders/src/components/Components.ts | 91 ++- .../builders/src/components/button/Button.ts | 143 +---- .../src/components/button/CustomIdButton.ts | 69 ++ .../src/components/button/LinkButton.ts | 34 + .../src/components/button/PremiumButton.ts | 26 + .../button/mixins/EmojiOrLabelButtonMixin.ts | 44 ++ .../components/selectMenu/BaseSelectMenu.ts | 41 +- .../selectMenu/ChannelSelectMenu.ts | 30 +- .../selectMenu/MentionableSelectMenu.ts | 27 +- .../components/selectMenu/RoleSelectMenu.ts | 25 +- .../components/selectMenu/StringSelectMenu.ts | 102 +-- .../selectMenu/StringSelectMenuOption.ts | 54 +- .../components/selectMenu/UserSelectMenu.ts | 27 +- .../src/components/textInput/Assertions.ts | 45 +- .../src/components/textInput/TextInput.ts | 94 +-- packages/builders/src/index.ts | 101 +-- .../src/interactions/commands/Command.ts | 83 +++ .../src/interactions/commands/SharedName.ts | 64 ++ .../commands/SharedNameAndDescription.ts | 67 ++ .../commands/chatInput/Assertions.ts | 154 +++++ .../commands/chatInput/ChatInputCommand.ts | 37 ++ .../chatInput/ChatInputCommandSubcommands.ts | 111 ++++ ...ionCommandNumericOptionMinMaxValueMixin.ts | 47 ++ ...plicationCommandOptionChannelTypesMixin.ts | 52 ++ ...ationCommandOptionWithAutocompleteMixin.ts | 29 + ...pplicationCommandOptionWithChoicesMixin.ts | 38 ++ .../mixins/SharedChatInputCommandOptions.ts | 200 ++++++ .../chatInput/mixins/SharedSubcommands.ts | 60 ++ .../options/ApplicationCommandOptionBase.ts | 59 ++ .../commands/chatInput/options/attachment.ts | 11 + .../commands/chatInput/options/boolean.ts | 11 + .../commands/chatInput/options/channel.ts | 19 + .../commands/chatInput/options/integer.ts | 23 + .../commands/chatInput/options/mentionable.ts | 11 + .../commands/chatInput/options/number.ts | 23 + .../commands/chatInput/options/role.ts | 11 + .../commands/chatInput/options/string.ts | 65 ++ .../commands/chatInput/options/user.ts | 11 + .../commands/contextMenu/Assertions.ts | 30 + .../contextMenu/ContextMenuCommand.ts | 29 + .../commands/contextMenu/MessageCommand.ts | 19 + .../commands/contextMenu/UserCommand.ts | 19 + .../contextMenuCommands/Assertions.ts | 65 -- .../ContextMenuCommandBuilder.ts | 239 ------- .../src/interactions/modals/Assertions.ts | 42 +- .../builders/src/interactions/modals/Modal.ts | 132 +++- .../interactions/slashCommands/Assertions.ts | 123 ---- .../slashCommands/SlashCommandBuilder.ts | 110 ---- .../slashCommands/SlashCommandSubcommands.ts | 131 ---- ...ionCommandNumericOptionMinMaxValueMixin.ts | 28 - .../mixins/ApplicationCommandOptionBase.ts | 57 -- ...plicationCommandOptionChannelTypesMixin.ts | 54 -- ...ationCommandOptionWithAutocompleteMixin.ts | 39 -- ...pplicationCommandOptionWithChoicesMixin.ts | 83 --- .../mixins/NameAndDescription.ts | 142 ----- .../mixins/SharedSlashCommand.ts | 162 ----- .../mixins/SharedSlashCommandOptions.ts | 145 ----- .../slashCommands/mixins/SharedSubcommands.ts | 66 -- .../slashCommands/options/attachment.ts | 21 - .../slashCommands/options/boolean.ts | 21 - .../slashCommands/options/channel.ts | 26 - .../slashCommands/options/integer.ts | 67 -- .../slashCommands/options/mentionable.ts | 21 - .../slashCommands/options/number.ts | 67 -- .../slashCommands/options/role.ts | 21 - .../slashCommands/options/string.ts | 73 --- .../slashCommands/options/user.ts | 21 - .../builders/src/messages/embed/Assertions.ts | 155 ++--- packages/builders/src/messages/embed/Embed.ts | 306 +++++---- .../src/messages/embed/EmbedAuthor.ts | 82 +++ .../builders/src/messages/embed/EmbedField.ts | 66 ++ .../src/messages/embed/EmbedFooter.ts | 64 ++ packages/builders/src/util/resolveBuilder.ts | 40 ++ pnpm-lock.yaml | 22 +- 91 files changed, 3782 insertions(+), 3834 deletions(-) create mode 100644 packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts rename packages/builders/__tests__/interactions/{SlashCommands => ChatInputCommands}/Options.test.ts (85%) delete mode 100644 packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts create mode 100644 packages/builders/src/Assertions.ts create mode 100644 packages/builders/src/components/button/CustomIdButton.ts create mode 100644 packages/builders/src/components/button/LinkButton.ts create mode 100644 packages/builders/src/components/button/PremiumButton.ts create mode 100644 packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts create mode 100644 packages/builders/src/interactions/commands/Command.ts create mode 100644 packages/builders/src/interactions/commands/SharedName.ts create mode 100644 packages/builders/src/interactions/commands/SharedNameAndDescription.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/Assertions.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/attachment.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/boolean.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/channel.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/integer.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/mentionable.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/number.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/role.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/string.ts create mode 100644 packages/builders/src/interactions/commands/chatInput/options/user.ts create mode 100644 packages/builders/src/interactions/commands/contextMenu/Assertions.ts create mode 100644 packages/builders/src/interactions/commands/contextMenu/ContextMenuCommand.ts create mode 100644 packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts create mode 100644 packages/builders/src/interactions/commands/contextMenu/UserCommand.ts delete mode 100644 packages/builders/src/interactions/contextMenuCommands/Assertions.ts delete mode 100644 packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts delete mode 100644 packages/builders/src/interactions/slashCommands/Assertions.ts delete mode 100644 packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts delete mode 100644 packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts delete mode 100644 packages/builders/src/interactions/slashCommands/mixins/SharedSubcommands.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/attachment.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/boolean.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/channel.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/integer.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/mentionable.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/number.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/role.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/string.ts delete mode 100644 packages/builders/src/interactions/slashCommands/options/user.ts create mode 100644 packages/builders/src/messages/embed/EmbedAuthor.ts create mode 100644 packages/builders/src/messages/embed/EmbedField.ts create mode 100644 packages/builders/src/messages/embed/EmbedFooter.ts create mode 100644 packages/builders/src/util/resolveBuilder.ts diff --git a/packages/builders/__tests__/components/actionRow.test.ts b/packages/builders/__tests__/components/actionRow.test.ts index b9f63b501529..4c461129eed9 100644 --- a/packages/builders/__tests__/components/actionRow.test.ts +++ b/packages/builders/__tests__/components/actionRow.test.ts @@ -7,8 +7,8 @@ import { import { describe, test, expect } from 'vitest'; import { ActionRowBuilder, - ButtonBuilder, createComponentBuilder, + PrimaryButtonBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, } from '../../src/index.js'; @@ -41,21 +41,14 @@ const rowWithSelectMenuData: APIActionRowComponent value: 'two', }, ], - max_values: 10, - min_values: 12, + max_values: 2, + min_values: 2, }, ], }; describe('Action Row Components', () => { describe('Assertion Tests', () => { - test('GIVEN valid components THEN do not throw', () => { - expect(() => new ActionRowBuilder().addComponents(new ButtonBuilder())).not.toThrowError(); - expect(() => new ActionRowBuilder().setComponents(new ButtonBuilder())).not.toThrowError(); - expect(() => new ActionRowBuilder().addComponents([new ButtonBuilder()])).not.toThrowError(); - expect(() => new ActionRowBuilder().setComponents([new ButtonBuilder()])).not.toThrowError(); - }); - test('GIVEN valid JSON input THEN valid JSON output is given', () => { const actionRowData: APIActionRowComponent = { type: ComponentType.ActionRow, @@ -72,22 +65,10 @@ describe('Action Row Components', () => { style: ButtonStyle.Link, url: 'https://google.com', }, - { - type: ComponentType.StringSelect, - placeholder: 'test', - custom_id: 'test', - options: [ - { - label: 'option', - value: 'option', - }, - ], - }, ], }; expect(new ActionRowBuilder(actionRowData).toJSON()).toEqual(actionRowData); - expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] }); expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError(); }); @@ -120,24 +101,23 @@ describe('Action Row Components', () => { value: 'two', }, ], - max_values: 10, - min_values: 12, + max_values: 1, + min_values: 1, }, ], }; expect(new ActionRowBuilder(rowWithButtonData).toJSON()).toEqual(rowWithButtonData); expect(new ActionRowBuilder(rowWithSelectMenuData).toJSON()).toEqual(rowWithSelectMenuData); - expect(new ActionRowBuilder().toJSON()).toEqual({ type: ComponentType.ActionRow, components: [] }); expect(() => createComponentBuilder({ type: ComponentType.ActionRow, components: [] })).not.toThrowError(); }); test('GIVEN valid builder options THEN valid JSON output is given 2', () => { - const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); + const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123'); const selectMenu = new StringSelectMenuBuilder() .setCustomId('1234') - .setMaxValues(10) - .setMinValues(12) + .setMaxValues(2) + .setMinValues(2) .setOptions( new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'), new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'), @@ -147,10 +127,39 @@ describe('Action Row Components', () => { new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'), ]); - expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData); - expect(new ActionRowBuilder().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData); - expect(new ActionRowBuilder().addComponents([button]).toJSON()).toEqual(rowWithButtonData); - expect(new ActionRowBuilder().addComponents([selectMenu]).toJSON()).toEqual(rowWithSelectMenuData); + expect(new ActionRowBuilder().addPrimaryButtonComponents(button).toJSON()).toEqual(rowWithButtonData); + expect(new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).toJSON()).toEqual(rowWithSelectMenuData); + expect(new ActionRowBuilder().addPrimaryButtonComponents([button]).toJSON()).toEqual(rowWithButtonData); + }); + + test('GIVEN 2 select menus THEN it throws', () => { + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('1234') + .setOptions( + new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'), + new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'), + ); + + expect(() => + new ActionRowBuilder() + .addStringSelectMenuComponent(selectMenu) + .addStringSelectMenuComponent(selectMenu) + .toJSON(), + ).toThrowError(); + }); + + test('GIVEN a button and a select menu THEN it throws', () => { + const button = new PrimaryButtonBuilder().setLabel('test').setCustomId('123'); + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('1234') + .setOptions( + new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'), + new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'), + ); + + expect(() => + new ActionRowBuilder().addStringSelectMenuComponent(selectMenu).addPrimaryButtonComponents(button).toJSON(), + ).toThrowError(); }); }); }); diff --git a/packages/builders/__tests__/components/button.test.ts b/packages/builders/__tests__/components/button.test.ts index 0eb5134d4312..7da9abe0fb42 100644 --- a/packages/builders/__tests__/components/button.test.ts +++ b/packages/builders/__tests__/components/button.test.ts @@ -5,45 +5,21 @@ import { type APIButtonComponentWithURL, } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { buttonLabelValidator, buttonStyleValidator } from '../../src/components/Assertions.js'; import { ButtonBuilder } from '../../src/components/button/Button.js'; - -const buttonComponent = () => new ButtonBuilder(); +import { PrimaryButtonBuilder, PremiumButtonBuilder, LinkButtonBuilder } from '../../src/index.js'; const longStr = 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'; describe('Button Components', () => { describe('Assertion Tests', () => { - test('GIVEN valid label THEN validator does not throw', () => { - expect(() => buttonLabelValidator.parse('foobar')).not.toThrowError(); - }); - - test('GIVEN invalid label THEN validator does throw', () => { - expect(() => buttonLabelValidator.parse(null)).toThrowError(); - expect(() => buttonLabelValidator.parse('')).toThrowError(); - - expect(() => buttonLabelValidator.parse(longStr)).toThrowError(); - }); - - test('GIVEN valid style THEN validator does not throw', () => { - expect(() => buttonStyleValidator.parse(3)).not.toThrowError(); - expect(() => buttonStyleValidator.parse(ButtonStyle.Secondary)).not.toThrowError(); - }); - - test('GIVEN invalid style THEN validator does throw', () => { - expect(() => buttonStyleValidator.parse(7)).toThrowError(); - }); - test('GIVEN valid fields THEN builder does not throw', () => { - expect(() => - buttonComponent().setCustomId('custom').setStyle(ButtonStyle.Primary).setLabel('test'), - ).not.toThrowError(); + expect(() => new PrimaryButtonBuilder().setCustomId('custom').setLabel('test')).not.toThrowError(); expect(() => { - const button = buttonComponent() + const button = new PrimaryButtonBuilder() .setCustomId('custom') - .setStyle(ButtonStyle.Primary) + .setLabel('test') .setDisabled(true) .setEmoji({ name: 'test' }); @@ -51,111 +27,41 @@ describe('Button Components', () => { }).not.toThrowError(); expect(() => { - const button = buttonComponent().setSKUId('123456789012345678').setStyle(ButtonStyle.Premium); + const button = new PremiumButtonBuilder().setSKUId('123456789012345678'); button.toJSON(); }).not.toThrowError(); - expect(() => buttonComponent().setURL('https://google.com')).not.toThrowError(); + expect(() => new LinkButtonBuilder().setURL('https://google.com')).not.toThrowError(); }); test('GIVEN invalid fields THEN build does throw', () => { expect(() => { - buttonComponent().setCustomId(longStr); - }).toThrowError(); - - expect(() => { - const button = buttonComponent() - .setCustomId('custom') - .setStyle(ButtonStyle.Primary) - .setDisabled(true) - .setLabel('test') - .setURL('https://google.com') - .setEmoji({ name: 'test' }); - - button.toJSON(); + new PrimaryButtonBuilder().setCustomId(longStr).toJSON(); }).toThrowError(); expect(() => { // @ts-expect-error: Invalid emoji - const button = buttonComponent().setEmoji('test'); - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent().setStyle(ButtonStyle.Primary); - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent().setStyle(ButtonStyle.Primary).setCustomId('test'); - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent().setStyle(ButtonStyle.Link); - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent().setStyle(ButtonStyle.Primary).setLabel('test').setURL('https://google.com'); + const button = new PrimaryButtonBuilder().setEmoji('test'); button.toJSON(); }).toThrowError(); expect(() => { - const button = buttonComponent().setStyle(ButtonStyle.Link).setLabel('test'); + const button = new PrimaryButtonBuilder(); button.toJSON(); }).toThrowError(); expect(() => { - const button = buttonComponent().setStyle(ButtonStyle.Primary).setSKUId('123456789012345678'); - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent() - .setStyle(ButtonStyle.Secondary) - .setLabel('button') - .setSKUId('123456789012345678'); - - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent() - .setStyle(ButtonStyle.Success) - .setEmoji({ name: '😇' }) - .setSKUId('123456789012345678'); - - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent() - .setStyle(ButtonStyle.Danger) - .setCustomId('test') - .setSKUId('123456789012345678'); - - button.toJSON(); - }).toThrowError(); - - expect(() => { - const button = buttonComponent() - .setStyle(ButtonStyle.Link) - .setURL('https://google.com') - .setSKUId('123456789012345678'); - + const button = new PrimaryButtonBuilder().setCustomId('test'); button.toJSON(); }).toThrowError(); // @ts-expect-error: Invalid style - expect(() => buttonComponent().setStyle(24)).toThrowError(); - expect(() => buttonComponent().setLabel(longStr)).toThrowError(); + expect(() => new PrimaryButtonBuilder().setCustomId('hi').setStyle(24).toJSON()).toThrowError(); + expect(() => new PrimaryButtonBuilder().setCustomId('hi').setLabel(longStr).toJSON()).toThrowError(); // @ts-expect-error: Invalid parameter for disabled - expect(() => buttonComponent().setDisabled(0)).toThrowError(); + expect(() => new PrimaryButtonBuilder().setCustomId('hi').setDisabled(0).toJSON()).toThrowError(); // @ts-expect-error: Invalid emoji - expect(() => buttonComponent().setEmoji('foo')).toThrowError(); - - expect(() => buttonComponent().setURL('foobar')).toThrowError(); + expect(() => new PrimaryButtonBuilder().setCustomId('hi').setEmoji('foo').toJSON()).toThrowError(); }); test('GiVEN valid input THEN valid JSON outputs are given', () => { @@ -167,13 +73,12 @@ describe('Button Components', () => { disabled: true, }; - expect(new ButtonBuilder(interactionData).toJSON()).toEqual(interactionData); + expect(new PrimaryButtonBuilder(interactionData).toJSON()).toEqual(interactionData); expect( - buttonComponent() + new PrimaryButtonBuilder() .setCustomId(interactionData.custom_id) .setLabel(interactionData.label!) - .setStyle(interactionData.style) .setDisabled(interactionData.disabled) .toJSON(), ).toEqual(interactionData); @@ -186,9 +91,7 @@ describe('Button Components', () => { url: 'https://google.com', }; - expect(new ButtonBuilder(linkData).toJSON()).toEqual(linkData); - - expect(buttonComponent().setLabel(linkData.label!).setDisabled(true).setURL(linkData.url)); + expect(new LinkButtonBuilder(linkData).toJSON()).toEqual(linkData); }); }); }); diff --git a/packages/builders/__tests__/components/components.test.ts b/packages/builders/__tests__/components/components.test.ts index fa0bd4607f65..0612d5a20ed2 100644 --- a/packages/builders/__tests__/components/components.test.ts +++ b/packages/builders/__tests__/components/components.test.ts @@ -11,14 +11,14 @@ import { import { describe, test, expect } from 'vitest'; import { ActionRowBuilder, - ButtonBuilder, createComponentBuilder, + CustomIdButtonBuilder, StringSelectMenuBuilder, TextInputBuilder, } from '../../src/index.js'; describe('createComponentBuilder', () => { - test.each([ButtonBuilder, StringSelectMenuBuilder, TextInputBuilder])( + test.each([StringSelectMenuBuilder, TextInputBuilder])( 'passing an instance of %j should return itself', (Builder) => { const builder = new Builder(); @@ -42,7 +42,7 @@ describe('createComponentBuilder', () => { type: ComponentType.Button, }; - expect(createComponentBuilder(button)).toBeInstanceOf(ButtonBuilder); + expect(createComponentBuilder(button)).toBeInstanceOf(CustomIdButtonBuilder); }); test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => { diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index 6e0c887dc274..455813c21b5e 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -3,6 +3,7 @@ import { describe, test, expect } from 'vitest'; import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js'; const selectMenu = () => new StringSelectMenuBuilder(); +const selectMenuWithId = () => new StringSelectMenuBuilder({ custom_id: 'hi' }); const selectMenuOption = () => new StringSelectMenuOptionBuilder(); const longStr = 'a'.repeat(256); @@ -16,10 +17,10 @@ const selectMenuOptionData: APISelectMenuOption = { }; const selectMenuDataWithoutOptions = { - type: ComponentType.SelectMenu, + type: ComponentType.StringSelect, custom_id: 'test', - max_values: 10, - min_values: 3, + max_values: 1, + min_values: 1, disabled: true, placeholder: 'test', } as const; @@ -109,49 +110,87 @@ describe('Select Menu Components', () => { }); test('GIVEN invalid inputs THEN Select Menu does throw', () => { - expect(() => selectMenu().setCustomId(longStr)).toThrowError(); - expect(() => selectMenu().setMaxValues(30)).toThrowError(); - expect(() => selectMenu().setMinValues(-20)).toThrowError(); + expect(() => selectMenu().setCustomId(longStr).toJSON()).toThrowError(); + expect(() => selectMenuWithId().setMaxValues(30).toJSON()).toThrowError(); + expect(() => selectMenuWithId().setMinValues(-20).toJSON()).toThrowError(); // @ts-expect-error: Invalid disabled value - expect(() => selectMenu().setDisabled(0)).toThrowError(); - expect(() => selectMenu().setPlaceholder(longStr)).toThrowError(); + expect(() => selectMenuWithId().setDisabled(0).toJSON()).toThrowError(); + expect(() => selectMenuWithId().setPlaceholder(longStr).toJSON()).toThrowError(); // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions({ label: 'test' })).toThrowError(); - expect(() => selectMenu().addOptions({ label: longStr, value: 'test' })).toThrowError(); - expect(() => selectMenu().addOptions({ value: longStr, label: 'test' })).toThrowError(); - expect(() => selectMenu().addOptions({ label: 'test', value: 'test', description: longStr })).toThrowError(); - // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions({ label: 'test', value: 'test', default: 100 })).toThrowError(); - // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError(); - // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions({ default: true })).toThrowError(); - // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions([{ label: 'test' }])).toThrowError(); - expect(() => selectMenu().addOptions([{ label: longStr, value: 'test' }])).toThrowError(); - expect(() => selectMenu().addOptions([{ value: longStr, label: 'test' }])).toThrowError(); - expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', description: longStr }])).toThrowError(); - // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions([{ label: 'test', value: 'test', default: 100 }])).toThrowError(); + expect(() => selectMenuWithId().addOptions({ label: 'test' }).toJSON()).toThrowError(); + expect(() => selectMenuWithId().addOptions({ label: longStr, value: 'test' }).toJSON()).toThrowError(); + expect(() => selectMenuWithId().addOptions({ value: longStr, label: 'test' }).toJSON()).toThrowError(); + expect(() => + selectMenuWithId().addOptions({ label: 'test', value: 'test', description: longStr }).toJSON(), + ).toThrowError(); + expect(() => + // @ts-expect-error: Invalid option + selectMenuWithId().addOptions({ label: 'test', value: 'test', default: 100 }).toJSON(), + ).toThrowError(); // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions([{ value: 'test' }])).toThrowError(); + expect(() => selectMenuWithId().addOptions({ value: 'test' }).toJSON()).toThrowError(); // @ts-expect-error: Invalid option - expect(() => selectMenu().addOptions([{ default: true }])).toThrowError(); + expect(() => selectMenuWithId().addOptions({ default: true }).toJSON()).toThrowError(); + expect(() => + selectMenuWithId() + // @ts-expect-error: Invalid option + .addOptions([{ label: 'test' }]) + .toJSON(), + ).toThrowError(); + expect(() => + selectMenuWithId() + .addOptions([{ label: longStr, value: 'test' }]) + .toJSON(), + ).toThrowError(); + expect(() => + selectMenuWithId() + .addOptions([{ value: longStr, label: 'test' }]) + .toJSON(), + ).toThrowError(); + expect(() => + selectMenuWithId() + .addOptions([{ label: 'test', value: 'test', description: longStr }]) + .toJSON(), + ).toThrowError(); + expect(() => + selectMenuWithId() + // @ts-expect-error: Invalid option + .addOptions([{ label: 'test', value: 'test', default: 100 }]) + .toJSON(), + ).toThrowError(); + expect(() => + selectMenuWithId() + // @ts-expect-error: Invalid option + .addOptions([{ value: 'test' }]) + .toJSON(), + ).toThrowError(); + expect(() => + selectMenuWithId() + // @ts-expect-error: Invalid option + .addOptions([{ default: true }]) + .toJSON(), + ).toThrowError(); const tooManyOptions = Array.from({ length: 26 }).fill({ label: 'test', value: 'test' }); - expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError(); - expect(() => selectMenu().setOptions(tooManyOptions)).toThrowError(); + expect(() => + selectMenu() + .setOptions(...tooManyOptions) + .toJSON(), + ).toThrowError(); + expect(() => selectMenu().setOptions(tooManyOptions).toJSON()).toThrowError(); expect(() => selectMenu() .addOptions({ label: 'test', value: 'test' }) - .addOptions(...tooManyOptions), + .addOptions(...tooManyOptions) + .toJSON(), ).toThrowError(); expect(() => selectMenu() .addOptions([{ label: 'test', value: 'test' }]) - .addOptions(tooManyOptions), + .addOptions(tooManyOptions) + .toJSON(), ).toThrowError(); expect(() => { @@ -162,7 +201,8 @@ describe('Select Menu Components', () => { .setDefault(-1) // @ts-expect-error: Invalid emoji .setEmoji({ name: 1 }) - .setDescription(longStr); + .setDescription(longStr) + .toJSON(); }).toThrowError(); }); @@ -212,17 +252,16 @@ describe('Select Menu Components', () => { ).toStrictEqual([selectMenuOptionData]); expect(() => - makeStringSelectMenuWithOptions().spliceOptions( - 0, - 0, - ...Array.from({ length: 26 }, () => selectMenuOptionData), - ), + makeStringSelectMenuWithOptions() + .spliceOptions(0, 0, ...Array.from({ length: 26 }, () => selectMenuOptionData)) + .toJSON(), ).toThrowError(); expect(() => makeStringSelectMenuWithOptions() .setOptions(Array.from({ length: 25 }, () => selectMenuOptionData)) - .spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData), + .spliceOptions(-1, 2, selectMenuOptionData, selectMenuOptionData) + .toJSON(), ).toThrowError(); }); }); diff --git a/packages/builders/__tests__/components/textInput.test.ts b/packages/builders/__tests__/components/textInput.test.ts index ab09ffe009b4..162bd7249f08 100644 --- a/packages/builders/__tests__/components/textInput.test.ts +++ b/packages/builders/__tests__/components/textInput.test.ts @@ -1,13 +1,5 @@ import { ComponentType, TextInputStyle, type APITextInputComponent } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { - labelValidator, - maxLengthValidator, - minLengthValidator, - placeholderValidator, - valueValidator, - textInputStyleValidator, -} from '../../src/components/textInput/Assertions.js'; import { TextInputBuilder } from '../../src/components/textInput/TextInput.js'; const superLongStr = 'a'.repeat(5_000); @@ -16,56 +8,6 @@ const textInputComponent = () => new TextInputBuilder(); describe('Text Input Components', () => { describe('Assertion Tests', () => { - test('GIVEN valid label THEN validator does not throw', () => { - expect(() => labelValidator.parse('foobar')).not.toThrowError(); - }); - - test('GIVEN invalid label THEN validator does throw', () => { - expect(() => labelValidator.parse(24)).toThrowError(); - expect(() => labelValidator.parse(undefined)).toThrowError(); - }); - - test('GIVEN valid style THEN validator does not throw', () => { - expect(() => textInputStyleValidator.parse(TextInputStyle.Paragraph)).not.toThrowError(); - expect(() => textInputStyleValidator.parse(TextInputStyle.Short)).not.toThrowError(); - }); - - test('GIVEN invalid style THEN validator does throw', () => { - expect(() => textInputStyleValidator.parse(24)).toThrowError(); - }); - - test('GIVEN valid min length THEN validator does not throw', () => { - expect(() => minLengthValidator.parse(10)).not.toThrowError(); - }); - - test('GIVEN invalid min length THEN validator does throw', () => { - expect(() => minLengthValidator.parse(-1)).toThrowError(); - }); - - test('GIVEN valid max length THEN validator does not throw', () => { - expect(() => maxLengthValidator.parse(10)).not.toThrowError(); - }); - - test('GIVEN invalid min length THEN validator does throw 2', () => { - expect(() => maxLengthValidator.parse(4_001)).toThrowError(); - }); - - test('GIVEN valid value THEN validator does not throw', () => { - expect(() => valueValidator.parse('foobar')).not.toThrowError(); - }); - - test('GIVEN invalid value THEN validator does throw', () => { - expect(() => valueValidator.parse(superLongStr)).toThrowError(); - }); - - test('GIVEN valid placeholder THEN validator does not throw', () => { - expect(() => placeholderValidator.parse('foobar')).not.toThrowError(); - }); - - test('GIVEN invalid value THEN validator does throw 2', () => { - expect(() => placeholderValidator.parse(superLongStr)).toThrowError(); - }); - test('GIVEN valid fields THEN builder does not throw', () => { expect(() => { textInputComponent().setCustomId('foobar').setLabel('test').setStyle(TextInputStyle.Paragraph).toJSON(); @@ -84,9 +26,7 @@ describe('Text Input Components', () => { }).not.toThrowError(); expect(() => { - // Issue #8107 - // @ts-expect-error: Shapeshift maps the enum key to the value when parsing - textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle('Short').toJSON(); + textInputComponent().setCustomId('Custom').setLabel('Guess').setStyle(TextInputStyle.Short).toJSON(); }).not.toThrowError(); }); }); diff --git a/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts b/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts new file mode 100644 index 000000000000..6f377958ded4 --- /dev/null +++ b/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts @@ -0,0 +1,516 @@ +import { + ApplicationCommandType, + ApplicationIntegrationType, + ChannelType, + InteractionContextType, + PermissionFlagsBits, +} from 'discord-api-types/v10'; +import { describe, test, expect } from 'vitest'; +import { + ChatInputCommandBooleanOption, + ChatInputCommandBuilder, + ChatInputCommandChannelOption, + ChatInputCommandIntegerOption, + ChatInputCommandMentionableOption, + ChatInputCommandNumberOption, + ChatInputCommandRoleOption, + ChatInputCommandAttachmentOption, + ChatInputCommandStringOption, + ChatInputCommandSubcommandBuilder, + ChatInputCommandSubcommandGroupBuilder, + ChatInputCommandUserOption, +} from '../../../src/index.js'; + +const getBuilder = () => new ChatInputCommandBuilder(); +const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command'); +const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123'); +const getIntegerOption = () => new ChatInputCommandIntegerOption().setName('owo').setDescription('Testing 123'); +const getNumberOption = () => new ChatInputCommandNumberOption().setName('owo').setDescription('Testing 123'); +const getBooleanOption = () => new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123'); +const getUserOption = () => new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123'); +const getChannelOption = () => new ChatInputCommandChannelOption().setName('owo').setDescription('Testing 123'); +const getRoleOption = () => new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123'); +const getAttachmentOption = () => new ChatInputCommandAttachmentOption().setName('owo').setDescription('Testing 123'); +const getMentionableOption = () => new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123'); +const getSubcommandGroup = () => + new ChatInputCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123'); +const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123'); + +class Collection { + public readonly [Symbol.toStringTag] = 'Map'; +} + +describe('ChatInput Commands', () => { + describe('ChatInputCommandBuilder', () => { + describe('Builder with no options', () => { + test('GIVEN empty builder THEN throw error when calling toJSON', () => { + expect(() => getBuilder().toJSON()).toThrowError(); + }); + + test('GIVEN valid builder THEN does not throw error', () => { + expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError(); + }); + }); + + describe('Builder with simple options', () => { + test('GIVEN valid builder THEN returns type included', () => { + expect(getNamedBuilder().toJSON()).includes({ type: ApplicationCommandType.ChatInput }); + }); + + test('GIVEN valid builder with options THEN does not throw error', () => { + expect(() => + getBuilder() + .setName('example') + .setDescription('Example command') + .addBooleanOptions((boolean) => + boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true), + ) + .addChannelOptions((channel) => channel.setName('iscool').setDescription('Are we cool or what?')) + .addMentionableOptions((mentionable) => + mentionable.setName('iscool').setDescription('Are we cool or what?'), + ) + .addRoleOptions((role) => role.setName('iscool').setDescription('Are we cool or what?')) + .addUserOptions((user) => user.setName('iscool').setDescription('Are we cool or what?')) + .addIntegerOptions((integer) => + integer + .setName('iscool') + .setDescription('Are we cool or what?') + .addChoices({ name: 'Very cool', value: 1_000 }) + .addChoices([{ name: 'Even cooler', value: 2_000 }]), + ) + .addNumberOptions((number) => + number + .setName('iscool') + .setDescription('Are we cool or what?') + .addChoices({ name: 'Very cool', value: 1.5 }) + .addChoices([{ name: 'Even cooler', value: 2.5 }]), + ) + .addStringOptions((string) => + string + .setName('iscool') + .setDescription('Are we cool or what?') + .addChoices({ name: 'Fancy Pants', value: 'fp_1' }, { name: 'Fancy Shoes', value: 'fs_1' }) + .addChoices([{ name: 'The Whole shebang', value: 'all' }]), + ) + .addIntegerOptions((integer) => + integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), + ) + .addNumberOptions((number) => + number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), + ) + .addStringOptions((string) => + string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), + ) + .toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => { + expect(() => + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + getNamedBuilder().addStringOptions(getStringOption().setAutocomplete('not a boolean')).toJSON(), + ).toThrowError(); + }); + + test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => { + expect(() => + getNamedBuilder() + .addStringOptions( + getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }), + ) + .toJSON(), + ).toThrowError(); + + expect(() => + getNamedBuilder() + .addStringOptions( + getStringOption() + .setAutocomplete(true) + .addChoices( + { name: 'Fancy Pants', value: 'fp_1' }, + { name: 'Fancy Shoes', value: 'fs_1' }, + { name: 'The Whole shebang', value: 'all' }, + ), + ) + .toJSON(), + ).toThrowError(); + + expect(() => + getNamedBuilder() + .addStringOptions( + getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true), + ) + .toJSON(), + ).toThrowError(); + }); + + test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => { + expect(() => + getNamedBuilder() + .addChannelOptions( + getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]), + ) + .toJSON(), + ).not.toThrowError(); + + expect(() => { + getNamedBuilder() + .addChannelOptions(getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText)) + .toJSON(); + }).not.toThrowError(); + }); + + test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => { + expect(() => + // @ts-expect-error: Invalid channel type + getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100)).toJSON(), + ).toThrowError(); + + expect(() => + // @ts-expect-error: Invalid channel types + getNamedBuilder().addChannelOptions(getChannelOption().addChannelTypes(100, 200)).toJSON(), + ).toThrowError(); + }); + + test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => { + // @ts-expect-error: Invalid max value + expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue('test')).toJSON()).toThrowError(); + + expect(() => + // @ts-expect-error: Invalid max value + getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue('test')).toJSON(), + ).toThrowError(); + + // @ts-expect-error: Invalid min value + expect(() => getNamedBuilder().addNumberOptions(getNumberOption().setMinValue('test')).toJSON()).toThrowError(); + + expect(() => + // @ts-expect-error: Invalid min value + getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue('test')).toJSON(), + ).toThrowError(); + + expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1.5)).toJSON()).toThrowError(); + }); + + test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => { + expect(() => + getNamedBuilder().addIntegerOptions(getIntegerOption().setMinValue(1)).toJSON(), + ).not.toThrowError(); + + expect(() => + getNamedBuilder().addNumberOptions(getNumberOption().setMinValue(1.5)).toJSON(), + ).not.toThrowError(); + + expect(() => + getNamedBuilder().addIntegerOptions(getIntegerOption().setMaxValue(1)).toJSON(), + ).not.toThrowError(); + + expect(() => + getNamedBuilder().addNumberOptions(getNumberOption().setMaxValue(1.5)).toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN an already built builder THEN does not throw an error', () => { + expect(() => getNamedBuilder().addStringOptions(getStringOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addIntegerOptions(getIntegerOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addNumberOptions(getNumberOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addUserOptions(getUserOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addChannelOptions(getChannelOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addRoleOptions(getRoleOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addAttachmentOptions(getAttachmentOption()).toJSON()).not.toThrowError(); + + expect(() => getNamedBuilder().addMentionableOptions(getMentionableOption()).toJSON()).not.toThrowError(); + }); + + test('GIVEN invalid name THEN throw error', () => { + expect(() => getBuilder().setName('TEST_COMMAND').setDescription(':3').toJSON()).toThrowError(); + expect(() => getBuilder().setName('ĂĂĂĂĂĂ').setDescription(':3').toJSON()).toThrowError(); + }); + + test('GIVEN valid names THEN does not throw error', () => { + expect(() => getBuilder().setName('hi_there').setDescription(':3')).not.toThrowError(); + expect(() => getBuilder().setName('o_comandă').setDescription(':3')).not.toThrowError(); + expect(() => getBuilder().setName('どうも').setDescription(':3')).not.toThrowError(); + }); + + test('GIVEN invalid returns for builder THEN throw error', () => { + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + expect(() => getNamedBuilder().addBooleanOptions(true).toJSON()).toThrowError(); + + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + expect(() => getNamedBuilder().addBooleanOptions(null).toJSON()).toThrowError(); + + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + expect(() => getNamedBuilder().addBooleanOptions(undefined).toJSON()).toThrowError(); + + expect(() => + getNamedBuilder() + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + .addBooleanOptions(() => ChatInputCommandStringOption) + .toJSON(), + ).toThrowError(); + expect(() => + getNamedBuilder() + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + .addBooleanOptions(() => new Collection()) + .toJSON(), + ).toThrowError(); + }); + + test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => { + expect(() => + getNamedBuilder().addStringOptions(getStringOption().setAutocomplete(true).setChoices()).toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => { + expect(() => + getNamedBuilder() + .addStringOptions(getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' })) + .toJSON(), + ).toThrowError(); + }); + + test('GIVEN an option, THEN setting choices should not throw an error', () => { + expect(() => + getNamedBuilder() + .addStringOptions(getStringOption().setChoices({ name: 'owo', value: 'uwu' })) + .toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN valid builder with NSFW, THEN does not throw error', () => { + expect(() => getNamedBuilder().setName('foo').setDescription('foo').setNSFW(true).toJSON()).not.toThrowError(); + }); + }); + + describe('Builder with subcommand (group) options', () => { + test('GIVEN builder with subcommand group THEN does not throw error', () => { + expect(() => + getNamedBuilder() + .addSubcommandGroups((group) => + group.setName('group').setDescription('Group us together!').addSubcommands(getSubcommand()), + ) + .toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN builder with subcommand THEN does not throw error', () => { + expect(() => + getNamedBuilder() + .addSubcommands((subcommand) => subcommand.setName('boop').setDescription('Boops a fellow nerd (you)')) + .toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN builder with subcommand THEN has regular ChatInput command fields', () => { + expect(() => + getBuilder() + .setName('name') + .setDescription('description') + .addSubcommands((option) => option.setName('ye').setDescription('ye')) + .addSubcommands((option) => option.setName('no').setDescription('no')) + .setDefaultMemberPermissions(1n) + .toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN builder with already built subcommand group THEN does not throw error', () => { + expect(() => + getNamedBuilder().addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand())).toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN builder with already built subcommand THEN does not throw error', () => { + expect(() => getNamedBuilder().addSubcommands(getSubcommand()).toJSON()).not.toThrowError(); + }); + + test('GIVEN builder with already built subcommand with options THEN does not throw error', () => { + expect(() => + getNamedBuilder().addSubcommands(getSubcommand().addBooleanOptions(getBooleanOption())).toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => { + expect(() => + // @ts-expect-error: Checking if check works JS-side too + getNamedBuilder().addSubcommands(getSubcommand()).addIntegerOptions(getInteger()).toJSON(), + ).toThrowError(); + }); + + test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => { + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + expect(() => getNamedBuilder().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError(); + }); + }); + + describe('Subcommand group builder', () => { + test('GIVEN no valid subcommand THEN throw error', () => { + expect(() => getSubcommandGroup().addSubcommands().toJSON()).toThrowError(); + + // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error + expect(() => getSubcommandGroup().addSubcommands(getSubcommandGroup()).toJSON()).toThrowError(); + }); + + test('GIVEN a valid subcommand THEN does not throw an error', () => { + expect(() => + getSubcommandGroup() + .addSubcommands((sub) => sub.setName('sub').setDescription('Testing 123')) + .toJSON(), + ).not.toThrowError(); + }); + }); + + describe('Subcommand builder', () => { + test('GIVEN a valid subcommand with options THEN does not throw error', () => { + expect(() => getSubcommand().addBooleanOptions(getBooleanOption()).toJSON()).not.toThrowError(); + }); + }); + + describe('ChatInput command localizations', () => { + const expectedSingleLocale = { 'en-US': 'foobar' }; + const expectedMultipleLocales = { + ...expectedSingleLocale, + bg: 'test', + }; + + test('GIVEN valid name localizations THEN does not throw error', () => { + expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError(); + expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError(); + }); + + test('GIVEN invalid name localizations THEN does throw error', () => { + // @ts-expect-error: Invalid localization + expect(() => getNamedBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError(); + // @ts-expect-error: Invalid localization + expect(() => getNamedBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError(); + }); + + test('GIVEN valid name localizations THEN valid data is stored', () => { + expect(getNamedBuilder().setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual( + expectedSingleLocale, + ); + expect( + getNamedBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON().name_localizations, + ).toEqual(expectedMultipleLocales); + expect(getNamedBuilder().clearNameLocalizations().toJSON().name_localizations).toBeUndefined(); + expect(getNamedBuilder().clearNameLocalization('en-US').toJSON().name_localizations).toEqual({ + 'en-US': undefined, + }); + }); + + test('GIVEN valid description localizations THEN does not throw error', () => { + expect(() => getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON()).not.toThrowError(); + expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' }).toJSON()).not.toThrowError(); + }); + + test('GIVEN invalid description localizations THEN does throw error', () => { + // @ts-expect-error: Invalid localization description + expect(() => getNamedBuilder().setDescriptionLocalization('en-U', 'foobar').toJSON()).toThrowError(); + // @ts-expect-error: Invalid localization description + expect(() => getNamedBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError(); + }); + + test('GIVEN valid description localizations THEN valid data is stored', () => { + expect( + getNamedBuilder().setDescriptionLocalization('en-US', 'foobar').toJSON(false).description_localizations, + ).toEqual(expectedSingleLocale); + expect( + getNamedBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON(false) + .description_localizations, + ).toEqual(expectedMultipleLocales); + expect( + getNamedBuilder().clearDescriptionLocalizations().toJSON(false).description_localizations, + ).toBeUndefined(); + expect(getNamedBuilder().clearDescriptionLocalization('en-US').toJSON(false).description_localizations).toEqual( + { + 'en-US': undefined, + }, + ); + }); + }); + + describe('permissions', () => { + test('GIVEN valid permission string THEN does not throw error', () => { + expect(() => getNamedBuilder().setDefaultMemberPermissions('1')).not.toThrowError(); + }); + + test('GIVEN valid permission bitfield THEN does not throw error', () => { + expect(() => + getNamedBuilder().setDefaultMemberPermissions( + PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles, + ), + ).not.toThrowError(); + }); + + test('GIVEN null permissions THEN does not throw error', () => { + expect(() => getNamedBuilder().clearDefaultMemberPermissions()).not.toThrowError(); + }); + + test('GIVEN invalid inputs THEN does throw error', () => { + expect(() => getNamedBuilder().setDefaultMemberPermissions('1.1').toJSON()).toThrowError(); + expect(() => getNamedBuilder().setDefaultMemberPermissions(1.1).toJSON()).toThrowError(); + }); + + test('GIVEN valid permission with options THEN does not throw error', () => { + expect(() => + getNamedBuilder().addBooleanOptions(getBooleanOption()).setDefaultMemberPermissions('1').toJSON(), + ).not.toThrowError(); + + expect(() => getNamedBuilder().addChannelOptions(getChannelOption())).not.toThrowError(); + }); + }); + + describe('contexts', () => { + test('GIVEN a builder with valid contexts THEN does not throw an error', () => { + expect(() => + getNamedBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]).toJSON(), + ).not.toThrowError(); + + expect(() => + getNamedBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM).toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN a builder with invalid contexts THEN does throw an error', () => { + // @ts-expect-error: Invalid contexts + expect(() => getNamedBuilder().setContexts(999).toJSON()).toThrowError(); + + // @ts-expect-error: Invalid contexts + expect(() => getNamedBuilder().setContexts([999, 998]).toJSON()).toThrowError(); + }); + }); + + describe('integration types', () => { + test('GIVEN a builder with valid integraton types THEN does not throw an error', () => { + expect(() => + getNamedBuilder() + .setIntegrationTypes([ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall]) + .toJSON(), + ).not.toThrowError(); + + expect(() => + getNamedBuilder() + .setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall) + .toJSON(), + ).not.toThrowError(); + }); + + test('GIVEN a builder with invalid integration types THEN does throw an error', () => { + // @ts-expect-error: Invalid integration types + expect(() => getNamedBuilder().setIntegrationTypes(999).toJSON()).toThrowError(); + + // @ts-expect-error: Invalid integration types + expect(() => getNamedBuilder().setIntegrationTypes([999, 998]).toJSON()).toThrowError(); + }); + }); + }); +}); diff --git a/packages/builders/__tests__/interactions/SlashCommands/Options.test.ts b/packages/builders/__tests__/interactions/ChatInputCommands/Options.test.ts similarity index 85% rename from packages/builders/__tests__/interactions/SlashCommands/Options.test.ts rename to packages/builders/__tests__/interactions/ChatInputCommands/Options.test.ts index 8c985bb11f9e..66fe99457865 100644 --- a/packages/builders/__tests__/interactions/SlashCommands/Options.test.ts +++ b/packages/builders/__tests__/interactions/ChatInputCommands/Options.test.ts @@ -13,32 +13,32 @@ import { } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; import { - SlashCommandAttachmentOption, - SlashCommandBooleanOption, - SlashCommandChannelOption, - SlashCommandIntegerOption, - SlashCommandMentionableOption, - SlashCommandNumberOption, - SlashCommandRoleOption, - SlashCommandStringOption, - SlashCommandUserOption, + ChatInputCommandAttachmentOption, + ChatInputCommandBooleanOption, + ChatInputCommandChannelOption, + ChatInputCommandIntegerOption, + ChatInputCommandMentionableOption, + ChatInputCommandNumberOption, + ChatInputCommandRoleOption, + ChatInputCommandStringOption, + ChatInputCommandUserOption, } from '../../../src/index.js'; const getBooleanOption = () => - new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true); + new ChatInputCommandBooleanOption().setName('owo').setDescription('Testing 123').setRequired(true); const getChannelOption = () => - new SlashCommandChannelOption() + new ChatInputCommandChannelOption() .setName('owo') .setDescription('Testing 123') .setRequired(true) .addChannelTypes(ChannelType.GuildText); const getStringOption = () => - new SlashCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true); + new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123').setRequired(true); const getIntegerOption = () => - new SlashCommandIntegerOption() + new ChatInputCommandIntegerOption() .setName('owo') .setDescription('Testing 123') .setRequired(true) @@ -46,22 +46,24 @@ const getIntegerOption = () => .setMaxValue(10); const getNumberOption = () => - new SlashCommandNumberOption() + new ChatInputCommandNumberOption() .setName('owo') .setDescription('Testing 123') .setRequired(true) .setMinValue(-1.23) .setMaxValue(10); -const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true); +const getUserOption = () => + new ChatInputCommandUserOption().setName('owo').setDescription('Testing 123').setRequired(true); -const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true); +const getRoleOption = () => + new ChatInputCommandRoleOption().setName('owo').setDescription('Testing 123').setRequired(true); const getMentionableOption = () => - new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true); + new ChatInputCommandMentionableOption().setName('owo').setDescription('Testing 123').setRequired(true); const getAttachmentOption = () => - new SlashCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true); + new ChatInputCommandAttachmentOption().setName('attachment').setDescription('attachment').setRequired(true); describe('Application Command toJSON() results', () => { test('GIVEN a boolean option THEN calling toJSON should return a valid JSON', () => { @@ -101,7 +103,6 @@ describe('Application Command toJSON() results', () => { max_value: 10, min_value: -1, autocomplete: true, - // TODO choices: [], }); diff --git a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts index ecec8c1b0d69..315816b047b3 100644 --- a/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts +++ b/packages/builders/__tests__/interactions/ContextMenuCommands.test.ts @@ -1,74 +1,31 @@ -import { ApplicationIntegrationType, InteractionContextType, PermissionFlagsBits } from 'discord-api-types/v10'; +import { + ApplicationCommandType, + ApplicationIntegrationType, + InteractionContextType, + PermissionFlagsBits, +} from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../../src/index.js'; +import { MessageContextCommandBuilder } from '../../src/index.js'; -const getBuilder = () => new ContextMenuCommandBuilder(); +const getBuilder = () => new MessageContextCommandBuilder(); describe('Context Menu Commands', () => { - describe('Assertions tests', () => { - test('GIVEN valid name THEN does not throw error', () => { - expect(() => ContextMenuCommandAssertions.validateName('ping')).not.toThrowError(); - }); - - test('GIVEN invalid name THEN throw error', () => { - expect(() => ContextMenuCommandAssertions.validateName(null)).toThrowError(); - - // Too short of a name - expect(() => ContextMenuCommandAssertions.validateName('')).toThrowError(); - - // Invalid characters used - expect(() => ContextMenuCommandAssertions.validateName('ABC123$%^&')).toThrowError(); - - // Too long of a name - expect(() => - ContextMenuCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'), - ).toThrowError(); - }); - - test('GIVEN valid type THEN does not throw error', () => { - expect(() => ContextMenuCommandAssertions.validateType(3)).not.toThrowError(); - }); - - test('GIVEN invalid type THEN throw error', () => { - expect(() => ContextMenuCommandAssertions.validateType(null)).toThrowError(); - - // Out of range - expect(() => ContextMenuCommandAssertions.validateType(1)).toThrowError(); - }); - - test('GIVEN valid required parameters THEN does not throw error', () => { - expect(() => ContextMenuCommandAssertions.validateRequiredParameters('owo', 2)).not.toThrowError(); - }); - - test('GIVEN valid default_permission THEN does not throw error', () => { - expect(() => ContextMenuCommandAssertions.validateDefaultPermission(true)).not.toThrowError(); - }); - - test('GIVEN invalid default_permission THEN throw error', () => { - expect(() => ContextMenuCommandAssertions.validateDefaultPermission(null)).toThrowError(); - }); - }); - describe('ContextMenuCommandBuilder', () => { describe('Builder tests', () => { test('GIVEN empty builder THEN throw error when calling toJSON', () => { expect(() => getBuilder().toJSON()).toThrowError(); }); - test('GIVEN valid builder THEN does not throw error', () => { - expect(() => getBuilder().setName('example').setType(3).toJSON()).not.toThrowError(); - }); - test('GIVEN invalid name THEN throw error', () => { - expect(() => getBuilder().setName('$$$')).toThrowError(); + expect(() => getBuilder().setName('$$$').toJSON()).toThrowError(); - expect(() => getBuilder().setName(' ')).toThrowError(); + expect(() => getBuilder().setName(' ').toJSON()).toThrowError(); }); test('GIVEN valid names THEN does not throw error', () => { - expect(() => getBuilder().setName('hi_there')).not.toThrowError(); + expect(() => getBuilder().setName('hi_there').toJSON()).not.toThrowError(); - expect(() => getBuilder().setName('A COMMAND')).not.toThrowError(); + expect(() => getBuilder().setName('A COMMAND').toJSON()).not.toThrowError(); // Translation: a_command expect(() => getBuilder().setName('o_comandă')).not.toThrowError(); @@ -76,20 +33,6 @@ describe('Context Menu Commands', () => { // Translation: thx (according to GTranslate) expect(() => getBuilder().setName('どうも')).not.toThrowError(); }); - - test('GIVEN valid types THEN does not throw error', () => { - expect(() => getBuilder().setType(2)).not.toThrowError(); - - expect(() => getBuilder().setType(3)).not.toThrowError(); - }); - - test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => { - expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError(); - }); - - test('GIVEN valid builder with dmPermission false THEN does not throw error', () => { - expect(() => getBuilder().setName('foo').setDMPermission(false)).not.toThrowError(); - }); }); describe('Context menu command localizations', () => { @@ -106,19 +49,22 @@ describe('Context Menu Commands', () => { test('GIVEN invalid name localizations THEN does throw error', () => { // @ts-expect-error: Invalid localization - expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError(); + expect(() => getBuilder().setNameLocalization('en-U', 'foobar').toJSON()).toThrowError(); // @ts-expect-error: Invalid localization - expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError(); + expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' }).toJSON()).toThrowError(); }); test('GIVEN valid name localizations THEN valid data is stored', () => { - expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale); - expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual( - expectedMultipleLocales, + expect(getBuilder().setName('hi').setNameLocalization('en-US', 'foobar').toJSON().name_localizations).toEqual( + expectedSingleLocale, ); - expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull(); - expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({ - 'en-US': null, + expect( + getBuilder().setName('hi').setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).toJSON() + .name_localizations, + ).toEqual(expectedMultipleLocales); + expect(getBuilder().setName('hi').clearNameLocalizations().toJSON().name_localizations).toBeUndefined(); + expect(getBuilder().setName('hi').clearNameLocalization('en-US').toJSON().name_localizations).toEqual({ + 'en-US': undefined, }); }); }); @@ -134,14 +80,10 @@ describe('Context Menu Commands', () => { ).not.toThrowError(); }); - test('GIVEN null permissions THEN does not throw error', () => { - expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError(); - }); - test('GIVEN invalid inputs THEN does throw error', () => { - expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError(); + expect(() => getBuilder().setName('hi').setDefaultMemberPermissions('1.1').toJSON()).toThrowError(); - expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError(); + expect(() => getBuilder().setName('hi').setDefaultMemberPermissions(1.1).toJSON()).toThrowError(); }); }); @@ -158,10 +100,10 @@ describe('Context Menu Commands', () => { test('GIVEN a builder with invalid contexts THEN does throw an error', () => { // @ts-expect-error: Invalid contexts - expect(() => getBuilder().setContexts(999)).toThrowError(); + expect(() => getBuilder().setName('hi').setContexts(999).toJSON()).toThrowError(); // @ts-expect-error: Invalid contexts - expect(() => getBuilder().setContexts([999, 998])).toThrowError(); + expect(() => getBuilder().setName('hi').setContexts([999, 998]).toJSON()).toThrowError(); }); }); @@ -184,10 +126,10 @@ describe('Context Menu Commands', () => { test('GIVEN a builder with invalid integration types THEN does throw an error', () => { // @ts-expect-error: Invalid integration types - expect(() => getBuilder().setIntegrationTypes(999)).toThrowError(); + expect(() => getBuilder().setName('hi').setIntegrationTypes(999).toJSON()).toThrowError(); // @ts-expect-error: Invalid integration types - expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError(); + expect(() => getBuilder().setName('hi').setIntegrationTypes([999, 998]).toJSON()).toThrowError(); }); }); }); diff --git a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts b/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts deleted file mode 100644 index 64e9d97a710d..000000000000 --- a/packages/builders/__tests__/interactions/SlashCommands/SlashCommands.test.ts +++ /dev/null @@ -1,593 +0,0 @@ -import { - ApplicationCommandType, - ApplicationIntegrationType, - ChannelType, - InteractionContextType, - PermissionFlagsBits, - type APIApplicationCommandOptionChoice, -} from 'discord-api-types/v10'; -import { describe, test, expect } from 'vitest'; -import { - SlashCommandAssertions, - SlashCommandBooleanOption, - SlashCommandBuilder, - SlashCommandChannelOption, - SlashCommandIntegerOption, - SlashCommandMentionableOption, - SlashCommandNumberOption, - SlashCommandRoleOption, - SlashCommandAttachmentOption, - SlashCommandStringOption, - SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, - SlashCommandUserOption, -} from '../../../src/index.js'; - -const largeArray = Array.from({ length: 26 }, () => 1 as unknown as APIApplicationCommandOptionChoice); - -const getBuilder = () => new SlashCommandBuilder(); -const getNamedBuilder = () => getBuilder().setName('example').setDescription('Example command'); -const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123'); -const getIntegerOption = () => new SlashCommandIntegerOption().setName('owo').setDescription('Testing 123'); -const getNumberOption = () => new SlashCommandNumberOption().setName('owo').setDescription('Testing 123'); -const getBooleanOption = () => new SlashCommandBooleanOption().setName('owo').setDescription('Testing 123'); -const getUserOption = () => new SlashCommandUserOption().setName('owo').setDescription('Testing 123'); -const getChannelOption = () => new SlashCommandChannelOption().setName('owo').setDescription('Testing 123'); -const getRoleOption = () => new SlashCommandRoleOption().setName('owo').setDescription('Testing 123'); -const getAttachmentOption = () => new SlashCommandAttachmentOption().setName('owo').setDescription('Testing 123'); -const getMentionableOption = () => new SlashCommandMentionableOption().setName('owo').setDescription('Testing 123'); -const getSubcommandGroup = () => new SlashCommandSubcommandGroupBuilder().setName('owo').setDescription('Testing 123'); -const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123'); - -class Collection { - public readonly [Symbol.toStringTag] = 'Map'; -} - -describe('Slash Commands', () => { - describe('Assertions tests', () => { - test('GIVEN valid name THEN does not throw error', () => { - expect(() => SlashCommandAssertions.validateName('ping')).not.toThrowError(); - expect(() => SlashCommandAssertions.validateName('hello-world_command')).not.toThrowError(); - expect(() => SlashCommandAssertions.validateName('aˇ㐆1٢〣²अก')).not.toThrowError(); - }); - - test('GIVEN invalid name THEN throw error', () => { - expect(() => SlashCommandAssertions.validateName(null)).toThrowError(); - - // Too short of a name - expect(() => SlashCommandAssertions.validateName('')).toThrowError(); - - // Invalid characters used - expect(() => SlashCommandAssertions.validateName('ABC')).toThrowError(); - expect(() => SlashCommandAssertions.validateName('ABC123$%^&')).toThrowError(); - expect(() => SlashCommandAssertions.validateName('help ping')).toThrowError(); - expect(() => SlashCommandAssertions.validateName('🦦')).toThrowError(); - - // Too long of a name - expect(() => - SlashCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'), - ).toThrowError(); - }); - - test('GIVEN valid description THEN does not throw error', () => { - expect(() => SlashCommandAssertions.validateDescription('This is an OwO moment fur sure!~')).not.toThrowError(); - }); - - test('GIVEN invalid description THEN throw error', () => { - expect(() => SlashCommandAssertions.validateDescription(null)).toThrowError(); - - // Too short of a description - expect(() => SlashCommandAssertions.validateDescription('')).toThrowError(); - - // Too long of a description - expect(() => - SlashCommandAssertions.validateDescription( - 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Magnam autem libero expedita vitae accusamus nostrum ipsam tempore repudiandae deserunt ipsum facilis, velit fugiat facere accusantium, explicabo corporis aliquam non quos.', - ), - ).toThrowError(); - }); - - test('GIVEN valid default_permission THEN does not throw error', () => { - expect(() => SlashCommandAssertions.validateDefaultPermission(true)).not.toThrowError(); - }); - - test('GIVEN invalid default_permission THEN throw error', () => { - expect(() => SlashCommandAssertions.validateDefaultPermission(null)).toThrowError(); - }); - - test('GIVEN valid array of options or choices THEN does not throw error', () => { - expect(() => SlashCommandAssertions.validateMaxOptionsLength([])).not.toThrowError(); - - expect(() => SlashCommandAssertions.validateChoicesLength(25)).not.toThrowError(); - expect(() => SlashCommandAssertions.validateChoicesLength(25, [])).not.toThrowError(); - }); - - test('GIVEN invalid options or choices THEN throw error', () => { - expect(() => SlashCommandAssertions.validateMaxOptionsLength(null)).toThrowError(); - - // Given an array that's too big - expect(() => SlashCommandAssertions.validateMaxOptionsLength(largeArray)).toThrowError(); - - expect(() => SlashCommandAssertions.validateChoicesLength(1, largeArray)).toThrowError(); - }); - - test('GIVEN valid required parameters THEN does not throw error', () => { - expect(() => - SlashCommandAssertions.validateRequiredParameters( - 'owo', - 'My fancy command that totally exists, to test assertions', - [], - ), - ).not.toThrowError(); - }); - }); - - describe('SlashCommandBuilder', () => { - describe('Builder with no options', () => { - test('GIVEN empty builder THEN throw error when calling toJSON', () => { - expect(() => getBuilder().toJSON()).toThrowError(); - }); - - test('GIVEN valid builder THEN does not throw error', () => { - expect(() => getBuilder().setName('example').setDescription('Example command').toJSON()).not.toThrowError(); - }); - }); - - describe('Builder with simple options', () => { - test('GIVEN valid builder THEN returns type included', () => { - expect(getNamedBuilder().toJSON()).includes({ type: ApplicationCommandType.ChatInput }); - }); - - test('GIVEN valid builder with options THEN does not throw error', () => { - expect(() => - getBuilder() - .setName('example') - .setDescription('Example command') - .setDMPermission(false) - .addBooleanOption((boolean) => - boolean.setName('iscool').setDescription('Are we cool or what?').setRequired(true), - ) - .addChannelOption((channel) => channel.setName('iscool').setDescription('Are we cool or what?')) - .addMentionableOption((mentionable) => mentionable.setName('iscool').setDescription('Are we cool or what?')) - .addRoleOption((role) => role.setName('iscool').setDescription('Are we cool or what?')) - .addUserOption((user) => user.setName('iscool').setDescription('Are we cool or what?')) - .addIntegerOption((integer) => - integer - .setName('iscool') - .setDescription('Are we cool or what?') - .addChoices({ name: 'Very cool', value: 1_000 }) - .addChoices([{ name: 'Even cooler', value: 2_000 }]), - ) - .addNumberOption((number) => - number - .setName('iscool') - .setDescription('Are we cool or what?') - .addChoices({ name: 'Very cool', value: 1.5 }) - .addChoices([{ name: 'Even cooler', value: 2.5 }]), - ) - .addStringOption((string) => - string - .setName('iscool') - .setDescription('Are we cool or what?') - .addChoices({ name: 'Fancy Pants', value: 'fp_1' }, { name: 'Fancy Shoes', value: 'fs_1' }) - .addChoices([{ name: 'The Whole shebang', value: 'all' }]), - ) - .addIntegerOption((integer) => - integer.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), - ) - .addNumberOption((number) => - number.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), - ) - .addStringOption((string) => - string.setName('iscool').setDescription('Are we cool or what?').setAutocomplete(true), - ) - .toJSON(), - ).not.toThrowError(); - }); - - test('GIVEN a builder with invalid autocomplete THEN does throw an error', () => { - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addStringOption(getStringOption().setAutocomplete('not a boolean'))).toThrowError(); - }); - - test('GIVEN a builder with both choices and autocomplete THEN does throw an error', () => { - expect(() => - getBuilder().addStringOption( - getStringOption().setAutocomplete(true).addChoices({ name: 'Fancy Pants', value: 'fp_1' }), - ), - ).toThrowError(); - - expect(() => - getBuilder().addStringOption( - getStringOption() - .setAutocomplete(true) - .addChoices( - { name: 'Fancy Pants', value: 'fp_1' }, - { name: 'Fancy Shoes', value: 'fs_1' }, - { name: 'The Whole shebang', value: 'all' }, - ), - ), - ).toThrowError(); - - expect(() => - getBuilder().addStringOption( - getStringOption().addChoices({ name: 'Fancy Pants', value: 'fp_1' }).setAutocomplete(true), - ), - ).toThrowError(); - - expect(() => { - const option = getStringOption(); - Reflect.set(option, 'autocomplete', true); - Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]); - return option.toJSON(); - }).toThrowError(); - - expect(() => { - const option = getNumberOption(); - Reflect.set(option, 'autocomplete', true); - Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]); - return option.toJSON(); - }).toThrowError(); - - expect(() => { - const option = getIntegerOption(); - Reflect.set(option, 'autocomplete', true); - Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]); - return option.toJSON(); - }).toThrowError(); - }); - - test('GIVEN a builder with valid channel options and channel_types THEN does not throw an error', () => { - expect(() => - getBuilder().addChannelOption( - getChannelOption().addChannelTypes(ChannelType.GuildText).addChannelTypes([ChannelType.GuildVoice]), - ), - ).not.toThrowError(); - - expect(() => { - getBuilder().addChannelOption( - getChannelOption().addChannelTypes(ChannelType.GuildAnnouncement, ChannelType.GuildText), - ); - }).not.toThrowError(); - }); - - test('GIVEN a builder with valid channel options and channel_types THEN does throw an error', () => { - // @ts-expect-error: Invalid channel type - expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100))).toThrowError(); - - // @ts-expect-error: Invalid channel types - expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes(100, 200))).toThrowError(); - }); - - test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => { - // @ts-expect-error: Invalid max value - expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue('test'))).toThrowError(); - - // @ts-expect-error: Invalid max value - expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue('test'))).toThrowError(); - - // @ts-expect-error: Invalid min value - expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue('test'))).toThrowError(); - - // @ts-expect-error: Invalid min value - expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue('test'))).toThrowError(); - - expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1.5))).toThrowError(); - }); - - test('GIVEN a builder with valid number min/max options THEN does not throw an error', () => { - expect(() => getBuilder().addIntegerOption(getIntegerOption().setMinValue(1))).not.toThrowError(); - - expect(() => getBuilder().addNumberOption(getNumberOption().setMinValue(1.5))).not.toThrowError(); - - expect(() => getBuilder().addIntegerOption(getIntegerOption().setMaxValue(1))).not.toThrowError(); - - expect(() => getBuilder().addNumberOption(getNumberOption().setMaxValue(1.5))).not.toThrowError(); - }); - - test('GIVEN an already built builder THEN does not throw an error', () => { - expect(() => getBuilder().addStringOption(getStringOption())).not.toThrowError(); - - expect(() => getBuilder().addIntegerOption(getIntegerOption())).not.toThrowError(); - - expect(() => getBuilder().addNumberOption(getNumberOption())).not.toThrowError(); - - expect(() => getBuilder().addBooleanOption(getBooleanOption())).not.toThrowError(); - - expect(() => getBuilder().addUserOption(getUserOption())).not.toThrowError(); - - expect(() => getBuilder().addChannelOption(getChannelOption())).not.toThrowError(); - - expect(() => getBuilder().addRoleOption(getRoleOption())).not.toThrowError(); - - expect(() => getBuilder().addAttachmentOption(getAttachmentOption())).not.toThrowError(); - - expect(() => getBuilder().addMentionableOption(getMentionableOption())).not.toThrowError(); - }); - - test('GIVEN no valid return for an addOption method THEN throw error', () => { - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addBooleanOption()).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addBooleanOption(getRoleOption())).toThrowError(); - }); - - test('GIVEN invalid name THEN throw error', () => { - expect(() => getBuilder().setName('TEST_COMMAND')).toThrowError(); - - expect(() => getBuilder().setName('ĂĂĂĂĂĂ')).toThrowError(); - }); - - test('GIVEN valid names THEN does not throw error', () => { - expect(() => getBuilder().setName('hi_there')).not.toThrowError(); - - // Translation: a_command - expect(() => getBuilder().setName('o_comandă')).not.toThrowError(); - - // Translation: thx (according to GTranslate) - expect(() => getBuilder().setName('どうも')).not.toThrowError(); - }); - - test('GIVEN invalid returns for builder THEN throw error', () => { - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addBooleanOption(true)).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addBooleanOption(null)).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addBooleanOption(undefined)).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addBooleanOption(() => SlashCommandStringOption)).toThrowError(); - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addBooleanOption(() => new Collection())).toThrowError(); - }); - - test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => { - expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError(); - }); - - test('GIVEN an option that is autocompletable and has choices, THEN passing nothing to setChoices should not throw an error', () => { - expect(() => - getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices()), - ).not.toThrowError(); - }); - - test('GIVEN an option that is autocompletable, THEN setting choices should throw an error', () => { - expect(() => - getBuilder().addStringOption( - getStringOption().setAutocomplete(true).setChoices({ name: 'owo', value: 'uwu' }), - ), - ).toThrowError(); - }); - - test('GIVEN an option, THEN setting choices should not throw an error', () => { - expect(() => - getBuilder().addStringOption(getStringOption().setChoices({ name: 'owo', value: 'uwu' })), - ).not.toThrowError(); - }); - - test('GIVEN valid builder with NSFW, THEN does not throw error', () => { - expect(() => getBuilder().setName('foo').setDescription('foo').setNSFW(true)).not.toThrowError(); - }); - }); - - describe('Builder with subcommand (group) options', () => { - test('GIVEN builder with subcommand group THEN does not throw error', () => { - expect(() => - getNamedBuilder().addSubcommandGroup((group) => group.setName('group').setDescription('Group us together!')), - ).not.toThrowError(); - }); - - test('GIVEN builder with subcommand THEN does not throw error', () => { - expect(() => - getNamedBuilder().addSubcommand((subcommand) => - subcommand.setName('boop').setDescription('Boops a fellow nerd (you)'), - ), - ).not.toThrowError(); - }); - - test('GIVEN builder with subcommand THEN has regular slash command fields', () => { - expect(() => - getBuilder() - .setName('name') - .setDescription('description') - .addSubcommand((option) => option.setName('ye').setDescription('ye')) - .addSubcommand((option) => option.setName('no').setDescription('no')) - .setDMPermission(false) - .setDefaultMemberPermissions(1n), - ).not.toThrowError(); - }); - - test('GIVEN builder with already built subcommand group THEN does not throw error', () => { - expect(() => getNamedBuilder().addSubcommandGroup(getSubcommandGroup())).not.toThrowError(); - }); - - test('GIVEN builder with already built subcommand THEN does not throw error', () => { - expect(() => getNamedBuilder().addSubcommand(getSubcommand())).not.toThrowError(); - }); - - test('GIVEN builder with already built subcommand with options THEN does not throw error', () => { - expect(() => - getNamedBuilder().addSubcommand(getSubcommand().addBooleanOption(getBooleanOption())), - ).not.toThrowError(); - }); - - test('GIVEN builder with a subcommand that tries to add an invalid result THEN throw error', () => { - expect(() => - // @ts-expect-error: Checking if check works JS-side too - getNamedBuilder().addSubcommand(getSubcommand()).addInteger(getInteger()), - ).toThrowError(); - }); - - test('GIVEN no valid return for an addSubcommand(Group) method THEN throw error', () => { - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addSubcommandGroup()).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addSubcommand()).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getBuilder().addSubcommand(getSubcommandGroup())).toThrowError(); - }); - }); - - describe('Subcommand group builder', () => { - test('GIVEN no valid subcommand THEN throw error', () => { - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getSubcommandGroup().addSubcommand()).toThrowError(); - - // @ts-expect-error: Checking if not providing anything, or an invalid return type causes an error - expect(() => getSubcommandGroup().addSubcommand(getSubcommandGroup())).toThrowError(); - }); - - test('GIVEN a valid subcommand THEN does not throw an error', () => { - expect(() => - getSubcommandGroup() - .addSubcommand((sub) => sub.setName('sub').setDescription('Testing 123')) - .toJSON(), - ).not.toThrowError(); - }); - }); - - describe('Subcommand builder', () => { - test('GIVEN a valid subcommand with options THEN does not throw error', () => { - expect(() => getSubcommand().addBooleanOption(getBooleanOption()).toJSON()).not.toThrowError(); - }); - }); - - describe('Slash command localizations', () => { - const expectedSingleLocale = { 'en-US': 'foobar' }; - const expectedMultipleLocales = { - ...expectedSingleLocale, - bg: 'test', - }; - - test('GIVEN valid name localizations THEN does not throw error', () => { - expect(() => getBuilder().setNameLocalization('en-US', 'foobar')).not.toThrowError(); - expect(() => getBuilder().setNameLocalizations({ 'en-US': 'foobar' })).not.toThrowError(); - }); - - test('GIVEN invalid name localizations THEN does throw error', () => { - // @ts-expect-error: Invalid localization - expect(() => getBuilder().setNameLocalization('en-U', 'foobar')).toThrowError(); - // @ts-expect-error: Invalid localization - expect(() => getBuilder().setNameLocalizations({ 'en-U': 'foobar' })).toThrowError(); - }); - - test('GIVEN valid name localizations THEN valid data is stored', () => { - expect(getBuilder().setNameLocalization('en-US', 'foobar').name_localizations).toEqual(expectedSingleLocale); - expect(getBuilder().setNameLocalizations({ 'en-US': 'foobar', bg: 'test' }).name_localizations).toEqual( - expectedMultipleLocales, - ); - expect(getBuilder().setNameLocalizations(null).name_localizations).toBeNull(); - expect(getBuilder().setNameLocalization('en-US', null).name_localizations).toEqual({ - 'en-US': null, - }); - }); - - test('GIVEN valid description localizations THEN does not throw error', () => { - expect(() => getBuilder().setDescriptionLocalization('en-US', 'foobar')).not.toThrowError(); - expect(() => getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar' })).not.toThrowError(); - }); - - test('GIVEN invalid description localizations THEN does throw error', () => { - // @ts-expect-error: Invalid localization description - expect(() => getBuilder().setDescriptionLocalization('en-U', 'foobar')).toThrowError(); - // @ts-expect-error: Invalid localization description - expect(() => getBuilder().setDescriptionLocalizations({ 'en-U': 'foobar' })).toThrowError(); - }); - - test('GIVEN valid description localizations THEN valid data is stored', () => { - expect(getBuilder().setDescriptionLocalization('en-US', 'foobar').description_localizations).toEqual( - expectedSingleLocale, - ); - expect( - getBuilder().setDescriptionLocalizations({ 'en-US': 'foobar', bg: 'test' }).description_localizations, - ).toEqual(expectedMultipleLocales); - expect(getBuilder().setDescriptionLocalizations(null).description_localizations).toBeNull(); - expect(getBuilder().setDescriptionLocalization('en-US', null).description_localizations).toEqual({ - 'en-US': null, - }); - }); - }); - - describe('permissions', () => { - test('GIVEN valid permission string THEN does not throw error', () => { - expect(() => getBuilder().setDefaultMemberPermissions('1')).not.toThrowError(); - }); - - test('GIVEN valid permission bitfield THEN does not throw error', () => { - expect(() => - getBuilder().setDefaultMemberPermissions(PermissionFlagsBits.AddReactions | PermissionFlagsBits.AttachFiles), - ).not.toThrowError(); - }); - - test('GIVEN null permissions THEN does not throw error', () => { - expect(() => getBuilder().setDefaultMemberPermissions(null)).not.toThrowError(); - }); - - test('GIVEN invalid inputs THEN does throw error', () => { - expect(() => getBuilder().setDefaultMemberPermissions('1.1')).toThrowError(); - - expect(() => getBuilder().setDefaultMemberPermissions(1.1)).toThrowError(); - }); - - test('GIVEN valid permission with options THEN does not throw error', () => { - expect(() => - getBuilder().addBooleanOption(getBooleanOption()).setDefaultMemberPermissions('1'), - ).not.toThrowError(); - - expect(() => getBuilder().addChannelOption(getChannelOption()).setDMPermission(false)).not.toThrowError(); - }); - }); - - describe('contexts', () => { - test('GIVEN a builder with valid contexts THEN does not throw an error', () => { - expect(() => - getBuilder().setContexts([InteractionContextType.Guild, InteractionContextType.BotDM]), - ).not.toThrowError(); - - expect(() => - getBuilder().setContexts(InteractionContextType.Guild, InteractionContextType.BotDM), - ).not.toThrowError(); - }); - - test('GIVEN a builder with invalid contexts THEN does throw an error', () => { - // @ts-expect-error: Invalid contexts - expect(() => getBuilder().setContexts(999)).toThrowError(); - - // @ts-expect-error: Invalid contexts - expect(() => getBuilder().setContexts([999, 998])).toThrowError(); - }); - }); - - describe('integration types', () => { - test('GIVEN a builder with valid integraton types THEN does not throw an error', () => { - expect(() => - getBuilder().setIntegrationTypes([ - ApplicationIntegrationType.GuildInstall, - ApplicationIntegrationType.UserInstall, - ]), - ).not.toThrowError(); - - expect(() => - getBuilder().setIntegrationTypes( - ApplicationIntegrationType.GuildInstall, - ApplicationIntegrationType.UserInstall, - ), - ).not.toThrowError(); - }); - - test('GIVEN a builder with invalid integration types THEN does throw an error', () => { - // @ts-expect-error: Invalid integration types - expect(() => getBuilder().setIntegrationTypes(999)).toThrowError(); - - // @ts-expect-error: Invalid integration types - expect(() => getBuilder().setIntegrationTypes([999, 998])).toThrowError(); - }); - }); - }); -}); diff --git a/packages/builders/__tests__/interactions/modal.test.ts b/packages/builders/__tests__/interactions/modal.test.ts index 17bdfefb4bf5..7adfcf8445f9 100644 --- a/packages/builders/__tests__/interactions/modal.test.ts +++ b/packages/builders/__tests__/interactions/modal.test.ts @@ -1,71 +1,21 @@ -import { - ComponentType, - TextInputStyle, - type APIModalInteractionResponseCallbackData, - type APITextInputComponent, -} from 'discord-api-types/v10'; +import { ComponentType, TextInputStyle, type APIModalInteractionResponseCallbackData } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { - ActionRowBuilder, - ButtonBuilder, - ModalBuilder, - TextInputBuilder, - type ModalActionRowComponentBuilder, -} from '../../src/index.js'; -import { - componentsValidator, - titleValidator, - validateRequiredParameters, -} from '../../src/interactions/modals/Assertions.js'; +import { ActionRowBuilder, ModalBuilder, TextInputBuilder } from '../../src/index.js'; const modal = () => new ModalBuilder(); +const textInput = () => + new ActionRowBuilder().addTextInputComponent( + new TextInputBuilder().setCustomId('text').setLabel(':3').setStyle(TextInputStyle.Short), + ); describe('Modals', () => { - describe('Assertion Tests', () => { - test('GIVEN valid title THEN validator does not throw', () => { - expect(() => titleValidator.parse('foobar')).not.toThrowError(); - }); - - test('GIVEN invalid title THEN validator does throw', () => { - expect(() => titleValidator.parse(42)).toThrowError(); - }); - - test('GIVEN valid components THEN validator does not throw', () => { - expect(() => componentsValidator.parse([new ActionRowBuilder(), new ActionRowBuilder()])).not.toThrowError(); - }); - - test('GIVEN invalid components THEN validator does throw', () => { - expect(() => componentsValidator.parse([new ButtonBuilder(), new TextInputBuilder()])).toThrowError(); - }); - - test('GIVEN valid required parameters THEN validator does not throw', () => { - expect(() => - validateRequiredParameters('123', 'title', [new ActionRowBuilder(), new ActionRowBuilder()]), - ).not.toThrowError(); - }); - - test('GIVEN invalid required parameters THEN validator does throw', () => { - expect(() => - // @ts-expect-error: Missing required parameter - validateRequiredParameters('123', undefined, [new ActionRowBuilder(), new ButtonBuilder()]), - ).toThrowError(); - }); - }); - test('GIVEN valid fields THEN builder does not throw', () => { - expect(() => - modal().setTitle('test').setCustomId('foobar').setComponents(new ActionRowBuilder()), - ).not.toThrowError(); - - expect(() => - // @ts-expect-error: You can pass a TextInputBuilder and it will add it to an action row - modal().setTitle('test').setCustomId('foobar').addComponents(new TextInputBuilder()), - ).not.toThrowError(); + expect(() => modal().setTitle('test').setCustomId('foobar').setActionRows(textInput()).toJSON()).not.toThrowError(); + expect(() => modal().setTitle('test').setCustomId('foobar').addActionRows(textInput()).toJSON()).not.toThrowError(); }); test('GIVEN invalid fields THEN builder does throw', () => { expect(() => modal().setTitle('test').setCustomId('foobar').toJSON()).toThrowError(); - // @ts-expect-error: CustomId is invalid expect(() => modal().setTitle('test').setCustomId(42).toJSON()).toThrowError(); }); @@ -106,68 +56,17 @@ describe('Modals', () => { modal() .setTitle(modalData.title) .setCustomId('custom id') - .setComponents( - new ActionRowBuilder().addComponents( + .setActionRows( + new ActionRowBuilder().addTextInputComponent( new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph), ), ) - .addComponents([ - new ActionRowBuilder().addComponents( + .addActionRows([ + new ActionRowBuilder().addTextInputComponent( new TextInputBuilder().setCustomId('custom id').setLabel('label').setStyle(TextInputStyle.Paragraph), ), ]) .toJSON(), ).toEqual(modalData); }); - - describe('equals()', () => { - const textInput1 = new TextInputBuilder() - .setCustomId('custom id') - .setLabel('label') - .setStyle(TextInputStyle.Paragraph); - - const textInput2: APITextInputComponent = { - type: ComponentType.TextInput, - custom_id: 'custom id', - label: 'label', - style: TextInputStyle.Paragraph, - }; - - test('GIVEN equal builders THEN returns true', () => { - const equalTextInput = new TextInputBuilder() - .setCustomId('custom id') - .setLabel('label') - .setStyle(TextInputStyle.Paragraph); - - expect(textInput1.equals(equalTextInput)).toBeTruthy(); - }); - - test('GIVEN the same builder THEN returns true', () => { - expect(textInput1.equals(textInput1)).toBeTruthy(); - }); - - test('GIVEN equal builder and data THEN returns true', () => { - expect(textInput1.equals(textInput2)).toBeTruthy(); - }); - - test('GIVEN different builders THEN returns false', () => { - const diffTextInput = new TextInputBuilder() - .setCustomId('custom id') - .setLabel('label 2') - .setStyle(TextInputStyle.Paragraph); - - expect(textInput1.equals(diffTextInput)).toBeFalsy(); - }); - - test('GIVEN different text input builder and data THEN returns false', () => { - const diffTextInputData: APITextInputComponent = { - type: ComponentType.TextInput, - custom_id: 'custom id', - label: 'label 2', - style: TextInputStyle.Short, - }; - - expect(textInput1.equals(diffTextInputData)).toBeFalsy(); - }); - }); }); diff --git a/packages/builders/__tests__/messages/embed.test.ts b/packages/builders/__tests__/messages/embed.test.ts index 3a0ef27024a9..caee2e9e8303 100644 --- a/packages/builders/__tests__/messages/embed.test.ts +++ b/packages/builders/__tests__/messages/embed.test.ts @@ -3,6 +3,16 @@ import { EmbedBuilder, embedLength } from '../../src/index.js'; const alpha = 'abcdefghijklmnopqrstuvwxyz'; +const dummy = { + title: 'ooooo aaaaa uuuuuu aaaa', +}; + +const base = { + author: undefined, + fields: [], + footer: undefined, +}; + describe('Embed', () => { describe('Embed getters', () => { test('GIVEN an embed with specific amount of characters THEN returns amount of characters', () => { @@ -14,127 +24,136 @@ describe('Embed', () => { footer: { text: alpha }, }); - expect(embedLength(embed.data)).toEqual(alpha.length * 6); + expect(embedLength(embed.toJSON())).toEqual(alpha.length * 6); }); test('GIVEN an embed with zero characters THEN returns amount of characters', () => { const embed = new EmbedBuilder(); - expect(embedLength(embed.data)).toEqual(0); + expect(embedLength(embed.toJSON(false))).toEqual(0); }); }); describe('Embed title', () => { test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => { const embed = new EmbedBuilder({ title: 'foo' }); - expect(embed.toJSON()).toStrictEqual({ title: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' }); }); test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => { const embed = new EmbedBuilder(); embed.setTitle('foo'); - expect(embed.toJSON()).toStrictEqual({ title: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ ...base, title: 'foo' }); }); test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => { - const embed = new EmbedBuilder({ title: 'foo' }); - embed.setTitle(null); + const embed = new EmbedBuilder({ title: 'foo', description: ':3' }); + embed.clearTitle(); - expect(embed.toJSON()).toStrictEqual({ title: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, description: ':3', title: undefined }); }); test('GIVEN an embed with an invalid title THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setTitle('a'.repeat(257))).toThrowError(); + embed.setTitle('a'.repeat(257)); + + expect(() => embed.toJSON()).toThrowError(); }); }); describe('Embed description', () => { test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => { const embed = new EmbedBuilder({ description: 'foo' }); - expect(embed.toJSON()).toStrictEqual({ description: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' }); }); test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => { const embed = new EmbedBuilder(); embed.setDescription('foo'); - expect(embed.toJSON()).toStrictEqual({ description: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ ...base, description: 'foo' }); }); test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => { - const embed = new EmbedBuilder({ description: 'foo' }); - embed.setDescription(null); + const embed = new EmbedBuilder({ description: 'foo', ...dummy }); + embed.clearDescription(); - expect(embed.toJSON()).toStrictEqual({ description: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, description: undefined }); }); test('GIVEN an embed with an invalid description THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setDescription('a'.repeat(4_097))).toThrowError(); + embed.setDescription('a'.repeat(4_097)); + expect(() => embed.toJSON()).toThrowError(); }); }); describe('Embed URL', () => { test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => { - const embed = new EmbedBuilder({ url: 'https://discord.js.org/' }); + const embed = new EmbedBuilder({ url: 'https://discord.js.org/', ...dummy }); expect(embed.toJSON()).toStrictEqual({ + ...base, + ...dummy, url: 'https://discord.js.org/', }); }); test('GIVEN an embed using Embed#setURL THEN returns valid toJSON data', () => { - const embed = new EmbedBuilder(); + const embed = new EmbedBuilder(dummy); embed.setURL('https://discord.js.org/'); expect(embed.toJSON()).toStrictEqual({ + ...base, + ...dummy, url: 'https://discord.js.org/', }); }); test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => { - const embed = new EmbedBuilder({ url: 'https://discord.js.org' }); - embed.setURL(null); + const embed = new EmbedBuilder({ url: 'https://discord.js.org', ...dummy }); + embed.clearURL(); - expect(embed.toJSON()).toStrictEqual({ url: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, url: undefined }); }); test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => { const embed = new EmbedBuilder(); - expect(() => embed.setURL(input)).toThrowError(); + embed.setURL(input); + expect(() => embed.toJSON()).toThrowError(); }); }); describe('Embed Color', () => { test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => { - const embed = new EmbedBuilder({ color: 0xff0000 }); - expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 }); + const embed = new EmbedBuilder({ color: 0xff0000, ...dummy }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 }); }); test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => { - expect(new EmbedBuilder().setColor(0xff0000).toJSON()).toStrictEqual({ color: 0xff0000 }); - expect(new EmbedBuilder().setColor([242, 66, 245]).toJSON()).toStrictEqual({ color: 0xf242f5 }); + expect(new EmbedBuilder(dummy).setColor(0xff0000).toJSON()).toStrictEqual({ ...base, ...dummy, color: 0xff0000 }); }); test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => { - const embed = new EmbedBuilder({ color: 0xff0000 }); - embed.setColor(null); + const embed = new EmbedBuilder({ ...dummy, color: 0xff0000 }); + embed.clearColor(); - expect(embed.toJSON()).toStrictEqual({ color: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, color: undefined }); }); test('GIVEN an embed with an invalid color THEN throws error', () => { const embed = new EmbedBuilder(); // @ts-expect-error: Invalid color - expect(() => embed.setColor('RED')).toThrowError(); + embed.setColor('RED'); + expect(() => embed.toJSON()).toThrowError(); + // @ts-expect-error: Invalid color - expect(() => embed.setColor([42, 36])).toThrowError(); - expect(() => embed.setColor([42, 36, 1_000])).toThrowError(); + embed.setColor([42, 36]); + expect(() => embed.toJSON()).toThrowError(); }); }); @@ -142,98 +161,92 @@ describe('Embed', () => { const now = new Date(); test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => { - const embed = new EmbedBuilder({ timestamp: now.toISOString() }); - expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); + const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() }); }); - test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => { - const embed = new EmbedBuilder(); + test('GIVEN an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => { + const embed = new EmbedBuilder(dummy); embed.setTimestamp(now); - expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() }); }); test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => { - const embed = new EmbedBuilder(); + const embed = new EmbedBuilder(dummy); embed.setTimestamp(now.getTime()); - expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: now.toISOString() }); }); test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => { - const embed = new EmbedBuilder(); + const embed = new EmbedBuilder(dummy); embed.setTimestamp(); - expect(embed.toJSON()).toStrictEqual({ timestamp: embed.data.timestamp }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: embed.toJSON().timestamp }); }); test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => { - const embed = new EmbedBuilder({ timestamp: now.toISOString() }); - embed.setTimestamp(null); + const embed = new EmbedBuilder({ timestamp: now.toISOString(), ...dummy }); + embed.clearTimestamp(); - expect(embed.toJSON()).toStrictEqual({ timestamp: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, timestamp: undefined }); }); }); describe('Embed Thumbnail', () => { test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => { const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); - expect(embed.toJSON()).toStrictEqual({ - thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, - }); + expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); }); test('GIVEN an embed using Embed#setThumbnail THEN returns valid toJSON data', () => { const embed = new EmbedBuilder(); embed.setThumbnail('https://discord.js.org/static/logo.svg'); - expect(embed.toJSON()).toStrictEqual({ - thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, - }); + expect(embed.toJSON()).toStrictEqual({ ...base, thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); }); test('GIVEN an embed with a pre-defined thumbnail THEN unset thumbnail THEN return valid toJSON data', () => { - const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); - embed.setThumbnail(null); + const embed = new EmbedBuilder({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, ...dummy }); + embed.clearThumbnail(); - expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, thumbnail: undefined }); }); test('GIVEN an embed with an invalid thumbnail THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setThumbnail('owo')).toThrowError(); + embed.setThumbnail('owo'); + expect(() => embed.toJSON()).toThrowError(); }); }); describe('Embed Image', () => { test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => { const embed = new EmbedBuilder({ image: { url: 'https://discord.js.org/static/logo.svg' } }); - expect(embed.toJSON()).toStrictEqual({ - image: { url: 'https://discord.js.org/static/logo.svg' }, - }); + expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } }); }); test('GIVEN an embed using Embed#setImage THEN returns valid toJSON data', () => { const embed = new EmbedBuilder(); embed.setImage('https://discord.js.org/static/logo.svg'); - expect(embed.toJSON()).toStrictEqual({ - image: { url: 'https://discord.js.org/static/logo.svg' }, - }); + expect(embed.toJSON()).toStrictEqual({ ...base, image: { url: 'https://discord.js.org/static/logo.svg' } }); }); test('GIVEN an embed with a pre-defined image THEN unset image THEN return valid toJSON data', () => { - const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' } }); - embed.setImage(null); + const embed = new EmbedBuilder({ image: { url: 'https://discord.js/org/static/logo.svg' }, ...dummy }); + embed.clearImage(); - expect(embed.toJSON()).toStrictEqual({ image: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, image: undefined }); }); test('GIVEN an embed with an invalid image THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setImage('owo')).toThrowError(); + embed.setImage('owo'); + expect(() => embed.toJSON()).toThrowError(); }); }); @@ -243,19 +256,19 @@ describe('Embed', () => { author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); expect(embed.toJSON()).toStrictEqual({ + ...base, author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); }); test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => { const embed = new EmbedBuilder(); - embed.setAuthor({ - name: 'Wumpus', - iconURL: 'https://discord.js.org/static/logo.svg', - url: 'https://discord.js.org', - }); + embed.setAuthor((author) => + author.setName('Wumpus').setIconURL('https://discord.js.org/static/logo.svg').setURL('https://discord.js.org'), + ); expect(embed.toJSON()).toStrictEqual({ + ...base, author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); }); @@ -263,16 +276,18 @@ describe('Embed', () => { test('GIVEN an embed with a pre-defined author THEN unset author THEN return valid toJSON data', () => { const embed = new EmbedBuilder({ author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, + ...dummy, }); - embed.setAuthor(null); + embed.clearAuthor(); - expect(embed.toJSON()).toStrictEqual({ author: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, author: undefined }); }); test('GIVEN an embed with an invalid author name THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setAuthor({ name: 'a'.repeat(257) })).toThrowError(); + embed.setAuthor({ name: 'a'.repeat(257) }); + expect(() => embed.toJSON()).toThrowError(); }); }); @@ -282,32 +297,36 @@ describe('Embed', () => { footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); expect(embed.toJSON()).toStrictEqual({ + ...base, footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); }); test('GIVEN an embed using Embed#setAuthor THEN returns valid toJSON data', () => { const embed = new EmbedBuilder(); - embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' }); + embed.setFooter({ text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }); expect(embed.toJSON()).toStrictEqual({ + ...base, footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); }); test('GIVEN an embed with a pre-defined footer THEN unset footer THEN return valid toJSON data', () => { const embed = new EmbedBuilder({ + ...dummy, footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); - embed.setFooter(null); + embed.clearFooter(); - expect(embed.toJSON()).toStrictEqual({ footer: undefined }); + expect(embed.toJSON()).toStrictEqual({ ...base, ...dummy, footer: undefined }); }); test('GIVEN an embed with invalid footer text THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => embed.setFooter({ text: 'a'.repeat(2_049) })).toThrowError(); + embed.setFooter({ text: 'a'.repeat(2_049) }); + expect(() => embed.toJSON()).toThrowError(); }); }); @@ -316,9 +335,7 @@ describe('Embed', () => { const embed = new EmbedBuilder({ fields: [{ name: 'foo', value: 'bar' }], }); - expect(embed.toJSON()).toStrictEqual({ - fields: [{ name: 'foo', value: 'bar' }], - }); + expect(embed.toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'bar' }] }); }); test('GIVEN an embed using Embed#addFields THEN returns valid toJSON data', () => { @@ -327,6 +344,7 @@ describe('Embed', () => { embed.addFields([{ name: 'foo', value: 'bar' }]); expect(embed.toJSON()).toStrictEqual({ + ...base, fields: [ { name: 'foo', value: 'bar' }, { name: 'foo', value: 'bar' }, @@ -338,56 +356,51 @@ describe('Embed', () => { const embed = new EmbedBuilder(); embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' }); - expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({ - fields: [{ name: 'foo', value: 'baz' }], - }); + expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({ ...base, fields: [{ name: 'foo', value: 'baz' }] }); }); test('GIVEN an embed using Embed#spliceFields THEN returns valid toJSON data 2', () => { const embed = new EmbedBuilder(); embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' }))); - expect(() => - embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))), - ).not.toThrowError(); + embed.spliceFields(0, 3, ...Array.from({ length: 5 }, () => ({ name: 'foo', value: 'bar' }))); + expect(() => embed.toJSON()).not.toThrowError(); }); test('GIVEN an embed using Embed#spliceFields that adds additional fields resulting in fields > 25 THEN throws error', () => { const embed = new EmbedBuilder(); embed.addFields(...Array.from({ length: 23 }, () => ({ name: 'foo', value: 'bar' }))); - expect(() => - embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))), - ).toThrowError(); + embed.spliceFields(0, 3, ...Array.from({ length: 8 }, () => ({ name: 'foo', value: 'bar' }))); + expect(() => embed.toJSON()).toThrowError(); }); test('GIVEN an embed using Embed#setFields THEN returns valid toJSON data', () => { const embed = new EmbedBuilder(); - expect(() => - embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))), - ).not.toThrowError(); - expect(() => - embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))), - ).not.toThrowError(); + embed.setFields(...Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))); + expect(() => embed.toJSON()).not.toThrowError(); + + embed.setFields(Array.from({ length: 25 }, () => ({ name: 'foo', value: 'bar' }))); + expect(() => embed.toJSON()).not.toThrowError(); }); test('GIVEN an embed using Embed#setFields that sets more than 25 fields THEN throws error', () => { const embed = new EmbedBuilder(); - expect(() => - embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))), - ).toThrowError(); - expect(() => embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' })))).toThrowError(); + embed.setFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))); + expect(() => embed.toJSON()).toThrowError(); + + embed.setFields(Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))); + expect(() => embed.toJSON()).toThrowError(); }); describe('GIVEN invalid field amount THEN throws error', () => { test('1', () => { const embed = new EmbedBuilder(); - expect(() => - embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))), - ).toThrowError(); + embed.addFields(...Array.from({ length: 26 }, () => ({ name: 'foo', value: 'bar' }))); + expect(() => embed.toJSON()).toThrowError(); }); }); @@ -395,7 +408,8 @@ describe('Embed', () => { test('2', () => { const embed = new EmbedBuilder(); - expect(() => embed.addFields({ name: '', value: 'bar' })).toThrowError(); + embed.addFields({ name: '', value: 'bar' }); + expect(() => embed.toJSON()).toThrowError(); }); }); @@ -403,7 +417,8 @@ describe('Embed', () => { test('3', () => { const embed = new EmbedBuilder(); - expect(() => embed.addFields({ name: 'a'.repeat(257), value: 'bar' })).toThrowError(); + embed.addFields({ name: 'a'.repeat(257), value: 'bar' }); + expect(() => embed.toJSON()).toThrowError(); }); }); @@ -411,7 +426,8 @@ describe('Embed', () => { test('4', () => { const embed = new EmbedBuilder(); - expect(() => embed.addFields({ name: '', value: 'a'.repeat(1_025) })).toThrowError(); + embed.addFields({ name: '', value: 'a'.repeat(1_025) }); + expect(() => embed.toJSON()).toThrowError(); }); }); }); diff --git a/packages/builders/__tests__/types.test.ts b/packages/builders/__tests__/types.test.ts index dfd6abef35f8..de94dbbda9b0 100644 --- a/packages/builders/__tests__/types.test.ts +++ b/packages/builders/__tests__/types.test.ts @@ -1,11 +1,15 @@ import { expectTypeOf } from 'vitest'; -import { SlashCommandBuilder, SlashCommandStringOption, SlashCommandSubcommandBuilder } from '../src/index.js'; +import { + ChatInputCommandBuilder, + ChatInputCommandStringOption, + ChatInputCommandSubcommandBuilder, +} from '../src/index.js'; -const getBuilder = () => new SlashCommandBuilder(); -const getStringOption = () => new SlashCommandStringOption().setName('owo').setDescription('Testing 123'); -const getSubcommand = () => new SlashCommandSubcommandBuilder().setName('owo').setDescription('Testing 123'); +const getBuilder = () => new ChatInputCommandBuilder(); +const getStringOption = () => new ChatInputCommandStringOption().setName('owo').setDescription('Testing 123'); +const getSubcommand = () => new ChatInputCommandSubcommandBuilder().setName('owo').setDescription('Testing 123'); -type BuilderPropsOnly = Pick< +type BuilderPropsOnly = Pick< Type, keyof { [Key in keyof Type as Type[Key] extends (...args: any) => any ? never : Key]: any; diff --git a/packages/builders/package.json b/packages/builders/package.json index 426c15a2dc51..d0e288c7cfa2 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -65,19 +65,17 @@ "homepage": "https://discord.js.org", "funding": "https://github.com/discordjs/discord.js?sponsor", "dependencies": { - "@discordjs/formatters": "workspace:^", "@discordjs/util": "workspace:^", - "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.37.101", - "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" + "tslib": "^2.6.3", + "zod": "^3.23.8" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", "@discordjs/scripts": "workspace:^", "@favware/cliff-jumper": "^4.1.0", - "@types/node": "^16.18.105", + "@types/node": "^18.19.44", "@vitest/coverage-v8": "^2.0.5", "cross-env": "^7.0.3", "esbuild-plugin-version-injector": "^1.2.1", diff --git a/packages/builders/src/Assertions.ts b/packages/builders/src/Assertions.ts new file mode 100644 index 000000000000..1176cdc4abdc --- /dev/null +++ b/packages/builders/src/Assertions.ts @@ -0,0 +1,20 @@ +import { Locale } from 'discord-api-types/v10'; +import { z } from 'zod'; + +export const customIdPredicate = z.string().min(1).max(100); + +export const memberPermissionsPredicate = z.coerce.bigint(); + +export const localeMapPredicate = z + .object( + Object.fromEntries(Object.values(Locale).map((loc) => [loc, z.string().optional()])) as Record< + Locale, + z.ZodOptional + >, + ) + .strict(); + +export const refineURLPredicate = (allowedProtocols: string[]) => (value: string) => { + const url = new URL(value); + return allowedProtocols.includes(url.protocol); +}; diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index ade84ac4690c..84d7268ddcf0 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -1,68 +1,60 @@ /* eslint-disable jsdoc/check-param-names */ -import { - type APIActionRowComponent, - ComponentType, - type APIMessageActionRowComponent, - type APIModalActionRowComponent, - type APIActionRowComponentTypes, +import type { + APITextInputComponent, + APIActionRowComponent, + APIActionRowComponentTypes, + APIChannelSelectComponent, + APIMentionableSelectComponent, + APIRoleSelectComponent, + APIStringSelectComponent, + APIUserSelectComponent, + APIButtonComponentWithCustomId, + APIButtonComponentWithSKUId, + APIButtonComponentWithURL, } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js'; +import { resolveBuilder } from '../util/resolveBuilder.js'; +import { isValidationEnabled } from '../util/validation.js'; +import { actionRowPredicate } from './Assertions.js'; import { ComponentBuilder } from './Component.js'; +import type { AnyActionRowComponentBuilder } from './Components.js'; import { createComponentBuilder } from './Components.js'; -import type { ButtonBuilder } from './button/Button.js'; -import type { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js'; -import type { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js'; -import type { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js'; -import type { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js'; -import type { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js'; -import type { TextInputBuilder } from './textInput/TextInput.js'; - -/** - * The builders that may be used for messages. - */ -export type MessageComponentBuilder = - | ActionRowBuilder - | MessageActionRowComponentBuilder; - -/** - * The builders that may be used for modals. - */ -export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder; - -/** - * The builders that may be used within an action row for messages. - */ -export type MessageActionRowComponentBuilder = - | ButtonBuilder - | ChannelSelectMenuBuilder - | MentionableSelectMenuBuilder - | RoleSelectMenuBuilder - | StringSelectMenuBuilder - | UserSelectMenuBuilder; - -/** - * The builders that may be used within an action row for modals. - */ -export type ModalActionRowComponentBuilder = TextInputBuilder; +import { + DangerButtonBuilder, + PrimaryButtonBuilder, + SecondaryButtonBuilder, + SuccessButtonBuilder, +} from './button/CustomIdButton.js'; +import { LinkButtonBuilder } from './button/LinkButton.js'; +import { PremiumButtonBuilder } from './button/PremiumButton.js'; +import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js'; +import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js'; +import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js'; +import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js'; +import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js'; +import { TextInputBuilder } from './textInput/TextInput.js'; -/** - * Any builder. - */ -export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder; +export interface ActionRowBuilderData + extends Partial, 'components'>> { + components: AnyActionRowComponentBuilder[]; +} /** * A builder that creates API-compatible JSON data for action rows. * * @typeParam ComponentType - The types of components this action row holds */ -export class ActionRowBuilder extends ComponentBuilder< - APIActionRowComponent -> { +export class ActionRowBuilder extends ComponentBuilder> { + private readonly data: ActionRowBuilderData; + /** * The components within this action row. */ - public readonly components: ComponentType[]; + public get components(): readonly AnyActionRowComponentBuilder[] { + return this.data.components; + } /** * Creates a new action row from API data. @@ -98,38 +90,256 @@ export class ActionRowBuilder extends * .addComponents(button2, button3); * ``` */ - public constructor({ components, ...data }: Partial> = {}) { - super({ type: ComponentType.ActionRow, ...data }); - this.components = (components?.map((component) => createComponentBuilder(component)) ?? []) as ComponentType[]; + public constructor({ components = [], ...data }: Partial> = {}) { + super(); + this.data = { + ...structuredClone(data), + type: ComponentType.ActionRow, + components: components.map((component) => createComponentBuilder(component)), + }; + } + + /** + * Adds primary button components to this action row. + * + * @param input - The buttons to add + */ + public addPrimaryButtonComponents( + ...input: RestOrArray< + APIButtonComponentWithCustomId | PrimaryButtonBuilder | ((builder: PrimaryButtonBuilder) => PrimaryButtonBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((component) => resolveBuilder(component, PrimaryButtonBuilder)); + + this.data.components.push(...resolved); + return this; } /** - * Adds components to this action row. + * Adds secondary button components to this action row. * - * @param components - The components to add + * @param input - The buttons to add */ - public addComponents(...components: RestOrArray) { - this.components.push(...normalizeArray(components)); + public addSecondaryButtonComponents( + ...input: RestOrArray< + | APIButtonComponentWithCustomId + | SecondaryButtonBuilder + | ((builder: SecondaryButtonBuilder) => SecondaryButtonBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((component) => resolveBuilder(component, SecondaryButtonBuilder)); + + this.data.components.push(...resolved); return this; } /** - * Sets components for this action row. + * Adds success button components to this action row. * - * @param components - The components to set + * @param input - The buttons to add */ - public setComponents(...components: RestOrArray) { - this.components.splice(0, this.components.length, ...normalizeArray(components)); + public addSuccessButtonComponents( + ...input: RestOrArray< + APIButtonComponentWithCustomId | SuccessButtonBuilder | ((builder: SuccessButtonBuilder) => SuccessButtonBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((component) => resolveBuilder(component, SuccessButtonBuilder)); + + this.data.components.push(...resolved); + return this; + } + + /** + * Adds danger button components to this action row. + */ + public addDangerButtonComponents( + ...input: RestOrArray< + APIButtonComponentWithCustomId | DangerButtonBuilder | ((builder: DangerButtonBuilder) => DangerButtonBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((component) => resolveBuilder(component, DangerButtonBuilder)); + + this.data.components.push(...resolved); + return this; + } + + /** + * Generically add any type of component to this action row, only takes in an instance of a component builder. + */ + public addComponents(...input: RestOrArray): this { + const normalized = normalizeArray(input); + this.data.components.push(...normalized); + + return this; + } + + /** + * Adds SKU id button components to this action row. + * + * @param input - The buttons to add + */ + public addPremiumButtonComponents( + ...input: RestOrArray< + APIButtonComponentWithSKUId | PremiumButtonBuilder | ((builder: PremiumButtonBuilder) => PremiumButtonBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((component) => resolveBuilder(component, PremiumButtonBuilder)); + + this.data.components.push(...resolved); + return this; + } + + /** + * Adds URL button components to this action row. + * + * @param input - The buttons to add + */ + public addLinkButtonComponents( + ...input: RestOrArray< + APIButtonComponentWithURL | LinkButtonBuilder | ((builder: LinkButtonBuilder) => LinkButtonBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((component) => resolveBuilder(component, LinkButtonBuilder)); + + this.data.components.push(...resolved); + return this; + } + + /** + * Adds a channel select menu component to this action row. + * + * @param input - A function that returns a component builder or an already built builder + */ + public addChannelSelectMenuComponent( + input: + | APIChannelSelectComponent + | ChannelSelectMenuBuilder + | ((builder: ChannelSelectMenuBuilder) => ChannelSelectMenuBuilder), + ): this { + this.data.components.push(resolveBuilder(input, ChannelSelectMenuBuilder)); + return this; + } + + /** + * Adds a mentionable select menu component to this action row. + * + * @param input - A function that returns a component builder or an already built builder + */ + public addMentionableSelectMenuComponent( + input: + | APIMentionableSelectComponent + | MentionableSelectMenuBuilder + | ((builder: MentionableSelectMenuBuilder) => MentionableSelectMenuBuilder), + ): this { + this.data.components.push(resolveBuilder(input, MentionableSelectMenuBuilder)); + return this; + } + + /** + * Adds a role select menu component to this action row. + * + * @param input - A function that returns a component builder or an already built builder + */ + public addRoleSelectMenuComponent( + input: APIRoleSelectComponent | RoleSelectMenuBuilder | ((builder: RoleSelectMenuBuilder) => RoleSelectMenuBuilder), + ): this { + this.data.components.push(resolveBuilder(input, RoleSelectMenuBuilder)); + return this; + } + + /** + * Adds a string select menu component to this action row. + * + * @param input - A function that returns a component builder or an already built builder + */ + public addStringSelectMenuComponent( + input: + | APIStringSelectComponent + | StringSelectMenuBuilder + | ((builder: StringSelectMenuBuilder) => StringSelectMenuBuilder), + ): this { + this.data.components.push(resolveBuilder(input, StringSelectMenuBuilder)); + return this; + } + + /** + * Adds a user select menu component to this action row. + * + * @param input - A function that returns a component builder or an already built builder + */ + public addUserSelectMenuComponent( + input: APIUserSelectComponent | UserSelectMenuBuilder | ((builder: UserSelectMenuBuilder) => UserSelectMenuBuilder), + ): this { + this.data.components.push(resolveBuilder(input, UserSelectMenuBuilder)); + return this; + } + + /** + * Adds a text input component to this action row. + * + * @param input - A function that returns a component builder or an already built builder + */ + public addTextInputComponent( + input: APITextInputComponent | TextInputBuilder | ((builder: TextInputBuilder) => TextInputBuilder), + ): this { + this.data.components.push(resolveBuilder(input, TextInputBuilder)); + return this; + } + + /** + * Removes, replaces, or inserts components for this action row. + * + * @remarks + * This method behaves similarly + * to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}. + * + * It's useful for modifying and adjusting order of the already-existing components of an action row. + * @example + * Remove the first component: + * ```ts + * actionRow.spliceComponents(0, 1); + * ``` + * @example + * Remove the first n components: + * ```ts + * const n = 4; + * actionRow.spliceComponents(0, n); + * ``` + * @example + * Remove the last component: + * ```ts + * actionRow.spliceComponents(-1, 1); + * ``` + * @param index - The index to start at + * @param deleteCount - The number of components to remove + * @param components - The replacing component objects + */ + public spliceComponents(index: number, deleteCount: number, ...components: AnyActionRowComponentBuilder[]): this { + this.data.components.splice(index, deleteCount, ...components); return this; } /** * {@inheritDoc ComponentBuilder.toJSON} */ - public toJSON(): APIActionRowComponent> { - return { - ...this.data, - components: this.components.map((component) => component.toJSON()), - } as APIActionRowComponent>; + public override toJSON(validationOverride?: boolean): APIActionRowComponent { + const { components, ...rest } = this.data; + + const data = { + ...structuredClone(rest), + components: components.map((component) => component.toJSON(validationOverride)), + }; + + if (validationOverride ?? isValidationEnabled()) { + actionRowPredicate.parse(data); + } + + return data as APIActionRowComponent; } } diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 926159eedc08..4b8c020665e3 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -1,127 +1,168 @@ -import { s } from '@sapphire/shapeshift'; -import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../util/validation.js'; -import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js'; - -export const customIdValidator = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(100) - .setValidationEnabled(isValidationEnabled); - -export const emojiValidator = s - .object({ - id: s.string(), - name: s.string(), - animated: s.boolean(), - }) - .partial() - .strict() - .setValidationEnabled(isValidationEnabled); - -export const disabledValidator = s.boolean(); +import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10'; +import { z } from 'zod'; +import { customIdPredicate, refineURLPredicate } from '../Assertions.js'; -export const buttonLabelValidator = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(80) - .setValidationEnabled(isValidationEnabled); +const labelPredicate = z.string().min(1).max(80); -export const buttonStyleValidator = s.nativeEnum(ButtonStyle); - -export const placeholderValidator = s.string().lengthLessThanOrEqual(150).setValidationEnabled(isValidationEnabled); -export const minMaxValidator = s - .number() - .int() - .greaterThanOrEqual(0) - .lessThanOrEqual(25) - .setValidationEnabled(isValidationEnabled); - -export const labelValueDescriptionValidator = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(100) - .setValidationEnabled(isValidationEnabled); - -export const jsonOptionValidator = s +export const emojiPredicate = z .object({ - label: labelValueDescriptionValidator, - value: labelValueDescriptionValidator, - description: labelValueDescriptionValidator.optional(), - emoji: emojiValidator.optional(), - default: s.boolean().optional(), + id: z.string().optional(), + name: z.string().min(2).max(32).optional(), + animated: z.boolean().optional(), }) - .setValidationEnabled(isValidationEnabled); - -export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled); - -export const optionsValidator = optionValidator - .array() - .lengthGreaterThanOrEqual(0) - .setValidationEnabled(isValidationEnabled); -export const optionsLengthValidator = s - .number() - .int() - .greaterThanOrEqual(0) - .lessThanOrEqual(25) - .setValidationEnabled(isValidationEnabled); - -export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) { - customIdValidator.parse(customId); - optionsValidator.parse(options); -} - -export const defaultValidator = s.boolean(); - -export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) { - labelValueDescriptionValidator.parse(label); - labelValueDescriptionValidator.parse(value); -} - -export const channelTypesValidator = s.nativeEnum(ChannelType).array().setValidationEnabled(isValidationEnabled); - -export const urlValidator = s - .string() - .url({ - allowedProtocols: ['http:', 'https:', 'discord:'], + .strict() + .refine((data) => data.id !== undefined || data.name !== undefined, { + message: "Either 'id' or 'name' must be provided", + }); + +const buttonPredicateBase = z.object({ + type: z.literal(ComponentType.Button), + disabled: z.boolean().optional(), +}); + +const buttonCustomIdPredicateBase = buttonPredicateBase.extend({ + custom_id: customIdPredicate, + emoji: emojiPredicate.optional(), + label: labelPredicate, +}); + +const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) }).strict(); +const buttonSecondaryPredicate = buttonCustomIdPredicateBase + .extend({ style: z.literal(ButtonStyle.Secondary) }) + .strict(); +const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) }).strict(); +const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) }).strict(); + +const buttonLinkPredicate = buttonPredicateBase + .extend({ + style: z.literal(ButtonStyle.Link), + url: z + .string() + .url() + .refine(refineURLPredicate(['http:', 'https:', 'discord:'])), + emoji: emojiPredicate.optional(), + label: labelPredicate, }) - .setValidationEnabled(isValidationEnabled); - -export function validateRequiredButtonParameters( - style?: ButtonStyle, - label?: string, - emoji?: APIMessageComponentEmoji, - customId?: string, - skuId?: string, - url?: string, -) { - if (style === ButtonStyle.Premium) { - if (!skuId) { - throw new RangeError('Premium buttons must have an SKU id.'); - } - - if (customId || label || url || emoji) { - throw new RangeError('Premium buttons cannot have a custom id, label, URL, or emoji.'); - } - } else { - if (skuId) { - throw new RangeError('Non-premium buttons must not have an SKU id.'); - } + .strict(); - if (url && customId) { - throw new RangeError('URL and custom id are mutually exclusive.'); - } - - if (!label && !emoji) { - throw new RangeError('Non-premium buttons must have a label and/or an emoji.'); +const buttonPremiumPredicate = buttonPredicateBase + .extend({ + style: z.literal(ButtonStyle.Premium), + sku_id: z.string(), + }) + .strict(); + +export const buttonPredicate = z.discriminatedUnion('style', [ + buttonLinkPredicate, + buttonPrimaryPredicate, + buttonSecondaryPredicate, + buttonSuccessPredicate, + buttonDangerPredicate, + buttonPremiumPredicate, +]); + +const selectMenuBasePredicate = z.object({ + placeholder: z.string().max(150).optional(), + min_values: z.number().min(0).max(25).optional(), + max_values: z.number().min(0).max(25).optional(), + custom_id: customIdPredicate, + disabled: z.boolean().optional(), +}); + +export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({ + type: z.literal(ComponentType.ChannelSelect), + channel_types: z.nativeEnum(ChannelType).array().optional(), + default_values: z + .object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) }) + .array() + .max(25) + .optional(), +}); + +export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({ + type: z.literal(ComponentType.MentionableSelect), + default_values: z + .object({ + id: z.string(), + type: z.union([z.literal(SelectMenuDefaultValueType.Role), z.literal(SelectMenuDefaultValueType.User)]), + }) + .array() + .max(25) + .optional(), +}); + +export const selectMenuRolePredicate = selectMenuBasePredicate.extend({ + type: z.literal(ComponentType.RoleSelect), + default_values: z + .object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) }) + .array() + .max(25) + .optional(), +}); + +export const selectMenuStringOptionPredicate = z.object({ + label: labelPredicate, + value: z.string().min(1).max(100), + description: z.string().min(1).max(100).optional(), + emoji: emojiPredicate.optional(), + default: z.boolean().optional(), +}); + +export const selectMenuStringPredicate = selectMenuBasePredicate + .extend({ + type: z.literal(ComponentType.StringSelect), + options: selectMenuStringOptionPredicate.array().min(1).max(25), + }) + .superRefine((menu, ctx) => { + const addIssue = (name: string, minimum: number) => + ctx.addIssue({ + code: 'too_small', + message: `The number of options must be greater than or equal to ${name}`, + inclusive: true, + minimum, + type: 'number', + path: ['options'], + }); + + if (menu.max_values !== undefined && menu.options.length < menu.max_values) { + addIssue('max_values', menu.max_values); } - if (style === ButtonStyle.Link) { - if (!url) { - throw new RangeError('Link buttons must have a URL.'); - } - } else if (url) { - throw new RangeError('Non-premium and non-link buttons cannot have a URL.'); + if (menu.min_values !== undefined && menu.options.length < menu.min_values) { + addIssue('min_values', menu.min_values); } - } -} + }); + +export const selectMenuUserPredicate = selectMenuBasePredicate.extend({ + type: z.literal(ComponentType.UserSelect), + default_values: z + .object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) }) + .array() + .max(25) + .optional(), +}); + +export const actionRowPredicate = z.object({ + type: z.literal(ComponentType.ActionRow), + components: z.union([ + z + .object({ type: z.literal(ComponentType.Button) }) + .array() + .min(1) + .max(5), + z + .object({ + type: z.union([ + z.literal(ComponentType.ChannelSelect), + z.literal(ComponentType.MentionableSelect), + z.literal(ComponentType.RoleSelect), + z.literal(ComponentType.StringSelect), + z.literal(ComponentType.UserSelect), + // And this! + z.literal(ComponentType.TextInput), + ]), + }) + .array() + .length(1), + ]), +}); diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index e5e59638dfb9..29bab02df41b 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -1,10 +1,5 @@ import type { JSONEncodable } from '@discordjs/util'; -import type { - APIActionRowComponent, - APIActionRowComponentTypes, - APIBaseComponent, - ComponentType, -} from 'discord-api-types/v10'; +import type { APIActionRowComponent, APIActionRowComponentTypes } from 'discord-api-types/v10'; /** * Any action row component data represented as an object. @@ -14,32 +9,15 @@ export type AnyAPIActionRowComponent = APIActionRowComponent> = APIBaseComponent, -> implements JSONEncodable -{ - /** - * The API data associated with this component. - */ - public readonly data: Partial; - +export abstract class ComponentBuilder implements JSONEncodable { /** * Serializes this builder to API-compatible JSON data. * - * @remarks - * This method runs validations on the data before serializing it. - * As such, it may throw an error if the data is invalid. - */ - public abstract toJSON(): AnyAPIActionRowComponent; - - /** - * Constructs a new kind of component. + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. * - * @param data - The data to construct a component out of + * @param validationOverride - Force validation to run/not run regardless of your global preference */ - public constructor(data: Partial) { - this.data = data; - } + public abstract toJSON(validationOverride?: boolean): Component; } diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index 18b0dff6dd77..5b907fd485cf 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -1,12 +1,17 @@ -import { ComponentType, type APIMessageComponent, type APIModalComponent } from 'discord-api-types/v10'; -import { - ActionRowBuilder, - type AnyComponentBuilder, - type MessageComponentBuilder, - type ModalComponentBuilder, -} from './ActionRow.js'; +import type { APIButtonComponent, APIMessageComponent, APIModalComponent } from 'discord-api-types/v10'; +import { ButtonStyle, ComponentType } from 'discord-api-types/v10'; +import { ActionRowBuilder } from './ActionRow.js'; +import type { AnyAPIActionRowComponent } from './Component.js'; import { ComponentBuilder } from './Component.js'; -import { ButtonBuilder } from './button/Button.js'; +import type { BaseButtonBuilder } from './button/Button.js'; +import { + DangerButtonBuilder, + PrimaryButtonBuilder, + SecondaryButtonBuilder, + SuccessButtonBuilder, +} from './button/CustomIdButton.js'; +import { LinkButtonBuilder } from './button/LinkButton.js'; +import { PremiumButtonBuilder } from './button/PremiumButton.js'; import { ChannelSelectMenuBuilder } from './selectMenu/ChannelSelectMenu.js'; import { MentionableSelectMenuBuilder } from './selectMenu/MentionableSelectMenu.js'; import { RoleSelectMenuBuilder } from './selectMenu/RoleSelectMenu.js'; @@ -14,6 +19,48 @@ import { StringSelectMenuBuilder } from './selectMenu/StringSelectMenu.js'; import { UserSelectMenuBuilder } from './selectMenu/UserSelectMenu.js'; import { TextInputBuilder } from './textInput/TextInput.js'; +/** + * The builders that may be used for messages. + */ +export type MessageComponentBuilder = ActionRowBuilder | MessageActionRowComponentBuilder; + +/** + * The builders that may be used for modals. + */ +export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder; + +/** + * Any button builder + */ +export type ButtonBuilder = + | DangerButtonBuilder + | LinkButtonBuilder + | PremiumButtonBuilder + | PrimaryButtonBuilder + | SecondaryButtonBuilder + | SuccessButtonBuilder; + +/** + * The builders that may be used within an action row for messages. + */ +export type MessageActionRowComponentBuilder = + | ButtonBuilder + | ChannelSelectMenuBuilder + | MentionableSelectMenuBuilder + | RoleSelectMenuBuilder + | StringSelectMenuBuilder + | UserSelectMenuBuilder; + +/** + * The builders that may be used within an action row for modals. + */ +export type ModalActionRowComponentBuilder = TextInputBuilder; + +/** + * Any action row component builder. + */ +export type AnyActionRowComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder; + /** * Components here are mapped to their respective builder. */ @@ -21,9 +68,9 @@ export interface MappedComponentTypes { /** * The action row component type is associated with an {@link ActionRowBuilder}. */ - [ComponentType.ActionRow]: ActionRowBuilder; + [ComponentType.ActionRow]: ActionRowBuilder; /** - * The button component type is associated with a {@link ButtonBuilder}. + * The button component type is associated with a {@link BaseButtonBuilder}. */ [ComponentType.Button]: ButtonBuilder; /** @@ -75,7 +122,7 @@ export function createComponentBuilder { if (data instanceof ComponentBuilder) { return data; } @@ -84,7 +131,7 @@ export function createComponentBuilder( case ComponentType.ActionRow: return new ActionRowBuilder(data); case ComponentType.Button: - return new ButtonBuilder(data); + return createButtonBuilder(data); case ComponentType.StringSelect: return new StringSelectMenuBuilder(data); case ComponentType.TextInput: @@ -102,3 +149,23 @@ export function createComponentBuilder( throw new Error(`Cannot properly serialize component type: ${data.type}`); } } + +function createButtonBuilder(data: APIButtonComponent): ButtonBuilder { + switch (data.style) { + case ButtonStyle.Primary: + return new PrimaryButtonBuilder(data); + case ButtonStyle.Secondary: + return new SecondaryButtonBuilder(data); + case ButtonStyle.Success: + return new SuccessButtonBuilder(data); + case ButtonStyle.Danger: + return new DangerButtonBuilder(data); + case ButtonStyle.Link: + return new LinkButtonBuilder(data); + case ButtonStyle.Premium: + return new PremiumButtonBuilder(data); + default: + // @ts-expect-error This case can still occur if we get a newer unsupported button style + throw new Error(`Cannot properly serialize button with style: ${data.style}`); + } +} diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index cc36d80dabcb..448059ddd941 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -1,115 +1,13 @@ -import { - ComponentType, - type APIButtonComponent, - type APIButtonComponentWithCustomId, - type APIButtonComponentWithSKUId, - type APIButtonComponentWithURL, - type APIMessageComponentEmoji, - type ButtonStyle, - type Snowflake, -} from 'discord-api-types/v10'; -import { - buttonLabelValidator, - buttonStyleValidator, - customIdValidator, - disabledValidator, - emojiValidator, - urlValidator, - validateRequiredButtonParameters, -} from '../Assertions.js'; +import type { APIButtonComponent } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation.js'; +import { buttonPredicate } from '../Assertions.js'; import { ComponentBuilder } from '../Component.js'; /** * A builder that creates API-compatible JSON data for buttons. */ -export class ButtonBuilder extends ComponentBuilder { - /** - * Creates a new button from API data. - * - * @param data - The API data to create this button with - * @example - * Creating a button from an API data object: - * ```ts - * const button = new ButtonBuilder({ - * custom_id: 'a cool button', - * style: ButtonStyle.Primary, - * label: 'Click Me', - * emoji: { - * name: 'smile', - * id: '123456789012345678', - * }, - * }); - * ``` - * @example - * Creating a button using setters and API data: - * ```ts - * const button = new ButtonBuilder({ - * style: ButtonStyle.Secondary, - * label: 'Click Me', - * }) - * .setEmoji({ name: '🙂' }) - * .setCustomId('another cool button'); - * ``` - */ - public constructor(data?: Partial) { - super({ type: ComponentType.Button, ...data }); - } - - /** - * Sets the style of this button. - * - * @param style - The style to use - */ - public setStyle(style: ButtonStyle) { - this.data.style = buttonStyleValidator.parse(style); - return this; - } - - /** - * Sets the URL for this button. - * - * @remarks - * This method is only available to buttons using the `Link` button style. - * Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`. - * @param url - The URL to use - */ - public setURL(url: string) { - (this.data as APIButtonComponentWithURL).url = urlValidator.parse(url); - return this; - } - - /** - * Sets the custom id for this button. - * - * @remarks - * This method is only applicable to buttons that are not using the `Link` button style. - * @param customId - The custom id to use - */ - public setCustomId(customId: string) { - (this.data as APIButtonComponentWithCustomId).custom_id = customIdValidator.parse(customId); - return this; - } - - /** - * Sets the SKU id that represents a purchasable SKU for this button. - * - * @remarks Only available when using premium-style buttons. - * @param skuId - The SKU id to use - */ - public setSKUId(skuId: Snowflake) { - (this.data as APIButtonComponentWithSKUId).sku_id = skuId; - return this; - } - - /** - * Sets the emoji to display on this button. - * - * @param emoji - The emoji to use - */ - public setEmoji(emoji: APIMessageComponentEmoji) { - (this.data as Exclude).emoji = emojiValidator.parse(emoji); - return this; - } +export abstract class BaseButtonBuilder extends ComponentBuilder { + protected declare readonly data: Partial; /** * Sets whether this button is disabled. @@ -117,35 +15,20 @@ export class ButtonBuilder extends ComponentBuilder { * @param disabled - Whether to disable this button */ public setDisabled(disabled = true) { - this.data.disabled = disabledValidator.parse(disabled); - return this; - } - - /** - * Sets the label for this button. - * - * @param label - The label to use - */ - public setLabel(label: string) { - (this.data as Exclude).label = buttonLabelValidator.parse(label); + this.data.disabled = disabled; return this; } /** * {@inheritDoc ComponentBuilder.toJSON} */ - public toJSON(): APIButtonComponent { - validateRequiredButtonParameters( - this.data.style, - (this.data as Exclude).label, - (this.data as Exclude).emoji, - (this.data as APIButtonComponentWithCustomId).custom_id, - (this.data as APIButtonComponentWithSKUId).sku_id, - (this.data as APIButtonComponentWithURL).url, - ); + public override toJSON(validationOverride?: boolean): ButtonData { + const clone = structuredClone(this.data); + + if (validationOverride ?? isValidationEnabled()) { + buttonPredicate.parse(clone); + } - return { - ...this.data, - } as APIButtonComponent; + return clone as ButtonData; } } diff --git a/packages/builders/src/components/button/CustomIdButton.ts b/packages/builders/src/components/button/CustomIdButton.ts new file mode 100644 index 000000000000..70ef0544fb77 --- /dev/null +++ b/packages/builders/src/components/button/CustomIdButton.ts @@ -0,0 +1,69 @@ +import { ButtonStyle, ComponentType, type APIButtonComponentWithCustomId } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { BaseButtonBuilder } from './Button.js'; +import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js'; + +export type CustomIdButtonStyle = APIButtonComponentWithCustomId['style']; + +/** + * A builder that creates API-compatible JSON data for buttons with custom IDs. + */ +export abstract class CustomIdButtonBuilder extends Mixin( + BaseButtonBuilder, + EmojiOrLabelButtonMixin, +) { + protected override readonly data: Partial; + + protected constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.Button }; + } + + /** + * Sets the custom id for this button. + * + * @remarks + * This method is only applicable to buttons that are not using the `Link` button style. + * @param customId - The custom id to use + */ + public setCustomId(customId: string) { + this.data.custom_id = customId; + return this; + } +} + +/** + * A builder that creates API-compatible JSON data for buttons with custom IDs (using the primary style). + */ +export class PrimaryButtonBuilder extends CustomIdButtonBuilder { + public constructor(data: Partial = {}) { + super({ ...data, style: ButtonStyle.Primary }); + } +} + +/** + * A builder that creates API-compatible JSON data for buttons with custom IDs (using the secondary style). + */ +export class SecondaryButtonBuilder extends CustomIdButtonBuilder { + public constructor(data: Partial = {}) { + super({ ...data, style: ButtonStyle.Secondary }); + } +} + +/** + * A builder that creates API-compatible JSON data for buttons with custom IDs (using the success style). + */ +export class SuccessButtonBuilder extends CustomIdButtonBuilder { + public constructor(data: Partial = {}) { + super({ ...data, style: ButtonStyle.Success }); + } +} + +/** + * A builder that creates API-compatible JSON data for buttons with custom IDs (using the danger style). + */ +export class DangerButtonBuilder extends CustomIdButtonBuilder { + public constructor(data: Partial = {}) { + super({ ...data, style: ButtonStyle.Danger }); + } +} diff --git a/packages/builders/src/components/button/LinkButton.ts b/packages/builders/src/components/button/LinkButton.ts new file mode 100644 index 000000000000..3031c17cbd47 --- /dev/null +++ b/packages/builders/src/components/button/LinkButton.ts @@ -0,0 +1,34 @@ +import { + ButtonStyle, + ComponentType, + type APIButtonComponent, + type APIButtonComponentWithURL, +} from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { BaseButtonBuilder } from './Button.js'; +import { EmojiOrLabelButtonMixin } from './mixins/EmojiOrLabelButtonMixin.js'; + +/** + * A builder that creates API-compatible JSON data for buttons with links. + */ +export class LinkButtonBuilder extends Mixin(BaseButtonBuilder, EmojiOrLabelButtonMixin) { + protected override readonly data: Partial; + + public constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Link }; + } + + /** + * Sets the URL for this button. + * + * @remarks + * This method is only available to buttons using the `Link` button style. + * Only three types of URL schemes are currently supported: `https://`, `http://`, and `discord://`. + * @param url - The URL to use + */ + public setURL(url: string) { + this.data.url = url; + return this; + } +} diff --git a/packages/builders/src/components/button/PremiumButton.ts b/packages/builders/src/components/button/PremiumButton.ts new file mode 100644 index 000000000000..8ab25d43d5f3 --- /dev/null +++ b/packages/builders/src/components/button/PremiumButton.ts @@ -0,0 +1,26 @@ +import type { APIButtonComponentWithSKUId, Snowflake } from 'discord-api-types/v10'; +import { ButtonStyle, ComponentType } from 'discord-api-types/v10'; +import { BaseButtonBuilder } from './Button.js'; + +/** + * A builder that creates API-compatible JSON data for premium buttons. + */ +export class PremiumButtonBuilder extends BaseButtonBuilder { + protected override readonly data: Partial; + + public constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.Button, style: ButtonStyle.Premium }; + } + + /** + * Sets the SKU id that represents a purchasable SKU for this button. + * + * @remarks Only available when using premium-style buttons. + * @param skuId - The SKU id to use + */ + public setSKUId(skuId: Snowflake) { + this.data.sku_id = skuId; + return this; + } +} diff --git a/packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts b/packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts new file mode 100644 index 000000000000..8e35da9c4436 --- /dev/null +++ b/packages/builders/src/components/button/mixins/EmojiOrLabelButtonMixin.ts @@ -0,0 +1,44 @@ +import type { APIButtonComponent, APIButtonComponentWithSKUId, APIMessageComponentEmoji } from 'discord-api-types/v10'; + +export interface EmojiOrLabelButtonData + extends Pick, 'emoji' | 'label'> {} + +export class EmojiOrLabelButtonMixin { + protected declare readonly data: EmojiOrLabelButtonData; + + /** + * Sets the emoji to display on this button. + * + * @param emoji - The emoji to use + */ + public setEmoji(emoji: APIMessageComponentEmoji) { + this.data.emoji = emoji; + return this; + } + + /** + * Clears the emoji on this button. + */ + public clearEmoji() { + this.data.emoji = undefined; + return this; + } + + /** + * Sets the label for this button. + * + * @param label - The label to use + */ + public setLabel(label: string) { + this.data.label = label; + return this; + } + + /** + * Clears the label on this button. + */ + public clearLabel() { + this.data.label = undefined; + return this; + } +} diff --git a/packages/builders/src/components/selectMenu/BaseSelectMenu.ts b/packages/builders/src/components/selectMenu/BaseSelectMenu.ts index 298d7dc5e1fd..75e34d28e50d 100644 --- a/packages/builders/src/components/selectMenu/BaseSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/BaseSelectMenu.ts @@ -1,5 +1,5 @@ +import type { JSONEncodable } from '@discordjs/util'; import type { APISelectMenuComponent } from 'discord-api-types/v10'; -import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js'; import { ComponentBuilder } from '../Component.js'; /** @@ -7,16 +7,29 @@ import { ComponentBuilder } from '../Component.js'; * * @typeParam SelectMenuType - The type of select menu this would be instantiated for. */ -export abstract class BaseSelectMenuBuilder< - SelectMenuType extends APISelectMenuComponent, -> extends ComponentBuilder { +export abstract class BaseSelectMenuBuilder + extends ComponentBuilder + implements JSONEncodable +{ + protected abstract readonly data: Partial< + Pick + >; + /** * Sets the placeholder for this select menu. * * @param placeholder - The placeholder to use */ public setPlaceholder(placeholder: string) { - this.data.placeholder = placeholderValidator.parse(placeholder); + this.data.placeholder = placeholder; + return this; + } + + /** + * Clears the placeholder for this select menu. + */ + public clearPlaceholder() { + this.data.placeholder = undefined; return this; } @@ -26,7 +39,7 @@ export abstract class BaseSelectMenuBuilder< * @param minValues - The minimum values that must be selected */ public setMinValues(minValues: number) { - this.data.min_values = minMaxValidator.parse(minValues); + this.data.min_values = minValues; return this; } @@ -36,7 +49,7 @@ export abstract class BaseSelectMenuBuilder< * @param maxValues - The maximum values that must be selected */ public setMaxValues(maxValues: number) { - this.data.max_values = minMaxValidator.parse(maxValues); + this.data.max_values = maxValues; return this; } @@ -46,7 +59,7 @@ export abstract class BaseSelectMenuBuilder< * @param customId - The custom id to use */ public setCustomId(customId: string) { - this.data.custom_id = customIdValidator.parse(customId); + this.data.custom_id = customId; return this; } @@ -56,17 +69,7 @@ export abstract class BaseSelectMenuBuilder< * @param disabled - Whether this select menu is disabled */ public setDisabled(disabled = true) { - this.data.disabled = disabledValidator.parse(disabled); + this.data.disabled = disabled; return this; } - - /** - * {@inheritDoc ComponentBuilder.toJSON} - */ - public toJSON(): SelectMenuType { - customIdValidator.parse(this.data.custom_id); - return { - ...this.data, - } as SelectMenuType; - } } diff --git a/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts b/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts index 204dcf84a178..913d61592e4e 100644 --- a/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts @@ -6,13 +6,16 @@ import { SelectMenuDefaultValueType, } from 'discord-api-types/v10'; import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js'; -import { channelTypesValidator, customIdValidator, optionsLengthValidator } from '../Assertions.js'; +import { isValidationEnabled } from '../../util/validation.js'; +import { selectMenuChannelPredicate } from '../Assertions.js'; import { BaseSelectMenuBuilder } from './BaseSelectMenu.js'; /** * A builder that creates API-compatible JSON data for channel select menus. */ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder { + protected override readonly data: Partial; + /** * Creates a new select menu from API data. * @@ -36,8 +39,9 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder) { - super({ ...data, type: ComponentType.ChannelSelect }); + public constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.ChannelSelect }; } /** @@ -48,7 +52,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedTypes = normalizeArray(types); this.data.channel_types ??= []; - this.data.channel_types.push(...channelTypesValidator.parse(normalizedTypes)); + this.data.channel_types.push(...normalizedTypes); return this; } @@ -60,7 +64,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedTypes = normalizeArray(types); this.data.channel_types ??= []; - this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(normalizedTypes)); + this.data.channel_types.splice(0, this.data.channel_types.length, ...normalizedTypes); return this; } @@ -71,7 +75,6 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(channels); - optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length); this.data.default_values ??= []; this.data.default_values.push( @@ -91,7 +94,6 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(channels); - optionsLengthValidator.parse(normalizedValues.length); this.data.default_values = normalizedValues.map((id) => ({ id, @@ -102,13 +104,15 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder { + protected override readonly data: Partial; + /** * Creates a new select menu from API data. * @@ -35,8 +38,9 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder) { - super({ ...data, type: ComponentType.MentionableSelect }); + public constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.MentionableSelect }; } /** @@ -46,7 +50,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(roles); - optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length); this.data.default_values ??= []; this.data.default_values.push( @@ -66,7 +69,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(users); - optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length); this.data.default_values ??= []; this.data.default_values.push( @@ -91,7 +93,6 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder ) { const normalizedValues = normalizeArray(values); - optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length); this.data.default_values ??= []; this.data.default_values.push(...normalizedValues); return this; @@ -109,8 +110,20 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder ) { const normalizedValues = normalizeArray(values); - optionsLengthValidator.parse(normalizedValues.length); this.data.default_values = normalizedValues; return this; } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public override toJSON(validationOverride?: boolean): APIMentionableSelectComponent { + const clone = structuredClone(this.data); + + if (validationOverride ?? isValidationEnabled()) { + selectMenuMentionablePredicate.parse(clone); + } + + return clone as APIMentionableSelectComponent; + } } diff --git a/packages/builders/src/components/selectMenu/RoleSelectMenu.ts b/packages/builders/src/components/selectMenu/RoleSelectMenu.ts index 640be8f81539..3da65696fdbc 100644 --- a/packages/builders/src/components/selectMenu/RoleSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/RoleSelectMenu.ts @@ -5,13 +5,16 @@ import { SelectMenuDefaultValueType, } from 'discord-api-types/v10'; import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js'; -import { optionsLengthValidator } from '../Assertions.js'; +import { isValidationEnabled } from '../../util/validation.js'; +import { selectMenuRolePredicate } from '../Assertions.js'; import { BaseSelectMenuBuilder } from './BaseSelectMenu.js'; /** * A builder that creates API-compatible JSON data for role select menus. */ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder { + protected override readonly data: Partial; + /** * Creates a new select menu from API data. * @@ -34,8 +37,9 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder) { - super({ ...data, type: ComponentType.RoleSelect }); + public constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.RoleSelect }; } /** @@ -45,7 +49,6 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(roles); - optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length); this.data.default_values ??= []; this.data.default_values.push( @@ -65,7 +68,6 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(roles); - optionsLengthValidator.parse(normalizedValues.length); this.data.default_values = normalizedValues.map((id) => ({ id, @@ -74,4 +76,17 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder> { + options: StringSelectMenuOptionBuilder[]; +} + /** * A builder that creates API-compatible JSON data for string select menus. */ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder { + protected override readonly data: StringSelectMenuData; + /** - * The options within this select menu. + * The options for this select menu. */ - public readonly options: StringSelectMenuOptionBuilder[]; + public get options(): readonly StringSelectMenuOptionBuilder[] { + return this.data.options; + } /** * Creates a new select menu from API data. @@ -45,10 +57,13 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder) { - const { options, ...initData } = data ?? {}; - super({ ...initData, type: ComponentType.StringSelect }); - this.options = options?.map((option: APISelectMenuOption) => new StringSelectMenuOptionBuilder(option)) ?? []; + public constructor({ options = [], ...data }: Partial = {}) { + super(); + this.data = { + ...structuredClone(data), + options: options.map((option) => new StringSelectMenuOptionBuilder(option)), + type: ComponentType.StringSelect, + }; } /** @@ -56,16 +71,18 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder) { + public addOptions( + ...options: RestOrArray< + | APISelectMenuOption + | StringSelectMenuOptionBuilder + | ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder) + > + ) { const normalizedOptions = normalizeArray(options); - optionsLengthValidator.parse(this.options.length + normalizedOptions.length); - this.options.push( - ...normalizedOptions.map((normalizedOption) => - normalizedOption instanceof StringSelectMenuOptionBuilder - ? normalizedOption - : new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)), - ), - ); + const resolved = normalizedOptions.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder)); + + this.data.options.push(...resolved); + return this; } @@ -74,8 +91,14 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder) { - return this.spliceOptions(0, this.options.length, ...options); + public setOptions( + ...options: RestOrArray< + | APISelectMenuOption + | StringSelectMenuOptionBuilder + | ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder) + > + ) { + return this.spliceOptions(0, this.options.length, ...normalizeArray(options)); } /** @@ -108,36 +131,35 @@ export class StringSelectMenuBuilder extends BaseSelectMenuBuilder + ...options: ( + | APISelectMenuOption + | StringSelectMenuOptionBuilder + | ((builder: StringSelectMenuOptionBuilder) => StringSelectMenuOptionBuilder) + )[] ) { - const normalizedOptions = normalizeArray(options); - - const clone = [...this.options]; + const resolved = options.map((option) => resolveBuilder(option, StringSelectMenuOptionBuilder)); - clone.splice( - index, - deleteCount, - ...normalizedOptions.map((normalizedOption) => - normalizedOption instanceof StringSelectMenuOptionBuilder - ? normalizedOption - : new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(normalizedOption)), - ), - ); + this.data.options ??= []; + this.data.options.splice(index, deleteCount, ...resolved); - optionsLengthValidator.parse(clone.length); - this.options.splice(0, this.options.length, ...clone); return this; } /** - * {@inheritDoc BaseSelectMenuBuilder.toJSON} + * {@inheritDoc ComponentBuilder.toJSON} */ - public override toJSON(): APIStringSelectComponent { - validateRequiredSelectMenuParameters(this.options, this.data.custom_id); + public override toJSON(validationOverride?: boolean): APIStringSelectComponent { + const { options, ...rest } = this.data; + const data = { + ...(structuredClone(rest) as APIStringSelectComponent), + // selectMenuStringPredicate covers the validation of options + options: options.map((option) => option.toJSON(false)), + }; + + if (validationOverride ?? isValidationEnabled()) { + selectMenuStringPredicate.parse(data); + } - return { - ...this.data, - options: this.options.map((option) => option.toJSON()), - } as APIStringSelectComponent; + return data as APIStringSelectComponent; } } diff --git a/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts b/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts index 3e45970878e2..c2faa5361934 100644 --- a/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts @@ -1,16 +1,14 @@ import type { JSONEncodable } from '@discordjs/util'; import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10'; -import { - defaultValidator, - emojiValidator, - labelValueDescriptionValidator, - validateRequiredSelectMenuOptionParameters, -} from '../Assertions.js'; +import { isValidationEnabled } from '../../util/validation.js'; +import { selectMenuStringOptionPredicate } from '../Assertions.js'; /** * A builder that creates API-compatible JSON data for string select menu options. */ export class StringSelectMenuOptionBuilder implements JSONEncodable { + private readonly data: Partial; + /** * Creates a new string select menu option from API data. * @@ -33,7 +31,9 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable = {}) {} + public constructor(data: Partial = {}) { + this.data = structuredClone(data); + } /** * Sets the label for this option. @@ -41,7 +41,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable { + protected override readonly data: Partial; + /** * Creates a new select menu from API data. * @@ -34,8 +37,9 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder) { - super({ ...data, type: ComponentType.UserSelect }); + public constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.UserSelect }; } /** @@ -45,9 +49,8 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(users); - optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length); - this.data.default_values ??= []; + this.data.default_values ??= []; this.data.default_values.push( ...normalizedValues.map((id) => ({ id, @@ -65,7 +68,6 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder) { const normalizedValues = normalizeArray(users); - optionsLengthValidator.parse(normalizedValues.length); this.data.default_values = normalizedValues.map((id) => ({ id, @@ -74,4 +76,17 @@ export class UserSelectMenuBuilder extends BaseSelectMenuBuilder - implements Equatable> -{ +export class TextInputBuilder extends ComponentBuilder { + private readonly data: Partial; + /** * Creates a new text input from API data. * @@ -44,8 +32,9 @@ export class TextInputBuilder * .setStyle(TextInputStyle.Paragraph); * ``` */ - public constructor(data?: APITextInputComponent & { type?: ComponentType.TextInput }) { - super({ type: ComponentType.TextInput, ...data }); + public constructor(data: Partial = {}) { + super(); + this.data = { ...structuredClone(data), type: ComponentType.TextInput }; } /** @@ -54,7 +43,7 @@ export class TextInputBuilder * @param customId - The custom id to use */ public setCustomId(customId: string) { - this.data.custom_id = customIdValidator.parse(customId); + this.data.custom_id = customId; return this; } @@ -64,7 +53,7 @@ export class TextInputBuilder * @param label - The label to use */ public setLabel(label: string) { - this.data.label = labelValidator.parse(label); + this.data.label = label; return this; } @@ -74,7 +63,7 @@ export class TextInputBuilder * @param style - The style to use */ public setStyle(style: TextInputStyle) { - this.data.style = textInputStyleValidator.parse(style); + this.data.style = style; return this; } @@ -84,7 +73,15 @@ export class TextInputBuilder * @param minLength - The minimum length of text for this text input */ public setMinLength(minLength: number) { - this.data.min_length = minLengthValidator.parse(minLength); + this.data.min_length = minLength; + return this; + } + + /** + * Clears the minimum length of text for this text input. + */ + public clearMinLength() { + this.data.min_length = undefined; return this; } @@ -94,7 +91,15 @@ export class TextInputBuilder * @param maxLength - The maximum length of text for this text input */ public setMaxLength(maxLength: number) { - this.data.max_length = maxLengthValidator.parse(maxLength); + this.data.max_length = maxLength; + return this; + } + + /** + * Clears the maximum length of text for this text input. + */ + public clearMaxLength() { + this.data.max_length = undefined; return this; } @@ -104,7 +109,15 @@ export class TextInputBuilder * @param placeholder - The placeholder to use */ public setPlaceholder(placeholder: string) { - this.data.placeholder = placeholderValidator.parse(placeholder); + this.data.placeholder = placeholder; + return this; + } + + /** + * Clears the placeholder for this text input. + */ + public clearPlaceholder() { + this.data.placeholder = undefined; return this; } @@ -114,7 +127,15 @@ export class TextInputBuilder * @param value - The value to use */ public setValue(value: string) { - this.data.value = valueValidator.parse(value); + this.data.value = value; + return this; + } + + /** + * Clears the value for this text input. + */ + public clearValue() { + this.data.value = undefined; return this; } @@ -124,29 +145,20 @@ export class TextInputBuilder * @param required - Whether this text input is required */ public setRequired(required = true) { - this.data.required = requiredValidator.parse(required); + this.data.required = required; return this; } /** * {@inheritDoc ComponentBuilder.toJSON} */ - public toJSON(): APITextInputComponent { - validateRequiredParameters(this.data.custom_id, this.data.style, this.data.label); - - return { - ...this.data, - } as APITextInputComponent; - } + public toJSON(validationOverride?: boolean): APITextInputComponent { + const clone = structuredClone(this.data); - /** - * Whether this is equal to another structure. - */ - public equals(other: APITextInputComponent | JSONEncodable): boolean { - if (isJSONEncodable(other)) { - return isEqual(other.toJSON(), this.data); + if (validationOverride ?? isValidationEnabled()) { + textInputPredicate.parse(clone); } - return isEqual(other, this.data); + return clone as APITextInputComponent; } } diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index 53908612197a..11e2c650d870 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -1,68 +1,71 @@ -export * as EmbedAssertions from './messages/embed/Assertions.js'; -export * from './messages/embed/Embed.js'; -// TODO: Consider removing this dep in the next major version -export * from '@discordjs/formatters'; - -export * as ComponentAssertions from './components/Assertions.js'; -export * from './components/ActionRow.js'; +export * from './components/button/mixins/EmojiOrLabelButtonMixin.js'; export * from './components/button/Button.js'; -export * from './components/Component.js'; -export * from './components/Components.js'; -export * from './components/textInput/TextInput.js'; -export * as TextInputAssertions from './components/textInput/Assertions.js'; -export * from './interactions/modals/Modal.js'; -export * as ModalAssertions from './interactions/modals/Assertions.js'; +export * from './components/button/CustomIdButton.js'; +export * from './components/button/LinkButton.js'; +export * from './components/button/PremiumButton.js'; export * from './components/selectMenu/BaseSelectMenu.js'; export * from './components/selectMenu/ChannelSelectMenu.js'; export * from './components/selectMenu/MentionableSelectMenu.js'; export * from './components/selectMenu/RoleSelectMenu.js'; export * from './components/selectMenu/StringSelectMenu.js'; -// TODO: Remove those aliases in v2 -export { - /** - * @deprecated Will be removed in the next major version, use {@link StringSelectMenuBuilder} instead. - */ - StringSelectMenuBuilder as SelectMenuBuilder, -} from './components/selectMenu/StringSelectMenu.js'; -export { - /** - * @deprecated Will be removed in the next major version, use {@link StringSelectMenuOptionBuilder} instead. - */ - StringSelectMenuOptionBuilder as SelectMenuOptionBuilder, -} from './components/selectMenu/StringSelectMenuOption.js'; export * from './components/selectMenu/StringSelectMenuOption.js'; export * from './components/selectMenu/UserSelectMenu.js'; -export * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js'; -export * from './interactions/slashCommands/SlashCommandBuilder.js'; -export * from './interactions/slashCommands/SlashCommandSubcommands.js'; -export * from './interactions/slashCommands/options/boolean.js'; -export * from './interactions/slashCommands/options/channel.js'; -export * from './interactions/slashCommands/options/integer.js'; -export * from './interactions/slashCommands/options/mentionable.js'; -export * from './interactions/slashCommands/options/number.js'; -export * from './interactions/slashCommands/options/role.js'; -export * from './interactions/slashCommands/options/attachment.js'; -export * from './interactions/slashCommands/options/string.js'; -export * from './interactions/slashCommands/options/user.js'; -export * from './interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; -export * from './interactions/slashCommands/mixins/ApplicationCommandOptionBase.js'; -export * from './interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.js'; -export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; -export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.js'; -export * from './interactions/slashCommands/mixins/NameAndDescription.js'; -export * from './interactions/slashCommands/mixins/SharedSlashCommandOptions.js'; -export * from './interactions/slashCommands/mixins/SharedSubcommands.js'; -export * from './interactions/slashCommands/mixins/SharedSlashCommand.js'; +export * from './components/textInput/TextInput.js'; +export * from './components/textInput/Assertions.js'; -export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions.js'; -export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder.js'; +export * from './components/ActionRow.js'; +export * from './components/Assertions.js'; +export * from './components/Component.js'; +export * from './components/Components.js'; + +export * from './interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; +export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.js'; +export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; +export * from './interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.js'; +export * from './interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.js'; +export * from './interactions/commands/chatInput/mixins/SharedSubcommands.js'; + +export * from './interactions/commands/chatInput/options/ApplicationCommandOptionBase.js'; +export * from './interactions/commands/chatInput/options/boolean.js'; +export * from './interactions/commands/chatInput/options/channel.js'; +export * from './interactions/commands/chatInput/options/integer.js'; +export * from './interactions/commands/chatInput/options/mentionable.js'; +export * from './interactions/commands/chatInput/options/number.js'; +export * from './interactions/commands/chatInput/options/role.js'; +export * from './interactions/commands/chatInput/options/attachment.js'; +export * from './interactions/commands/chatInput/options/string.js'; +export * from './interactions/commands/chatInput/options/user.js'; + +export * from './interactions/commands/chatInput/Assertions.js'; +export * from './interactions/commands/chatInput/ChatInputCommand.js'; +export * from './interactions/commands/chatInput/ChatInputCommandSubcommands.js'; + +export * from './interactions/commands/contextMenu/Assertions.js'; +export * from './interactions/commands/contextMenu/ContextMenuCommand.js'; +export * from './interactions/commands/contextMenu/MessageCommand.js'; +export * from './interactions/commands/contextMenu/UserCommand.js'; + +export * from './interactions/commands/Command.js'; +export * from './interactions/commands/SharedName.js'; +export * from './interactions/commands/SharedNameAndDescription.js'; + +export * from './interactions/modals/Assertions.js'; +export * from './interactions/modals/Modal.js'; + +export * from './messages/embed/Assertions.js'; +export * from './messages/embed/Embed.js'; +export * from './messages/embed/EmbedAuthor.js'; +export * from './messages/embed/EmbedField.js'; +export * from './messages/embed/EmbedFooter.js'; export * from './util/componentUtil.js'; export * from './util/normalizeArray.js'; export * from './util/validation.js'; +export * from './Assertions.js'; + /** * The {@link https://github.com/discordjs/discord.js/blob/main/packages/builders#readme | @discordjs/builders} version * that you are currently using. diff --git a/packages/builders/src/interactions/commands/Command.ts b/packages/builders/src/interactions/commands/Command.ts new file mode 100644 index 000000000000..95fa60e642ea --- /dev/null +++ b/packages/builders/src/interactions/commands/Command.ts @@ -0,0 +1,83 @@ +import type { JSONEncodable } from '@discordjs/util'; +import type { + ApplicationIntegrationType, + InteractionContextType, + Permissions, + RESTPostAPIApplicationCommandsJSONBody, +} from 'discord-api-types/v10'; +import type { RestOrArray } from '../../util/normalizeArray.js'; +import { normalizeArray } from '../../util/normalizeArray.js'; + +export interface CommandData + extends Partial< + Pick< + RESTPostAPIApplicationCommandsJSONBody, + 'contexts' | 'default_member_permissions' | 'integration_types' | 'nsfw' + > + > {} + +export abstract class CommandBuilder + implements JSONEncodable +{ + protected declare readonly data: CommandData; + + /** + * Sets the contexts of this command. + * + * @param contexts - The contexts + */ + public setContexts(...contexts: RestOrArray) { + this.data.contexts = normalizeArray(contexts); + return this; + } + + /** + * Sets the integration types of this command. + * + * @param integrationTypes - The integration types + */ + public setIntegrationTypes(...integrationTypes: RestOrArray) { + this.data.integration_types = normalizeArray(integrationTypes); + return this; + } + + /** + * Sets the default permissions a member should have in order to run the command. + * + * @remarks + * You can set this to `'0'` to disable the command by default. + * @param permissions - The permissions bit field to set + * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} + */ + public setDefaultMemberPermissions(permissions: Permissions | bigint | number) { + this.data.default_member_permissions = typeof permissions === 'string' ? permissions : permissions.toString(); + return this; + } + + /** + * Clears the default permissions a member should have in order to run the command. + */ + public clearDefaultMemberPermissions() { + this.data.default_member_permissions = undefined; + return this; + } + + /** + * Sets whether this command is NSFW. + * + * @param nsfw - Whether this command is NSFW + */ + public setNSFW(nsfw = true) { + this.data.nsfw = nsfw; + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public abstract toJSON(validationOverride?: boolean): Command; +} diff --git a/packages/builders/src/interactions/commands/SharedName.ts b/packages/builders/src/interactions/commands/SharedName.ts new file mode 100644 index 000000000000..18df419737c8 --- /dev/null +++ b/packages/builders/src/interactions/commands/SharedName.ts @@ -0,0 +1,64 @@ +import type { LocaleString, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v10'; + +export interface SharedNameData + extends Partial> {} + +/** + * This mixin holds name and description symbols for chat input commands. + */ +export class SharedName { + protected readonly data: SharedNameData = {}; + + /** + * Sets the name of this command. + * + * @param name - The name to use + */ + public setName(name: string): this { + this.data.name = name; + return this; + } + + /** + * Sets a name localization for this command. + * + * @param locale - The locale to set + * @param localizedName - The localized name for the given `locale` + */ + public setNameLocalization(locale: LocaleString, localizedName: string) { + this.data.name_localizations ??= {}; + this.data.name_localizations[locale] = localizedName; + + return this; + } + + /** + * Clears a name localization for this command. + * + * @param locale - The locale to clear + */ + public clearNameLocalization(locale: LocaleString) { + this.data.name_localizations ??= {}; + this.data.name_localizations[locale] = undefined; + + return this; + } + + /** + * Sets the name localizations for this command. + * + * @param localizedNames - The object of localized names to set + */ + public setNameLocalizations(localizedNames: Partial>) { + this.data.name_localizations = structuredClone(localizedNames); + return this; + } + + /** + * Clears all name localizations for this command. + */ + public clearNameLocalizations() { + this.data.name_localizations = undefined; + return this; + } +} diff --git a/packages/builders/src/interactions/commands/SharedNameAndDescription.ts b/packages/builders/src/interactions/commands/SharedNameAndDescription.ts new file mode 100644 index 000000000000..4a12202db55b --- /dev/null +++ b/packages/builders/src/interactions/commands/SharedNameAndDescription.ts @@ -0,0 +1,67 @@ +import type { APIApplicationCommand, LocaleString } from 'discord-api-types/v10'; +import type { SharedNameData } from './SharedName.js'; +import { SharedName } from './SharedName.js'; + +export interface SharedNameAndDescriptionData + extends SharedNameData, + Partial> {} + +/** + * This mixin holds name and description symbols for chat input commands. + */ +export class SharedNameAndDescription extends SharedName { + protected override readonly data: SharedNameAndDescriptionData = {}; + + /** + * Sets the description of this command. + * + * @param description - The description to use + */ + public setDescription(description: string) { + this.data.description = description; + return this; + } + + /** + * Sets a description localization for this command. + * + * @param locale - The locale to set + * @param localizedDescription - The localized description for the given `locale` + */ + public setDescriptionLocalization(locale: LocaleString, localizedDescription: string) { + this.data.description_localizations ??= {}; + this.data.description_localizations[locale] = localizedDescription; + + return this; + } + + /** + * Clears a description localization for this command. + * + * @param locale - The locale to clear + */ + public clearDescriptionLocalization(locale: LocaleString) { + this.data.description_localizations ??= {}; + this.data.description_localizations[locale] = undefined; + + return this; + } + + /** + * Sets the description localizations for this command. + * + * @param localizedDescriptions - The object of localized descriptions to set + */ + public setDescriptionLocalizations(localizedDescriptions: Partial>) { + this.data.description_localizations = structuredClone(localizedDescriptions); + return this; + } + + /** + * Clears all description localizations for this command. + */ + public clearDescriptionLocalizations() { + this.data.description_localizations = undefined; + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/Assertions.ts b/packages/builders/src/interactions/commands/chatInput/Assertions.ts new file mode 100644 index 000000000000..e0f9dd009144 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/Assertions.ts @@ -0,0 +1,154 @@ +import { + ApplicationIntegrationType, + InteractionContextType, + ApplicationCommandOptionType, +} from 'discord-api-types/v10'; +import type { ZodTypeAny } from 'zod'; +import { z } from 'zod'; +import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js'; +import { ApplicationCommandOptionAllowedChannelTypes } from './mixins/ApplicationCommandOptionChannelTypesMixin.js'; + +const namePredicate = z + .string() + .min(1) + .max(32) + .regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u); + +const descriptionPredicate = z.string().min(1).max(100); + +const sharedNameAndDescriptionPredicate = z.object({ + name: namePredicate, + name_localizations: localeMapPredicate.optional(), + description: descriptionPredicate, + description_localizations: localeMapPredicate.optional(), +}); + +const numericMixinNumberOptionPredicate = z.object({ + max_value: z.number().safe().optional(), + min_value: z.number().safe().optional(), +}); + +const numericMixinIntegerOptionPredicate = z.object({ + max_value: z.number().safe().int().optional(), + min_value: z.number().safe().int().optional(), +}); + +const channelMixinOptionPredicate = z.object({ + channel_types: z + .union( + ApplicationCommandOptionAllowedChannelTypes.map((type) => z.literal(type)) as unknown as [ + ZodTypeAny, + ZodTypeAny, + ...ZodTypeAny[], + ], + ) + .array() + .optional(), +}); + +const autocompleteMixinOptionPredicate = z.object({ + autocomplete: z.literal(true), + choices: z.union([z.never(), z.never().array(), z.undefined()]), +}); + +const choiceValueStringPredicate = z.string().min(1).max(100); +const choiceValueNumberPredicate = z.number().safe(); +const choiceBasePredicate = z.object({ + name: choiceValueStringPredicate, + name_localizations: localeMapPredicate.optional(), +}); +const choiceStringPredicate = choiceBasePredicate.extend({ + value: choiceValueStringPredicate, +}); +const choiceNumberPredicate = choiceBasePredicate.extend({ + value: choiceValueNumberPredicate, +}); + +const choiceBaseMixinPredicate = z.object({ + autocomplete: z.literal(false).optional(), +}); +const choiceStringMixinPredicate = choiceBaseMixinPredicate.extend({ + choices: choiceStringPredicate.array().max(25).optional(), +}); +const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({ + choices: choiceNumberPredicate.array().max(25).optional(), +}); + +const basicOptionTypes = [ + ApplicationCommandOptionType.Attachment, + ApplicationCommandOptionType.Boolean, + ApplicationCommandOptionType.Channel, + ApplicationCommandOptionType.Integer, + ApplicationCommandOptionType.Mentionable, + ApplicationCommandOptionType.Number, + ApplicationCommandOptionType.Role, + ApplicationCommandOptionType.String, + ApplicationCommandOptionType.User, +] as const; + +const basicOptionTypesPredicate = z.union( + basicOptionTypes.map((type) => z.literal(type)) as unknown as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]], +); + +export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({ + required: z.boolean().optional(), + type: basicOptionTypesPredicate, +}); + +const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [ + autocompleteMixinOptionPredicate, + choiceStringMixinPredicate, +]); + +const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [ + autocompleteMixinOptionPredicate, + choiceNumberMixinPredicate, +]); + +export const channelOptionPredicate = basicOptionPredicate.merge(channelMixinOptionPredicate); + +export const integerOptionPredicate = basicOptionPredicate + .merge(numericMixinIntegerOptionPredicate) + .and(autocompleteOrNumberChoicesMixinOptionPredicate); + +export const numberOptionPredicate = basicOptionPredicate + .merge(numericMixinNumberOptionPredicate) + .and(autocompleteOrNumberChoicesMixinOptionPredicate); + +export const stringOptionPredicate = basicOptionPredicate + .extend({ + max_length: z.number().min(0).max(6_000).optional(), + min_length: z.number().min(1).max(6_000).optional(), + }) + .and(autocompleteOrStringChoicesMixinOptionPredicate); + +const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({ + contexts: z.array(z.nativeEnum(InteractionContextType)).optional(), + default_member_permissions: memberPermissionsPredicate.optional(), + integration_types: z.array(z.nativeEnum(ApplicationIntegrationType)).optional(), + nsfw: z.boolean().optional(), +}); + +// Because you can only add options via builders, there's no need to validate whole objects here otherwise +const chatInputCommandOptionsPredicate = z.union([ + z.object({ type: basicOptionTypesPredicate }).array(), + z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }).array(), + z.object({ type: z.literal(ApplicationCommandOptionType.SubcommandGroup) }).array(), +]); + +export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({ + options: chatInputCommandOptionsPredicate.optional(), +}); + +export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({ + type: z.literal(ApplicationCommandOptionType.SubcommandGroup), + options: z + .array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) })) + .min(1) + .max(25), +}); + +export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({ + type: z.literal(ApplicationCommandOptionType.Subcommand), + options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25), +}); diff --git a/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts new file mode 100644 index 000000000000..422b5d9371ae --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts @@ -0,0 +1,37 @@ +import { ApplicationCommandType, type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { isValidationEnabled } from '../../../util/validation.js'; +import { CommandBuilder } from '../Command.js'; +import { SharedNameAndDescription } from '../SharedNameAndDescription.js'; +import { chatInputCommandPredicate } from './Assertions.js'; +import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js'; +import { SharedChatInputCommandSubcommands } from './mixins/SharedSubcommands.js'; + +/** + * A builder that creates API-compatible JSON data for chat input commands. + */ +export class ChatInputCommandBuilder extends Mixin( + CommandBuilder, + SharedChatInputCommandOptions, + SharedNameAndDescription, + SharedChatInputCommandSubcommands, +) { + /** + * {@inheritDoc CommandBuilder.toJSON} + */ + public toJSON(validationOverride?: boolean): RESTPostAPIChatInputApplicationCommandsJSONBody { + const { options, ...rest } = this.data; + + const data: RESTPostAPIChatInputApplicationCommandsJSONBody = { + ...structuredClone(rest as Omit), + type: ApplicationCommandType.ChatInput, + options: options?.map((option) => option.toJSON(validationOverride)), + }; + + if (validationOverride ?? isValidationEnabled()) { + chatInputCommandPredicate.parse(data); + } + + return data; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts b/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts new file mode 100644 index 000000000000..7ecfd6a641b9 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts @@ -0,0 +1,111 @@ +import type { JSONEncodable } from '@discordjs/util'; +import type { + APIApplicationCommandSubcommandOption, + APIApplicationCommandSubcommandGroupOption, +} from 'discord-api-types/v10'; +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js'; +import { resolveBuilder } from '../../../util/resolveBuilder.js'; +import { isValidationEnabled } from '../../../util/validation.js'; +import type { SharedNameAndDescriptionData } from '../SharedNameAndDescription.js'; +import { SharedNameAndDescription } from '../SharedNameAndDescription.js'; +import { chatInputCommandSubcommandGroupPredicate, chatInputCommandSubcommandPredicate } from './Assertions.js'; +import { SharedChatInputCommandOptions } from './mixins/SharedChatInputCommandOptions.js'; + +export interface ChatInputCommandSubcommandGroupData { + options?: ChatInputCommandSubcommandBuilder[]; +} + +/** + * Represents a folder for subcommands. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups} + */ +export class ChatInputCommandSubcommandGroupBuilder + extends SharedNameAndDescription + implements JSONEncodable +{ + protected declare readonly data: ChatInputCommandSubcommandGroupData & SharedNameAndDescriptionData; + + public get options(): readonly ChatInputCommandSubcommandBuilder[] { + return (this.data.options ??= []); + } + + /** + * Adds a new subcommand to this group. + * + * @param input - A function that returns a subcommand builder or an already built builder + */ + public addSubcommands( + ...input: RestOrArray< + | ChatInputCommandSubcommandBuilder + | ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder) + > + ) { + const normalized = normalizeArray(input); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const result = normalized.map((builder) => resolveBuilder(builder, ChatInputCommandSubcommandBuilder)); + + this.data.options ??= []; + this.data.options.push(...result); + + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandGroupOption { + const { options, ...rest } = this.data; + + const data = { + ...(structuredClone(rest) as Omit), + type: ApplicationCommandOptionType.SubcommandGroup as const, + options: options?.map((option) => option.toJSON(validationOverride)) ?? [], + }; + + if (validationOverride ?? isValidationEnabled()) { + chatInputCommandSubcommandGroupPredicate.parse(data); + } + + return data; + } +} + +/** + * A builder that creates API-compatible JSON data for chat input command subcommands. + * + * @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups} + */ +export class ChatInputCommandSubcommandBuilder + extends Mixin(SharedNameAndDescription, SharedChatInputCommandOptions) + implements JSONEncodable +{ + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public toJSON(validationOverride?: boolean): APIApplicationCommandSubcommandOption { + const { options, ...rest } = this.data; + + const data = { + ...(structuredClone(rest) as Omit), + type: ApplicationCommandOptionType.Subcommand as const, + options: options?.map((option) => option.toJSON(validationOverride)) ?? [], + }; + + if (validationOverride ?? isValidationEnabled()) { + chatInputCommandSubcommandPredicate.parse(data); + } + + return data; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts new file mode 100644 index 000000000000..409cae2998aa --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts @@ -0,0 +1,47 @@ +import type { APIApplicationCommandIntegerOption } from 'discord-api-types/v10'; + +export interface ApplicationCommandNumericOptionMinMaxValueData + extends Pick {} + +/** + * This mixin holds minimum and maximum symbols used for options. + */ +export abstract class ApplicationCommandNumericOptionMinMaxValueMixin { + protected declare readonly data: ApplicationCommandNumericOptionMinMaxValueData; + + /** + * Sets the maximum number value of this option. + * + * @param max - The maximum value this option can be + */ + public setMaxValue(max: number): this { + this.data.max_value = max; + return this; + } + + /** + * Removes the maximum number value of this option. + */ + public clearMaxValue(): this { + this.data.max_value = undefined; + return this; + } + + /** + * Sets the minimum number value of this option. + * + * @param min - The minimum value this option can be + */ + public setMinValue(min: number): this { + this.data.min_value = min; + return this; + } + + /** + * Removes the minimum number value of this option. + */ + public clearMinValue(): this { + this.data.min_value = undefined; + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts new file mode 100644 index 000000000000..3502e745868a --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionChannelTypesMixin.ts @@ -0,0 +1,52 @@ +import { ChannelType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10'; +import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray'; + +export const ApplicationCommandOptionAllowedChannelTypes = [ + ChannelType.GuildText, + ChannelType.GuildVoice, + ChannelType.GuildCategory, + ChannelType.GuildAnnouncement, + ChannelType.AnnouncementThread, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildStageVoice, + ChannelType.GuildForum, + ChannelType.GuildMedia, +] as const; + +/** + * Allowed channel types used for a channel option. + */ +export type ApplicationCommandOptionAllowedChannelTypes = (typeof ApplicationCommandOptionAllowedChannelTypes)[number]; + +export interface ApplicationCommandOptionChannelTypesData + extends Pick {} + +/** + * This mixin holds channel type symbols used for options. + */ +export class ApplicationCommandOptionChannelTypesMixin { + protected declare readonly data: ApplicationCommandOptionChannelTypesData; + + /** + * Adds channel types to this option. + * + * @param channelTypes - The channel types + */ + public addChannelTypes(...channelTypes: RestOrArray) { + this.data.channel_types ??= []; + this.data.channel_types.push(...normalizeArray(channelTypes)); + + return this; + } + + /** + * Sets the channel types for this option. + * + * @param channelTypes - The channel types + */ + public setChannelTypes(...channelTypes: RestOrArray) { + this.data.channel_types = normalizeArray(channelTypes); + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts new file mode 100644 index 000000000000..2e9271e2246f --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts @@ -0,0 +1,29 @@ +import type { + APIApplicationCommandIntegerOption, + APIApplicationCommandNumberOption, + APIApplicationCommandStringOption, +} from 'discord-api-types/v10'; + +export type AutocompletableOptions = + | APIApplicationCommandIntegerOption + | APIApplicationCommandNumberOption + | APIApplicationCommandStringOption; + +export interface ApplicationCommandOptionWithAutocompleteData extends Pick {} + +/** + * This mixin holds choices and autocomplete symbols used for options. + */ +export class ApplicationCommandOptionWithAutocompleteMixin { + protected declare readonly data: ApplicationCommandOptionWithAutocompleteData; + + /** + * Whether this option uses autocomplete. + * + * @param autocomplete - Whether this option should use autocomplete + */ + public setAutocomplete(autocomplete = true): this { + this.data.autocomplete = autocomplete; + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.ts b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.ts new file mode 100644 index 000000000000..93223390df48 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/mixins/ApplicationCommandOptionWithChoicesMixin.ts @@ -0,0 +1,38 @@ +import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v10'; +import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js'; + +// Unlike other places, we're not `Pick`ing from discord-api-types. The union includes `[]` and it breaks everything. +export interface ApplicationCommandOptionWithChoicesData { + choices?: APIApplicationCommandOptionChoice[]; +} + +/** + * This mixin holds choices and autocomplete symbols used for options. + */ +export class ApplicationCommandOptionWithChoicesMixin { + protected declare readonly data: ApplicationCommandOptionWithChoicesData; + + /** + * Adds multiple choices to this option. + * + * @param choices - The choices to add + */ + public addChoices(...choices: RestOrArray>): this { + const normalizedChoices = normalizeArray(choices); + + this.data.choices ??= []; + this.data.choices.push(...normalizedChoices); + + return this; + } + + /** + * Sets multiple choices for this option. + * + * @param choices - The choices to set + */ + public setChoices(...choices: RestOrArray>): this { + this.data.choices = normalizeArray(choices); + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.ts b/packages/builders/src/interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.ts new file mode 100644 index 000000000000..dd6624c0918c --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/mixins/SharedChatInputCommandOptions.ts @@ -0,0 +1,200 @@ +import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js'; +import { resolveBuilder } from '../../../../util/resolveBuilder.js'; +import type { ApplicationCommandOptionBase } from '../options/ApplicationCommandOptionBase.js'; +import { ChatInputCommandAttachmentOption } from '../options/attachment.js'; +import { ChatInputCommandBooleanOption } from '../options/boolean.js'; +import { ChatInputCommandChannelOption } from '../options/channel.js'; +import { ChatInputCommandIntegerOption } from '../options/integer.js'; +import { ChatInputCommandMentionableOption } from '../options/mentionable.js'; +import { ChatInputCommandNumberOption } from '../options/number.js'; +import { ChatInputCommandRoleOption } from '../options/role.js'; +import { ChatInputCommandStringOption } from '../options/string.js'; +import { ChatInputCommandUserOption } from '../options/user.js'; + +export interface SharedChatInputCommandOptionsData { + options?: ApplicationCommandOptionBase[]; +} + +/** + * This mixin holds symbols that can be shared in chat input command options. + * + * @typeParam TypeAfterAddingOptions - The type this class should return after adding an option. + */ +export class SharedChatInputCommandOptions { + protected declare readonly data: SharedChatInputCommandOptionsData; + + public get options(): readonly ApplicationCommandOptionBase[] { + return (this.data.options ??= []); + } + + /** + * Adds boolean options. + * + * @param options - Options to add + */ + public addBooleanOptions( + ...options: RestOrArray< + ChatInputCommandBooleanOption | ((builder: ChatInputCommandBooleanOption) => ChatInputCommandBooleanOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandBooleanOption, ...options); + } + + /** + * Adds user options. + * + * @param options - Options to add + */ + public addUserOptions( + ...options: RestOrArray< + ChatInputCommandUserOption | ((builder: ChatInputCommandUserOption) => ChatInputCommandUserOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandUserOption, ...options); + } + + /** + * Adds channel options. + * + * @param options - Options to add + */ + public addChannelOptions( + ...options: RestOrArray< + ChatInputCommandChannelOption | ((builder: ChatInputCommandChannelOption) => ChatInputCommandChannelOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandChannelOption, ...options); + } + + /** + * Adds role options. + * + * @param options - Options to add + */ + public addRoleOptions( + ...options: RestOrArray< + ChatInputCommandRoleOption | ((builder: ChatInputCommandRoleOption) => ChatInputCommandRoleOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandRoleOption, ...options); + } + + /** + * Adds attachment options. + * + * @param options - Options to add + */ + public addAttachmentOptions( + ...options: RestOrArray< + | ChatInputCommandAttachmentOption + | ((builder: ChatInputCommandAttachmentOption) => ChatInputCommandAttachmentOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandAttachmentOption, ...options); + } + + /** + * Adds mentionable options. + * + * @param options - Options to add + */ + public addMentionableOptions( + ...options: RestOrArray< + | ChatInputCommandMentionableOption + | ((builder: ChatInputCommandMentionableOption) => ChatInputCommandMentionableOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandMentionableOption, ...options); + } + + /** + * Adds string options. + * + * @param options - Options to add + */ + public addStringOptions( + ...options: RestOrArray< + ChatInputCommandStringOption | ((builder: ChatInputCommandStringOption) => ChatInputCommandStringOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandStringOption, ...options); + } + + /** + * Adds integer options. + * + * @param options - Options to add + */ + public addIntegerOptions( + ...options: RestOrArray< + ChatInputCommandIntegerOption | ((builder: ChatInputCommandIntegerOption) => ChatInputCommandIntegerOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandIntegerOption, ...options); + } + + /** + * Adds number options. + * + * @param options - Options to add + */ + public addNumberOptions( + ...options: RestOrArray< + ChatInputCommandNumberOption | ((builder: ChatInputCommandNumberOption) => ChatInputCommandNumberOption) + > + ) { + return this.sharedAddOptions(ChatInputCommandNumberOption, ...options); + } + + /** + * Removes, replaces, or inserts options for this command. + * + * @remarks + * This method behaves similarly + * to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}. + * + * It's useful for modifying and adjusting order of the already-existing options for this command. + * @example + * Remove the first option: + * ```ts + * actionRow.spliceOptions(0, 1); + * ``` + * @example + * Remove the first n options: + * ```ts + * const n = 4; + * actionRow.spliceOptions(0, n); + * ``` + * @example + * Remove the last option: + * ```ts + * actionRow.spliceOptions(-1, 1); + * ``` + * @param index - The index to start at + * @param deleteCount - The number of options to remove + * @param options - The replacing option objects + */ + public spliceOptions(index: number, deleteCount: number, ...options: ApplicationCommandOptionBase[]): this { + this.data.options ??= []; + this.data.options.splice(index, deleteCount, ...options); + return this; + } + + /** + * Where the actual adding magic happens. ✨ + * + * @internal + */ + private sharedAddOptions( + Instance: new () => OptionBuilder, + ...options: RestOrArray OptionBuilder)> + ): this { + const normalized = normalizeArray(options); + const resolved = normalized.map((option) => resolveBuilder(option, Instance)); + + this.data.options ??= []; + this.data.options.push(...resolved); + + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts b/packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts new file mode 100644 index 000000000000..a3d03edf8fa6 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/mixins/SharedSubcommands.ts @@ -0,0 +1,60 @@ +import type { RestOrArray } from '../../../../util/normalizeArray.js'; +import { normalizeArray } from '../../../../util/normalizeArray.js'; +import { resolveBuilder } from '../../../../util/resolveBuilder.js'; +import { + ChatInputCommandSubcommandGroupBuilder, + ChatInputCommandSubcommandBuilder, +} from '../ChatInputCommandSubcommands.js'; + +export interface SharedChatInputCommandSubcommandsData { + options?: (ChatInputCommandSubcommandBuilder | ChatInputCommandSubcommandGroupBuilder)[]; +} + +/** + * This mixin holds symbols that can be shared in chat input subcommands. + * + * @typeParam TypeAfterAddingSubcommands - The type this class should return after adding a subcommand or subcommand group. + */ +export class SharedChatInputCommandSubcommands { + protected declare readonly data: SharedChatInputCommandSubcommandsData; + + /** + * Adds subcommand groups to this command. + * + * @param input - Subcommand groups to add + */ + public addSubcommandGroups( + ...input: RestOrArray< + | ChatInputCommandSubcommandGroupBuilder + | ((subcommandGroup: ChatInputCommandSubcommandGroupBuilder) => ChatInputCommandSubcommandGroupBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandGroupBuilder)); + + this.data.options ??= []; + this.data.options.push(...resolved); + + return this; + } + + /** + * Adds subcommands to this command. + * + * @param input - Subcommands to add + */ + public addSubcommands( + ...input: RestOrArray< + | ChatInputCommandSubcommandBuilder + | ((subcommandGroup: ChatInputCommandSubcommandBuilder) => ChatInputCommandSubcommandBuilder) + > + ): this { + const normalized = normalizeArray(input); + const resolved = normalized.map((value) => resolveBuilder(value, ChatInputCommandSubcommandBuilder)); + + this.data.options ??= []; + this.data.options.push(...resolved); + + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts b/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts new file mode 100644 index 000000000000..7016083e6acf --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts @@ -0,0 +1,59 @@ +import type { JSONEncodable } from '@discordjs/util'; +import type { + APIApplicationCommandBasicOption, + APIApplicationCommandOption, + ApplicationCommandOptionType, +} from 'discord-api-types/v10'; +import type { z } from 'zod'; +import { isValidationEnabled } from '../../../../util/validation.js'; +import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js'; +import { SharedNameAndDescription } from '../../SharedNameAndDescription.js'; +import { basicOptionPredicate } from '../Assertions.js'; + +export interface ApplicationCommandOptionBaseData extends Partial> { + type: ApplicationCommandOptionType; +} + +/** + * The base application command option builder that contains common symbols for application command builders. + */ +export abstract class ApplicationCommandOptionBase + extends SharedNameAndDescription + implements JSONEncodable +{ + protected static readonly predicate: z.ZodTypeAny = basicOptionPredicate; + + protected declare readonly data: ApplicationCommandOptionBaseData & SharedNameAndDescriptionData; + + public constructor(type: ApplicationCommandOptionType) { + super(); + this.data.type = type; + } + + /** + * Sets whether this option is required. + * + * @param required - Whether this option should be required + */ + public setRequired(required = true) { + this.data.required = required; + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public toJSON(validationOverride?: boolean): APIApplicationCommandBasicOption { + const clone = structuredClone(this.data); + + if (validationOverride ?? isValidationEnabled()) { + (this.constructor as typeof ApplicationCommandOptionBase).predicate.parse(clone); + } + + return clone as APIApplicationCommandBasicOption; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/attachment.ts b/packages/builders/src/interactions/commands/chatInput/options/attachment.ts new file mode 100644 index 000000000000..2fe3b5f2e244 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/attachment.ts @@ -0,0 +1,11 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command attachment option. + */ +export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase { + public constructor() { + super(ApplicationCommandOptionType.Attachment); + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/boolean.ts b/packages/builders/src/interactions/commands/chatInput/options/boolean.ts new file mode 100644 index 000000000000..38fa850143dd --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/boolean.ts @@ -0,0 +1,11 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command boolean option. + */ +export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase { + public constructor() { + super(ApplicationCommandOptionType.Boolean); + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/channel.ts b/packages/builders/src/interactions/commands/chatInput/options/channel.ts new file mode 100644 index 000000000000..3200d0e50302 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/channel.ts @@ -0,0 +1,19 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { channelOptionPredicate } from '../Assertions.js'; +import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command channel option. + */ +export class ChatInputCommandChannelOption extends Mixin( + ApplicationCommandOptionBase, + ApplicationCommandOptionChannelTypesMixin, +) { + protected static override readonly predicate = channelOptionPredicate; + + public constructor() { + super(ApplicationCommandOptionType.Channel); + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/integer.ts b/packages/builders/src/interactions/commands/chatInput/options/integer.ts new file mode 100644 index 000000000000..d730fb8696e1 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/integer.ts @@ -0,0 +1,23 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { integerOptionPredicate } from '../Assertions.js'; +import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; +import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; +import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command integer option. + */ +export class ChatInputCommandIntegerOption extends Mixin( + ApplicationCommandOptionBase, + ApplicationCommandNumericOptionMinMaxValueMixin, + ApplicationCommandOptionWithAutocompleteMixin, + ApplicationCommandOptionWithChoicesMixin, +) { + protected static override readonly predicate = integerOptionPredicate; + + public constructor() { + super(ApplicationCommandOptionType.Integer); + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/mentionable.ts b/packages/builders/src/interactions/commands/chatInput/options/mentionable.ts new file mode 100644 index 000000000000..65810328c842 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/mentionable.ts @@ -0,0 +1,11 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command mentionable option. + */ +export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase { + public constructor() { + super(ApplicationCommandOptionType.Mentionable); + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/number.ts b/packages/builders/src/interactions/commands/chatInput/options/number.ts new file mode 100644 index 000000000000..5f81065c5f6b --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/number.ts @@ -0,0 +1,23 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { numberOptionPredicate } from '../Assertions.js'; +import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; +import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; +import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command number option. + */ +export class ChatInputCommandNumberOption extends Mixin( + ApplicationCommandOptionBase, + ApplicationCommandNumericOptionMinMaxValueMixin, + ApplicationCommandOptionWithAutocompleteMixin, + ApplicationCommandOptionWithChoicesMixin, +) { + protected static override readonly predicate = numberOptionPredicate; + + public constructor() { + super(ApplicationCommandOptionType.Number); + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/role.ts b/packages/builders/src/interactions/commands/chatInput/options/role.ts new file mode 100644 index 000000000000..064509cfb0b3 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/role.ts @@ -0,0 +1,11 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command role option. + */ +export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase { + public constructor() { + super(ApplicationCommandOptionType.Role); + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/string.ts b/packages/builders/src/interactions/commands/chatInput/options/string.ts new file mode 100644 index 000000000000..0e3e81563d71 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/string.ts @@ -0,0 +1,65 @@ +import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { stringOptionPredicate } from '../Assertions.js'; +import type { ApplicationCommandOptionWithAutocompleteData } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; +import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; +import type { ApplicationCommandOptionWithChoicesData } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js'; +import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; +import type { ApplicationCommandOptionBaseData } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command string option. + */ +export class ChatInputCommandStringOption extends Mixin( + ApplicationCommandOptionBase, + ApplicationCommandOptionWithAutocompleteMixin, + ApplicationCommandOptionWithChoicesMixin, +) { + protected static override readonly predicate = stringOptionPredicate; + + protected declare readonly data: ApplicationCommandOptionBaseData & + ApplicationCommandOptionWithAutocompleteData & + ApplicationCommandOptionWithChoicesData & + Partial>; + + public constructor() { + super(ApplicationCommandOptionType.String); + } + + /** + * Sets the maximum length of this string option. + * + * @param max - The maximum length this option can be + */ + public setMaxLength(max: number): this { + this.data.max_length = max; + return this; + } + + /** + * Clears the maximum length of this string option. + */ + public clearMaxLength(): this { + this.data.max_length = undefined; + return this; + } + + /** + * Sets the minimum length of this string option. + * + * @param min - The minimum length this option can be + */ + public setMinLength(min: number): this { + this.data.min_length = min; + return this; + } + + /** + * Clears the minimum length of this string option. + */ + public clearMinLength(): this { + this.data.min_length = undefined; + return this; + } +} diff --git a/packages/builders/src/interactions/commands/chatInput/options/user.ts b/packages/builders/src/interactions/commands/chatInput/options/user.ts new file mode 100644 index 000000000000..9f59e129ab93 --- /dev/null +++ b/packages/builders/src/interactions/commands/chatInput/options/user.ts @@ -0,0 +1,11 @@ +import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; + +/** + * A chat input command user option. + */ +export class ChatInputCommandUserOption extends ApplicationCommandOptionBase { + public constructor() { + super(ApplicationCommandOptionType.User); + } +} diff --git a/packages/builders/src/interactions/commands/contextMenu/Assertions.ts b/packages/builders/src/interactions/commands/contextMenu/Assertions.ts new file mode 100644 index 000000000000..16a8d8039ed9 --- /dev/null +++ b/packages/builders/src/interactions/commands/contextMenu/Assertions.ts @@ -0,0 +1,30 @@ +import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10'; +import { z } from 'zod'; +import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js'; + +const namePredicate = z + .string() + .min(1) + .max(32) + // eslint-disable-next-line prefer-named-capture-group + .regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u); + +const contextsPredicate = z.array(z.nativeEnum(InteractionContextType)); +const integrationTypesPredicate = z.array(z.nativeEnum(ApplicationIntegrationType)); + +const baseContextMenuCommandPredicate = z.object({ + contexts: contextsPredicate.optional(), + default_member_permissions: memberPermissionsPredicate.optional(), + name: namePredicate, + name_localizations: localeMapPredicate.optional(), + integration_types: integrationTypesPredicate.optional(), + nsfw: z.boolean().optional(), +}); + +export const userCommandPredicate = baseContextMenuCommandPredicate.extend({ + type: z.literal(ApplicationCommandType.User), +}); + +export const messageCommandPredicate = baseContextMenuCommandPredicate.extend({ + type: z.literal(ApplicationCommandType.Message), +}); diff --git a/packages/builders/src/interactions/commands/contextMenu/ContextMenuCommand.ts b/packages/builders/src/interactions/commands/contextMenu/ContextMenuCommand.ts new file mode 100644 index 000000000000..1d12a8134612 --- /dev/null +++ b/packages/builders/src/interactions/commands/contextMenu/ContextMenuCommand.ts @@ -0,0 +1,29 @@ +import type { ApplicationCommandType, RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10'; +import { Mixin } from 'ts-mixer'; +import { CommandBuilder } from '../Command.js'; +import { SharedName } from '../SharedName.js'; + +/** + * The type a context menu command can be. + */ +export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User; + +/** + * A builder that creates API-compatible JSON data for context menu commands. + */ +export abstract class ContextMenuCommandBuilder extends Mixin( + CommandBuilder, + SharedName, +) { + protected override readonly data: Partial; + + public constructor(data: Partial = {}) { + super(); + this.data = structuredClone(data); + } + + /** + * {@inheritDoc CommandBuilder.toJSON} + */ + public abstract override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody; +} diff --git a/packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts b/packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts new file mode 100644 index 000000000000..ccaab7bc33eb --- /dev/null +++ b/packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts @@ -0,0 +1,19 @@ +import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../../util/validation.js'; +import { messageCommandPredicate } from './Assertions.js'; +import { ContextMenuCommandBuilder } from './ContextMenuCommand.js'; + +export class MessageContextCommandBuilder extends ContextMenuCommandBuilder { + /** + * {@inheritDoc CommandBuilder.toJSON} + */ + public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody { + const data = { ...structuredClone(this.data), type: ApplicationCommandType.Message }; + + if (validationOverride ?? isValidationEnabled()) { + messageCommandPredicate.parse(data); + } + + return data as RESTPostAPIContextMenuApplicationCommandsJSONBody; + } +} diff --git a/packages/builders/src/interactions/commands/contextMenu/UserCommand.ts b/packages/builders/src/interactions/commands/contextMenu/UserCommand.ts new file mode 100644 index 000000000000..b911fb11f387 --- /dev/null +++ b/packages/builders/src/interactions/commands/contextMenu/UserCommand.ts @@ -0,0 +1,19 @@ +import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../../util/validation.js'; +import { userCommandPredicate } from './Assertions.js'; +import { ContextMenuCommandBuilder } from './ContextMenuCommand.js'; + +export class UserContextCommandBuilder extends ContextMenuCommandBuilder { + /** + * {@inheritDoc CommandBuilder.toJSON} + */ + public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody { + const data = { ...structuredClone(this.data), type: ApplicationCommandType.User }; + + if (validationOverride ?? isValidationEnabled()) { + userCommandPredicate.parse(data); + } + + return data as RESTPostAPIContextMenuApplicationCommandsJSONBody; + } +} diff --git a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts deleted file mode 100644 index 72d6c50f05cf..000000000000 --- a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; -import type { ContextMenuCommandType } from './ContextMenuCommandBuilder.js'; - -const namePredicate = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(32) - // eslint-disable-next-line prefer-named-capture-group - .regex(/^( *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]+ *)+$/u) - .setValidationEnabled(isValidationEnabled); -const typePredicate = s - .union([s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message)]) - .setValidationEnabled(isValidationEnabled); -const booleanPredicate = s.boolean(); - -export function validateDefaultPermission(value: unknown): asserts value is boolean { - booleanPredicate.parse(value); -} - -export function validateName(name: unknown): asserts name is string { - namePredicate.parse(name); -} - -export function validateType(type: unknown): asserts type is ContextMenuCommandType { - typePredicate.parse(type); -} - -export function validateRequiredParameters(name: string, type: number) { - // Assert name matches all conditions - validateName(name); - - // Assert type is valid - validateType(type); -} - -const dmPermissionPredicate = s.boolean().nullish(); - -export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined { - dmPermissionPredicate.parse(value); -} - -const memberPermissionPredicate = s - .union([ - s.bigint().transform((value) => value.toString()), - s - .number() - .safeInt() - .transform((value) => value.toString()), - s.string().regex(/^\d+$/), - ]) - .nullish(); - -export function validateDefaultMemberPermissions(permissions: unknown) { - return memberPermissionPredicate.parse(permissions); -} - -export const contextsPredicate = s.array( - s.nativeEnum(InteractionContextType).setValidationEnabled(isValidationEnabled), -); - -export const integrationTypesPredicate = s.array( - s.nativeEnum(ApplicationIntegrationType).setValidationEnabled(isValidationEnabled), -); diff --git a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts b/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts deleted file mode 100644 index db0e9712f4f1..000000000000 --- a/packages/builders/src/interactions/contextMenuCommands/ContextMenuCommandBuilder.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { - ApplicationCommandType, - ApplicationIntegrationType, - InteractionContextType, - LocaleString, - LocalizationMap, - Permissions, - RESTPostAPIContextMenuApplicationCommandsJSONBody, -} from 'discord-api-types/v10'; -import type { RestOrArray } from '../../util/normalizeArray.js'; -import { normalizeArray } from '../../util/normalizeArray.js'; -import { validateLocale, validateLocalizationMap } from '../slashCommands/Assertions.js'; -import { - validateRequiredParameters, - validateName, - validateType, - validateDefaultPermission, - validateDefaultMemberPermissions, - validateDMPermission, - contextsPredicate, - integrationTypesPredicate, -} from './Assertions.js'; - -/** - * The type a context menu command can be. - */ -export type ContextMenuCommandType = ApplicationCommandType.Message | ApplicationCommandType.User; - -/** - * A builder that creates API-compatible JSON data for context menu commands. - */ -export class ContextMenuCommandBuilder { - /** - * The name of this command. - */ - public readonly name: string = undefined!; - - /** - * The name localizations of this command. - */ - public readonly name_localizations?: LocalizationMap; - - /** - * The type of this command. - */ - public readonly type: ContextMenuCommandType = undefined!; - - /** - * The contexts for this command. - */ - public readonly contexts?: InteractionContextType[]; - - /** - * Whether this command is enabled by default when the application is added to a guild. - * - * @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead. - */ - public readonly default_permission: boolean | undefined = undefined; - - /** - * The set of permissions represented as a bit set for the command. - */ - public readonly default_member_permissions: Permissions | null | undefined = undefined; - - /** - * Indicates whether the command is available in direct messages with the application. - * - * @remarks - * By default, commands are visible. This property is only for global commands. - * @deprecated - * Use {@link ContextMenuCommandBuilder.contexts} instead. - */ - public readonly dm_permission: boolean | undefined = undefined; - - /** - * The integration types for this command. - */ - public readonly integration_types?: ApplicationIntegrationType[]; - - /** - * Sets the contexts of this command. - * - * @param contexts - The contexts - */ - public setContexts(...contexts: RestOrArray) { - Reflect.set(this, 'contexts', contextsPredicate.parse(normalizeArray(contexts))); - - return this; - } - - /** - * Sets integration types of this command. - * - * @param integrationTypes - The integration types - */ - public setIntegrationTypes(...integrationTypes: RestOrArray) { - Reflect.set(this, 'integration_types', integrationTypesPredicate.parse(normalizeArray(integrationTypes))); - - return this; - } - - /** - * Sets the name of this command. - * - * @param name - The name to use - */ - public setName(name: string) { - // Assert the name matches the conditions - validateName(name); - - Reflect.set(this, 'name', name); - - return this; - } - - /** - * Sets the type of this command. - * - * @param type - The type to use - */ - public setType(type: ContextMenuCommandType) { - // Assert the type is valid - validateType(type); - - Reflect.set(this, 'type', type); - - return this; - } - - /** - * Sets whether the command is enabled by default when the application is added to a guild. - * - * @remarks - * If set to `false`, you will have to later `PUT` the permissions for this command. - * @param value - Whether to enable this command by default - * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} - * @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead. - */ - public setDefaultPermission(value: boolean) { - // Assert the value matches the conditions - validateDefaultPermission(value); - - Reflect.set(this, 'default_permission', value); - - return this; - } - - /** - * Sets the default permissions a member should have in order to run this command. - * - * @remarks - * You can set this to `'0'` to disable the command by default. - * @param permissions - The permissions bit field to set - * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} - */ - public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) { - // Assert the value and parse it - const permissionValue = validateDefaultMemberPermissions(permissions); - - Reflect.set(this, 'default_member_permissions', permissionValue); - - return this; - } - - /** - * Sets if the command is available in direct messages with the application. - * - * @remarks - * By default, commands are visible. This method is only for global commands. - * @param enabled - Whether the command should be enabled in direct messages - * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} - * @deprecated Use {@link ContextMenuCommandBuilder.setContexts} instead. - */ - public setDMPermission(enabled: boolean | null | undefined) { - // Assert the value matches the conditions - validateDMPermission(enabled); - - Reflect.set(this, 'dm_permission', enabled); - - return this; - } - - /** - * Sets a name localization for this command. - * - * @param locale - The locale to set - * @param localizedName - The localized name for the given `locale` - */ - public setNameLocalization(locale: LocaleString, localizedName: string | null) { - if (!this.name_localizations) { - Reflect.set(this, 'name_localizations', {}); - } - - const parsedLocale = validateLocale(locale); - - if (localizedName === null) { - this.name_localizations![parsedLocale] = null; - return this; - } - - validateName(localizedName); - - this.name_localizations![parsedLocale] = localizedName; - return this; - } - - /** - * Sets the name localizations for this command. - * - * @param localizedNames - The object of localized names to set - */ - public setNameLocalizations(localizedNames: LocalizationMap | null) { - if (localizedNames === null) { - Reflect.set(this, 'name_localizations', null); - return this; - } - - Reflect.set(this, 'name_localizations', {}); - - for (const args of Object.entries(localizedNames)) - this.setNameLocalization(...(args as [LocaleString, string | null])); - return this; - } - - /** - * Serializes this builder to API-compatible JSON data. - * - * @remarks - * This method runs validations on the data before serializing it. - * As such, it may throw an error if the data is invalid. - */ - public toJSON(): RESTPostAPIContextMenuApplicationCommandsJSONBody { - validateRequiredParameters(this.name, this.type); - - validateLocalizationMap(this.name_localizations); - - return { ...this }; - } -} diff --git a/packages/builders/src/interactions/modals/Assertions.ts b/packages/builders/src/interactions/modals/Assertions.ts index 79597ff47076..84bf3686155b 100644 --- a/packages/builders/src/interactions/modals/Assertions.ts +++ b/packages/builders/src/interactions/modals/Assertions.ts @@ -1,25 +1,21 @@ -import { s } from '@sapphire/shapeshift'; -import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js'; -import { customIdValidator } from '../../components/Assertions.js'; -import { isValidationEnabled } from '../../util/validation.js'; +import { ComponentType } from 'discord-api-types/v10'; +import { z } from 'zod'; +import { customIdPredicate } from '../../Assertions.js'; -export const titleValidator = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(45) - .setValidationEnabled(isValidationEnabled); -export const componentsValidator = s - .instance(ActionRowBuilder) - .array() - .lengthGreaterThanOrEqual(1) - .setValidationEnabled(isValidationEnabled); +const titlePredicate = z.string().min(1).max(45); -export function validateRequiredParameters( - customId?: string, - title?: string, - components?: ActionRowBuilder[], -) { - customIdValidator.parse(customId); - titleValidator.parse(title); - componentsValidator.parse(components); -} +export const modalPredicate = z.object({ + title: titlePredicate, + custom_id: customIdPredicate, + components: z + .object({ + type: z.literal(ComponentType.ActionRow), + components: z + .object({ type: z.literal(ComponentType.TextInput) }) + .array() + .length(1), + }) + .array() + .min(1) + .max(5), +}); diff --git a/packages/builders/src/interactions/modals/Modal.ts b/packages/builders/src/interactions/modals/Modal.ts index 948d774df203..6191f1d9197d 100644 --- a/packages/builders/src/interactions/modals/Modal.ts +++ b/packages/builders/src/interactions/modals/Modal.ts @@ -6,11 +6,16 @@ import type { APIModalActionRowComponent, APIModalInteractionResponseCallbackData, } from 'discord-api-types/v10'; -import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../../components/ActionRow.js'; -import { customIdValidator } from '../../components/Assertions.js'; +import { ActionRowBuilder } from '../../components/ActionRow.js'; import { createComponentBuilder } from '../../components/Components.js'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; -import { titleValidator, validateRequiredParameters } from './Assertions.js'; +import { resolveBuilder } from '../../util/resolveBuilder.js'; +import { isValidationEnabled } from '../../util/validation.js'; +import { modalPredicate } from './Assertions.js'; + +export interface ModalBuilderData extends Partial> { + components: ActionRowBuilder[]; +} /** * A builder that creates API-compatible JSON data for modals. @@ -19,22 +24,25 @@ export class ModalBuilder implements JSONEncodable; + private readonly data: ModalBuilderData; /** * The components within this modal. */ - public readonly components: ActionRowBuilder[] = []; + public get components(): readonly ActionRowBuilder[] { + return this.data.components; + } /** * Creates a new modal from API data. * * @param data - The API data to create this modal with */ - public constructor({ components, ...data }: Partial = {}) { - this.data = { ...data }; - this.components = (components?.map((component) => createComponentBuilder(component)) ?? - []) as ActionRowBuilder[]; + public constructor({ components = [], ...data }: Partial = {}) { + this.data = { + ...structuredClone(data), + components: components.map((component) => createComponentBuilder(component)), + }; } /** @@ -43,7 +51,7 @@ export class ModalBuilder implements JSONEncodable | APIActionRowComponent + | ActionRowBuilder + | APIActionRowComponent + | ((builder: ActionRowBuilder) => ActionRowBuilder) > ) { - this.components.push( - ...normalizeArray(components).map((component) => - component instanceof ActionRowBuilder - ? component - : new ActionRowBuilder(component), - ), - ); + const normalized = normalizeArray(components); + const resolved = normalized.map((row) => resolveBuilder(row, ActionRowBuilder)); + + this.data.components.push(...resolved); + return this; } /** - * Sets components for this modal. + * Sets the action rows for this modal. * * @param components - The components to set */ - public setComponents(...components: RestOrArray>) { - this.components.splice(0, this.components.length, ...normalizeArray(components)); + public setActionRows( + ...components: RestOrArray< + | ActionRowBuilder + | APIActionRowComponent + | ((builder: ActionRowBuilder) => ActionRowBuilder) + > + ) { + const normalized = normalizeArray(components); + this.spliceActionRows(0, this.data.components.length, ...normalized); + return this; } /** - * {@inheritDoc ComponentBuilder.toJSON} + * Removes, replaces, or inserts action rows for this modal. + * + * @remarks + * This method behaves similarly + * to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice | Array.prototype.splice()}. + * The maximum amount of action rows that can be added is 5. + * + * It's useful for modifying and adjusting order of the already-existing action rows of a modal. + * @example + * Remove the first action row: + * ```ts + * embed.spliceActionRows(0, 1); + * ``` + * @example + * Remove the first n action rows: + * ```ts + * const n = 4; + * embed.spliceActionRows(0, n); + * ``` + * @example + * Remove the last action row: + * ```ts + * embed.spliceActionRows(-1, 1); + * ``` + * @param index - The index to start at + * @param deleteCount - The number of action rows to remove + * @param rows - The replacing action row objects */ - public toJSON(): APIModalInteractionResponseCallbackData { - validateRequiredParameters(this.data.custom_id, this.data.title, this.components); + public spliceActionRows( + index: number, + deleteCount: number, + ...rows: ( + | ActionRowBuilder + | APIActionRowComponent + | ((builder: ActionRowBuilder) => ActionRowBuilder) + )[] + ): this { + const resolved = rows.map((row) => resolveBuilder(row, ActionRowBuilder)); + this.data.components.splice(index, deleteCount, ...resolved); + + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public toJSON(validationOverride?: boolean): APIModalInteractionResponseCallbackData { + const { components, ...rest } = this.data; + + const data = { + ...structuredClone(rest), + components: components.map((component) => component.toJSON(validationOverride)), + }; + + if (validationOverride ?? isValidationEnabled()) { + modalPredicate.parse(data); + } - return { - ...this.data, - components: this.components.map((component) => component.toJSON()), - } as APIModalInteractionResponseCallbackData; + return data as APIModalInteractionResponseCallbackData; } } diff --git a/packages/builders/src/interactions/slashCommands/Assertions.ts b/packages/builders/src/interactions/slashCommands/Assertions.ts deleted file mode 100644 index cc12e4dfef9a..000000000000 --- a/packages/builders/src/interactions/slashCommands/Assertions.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import { - ApplicationIntegrationType, - InteractionContextType, - Locale, - type APIApplicationCommandOptionChoice, - type LocalizationMap, -} from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; -import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder.js'; -import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands.js'; -import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase.js'; - -const namePredicate = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(32) - .regex(/^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u) - .setValidationEnabled(isValidationEnabled); - -export function validateName(name: unknown): asserts name is string { - namePredicate.parse(name); -} - -const descriptionPredicate = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(100) - .setValidationEnabled(isValidationEnabled); -const localePredicate = s.nativeEnum(Locale); - -export function validateDescription(description: unknown): asserts description is string { - descriptionPredicate.parse(description); -} - -const maxArrayLengthPredicate = s.unknown().array().lengthLessThanOrEqual(25).setValidationEnabled(isValidationEnabled); -export function validateLocale(locale: unknown) { - return localePredicate.parse(locale); -} - -export function validateMaxOptionsLength(options: unknown): asserts options is ToAPIApplicationCommandOptions[] { - maxArrayLengthPredicate.parse(options); -} - -export function validateRequiredParameters( - name: string, - description: string, - options: ToAPIApplicationCommandOptions[], -) { - // Assert name matches all conditions - validateName(name); - - // Assert description conditions - validateDescription(description); - - // Assert options conditions - validateMaxOptionsLength(options); -} - -const booleanPredicate = s.boolean(); - -export function validateDefaultPermission(value: unknown): asserts value is boolean { - booleanPredicate.parse(value); -} - -export function validateRequired(required: unknown): asserts required is boolean { - booleanPredicate.parse(required); -} - -const choicesLengthPredicate = s.number().lessThanOrEqual(25).setValidationEnabled(isValidationEnabled); - -export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void { - choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding); -} - -export function assertReturnOfBuilder< - ReturnType extends ApplicationCommandOptionBase | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder, ->(input: unknown, ExpectedInstanceOf: new () => ReturnType): asserts input is ReturnType { - s.instance(ExpectedInstanceOf).parse(input); -} - -export const localizationMapPredicate = s - .object(Object.fromEntries(Object.values(Locale).map((locale) => [locale, s.string().nullish()]))) - .strict() - .nullish() - .setValidationEnabled(isValidationEnabled); - -export function validateLocalizationMap(value: unknown): asserts value is LocalizationMap { - localizationMapPredicate.parse(value); -} - -const dmPermissionPredicate = s.boolean().nullish(); - -export function validateDMPermission(value: unknown): asserts value is boolean | null | undefined { - dmPermissionPredicate.parse(value); -} - -const memberPermissionPredicate = s - .union([ - s.bigint().transform((value) => value.toString()), - s - .number() - .safeInt() - .transform((value) => value.toString()), - s.string().regex(/^\d+$/), - ]) - .nullish(); - -export function validateDefaultMemberPermissions(permissions: unknown) { - return memberPermissionPredicate.parse(permissions); -} - -export function validateNSFW(value: unknown): asserts value is boolean { - booleanPredicate.parse(value); -} - -export const contextsPredicate = s.array( - s.nativeEnum(InteractionContextType).setValidationEnabled(isValidationEnabled), -); - -export const integrationTypesPredicate = s.array( - s.nativeEnum(ApplicationIntegrationType).setValidationEnabled(isValidationEnabled), -); diff --git a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts b/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts deleted file mode 100644 index ef6ae652eb8f..000000000000 --- a/packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { - APIApplicationCommandOption, - ApplicationIntegrationType, - InteractionContextType, - LocalizationMap, - Permissions, -} from 'discord-api-types/v10'; -import { mix } from 'ts-mixer'; -import { SharedNameAndDescription } from './mixins/NameAndDescription.js'; -import { SharedSlashCommand } from './mixins/SharedSlashCommand.js'; -import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js'; -import { SharedSlashCommandSubcommands } from './mixins/SharedSubcommands.js'; - -/** - * A builder that creates API-compatible JSON data for slash commands. - */ -@mix(SharedSlashCommandOptions, SharedNameAndDescription, SharedSlashCommandSubcommands, SharedSlashCommand) -export class SlashCommandBuilder { - /** - * The name of this command. - */ - public readonly name: string = undefined!; - - /** - * The name localizations of this command. - */ - public readonly name_localizations?: LocalizationMap; - - /** - * The description of this command. - */ - public readonly description: string = undefined!; - - /** - * The description localizations of this command. - */ - public readonly description_localizations?: LocalizationMap; - - /** - * The options of this command. - */ - public readonly options: ToAPIApplicationCommandOptions[] = []; - - /** - * The contexts for this command. - */ - public readonly contexts?: InteractionContextType[]; - - /** - * Whether this command is enabled by default when the application is added to a guild. - * - * @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead. - */ - public readonly default_permission: boolean | undefined = undefined; - - /** - * The set of permissions represented as a bit set for the command. - */ - public readonly default_member_permissions: Permissions | null | undefined = undefined; - - /** - * Indicates whether the command is available in direct messages with the application. - * - * @remarks - * By default, commands are visible. This property is only for global commands. - * @deprecated - * Use {@link SlashCommandBuilder.contexts} instead. - */ - public readonly dm_permission: boolean | undefined = undefined; - - /** - * The integration types for this command. - */ - public readonly integration_types?: ApplicationIntegrationType[]; - - /** - * Whether this command is NSFW. - */ - public readonly nsfw: boolean | undefined = undefined; -} - -export interface SlashCommandBuilder - extends SharedNameAndDescription, - SharedSlashCommandOptions, - SharedSlashCommandSubcommands, - SharedSlashCommand {} - -/** - * An interface specifically for slash command subcommands. - */ -export interface SlashCommandSubcommandsOnlyBuilder - extends SharedNameAndDescription, - SharedSlashCommandSubcommands, - SharedSlashCommand {} - -/** - * An interface specifically for slash command options. - */ -export interface SlashCommandOptionsOnlyBuilder - extends SharedNameAndDescription, - SharedSlashCommandOptions, - SharedSlashCommand {} - -/** - * An interface that ensures the `toJSON()` call will return something - * that can be serialized into API-compatible data. - */ -export interface ToAPIApplicationCommandOptions { - toJSON(): APIApplicationCommandOption; -} diff --git a/packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts b/packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts deleted file mode 100644 index 58159e4f98a0..000000000000 --- a/packages/builders/src/interactions/slashCommands/SlashCommandSubcommands.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - ApplicationCommandOptionType, - type APIApplicationCommandSubcommandGroupOption, - type APIApplicationCommandSubcommandOption, -} from 'discord-api-types/v10'; -import { mix } from 'ts-mixer'; -import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions.js'; -import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder.js'; -import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase.js'; -import { SharedNameAndDescription } from './mixins/NameAndDescription.js'; -import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js'; - -/** - * Represents a folder for subcommands. - * - * @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups} - */ -@mix(SharedNameAndDescription) -export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationCommandOptions { - /** - * The name of this subcommand group. - */ - public readonly name: string = undefined!; - - /** - * The description of this subcommand group. - */ - public readonly description: string = undefined!; - - /** - * The subcommands within this subcommand group. - */ - public readonly options: SlashCommandSubcommandBuilder[] = []; - - /** - * Adds a new subcommand to this group. - * - * @param input - A function that returns a subcommand builder or an already built builder - */ - public addSubcommand( - input: - | SlashCommandSubcommandBuilder - | ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder), - ) { - const { options } = this; - - // First, assert options conditions - we cannot have more than 25 options - validateMaxOptionsLength(options); - - // Get the final result - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input; - - // eslint-disable-next-line @typescript-eslint/no-use-before-define - assertReturnOfBuilder(result, SlashCommandSubcommandBuilder); - - // Push it - options.push(result); - - return this; - } - - /** - * Serializes this builder to API-compatible JSON data. - * - * @remarks - * This method runs validations on the data before serializing it. - * As such, it may throw an error if the data is invalid. - */ - public toJSON(): APIApplicationCommandSubcommandGroupOption { - validateRequiredParameters(this.name, this.description, this.options); - - return { - type: ApplicationCommandOptionType.SubcommandGroup, - name: this.name, - name_localizations: this.name_localizations, - description: this.description, - description_localizations: this.description_localizations, - options: this.options.map((option) => option.toJSON()), - }; - } -} - -export interface SlashCommandSubcommandGroupBuilder extends SharedNameAndDescription {} - -/** - * A builder that creates API-compatible JSON data for slash command subcommands. - * - * @see {@link https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups} - */ -@mix(SharedNameAndDescription, SharedSlashCommandOptions) -export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOptions { - /** - * The name of this subcommand. - */ - public readonly name: string = undefined!; - - /** - * The description of this subcommand. - */ - public readonly description: string = undefined!; - - /** - * The options within this subcommand. - */ - public readonly options: ApplicationCommandOptionBase[] = []; - - /** - * Serializes this builder to API-compatible JSON data. - * - * @remarks - * This method runs validations on the data before serializing it. - * As such, it may throw an error if the data is invalid. - */ - public toJSON(): APIApplicationCommandSubcommandOption { - validateRequiredParameters(this.name, this.description, this.options); - - return { - type: ApplicationCommandOptionType.Subcommand, - name: this.name, - name_localizations: this.name_localizations, - description: this.description, - description_localizations: this.description_localizations, - options: this.options.map((option) => option.toJSON()), - }; - } -} - -export interface SlashCommandSubcommandBuilder - extends SharedNameAndDescription, - SharedSlashCommandOptions {} diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts deleted file mode 100644 index 0cdbdbe6266f..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * This mixin holds minimum and maximum symbols used for options. - */ -export abstract class ApplicationCommandNumericOptionMinMaxValueMixin { - /** - * The maximum value of this option. - */ - public readonly max_value?: number; - - /** - * The minimum value of this option. - */ - public readonly min_value?: number; - - /** - * Sets the maximum number value of this option. - * - * @param max - The maximum value this option can be - */ - public abstract setMaxValue(max: number): this; - - /** - * Sets the minimum number value of this option. - * - * @param min - The minimum value this option can be - */ - public abstract setMinValue(min: number): this; -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts deleted file mode 100644 index 51f450e0f355..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v10'; -import { validateRequiredParameters, validateRequired, validateLocalizationMap } from '../Assertions.js'; -import { SharedNameAndDescription } from './NameAndDescription.js'; - -/** - * The base application command option builder that contains common symbols for application command builders. - */ -export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription { - /** - * The type of this option. - */ - public abstract readonly type: ApplicationCommandOptionType; - - /** - * Whether this option is required. - * - * @defaultValue `false` - */ - public readonly required: boolean = false; - - /** - * Sets whether this option is required. - * - * @param required - Whether this option should be required - */ - public setRequired(required: boolean) { - // Assert that you actually passed a boolean - validateRequired(required); - - Reflect.set(this, 'required', required); - - return this; - } - - /** - * Serializes this builder to API-compatible JSON data. - * - * @remarks - * This method runs validations on the data before serializing it. - * As such, it may throw an error if the data is invalid. - */ - public abstract toJSON(): APIApplicationCommandBasicOption; - - /** - * This method runs required validators on this builder. - */ - protected runRequiredValidations() { - validateRequiredParameters(this.name, this.description, []); - - // Validate localizations - validateLocalizationMap(this.name_localizations); - validateLocalizationMap(this.description_localizations); - - // Assert that you actually passed a boolean - validateRequired(this.required); - } -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts deleted file mode 100644 index 98f3242bcd49..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import { ChannelType } from 'discord-api-types/v10'; -import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray'; - -/** - * The allowed channel types used for a channel option in a slash command builder. - * - * @privateRemarks This can't be dynamic because const enums are erased at runtime. - * @internal - */ -const allowedChannelTypes = [ - ChannelType.GuildText, - ChannelType.GuildVoice, - ChannelType.GuildCategory, - ChannelType.GuildAnnouncement, - ChannelType.AnnouncementThread, - ChannelType.PublicThread, - ChannelType.PrivateThread, - ChannelType.GuildStageVoice, - ChannelType.GuildForum, - ChannelType.GuildMedia, -] as const; - -/** - * The type of allowed channel types used for a channel option. - */ -export type ApplicationCommandOptionAllowedChannelTypes = (typeof allowedChannelTypes)[number]; - -const channelTypesPredicate = s.array(s.union(allowedChannelTypes.map((type) => s.literal(type)))); - -/** - * This mixin holds channel type symbols used for options. - */ -export class ApplicationCommandOptionChannelTypesMixin { - /** - * The channel types of this option. - */ - public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[]; - - /** - * Adds channel types to this option. - * - * @param channelTypes - The channel types - */ - public addChannelTypes(...channelTypes: RestOrArray) { - if (this.channel_types === undefined) { - Reflect.set(this, 'channel_types', []); - } - - this.channel_types!.push(...channelTypesPredicate.parse(normalizeArray(channelTypes))); - - return this; - } -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts deleted file mode 100644 index 6f2ceee10966..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import type { ApplicationCommandOptionType } from 'discord-api-types/v10'; - -const booleanPredicate = s.boolean(); - -/** - * This mixin holds choices and autocomplete symbols used for options. - */ -export class ApplicationCommandOptionWithAutocompleteMixin { - /** - * Whether this option utilizes autocomplete. - */ - public readonly autocomplete?: boolean; - - /** - * The type of this option. - * - * @privateRemarks Since this is present and this is a mixin, this is needed. - */ - public readonly type!: ApplicationCommandOptionType; - - /** - * Whether this option uses autocomplete. - * - * @param autocomplete - Whether this option should use autocomplete - */ - public setAutocomplete(autocomplete: boolean): this { - // Assert that you actually passed a boolean - booleanPredicate.parse(autocomplete); - - if (autocomplete && 'choices' in this && Array.isArray(this.choices) && this.choices.length > 0) { - throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); - } - - Reflect.set(this, 'autocomplete', autocomplete); - - return this; - } -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.ts deleted file mode 100644 index 68359b4b2130..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import { ApplicationCommandOptionType, type APIApplicationCommandOptionChoice } from 'discord-api-types/v10'; -import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js'; -import { localizationMapPredicate, validateChoicesLength } from '../Assertions.js'; - -const stringPredicate = s.string().lengthGreaterThanOrEqual(1).lengthLessThanOrEqual(100); -const numberPredicate = s.number().greaterThan(Number.NEGATIVE_INFINITY).lessThan(Number.POSITIVE_INFINITY); -const choicesPredicate = s - .object({ - name: stringPredicate, - name_localizations: localizationMapPredicate, - value: s.union([stringPredicate, numberPredicate]), - }) - .array(); - -/** - * This mixin holds choices and autocomplete symbols used for options. - */ -export class ApplicationCommandOptionWithChoicesMixin { - /** - * The choices of this option. - */ - public readonly choices?: APIApplicationCommandOptionChoice[]; - - /** - * The type of this option. - * - * @privateRemarks Since this is present and this is a mixin, this is needed. - */ - public readonly type!: ApplicationCommandOptionType; - - /** - * Adds multiple choices to this option. - * - * @param choices - The choices to add - */ - public addChoices(...choices: RestOrArray>): this { - const normalizedChoices = normalizeArray(choices); - if (normalizedChoices.length > 0 && 'autocomplete' in this && this.autocomplete) { - throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); - } - - choicesPredicate.parse(normalizedChoices); - - if (this.choices === undefined) { - Reflect.set(this, 'choices', []); - } - - validateChoicesLength(normalizedChoices.length, this.choices); - - for (const { name, name_localizations, value } of normalizedChoices) { - // Validate the value - if (this.type === ApplicationCommandOptionType.String) { - stringPredicate.parse(value); - } else { - numberPredicate.parse(value); - } - - this.choices!.push({ name, name_localizations, value }); - } - - return this; - } - - /** - * Sets multiple choices for this option. - * - * @param choices - The choices to set - */ - public setChoices>(...choices: RestOrArray): this { - const normalizedChoices = normalizeArray(choices); - if (normalizedChoices.length > 0 && 'autocomplete' in this && this.autocomplete) { - throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); - } - - choicesPredicate.parse(normalizedChoices); - - Reflect.set(this, 'choices', []); - this.addChoices(normalizedChoices); - - return this; - } -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts b/packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts deleted file mode 100644 index 644c9bac6fa6..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/NameAndDescription.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { LocaleString, LocalizationMap } from 'discord-api-types/v10'; -import { validateDescription, validateLocale, validateName } from '../Assertions.js'; - -/** - * This mixin holds name and description symbols for slash commands. - */ -export class SharedNameAndDescription { - /** - * The name of this command. - */ - public readonly name!: string; - - /** - * The name localizations of this command. - */ - public readonly name_localizations?: LocalizationMap; - - /** - * The description of this command. - */ - public readonly description!: string; - - /** - * The description localizations of this command. - */ - public readonly description_localizations?: LocalizationMap; - - /** - * Sets the name of this command. - * - * @param name - The name to use - */ - public setName(name: string): this { - // Assert the name matches the conditions - validateName(name); - - Reflect.set(this, 'name', name); - - return this; - } - - /** - * Sets the description of this command. - * - * @param description - The description to use - */ - public setDescription(description: string) { - // Assert the description matches the conditions - validateDescription(description); - - Reflect.set(this, 'description', description); - - return this; - } - - /** - * Sets a name localization for this command. - * - * @param locale - The locale to set - * @param localizedName - The localized name for the given `locale` - */ - public setNameLocalization(locale: LocaleString, localizedName: string | null) { - if (!this.name_localizations) { - Reflect.set(this, 'name_localizations', {}); - } - - const parsedLocale = validateLocale(locale); - - if (localizedName === null) { - this.name_localizations![parsedLocale] = null; - return this; - } - - validateName(localizedName); - - this.name_localizations![parsedLocale] = localizedName; - return this; - } - - /** - * Sets the name localizations for this command. - * - * @param localizedNames - The object of localized names to set - */ - public setNameLocalizations(localizedNames: LocalizationMap | null) { - if (localizedNames === null) { - Reflect.set(this, 'name_localizations', null); - return this; - } - - Reflect.set(this, 'name_localizations', {}); - - for (const args of Object.entries(localizedNames)) { - this.setNameLocalization(...(args as [LocaleString, string | null])); - } - - return this; - } - - /** - * Sets a description localization for this command. - * - * @param locale - The locale to set - * @param localizedDescription - The localized description for the given locale - */ - public setDescriptionLocalization(locale: LocaleString, localizedDescription: string | null) { - if (!this.description_localizations) { - Reflect.set(this, 'description_localizations', {}); - } - - const parsedLocale = validateLocale(locale); - - if (localizedDescription === null) { - this.description_localizations![parsedLocale] = null; - return this; - } - - validateDescription(localizedDescription); - - this.description_localizations![parsedLocale] = localizedDescription; - return this; - } - - /** - * Sets the description localizations for this command. - * - * @param localizedDescriptions - The object of localized descriptions to set - */ - public setDescriptionLocalizations(localizedDescriptions: LocalizationMap | null) { - if (localizedDescriptions === null) { - Reflect.set(this, 'description_localizations', null); - return this; - } - - Reflect.set(this, 'description_localizations', {}); - for (const args of Object.entries(localizedDescriptions)) { - this.setDescriptionLocalization(...(args as [LocaleString, string | null])); - } - - return this; - } -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts deleted file mode 100644 index 32b48edd459d..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommand.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { - ApplicationCommandType, - type ApplicationIntegrationType, - type InteractionContextType, - type LocalizationMap, - type Permissions, - type RESTPostAPIChatInputApplicationCommandsJSONBody, -} from 'discord-api-types/v10'; -import type { RestOrArray } from '../../../util/normalizeArray.js'; -import { normalizeArray } from '../../../util/normalizeArray.js'; -import { - contextsPredicate, - integrationTypesPredicate, - validateDMPermission, - validateDefaultMemberPermissions, - validateDefaultPermission, - validateLocalizationMap, - validateNSFW, - validateRequiredParameters, -} from '../Assertions.js'; -import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder.js'; - -/** - * This mixin holds symbols that can be shared in slashcommands independent of options or subcommands. - */ -export class SharedSlashCommand { - public readonly name: string = undefined!; - - public readonly name_localizations?: LocalizationMap; - - public readonly description: string = undefined!; - - public readonly description_localizations?: LocalizationMap; - - public readonly options: ToAPIApplicationCommandOptions[] = []; - - public readonly contexts?: InteractionContextType[]; - - /** - * @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead. - */ - public readonly default_permission: boolean | undefined = undefined; - - public readonly default_member_permissions: Permissions | null | undefined = undefined; - - /** - * @deprecated Use {@link SharedSlashCommand.contexts} instead. - */ - public readonly dm_permission: boolean | undefined = undefined; - - public readonly integration_types?: ApplicationIntegrationType[]; - - public readonly nsfw: boolean | undefined = undefined; - - /** - * Sets the contexts of this command. - * - * @param contexts - The contexts - */ - public setContexts(...contexts: RestOrArray) { - Reflect.set(this, 'contexts', contextsPredicate.parse(normalizeArray(contexts))); - - return this; - } - - /** - * Sets the integration types of this command. - * - * @param integrationTypes - The integration types - */ - public setIntegrationTypes(...integrationTypes: RestOrArray) { - Reflect.set(this, 'integration_types', integrationTypesPredicate.parse(normalizeArray(integrationTypes))); - - return this; - } - - /** - * Sets whether the command is enabled by default when the application is added to a guild. - * - * @remarks - * If set to `false`, you will have to later `PUT` the permissions for this command. - * @param value - Whether or not to enable this command by default - * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} - * @deprecated Use {@link SharedSlashCommand.setDefaultMemberPermissions} or {@link SharedSlashCommand.setDMPermission} instead. - */ - public setDefaultPermission(value: boolean) { - // Assert the value matches the conditions - validateDefaultPermission(value); - - Reflect.set(this, 'default_permission', value); - - return this; - } - - /** - * Sets the default permissions a member should have in order to run the command. - * - * @remarks - * You can set this to `'0'` to disable the command by default. - * @param permissions - The permissions bit field to set - * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} - */ - public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) { - // Assert the value and parse it - const permissionValue = validateDefaultMemberPermissions(permissions); - - Reflect.set(this, 'default_member_permissions', permissionValue); - - return this; - } - - /** - * Sets if the command is available in direct messages with the application. - * - * @remarks - * By default, commands are visible. This method is only for global commands. - * @param enabled - Whether the command should be enabled in direct messages - * @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions} - * @deprecated - * Use {@link SharedSlashCommand.setContexts} instead. - */ - public setDMPermission(enabled: boolean | null | undefined) { - // Assert the value matches the conditions - validateDMPermission(enabled); - - Reflect.set(this, 'dm_permission', enabled); - - return this; - } - - /** - * Sets whether this command is NSFW. - * - * @param nsfw - Whether this command is NSFW - */ - public setNSFW(nsfw = true) { - // Assert the value matches the conditions - validateNSFW(nsfw); - Reflect.set(this, 'nsfw', nsfw); - return this; - } - - /** - * Serializes this builder to API-compatible JSON data. - * - * @remarks - * This method runs validations on the data before serializing it. - * As such, it may throw an error if the data is invalid. - */ - public toJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody { - validateRequiredParameters(this.name, this.description, this.options); - - validateLocalizationMap(this.name_localizations); - validateLocalizationMap(this.description_localizations); - - return { - ...this, - type: ApplicationCommandType.ChatInput, - options: this.options.map((option) => option.toJSON()), - }; - } -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts b/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts deleted file mode 100644 index 6a6d1297ecec..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions.js'; -import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder'; -import { SlashCommandAttachmentOption } from '../options/attachment.js'; -import { SlashCommandBooleanOption } from '../options/boolean.js'; -import { SlashCommandChannelOption } from '../options/channel.js'; -import { SlashCommandIntegerOption } from '../options/integer.js'; -import { SlashCommandMentionableOption } from '../options/mentionable.js'; -import { SlashCommandNumberOption } from '../options/number.js'; -import { SlashCommandRoleOption } from '../options/role.js'; -import { SlashCommandStringOption } from '../options/string.js'; -import { SlashCommandUserOption } from '../options/user.js'; -import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; - -/** - * This mixin holds symbols that can be shared in slash command options. - * - * @typeParam TypeAfterAddingOptions - The type this class should return after adding an option. - */ -export class SharedSlashCommandOptions< - TypeAfterAddingOptions extends SharedSlashCommandOptions, -> { - public readonly options!: ToAPIApplicationCommandOptions[]; - - /** - * Adds a boolean option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addBooleanOption( - input: SlashCommandBooleanOption | ((builder: SlashCommandBooleanOption) => SlashCommandBooleanOption), - ) { - return this._sharedAddOptionMethod(input, SlashCommandBooleanOption); - } - - /** - * Adds a user option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addUserOption(input: SlashCommandUserOption | ((builder: SlashCommandUserOption) => SlashCommandUserOption)) { - return this._sharedAddOptionMethod(input, SlashCommandUserOption); - } - - /** - * Adds a channel option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addChannelOption( - input: SlashCommandChannelOption | ((builder: SlashCommandChannelOption) => SlashCommandChannelOption), - ) { - return this._sharedAddOptionMethod(input, SlashCommandChannelOption); - } - - /** - * Adds a role option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addRoleOption(input: SlashCommandRoleOption | ((builder: SlashCommandRoleOption) => SlashCommandRoleOption)) { - return this._sharedAddOptionMethod(input, SlashCommandRoleOption); - } - - /** - * Adds an attachment option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addAttachmentOption( - input: SlashCommandAttachmentOption | ((builder: SlashCommandAttachmentOption) => SlashCommandAttachmentOption), - ) { - return this._sharedAddOptionMethod(input, SlashCommandAttachmentOption); - } - - /** - * Adds a mentionable option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addMentionableOption( - input: SlashCommandMentionableOption | ((builder: SlashCommandMentionableOption) => SlashCommandMentionableOption), - ) { - return this._sharedAddOptionMethod(input, SlashCommandMentionableOption); - } - - /** - * Adds a string option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addStringOption( - input: SlashCommandStringOption | ((builder: SlashCommandStringOption) => SlashCommandStringOption), - ) { - return this._sharedAddOptionMethod(input, SlashCommandStringOption); - } - - /** - * Adds an integer option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addIntegerOption( - input: SlashCommandIntegerOption | ((builder: SlashCommandIntegerOption) => SlashCommandIntegerOption), - ) { - return this._sharedAddOptionMethod(input, SlashCommandIntegerOption); - } - - /** - * Adds a number option. - * - * @param input - A function that returns an option builder or an already built builder - */ - public addNumberOption( - input: SlashCommandNumberOption | ((builder: SlashCommandNumberOption) => SlashCommandNumberOption), - ) { - return this._sharedAddOptionMethod(input, SlashCommandNumberOption); - } - - /** - * Where the actual adding magic happens. ✨ - * - * @param input - The input. What else? - * @param Instance - The instance of whatever is being added - * @internal - */ - private _sharedAddOptionMethod( - input: OptionBuilder | ((builder: OptionBuilder) => OptionBuilder), - Instance: new () => OptionBuilder, - ): TypeAfterAddingOptions { - const { options } = this; - - // First, assert options conditions - we cannot have more than 25 options - validateMaxOptionsLength(options); - - // Get the final result - const result = typeof input === 'function' ? input(new Instance()) : input; - - assertReturnOfBuilder(result, Instance); - - // Push it - options.push(result); - - return this as unknown as TypeAfterAddingOptions; - } -} diff --git a/packages/builders/src/interactions/slashCommands/mixins/SharedSubcommands.ts b/packages/builders/src/interactions/slashCommands/mixins/SharedSubcommands.ts deleted file mode 100644 index ce9274f3b6d7..000000000000 --- a/packages/builders/src/interactions/slashCommands/mixins/SharedSubcommands.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions.js'; -import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder.js'; -import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from '../SlashCommandSubcommands.js'; - -/** - * This mixin holds symbols that can be shared in slash subcommands. - * - * @typeParam TypeAfterAddingSubcommands - The type this class should return after adding a subcommand or subcommand group. - */ -export class SharedSlashCommandSubcommands< - TypeAfterAddingSubcommands extends SharedSlashCommandSubcommands, -> { - public readonly options: ToAPIApplicationCommandOptions[] = []; - - /** - * Adds a new subcommand group to this command. - * - * @param input - A function that returns a subcommand group builder or an already built builder - */ - public addSubcommandGroup( - input: - | SlashCommandSubcommandGroupBuilder - | ((subcommandGroup: SlashCommandSubcommandGroupBuilder) => SlashCommandSubcommandGroupBuilder), - ): TypeAfterAddingSubcommands { - const { options } = this; - - // First, assert options conditions - we cannot have more than 25 options - validateMaxOptionsLength(options); - - // Get the final result - const result = typeof input === 'function' ? input(new SlashCommandSubcommandGroupBuilder()) : input; - - assertReturnOfBuilder(result, SlashCommandSubcommandGroupBuilder); - - // Push it - options.push(result); - - return this as unknown as TypeAfterAddingSubcommands; - } - - /** - * Adds a new subcommand to this command. - * - * @param input - A function that returns a subcommand builder or an already built builder - */ - public addSubcommand( - input: - | SlashCommandSubcommandBuilder - | ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder), - ): TypeAfterAddingSubcommands { - const { options } = this; - - // First, assert options conditions - we cannot have more than 25 options - validateMaxOptionsLength(options); - - // Get the final result - const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input; - - assertReturnOfBuilder(result, SlashCommandSubcommandBuilder); - - // Push it - options.push(result); - - return this as unknown as TypeAfterAddingSubcommands; - } -} diff --git a/packages/builders/src/interactions/slashCommands/options/attachment.ts b/packages/builders/src/interactions/slashCommands/options/attachment.ts deleted file mode 100644 index cb31812f1c4a..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/attachment.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApplicationCommandOptionType, type APIApplicationCommandAttachmentOption } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; - -/** - * A slash command attachment option. - */ -export class SlashCommandAttachmentOption extends ApplicationCommandOptionBase { - /** - * The type of this option. - */ - public override readonly type = ApplicationCommandOptionType.Attachment as const; - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandAttachmentOption { - this.runRequiredValidations(); - - return { ...this }; - } -} diff --git a/packages/builders/src/interactions/slashCommands/options/boolean.ts b/packages/builders/src/interactions/slashCommands/options/boolean.ts deleted file mode 100644 index 5d82ea77c8ae..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/boolean.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApplicationCommandOptionType, type APIApplicationCommandBooleanOption } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; - -/** - * A slash command boolean option. - */ -export class SlashCommandBooleanOption extends ApplicationCommandOptionBase { - /** - * The type of this option. - */ - public readonly type = ApplicationCommandOptionType.Boolean as const; - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandBooleanOption { - this.runRequiredValidations(); - - return { ...this }; - } -} diff --git a/packages/builders/src/interactions/slashCommands/options/channel.ts b/packages/builders/src/interactions/slashCommands/options/channel.ts deleted file mode 100644 index 89400820c004..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/channel.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApplicationCommandOptionType, type APIApplicationCommandChannelOption } from 'discord-api-types/v10'; -import { mix } from 'ts-mixer'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; -import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin.js'; - -/** - * A slash command channel option. - */ -@mix(ApplicationCommandOptionChannelTypesMixin) -export class SlashCommandChannelOption extends ApplicationCommandOptionBase { - /** - * The type of this option. - */ - public override readonly type = ApplicationCommandOptionType.Channel as const; - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandChannelOption { - this.runRequiredValidations(); - - return { ...this }; - } -} - -export interface SlashCommandChannelOption extends ApplicationCommandOptionChannelTypesMixin {} diff --git a/packages/builders/src/interactions/slashCommands/options/integer.ts b/packages/builders/src/interactions/slashCommands/options/integer.ts deleted file mode 100644 index 4346595878a9..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/integer.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import { ApplicationCommandOptionType, type APIApplicationCommandIntegerOption } from 'discord-api-types/v10'; -import { mix } from 'ts-mixer'; -import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; -import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; -import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js'; - -const numberValidator = s.number().int(); - -/** - * A slash command integer option. - */ -@mix( - ApplicationCommandNumericOptionMinMaxValueMixin, - ApplicationCommandOptionWithAutocompleteMixin, - ApplicationCommandOptionWithChoicesMixin, -) -export class SlashCommandIntegerOption - extends ApplicationCommandOptionBase - implements ApplicationCommandNumericOptionMinMaxValueMixin -{ - /** - * The type of this option. - */ - public readonly type = ApplicationCommandOptionType.Integer as const; - - /** - * {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMaxValue} - */ - public setMaxValue(max: number): this { - numberValidator.parse(max); - - Reflect.set(this, 'max_value', max); - - return this; - } - - /** - * {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMinValue} - */ - public setMinValue(min: number): this { - numberValidator.parse(min); - - Reflect.set(this, 'min_value', min); - - return this; - } - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandIntegerOption { - this.runRequiredValidations(); - - if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) { - throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); - } - - return { ...this } as APIApplicationCommandIntegerOption; - } -} - -export interface SlashCommandIntegerOption - extends ApplicationCommandNumericOptionMinMaxValueMixin, - ApplicationCommandOptionWithChoicesMixin, - ApplicationCommandOptionWithAutocompleteMixin {} diff --git a/packages/builders/src/interactions/slashCommands/options/mentionable.ts b/packages/builders/src/interactions/slashCommands/options/mentionable.ts deleted file mode 100644 index 56292f612675..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/mentionable.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApplicationCommandOptionType, type APIApplicationCommandMentionableOption } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; - -/** - * A slash command mentionable option. - */ -export class SlashCommandMentionableOption extends ApplicationCommandOptionBase { - /** - * The type of this option. - */ - public readonly type = ApplicationCommandOptionType.Mentionable as const; - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandMentionableOption { - this.runRequiredValidations(); - - return { ...this }; - } -} diff --git a/packages/builders/src/interactions/slashCommands/options/number.ts b/packages/builders/src/interactions/slashCommands/options/number.ts deleted file mode 100644 index b53bb21b5973..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/number.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import { ApplicationCommandOptionType, type APIApplicationCommandNumberOption } from 'discord-api-types/v10'; -import { mix } from 'ts-mixer'; -import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; -import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; -import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js'; - -const numberValidator = s.number(); - -/** - * A slash command number option. - */ -@mix( - ApplicationCommandNumericOptionMinMaxValueMixin, - ApplicationCommandOptionWithAutocompleteMixin, - ApplicationCommandOptionWithChoicesMixin, -) -export class SlashCommandNumberOption - extends ApplicationCommandOptionBase - implements ApplicationCommandNumericOptionMinMaxValueMixin -{ - /** - * The type of this option. - */ - public readonly type = ApplicationCommandOptionType.Number as const; - - /** - * {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMaxValue} - */ - public setMaxValue(max: number): this { - numberValidator.parse(max); - - Reflect.set(this, 'max_value', max); - - return this; - } - - /** - * {@inheritDoc ApplicationCommandNumericOptionMinMaxValueMixin.setMinValue} - */ - public setMinValue(min: number): this { - numberValidator.parse(min); - - Reflect.set(this, 'min_value', min); - - return this; - } - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandNumberOption { - this.runRequiredValidations(); - - if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) { - throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); - } - - return { ...this } as APIApplicationCommandNumberOption; - } -} - -export interface SlashCommandNumberOption - extends ApplicationCommandNumericOptionMinMaxValueMixin, - ApplicationCommandOptionWithChoicesMixin, - ApplicationCommandOptionWithAutocompleteMixin {} diff --git a/packages/builders/src/interactions/slashCommands/options/role.ts b/packages/builders/src/interactions/slashCommands/options/role.ts deleted file mode 100644 index 8dca05d0adc6..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/role.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApplicationCommandOptionType, type APIApplicationCommandRoleOption } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; - -/** - * A slash command role option. - */ -export class SlashCommandRoleOption extends ApplicationCommandOptionBase { - /** - * The type of this option. - */ - public override readonly type = ApplicationCommandOptionType.Role as const; - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandRoleOption { - this.runRequiredValidations(); - - return { ...this }; - } -} diff --git a/packages/builders/src/interactions/slashCommands/options/string.ts b/packages/builders/src/interactions/slashCommands/options/string.ts deleted file mode 100644 index ebe2bd8e2c2c..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/string.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { s } from '@sapphire/shapeshift'; -import { ApplicationCommandOptionType, type APIApplicationCommandStringOption } from 'discord-api-types/v10'; -import { mix } from 'ts-mixer'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; -import { ApplicationCommandOptionWithAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithAutocompleteMixin.js'; -import { ApplicationCommandOptionWithChoicesMixin } from '../mixins/ApplicationCommandOptionWithChoicesMixin.js'; - -const minLengthValidator = s.number().greaterThanOrEqual(0).lessThanOrEqual(6_000); -const maxLengthValidator = s.number().greaterThanOrEqual(1).lessThanOrEqual(6_000); - -/** - * A slash command string option. - */ -@mix(ApplicationCommandOptionWithAutocompleteMixin, ApplicationCommandOptionWithChoicesMixin) -export class SlashCommandStringOption extends ApplicationCommandOptionBase { - /** - * The type of this option. - */ - public readonly type = ApplicationCommandOptionType.String as const; - - /** - * The maximum length of this option. - */ - public readonly max_length?: number; - - /** - * The minimum length of this option. - */ - public readonly min_length?: number; - - /** - * Sets the maximum length of this string option. - * - * @param max - The maximum length this option can be - */ - public setMaxLength(max: number): this { - maxLengthValidator.parse(max); - - Reflect.set(this, 'max_length', max); - - return this; - } - - /** - * Sets the minimum length of this string option. - * - * @param min - The minimum length this option can be - */ - public setMinLength(min: number): this { - minLengthValidator.parse(min); - - Reflect.set(this, 'min_length', min); - - return this; - } - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandStringOption { - this.runRequiredValidations(); - - if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) { - throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); - } - - return { ...this } as APIApplicationCommandStringOption; - } -} - -export interface SlashCommandStringOption - extends ApplicationCommandOptionWithChoicesMixin, - ApplicationCommandOptionWithAutocompleteMixin {} diff --git a/packages/builders/src/interactions/slashCommands/options/user.ts b/packages/builders/src/interactions/slashCommands/options/user.ts deleted file mode 100644 index 471faf96ce44..000000000000 --- a/packages/builders/src/interactions/slashCommands/options/user.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ApplicationCommandOptionType, type APIApplicationCommandUserOption } from 'discord-api-types/v10'; -import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase.js'; - -/** - * A slash command user option. - */ -export class SlashCommandUserOption extends ApplicationCommandOptionBase { - /** - * The type of this option. - */ - public readonly type = ApplicationCommandOptionType.User as const; - - /** - * {@inheritDoc ApplicationCommandOptionBase.toJSON} - */ - public toJSON(): APIApplicationCommandUserOption { - this.runRequiredValidations(); - - return { ...this }; - } -} diff --git a/packages/builders/src/messages/embed/Assertions.ts b/packages/builders/src/messages/embed/Assertions.ts index 8bf9b3eeff09..b47a0ee96c75 100644 --- a/packages/builders/src/messages/embed/Assertions.ts +++ b/packages/builders/src/messages/embed/Assertions.ts @@ -1,99 +1,70 @@ -import { s } from '@sapphire/shapeshift'; -import type { APIEmbedField } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; +import { z } from 'zod'; +import { refineURLPredicate } from '../../Assertions.js'; +import { embedLength } from '../../util/componentUtil.js'; -export const fieldNamePredicate = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(256) - .setValidationEnabled(isValidationEnabled); - -export const fieldValuePredicate = s - .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(1_024) - .setValidationEnabled(isValidationEnabled); - -export const fieldInlinePredicate = s.boolean().optional(); - -export const embedFieldPredicate = s - .object({ - name: fieldNamePredicate, - value: fieldValuePredicate, - inline: fieldInlinePredicate, - }) - .setValidationEnabled(isValidationEnabled); - -export const embedFieldsArrayPredicate = embedFieldPredicate.array().setValidationEnabled(isValidationEnabled); - -export const fieldLengthPredicate = s.number().lessThanOrEqual(25).setValidationEnabled(isValidationEnabled); - -export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void { - fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding); -} - -export const authorNamePredicate = fieldNamePredicate.nullable().setValidationEnabled(isValidationEnabled); - -export const imageURLPredicate = s - .string() - .url({ - allowedProtocols: ['http:', 'https:', 'attachment:'], - }) - .nullish() - .setValidationEnabled(isValidationEnabled); - -export const urlPredicate = s - .string() - .url({ - allowedProtocols: ['http:', 'https:'], - }) - .nullish() - .setValidationEnabled(isValidationEnabled); - -export const embedAuthorPredicate = s - .object({ - name: authorNamePredicate, - iconURL: imageURLPredicate, - url: urlPredicate, - }) - .setValidationEnabled(isValidationEnabled); - -export const RGBPredicate = s - .number() - .int() - .greaterThanOrEqual(0) - .lessThanOrEqual(255) - .setValidationEnabled(isValidationEnabled); -export const colorPredicate = s - .number() - .int() - .greaterThanOrEqual(0) - .lessThanOrEqual(0xffffff) - .or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate])) - .nullable() - .setValidationEnabled(isValidationEnabled); +const namePredicate = z.string().min(1).max(256); -export const descriptionPredicate = s +const iconURLPredicate = z .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(4_096) - .nullable() - .setValidationEnabled(isValidationEnabled); + .url() + .refine(refineURLPredicate(['http:', 'https:', 'attachment:']), { + message: 'Invalid protocol for icon URL. Must be http:, https:, or attachment:', + }); -export const footerTextPredicate = s +const URLPredicate = z .string() - .lengthGreaterThanOrEqual(1) - .lengthLessThanOrEqual(2_048) - .nullable() - .setValidationEnabled(isValidationEnabled); - -export const embedFooterPredicate = s + .url() + .refine(refineURLPredicate(['http:', 'https:']), { message: 'Invalid protocol for URL. Must be http: or https:' }); + +export const embedFieldPredicate = z.object({ + name: namePredicate, + value: z.string().min(1).max(1_024), + inline: z.boolean().optional(), +}); + +export const embedAuthorPredicate = z.object({ + name: namePredicate, + icon_url: iconURLPredicate.optional(), + url: URLPredicate.optional(), +}); + +export const embedFooterPredicate = z.object({ + text: z.string().min(1).max(2_048), + icon_url: iconURLPredicate.optional(), +}); + +export const embedPredicate = z .object({ - text: footerTextPredicate, - iconURL: imageURLPredicate, + title: namePredicate.optional(), + description: z.string().min(1).max(4_096).optional(), + url: URLPredicate.optional(), + timestamp: z.string().optional(), + color: z.number().int().min(0).max(0xffffff).optional(), + footer: embedFooterPredicate.optional(), + image: z.object({ url: URLPredicate }).optional(), + thumbnail: z.object({ url: URLPredicate }).optional(), + author: embedAuthorPredicate.optional(), + fields: z.array(embedFieldPredicate).max(25).optional(), }) - .setValidationEnabled(isValidationEnabled); - -export const timestampPredicate = s.union([s.number(), s.date()]).nullable().setValidationEnabled(isValidationEnabled); - -export const titlePredicate = fieldNamePredicate.nullable().setValidationEnabled(isValidationEnabled); + .refine( + (embed) => { + return ( + embed.title !== undefined || + embed.description !== undefined || + (embed.fields !== undefined && embed.fields.length > 0) || + embed.footer !== undefined || + embed.author !== undefined || + embed.image !== undefined || + embed.thumbnail !== undefined + ); + }, + { + message: 'Embed must have at least a title, description, a field, a footer, an author, an image, OR a thumbnail.', + }, + ) + .refine( + (embed) => { + return embedLength(embed) <= 6_000; + }, + { message: 'Embeds must not exceed 6000 characters in total.' }, + ); diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index 683e0598c188..25e408189120 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -1,77 +1,38 @@ -import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter, APIEmbedImage } from 'discord-api-types/v10'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; -import { - colorPredicate, - descriptionPredicate, - embedAuthorPredicate, - embedFieldsArrayPredicate, - embedFooterPredicate, - imageURLPredicate, - timestampPredicate, - titlePredicate, - urlPredicate, - validateFieldLength, -} from './Assertions.js'; +import type { JSONEncodable } from '@discordjs/util'; +import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter } from 'discord-api-types/v10'; +import type { RestOrArray } from '../../util/normalizeArray.js'; +import { normalizeArray } from '../../util/normalizeArray.js'; +import { resolveBuilder } from '../../util/resolveBuilder.js'; +import { isValidationEnabled } from '../../util/validation.js'; +import { embedPredicate } from './Assertions.js'; +import { EmbedAuthorBuilder } from './EmbedAuthor.js'; +import { EmbedFieldBuilder } from './EmbedField.js'; +import { EmbedFooterBuilder } from './EmbedFooter.js'; /** - * A tuple satisfying the RGB color model. - * - * @see {@link https://developer.mozilla.org/docs/Glossary/RGB} + * Data stored in the process of constructing an embed. */ -export type RGBTuple = [red: number, green: number, blue: number]; - -/** - * The base icon data typically used in payloads. - */ -export interface IconData { - /** - * The URL of the icon. - */ - iconURL?: string; - /** - * The proxy URL of the icon. - */ - proxyIconURL?: string; +export interface EmbedBuilderData extends Omit { + author?: EmbedAuthorBuilder; + fields: EmbedFieldBuilder[]; + footer?: EmbedFooterBuilder; } /** - * Represents the author data of an embed. - */ -export interface EmbedAuthorData extends IconData, Omit {} - -/** - * Represents the author options of an embed. - */ -export interface EmbedAuthorOptions extends Omit {} - -/** - * Represents the footer data of an embed. - */ -export interface EmbedFooterData extends IconData, Omit {} - -/** - * Represents the footer options of an embed. - */ -export interface EmbedFooterOptions extends Omit {} - -/** - * Represents the image data of an embed. + * A builder that creates API-compatible JSON data for embeds. */ -export interface EmbedImageData extends Omit { +export class EmbedBuilder implements JSONEncodable { /** - * The proxy URL for the image. + * The API data associated with this embed. */ - proxyURL?: string; -} + private readonly data: EmbedBuilderData; -/** - * A builder that creates API-compatible JSON data for embeds. - */ -export class EmbedBuilder { /** - * The API data associated with this embed. + * Gets the fields of this embed. */ - public readonly data: APIEmbed; + public get fields(): readonly EmbedFieldBuilder[] { + return this.data.fields; + } /** * Creates a new embed from API data. @@ -79,8 +40,12 @@ export class EmbedBuilder { * @param data - The API data to create this embed with */ public constructor(data: APIEmbed = {}) { - this.data = { ...data }; - if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString(); + this.data = { + ...structuredClone(data), + author: data.author && new EmbedAuthorBuilder(data.author), + fields: data.fields?.map((field) => new EmbedFieldBuilder(field)) ?? [], + footer: data.footer && new EmbedFooterBuilder(data.footer), + }; } /** @@ -107,16 +72,13 @@ export class EmbedBuilder { * ``` * @param fields - The fields to add */ - public addFields(...fields: RestOrArray): this { + public addFields( + ...fields: RestOrArray EmbedFieldBuilder)> + ): this { const normalizedFields = normalizeArray(fields); - // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(normalizedFields.length, this.data.fields); + const resolved = normalizedFields.map((field) => resolveBuilder(field, EmbedFieldBuilder)); - // Data assertions - embedFieldsArrayPredicate.parse(normalizedFields); - - if (this.data.fields) this.data.fields.push(...normalizedFields); - else this.data.fields = normalizedFields; + this.data.fields.push(...resolved); return this; } @@ -149,14 +111,14 @@ export class EmbedBuilder { * @param deleteCount - The number of fields to remove * @param fields - The replacing field objects */ - public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { - // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(fields.length - deleteCount, this.data.fields); - - // Data assertions - embedFieldsArrayPredicate.parse(fields); - if (this.data.fields) this.data.fields.splice(index, deleteCount, ...fields); - else this.data.fields = fields; + public spliceFields( + index: number, + deleteCount: number, + ...fields: (APIEmbedField | EmbedFieldBuilder | ((builder: EmbedFieldBuilder) => EmbedFieldBuilder))[] + ): this { + const resolved = fields.map((field) => resolveBuilder(field, EmbedFieldBuilder)); + this.data.fields.splice(index, deleteCount, ...resolved); + return this; } @@ -170,8 +132,10 @@ export class EmbedBuilder { * You can set a maximum of 25 fields. * @param fields - The fields to set */ - public setFields(...fields: RestOrArray): this { - this.spliceFields(0, this.data.fields?.length ?? 0, ...normalizeArray(fields)); + public setFields( + ...fields: RestOrArray EmbedFieldBuilder)> + ): this { + this.spliceFields(0, this.data.fields.length, ...normalizeArray(fields)); return this; } @@ -180,17 +144,28 @@ export class EmbedBuilder { * * @param options - The options to use */ + public setAuthor( + options: APIEmbedAuthor | EmbedAuthorBuilder | ((builder: EmbedAuthorBuilder) => EmbedAuthorBuilder), + ): this { + this.data.author = resolveBuilder(options, EmbedAuthorBuilder); + return this; + } - public setAuthor(options: EmbedAuthorOptions | null): this { - if (options === null) { - this.data.author = undefined; - return this; - } - - // Data assertions - embedAuthorPredicate.parse(options); + /** + * Updates the author of this embed (and creates it if it doesn't exist). + * + * @param updater - The function to update the author with + */ + public updateAuthor(updater: (builder: EmbedAuthorBuilder) => void) { + updater((this.data.author ??= new EmbedAuthorBuilder())); + return this; + } - this.data.author = { name: options.name, url: options.url, icon_url: options.iconURL }; + /** + * Clears the author of this embed. + */ + public clearAuthor(): this { + this.data.author = undefined; return this; } @@ -199,17 +174,16 @@ export class EmbedBuilder { * * @param color - The color to use */ - public setColor(color: RGBTuple | number | null): this { - // Data assertions - colorPredicate.parse(color); - - if (Array.isArray(color)) { - const [red, green, blue] = color; - this.data.color = (red << 16) + (green << 8) + blue; - return this; - } + public setColor(color: number): this { + this.data.color = color; + return this; + } - this.data.color = color ?? undefined; + /** + * Clears the color of this embed. + */ + public clearColor(): this { + this.data.color = undefined; return this; } @@ -218,11 +192,16 @@ export class EmbedBuilder { * * @param description - The description to use */ - public setDescription(description: string | null): this { - // Data assertions - descriptionPredicate.parse(description); + public setDescription(description: string): this { + this.data.description = description; + return this; + } - this.data.description = description ?? undefined; + /** + * Clears the description of this embed. + */ + public clearDescription(): this { + this.data.description = undefined; return this; } @@ -231,16 +210,28 @@ export class EmbedBuilder { * * @param options - The footer to use */ - public setFooter(options: EmbedFooterOptions | null): this { - if (options === null) { - this.data.footer = undefined; - return this; - } + public setFooter( + options: APIEmbedFooter | EmbedFooterBuilder | ((builder: EmbedFooterBuilder) => EmbedFooterBuilder), + ): this { + this.data.footer = resolveBuilder(options, EmbedFooterBuilder); + return this; + } - // Data assertions - embedFooterPredicate.parse(options); + /** + * Updates the footer of this embed (and creates it if it doesn't exist). + * + * @param updater - The function to update the footer with + */ + public updateFooter(updater: (builder: EmbedFooterBuilder) => void) { + updater((this.data.footer ??= new EmbedFooterBuilder())); + return this; + } - this.data.footer = { text: options.text, icon_url: options.iconURL }; + /** + * Clears the footer of this embed. + */ + public clearFooter(): this { + this.data.footer = undefined; return this; } @@ -249,11 +240,16 @@ export class EmbedBuilder { * * @param url - The image URL to use */ - public setImage(url: string | null): this { - // Data assertions - imageURLPredicate.parse(url); + public setImage(url: string): this { + this.data.image = { url }; + return this; + } - this.data.image = url ? { url } : undefined; + /** + * Clears the image of this embed. + */ + public clearImage(): this { + this.data.image = undefined; return this; } @@ -262,11 +258,16 @@ export class EmbedBuilder { * * @param url - The thumbnail URL to use */ - public setThumbnail(url: string | null): this { - // Data assertions - imageURLPredicate.parse(url); + public setThumbnail(url: string): this { + this.data.thumbnail = { url }; + return this; + } - this.data.thumbnail = url ? { url } : undefined; + /** + * Clears the thumbnail of this embed. + */ + public clearThumbnail(): this { + this.data.thumbnail = undefined; return this; } @@ -275,11 +276,16 @@ export class EmbedBuilder { * * @param timestamp - The timestamp or date to use */ - public setTimestamp(timestamp: Date | number | null = Date.now()): this { - // Data assertions - timestampPredicate.parse(timestamp); + public setTimestamp(timestamp: Date | number | string = Date.now()): this { + this.data.timestamp = new Date(timestamp).toISOString(); + return this; + } - this.data.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined; + /** + * Clears the timestamp of this embed. + */ + public clearTimestamp(): this { + this.data.timestamp = undefined; return this; } @@ -288,11 +294,16 @@ export class EmbedBuilder { * * @param title - The title to use */ - public setTitle(title: string | null): this { - // Data assertions - titlePredicate.parse(title); + public setTitle(title: string): this { + this.data.title = title; + return this; + } - this.data.title = title ?? undefined; + /** + * Clears the title of this embed. + */ + public clearTitle(): this { + this.data.title = undefined; return this; } @@ -301,22 +312,41 @@ export class EmbedBuilder { * * @param url - The URL to use */ - public setURL(url: string | null): this { - // Data assertions - urlPredicate.parse(url); + public setURL(url: string): this { + this.data.url = url; + return this; + } - this.data.url = url ?? undefined; + /** + * Clears the URL of this embed. + */ + public clearURL(): this { + this.data.url = undefined; return this; } /** * Serializes this builder to API-compatible JSON data. * - * @remarks - * This method runs validations on the data before serializing it. - * As such, it may throw an error if the data is invalid. + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference */ - public toJSON(): APIEmbed { - return { ...this.data }; + public toJSON(validationOverride?: boolean): APIEmbed { + const { author, fields, footer, ...rest } = this.data; + + const data = { + ...structuredClone(rest), + // Disable validation because the embedPredicate below will validate those as well + author: this.data.author?.toJSON(false), + fields: this.data.fields?.map((field) => field.toJSON(false)), + footer: this.data.footer?.toJSON(false), + }; + + if (validationOverride ?? isValidationEnabled()) { + embedPredicate.parse(data); + } + + return data; } } diff --git a/packages/builders/src/messages/embed/EmbedAuthor.ts b/packages/builders/src/messages/embed/EmbedAuthor.ts new file mode 100644 index 000000000000..0c3d0b6fb776 --- /dev/null +++ b/packages/builders/src/messages/embed/EmbedAuthor.ts @@ -0,0 +1,82 @@ +import type { APIEmbedAuthor } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation.js'; +import { embedAuthorPredicate } from './Assertions.js'; + +/** + * A builder that creates API-compatible JSON data for the embed author. + */ +export class EmbedAuthorBuilder { + private readonly data: Partial; + + /** + * Creates a new embed author from API data. + * + * @param data - The API data to use + */ + public constructor(data?: Partial) { + this.data = structuredClone(data) ?? {}; + } + + /** + * Sets the name for this embed author. + * + * @param name - The name to use + */ + public setName(name: string): this { + this.data.name = name; + return this; + } + + /** + * Sets the URL for this embed author. + * + * @param url - The url to use + */ + public setURL(url: string): this { + this.data.url = url; + return this; + } + + /** + * Clears the URL for this embed author. + */ + public clearURL(): this { + this.data.url = undefined; + return this; + } + + /** + * Sets the icon URL for this embed author. + * + * @param iconURL - The icon URL to use + */ + public setIconURL(iconURL: string): this { + this.data.icon_url = iconURL; + return this; + } + + /** + * Clears the icon URL for this embed author. + */ + public clearIconURL(): this { + this.data.icon_url = undefined; + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public toJSON(validationOverride?: boolean): APIEmbedAuthor { + const clone = structuredClone(this.data); + + if (validationOverride ?? isValidationEnabled()) { + embedAuthorPredicate.parse(clone); + } + + return clone as APIEmbedAuthor; + } +} diff --git a/packages/builders/src/messages/embed/EmbedField.ts b/packages/builders/src/messages/embed/EmbedField.ts new file mode 100644 index 000000000000..e385fad3ec14 --- /dev/null +++ b/packages/builders/src/messages/embed/EmbedField.ts @@ -0,0 +1,66 @@ +import type { APIEmbedField } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation.js'; +import { embedFieldPredicate } from './Assertions.js'; + +/** + * A builder that creates API-compatible JSON data for embed fields. + */ +export class EmbedFieldBuilder { + private readonly data: Partial; + + /** + * Creates a new embed field from API data. + * + * @param data - The API data to use + */ + public constructor(data?: Partial) { + this.data = structuredClone(data) ?? {}; + } + + /** + * Sets the name for this embed field. + * + * @param name - The name to use + */ + public setName(name: string): this { + this.data.name = name; + return this; + } + + /** + * Sets the value for this embed field. + * + * @param value - The value to use + */ + public setValue(value: string): this { + this.data.value = value; + return this; + } + + /** + * Sets whether this field should display inline. + * + * @param inline - Whether this field should display inline + */ + public setInline(inline = true): this { + this.data.inline = inline; + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public toJSON(validationOverride?: boolean): APIEmbedField { + const clone = structuredClone(this.data); + + if (validationOverride ?? isValidationEnabled()) { + embedFieldPredicate.parse(clone); + } + + return clone as APIEmbedField; + } +} diff --git a/packages/builders/src/messages/embed/EmbedFooter.ts b/packages/builders/src/messages/embed/EmbedFooter.ts new file mode 100644 index 000000000000..5b3e0c0f8543 --- /dev/null +++ b/packages/builders/src/messages/embed/EmbedFooter.ts @@ -0,0 +1,64 @@ +import type { APIEmbedFooter } from 'discord-api-types/v10'; +import { isValidationEnabled } from '../../util/validation.js'; +import { embedFooterPredicate } from './Assertions.js'; + +/** + * A builder that creates API-compatible JSON data for the embed footer. + */ +export class EmbedFooterBuilder { + private readonly data: Partial; + + /** + * Creates a new embed footer from API data. + * + * @param data - The API data to use + */ + public constructor(data?: Partial) { + this.data = structuredClone(data) ?? {}; + } + + /** + * Sets the text for this embed footer. + * + * @param text - The text to use + */ + public setText(text: string): this { + this.data.text = text; + return this; + } + + /** + * Sets the url for this embed footer. + * + * @param url - The url to use + */ + public setIconURL(url: string): this { + this.data.icon_url = url; + return this; + } + + /** + * Clears the icon URL for this embed footer. + */ + public clearIconURL(): this { + this.data.icon_url = undefined; + return this; + } + + /** + * Serializes this builder to API-compatible JSON data. + * + * Note that by disabling validation, there is no guarantee that the resulting object will be valid. + * + * @param validationOverride - Force validation to run/not run regardless of your global preference + */ + public toJSON(validationOverride?: boolean): APIEmbedFooter { + const clone = structuredClone(this.data); + + if (validationOverride ?? isValidationEnabled()) { + embedFooterPredicate.parse(clone); + } + + return clone as APIEmbedFooter; + } +} diff --git a/packages/builders/src/util/resolveBuilder.ts b/packages/builders/src/util/resolveBuilder.ts new file mode 100644 index 000000000000..56d23589c8d0 --- /dev/null +++ b/packages/builders/src/util/resolveBuilder.ts @@ -0,0 +1,40 @@ +import type { JSONEncodable } from '@discordjs/util'; + +/** + * @privateRemarks + * This is a type-guard util, because if you were to in-line `builder instanceof Constructor` in the `resolveBuilder` + * function, TS doesn't narrow out the type `Builder`, causing a type error on the last return statement. + * @internal + */ +function isBuilder>( + builder: unknown, + Constructor: new () => Builder, +): builder is Builder { + return builder instanceof Constructor; +} + +/** + * "Resolves" a builder from the 3 ways it can be input: + * 1. A clean instance + * 2. A data object that can be used to construct the builder + * 3. A function that takes a builder and returns a builder e.g. `builder => builder.setFoo('bar')` + * + * @typeParam Builder - The builder type + * @typeParam BuilderData - The data object that can be used to construct the builder + * @param builder - The user input, as described in the function description + * @param Constructor - The constructor of the builder + */ +export function resolveBuilder, BuilderData extends Record>( + builder: Builder | BuilderData | ((builder: Builder) => Builder), + Constructor: new (data?: BuilderData) => Builder, +): Builder { + if (isBuilder(builder, Constructor)) { + return builder; + } + + if (typeof builder === 'function') { + return builder(new Constructor()); + } + + return new Constructor(builder); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d28d3f5baf4..3665411b2dd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -670,27 +670,21 @@ importers: packages/builders: dependencies: - '@discordjs/formatters': - specifier: workspace:^ - version: link:../formatters '@discordjs/util': specifier: workspace:^ version: link:../util - '@sapphire/shapeshift': - specifier: ^4.0.0 - version: 4.0.0 discord-api-types: specifier: ^0.37.101 version: 0.37.101 - fast-deep-equal: - specifier: ^3.1.3 - version: 3.1.3 ts-mixer: specifier: ^6.0.4 version: 6.0.4 tslib: specifier: ^2.6.3 version: 2.6.3 + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -702,11 +696,11 @@ importers: specifier: ^4.1.0 version: 4.1.0 '@types/node': - specifier: ^16.18.105 - version: 16.18.105 + specifier: ^18.19.44 + version: 18.19.45 '@vitest/coverage-v8': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@16.18.105)(happy-dom@14.12.3)(terser@5.31.6)) + version: 2.0.5(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -727,7 +721,7 @@ importers: version: 3.3.3 tsup: specifier: ^8.2.4 - version: 8.2.4(@microsoft/api-extractor@7.43.0(@types/node@16.18.105))(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0) + version: 8.2.4(@microsoft/api-extractor@7.43.0(@types/node@18.19.45))(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0) turbo: specifier: ^2.0.14 version: 2.0.14 @@ -736,7 +730,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@edge-runtime/vm@3.2.0)(@types/node@16.18.105)(happy-dom@14.12.3)(terser@5.31.6) + version: 2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6) packages/collection: devDependencies: From 493a079fdf5864ce7e2965025ebe840e61fc988d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Leit=C3=A3o?= <38259440+ImRodry@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:21:42 +0100 Subject: [PATCH 49/65] refactor(CommandInteractionOptionResolver): remove getFull from getFocused() (#9789) * refactor(CommandInteractionOptionResolver): remove getFull from getFocused() * docs: update return type Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/structures/CommandInteractionOptionResolver.js | 9 ++++----- packages/discord.js/typings/index.d.ts | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/discord.js/src/structures/CommandInteractionOptionResolver.js b/packages/discord.js/src/structures/CommandInteractionOptionResolver.js index 621dbf44e1f2..b59f6328e40c 100644 --- a/packages/discord.js/src/structures/CommandInteractionOptionResolver.js +++ b/packages/discord.js/src/structures/CommandInteractionOptionResolver.js @@ -294,14 +294,13 @@ class CommandInteractionOptionResolver { /** * Gets the focused option. - * @param {boolean} [getFull=false] Whether to get the full option object - * @returns {string|AutocompleteFocusedOption} - * The value of the option, or the whole option if getFull is true + * @returns {AutocompleteFocusedOption} + * The whole object of the option that is focused */ - getFocused(getFull = false) { + getFocused() { const focusedOption = this._hoistedOptions.find(option => option.focused); if (!focusedOption) throw new DiscordjsTypeError(ErrorCodes.AutocompleteInteractionOptionNoFocusedOption); - return getFull ? focusedOption : focusedOption.value; + return focusedOption; } } diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index c28be62901c2..ef876764c293 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -1312,8 +1312,7 @@ export class CommandInteractionOptionResolver['member' | 'role' | 'user']> | null; public getMessage(name: string, required: true): NonNullable['message']>; public getMessage(name: string, required?: boolean): NonNullable['message']> | null; - public getFocused(getFull: true): AutocompleteFocusedOption; - public getFocused(getFull?: boolean): string; + public getFocused(): AutocompleteFocusedOption; } export class ContextMenuCommandInteraction extends CommandInteraction { From 05541d8288435cd7d5777c54649dd74db2df488c Mon Sep 17 00:00:00 2001 From: MrMythicalYT <91077061+MrMythicalYT@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:29:31 -0400 Subject: [PATCH 50/65] fix(User): remove `fetchFlags()` (#8755) Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- packages/discord.js/src/managers/UserManager.js | 10 ---------- packages/discord.js/src/structures/User.js | 9 --------- packages/discord.js/typings/index.d.ts | 2 -- 3 files changed, 21 deletions(-) diff --git a/packages/discord.js/src/managers/UserManager.js b/packages/discord.js/src/managers/UserManager.js index 18fdbfe46852..b0026da2c69a 100644 --- a/packages/discord.js/src/managers/UserManager.js +++ b/packages/discord.js/src/managers/UserManager.js @@ -95,16 +95,6 @@ class UserManager extends CachedManager { return this._add(data, cache); } - /** - * Fetches a user's flags. - * @param {UserResolvable} user The UserResolvable to identify - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise} - */ - async fetchFlags(user, options) { - return (await this.fetch(user, options)).flags; - } - /** * Sends a message to a user. * @param {UserResolvable} user The UserResolvable to identify diff --git a/packages/discord.js/src/structures/User.js b/packages/discord.js/src/structures/User.js index 0cda0e591b97..a7df27f42a9f 100644 --- a/packages/discord.js/src/structures/User.js +++ b/packages/discord.js/src/structures/User.js @@ -342,15 +342,6 @@ class User extends Base { ); } - /** - * Fetches this user's flags. - * @param {boolean} [force=false] Whether to skip the cache check and request the API - * @returns {Promise} - */ - fetchFlags(force = false) { - return this.client.users.fetchFlags(this.id, { force }); - } - /** * Fetches this user. * @param {boolean} [force=true] Whether to skip the cache check and request the API diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index ef876764c293..7c69d4fd3635 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3460,7 +3460,6 @@ export class User extends Base { public displayAvatarURL(options?: ImageURLOptions): string; public equals(user: User): boolean; public fetch(force?: boolean): Promise; - public fetchFlags(force?: boolean): Promise; public toString(): UserMention; } @@ -4692,7 +4691,6 @@ export class UserManager extends CachedManager public createDM(user: UserResolvable, options?: BaseFetchOptions): Promise; public deleteDM(user: UserResolvable): Promise; public fetch(user: UserResolvable, options?: BaseFetchOptions): Promise; - public fetchFlags(user: UserResolvable, options?: BaseFetchOptions): Promise; public send(user: UserResolvable, options: string | MessagePayload | MessageCreateOptions): Promise; } From b339a7cb086f22734a27fefd242d7c786fed1fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= <9092381+Renegade334@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:33:25 +0100 Subject: [PATCH 51/65] fix(ThreadMember): remove audit log reason parameter (#10023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(ThreadMember): remove audit log reason Co-authored-by: René Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../discord.js/src/managers/ThreadMemberManager.js | 10 ++++------ packages/discord.js/src/structures/ThreadMember.js | 5 ++--- packages/discord.js/typings/index.d.ts | 6 +++--- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/discord.js/src/managers/ThreadMemberManager.js b/packages/discord.js/src/managers/ThreadMemberManager.js index e4ae5bf7d49e..47b337de3cf4 100644 --- a/packages/discord.js/src/managers/ThreadMemberManager.js +++ b/packages/discord.js/src/managers/ThreadMemberManager.js @@ -91,26 +91,24 @@ class ThreadMemberManager extends CachedManager { /** * Adds a member to the thread. * @param {UserResolvable|'@me'} member The member to add - * @param {string} [reason] The reason for adding this member * @returns {Promise} */ - async add(member, reason) { + async add(member) { const id = member === '@me' ? member : this.client.users.resolveId(member); if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'member', 'UserResolvable'); - await this.client.rest.put(Routes.threadMembers(this.thread.id, id), { reason }); + await this.client.rest.put(Routes.threadMembers(this.thread.id, id)); return id; } /** * Remove a user from the thread. * @param {UserResolvable|'@me'} member The member to remove - * @param {string} [reason] The reason for removing this member from the thread * @returns {Promise} */ - async remove(member, reason) { + async remove(member) { const id = member === '@me' ? member : this.client.users.resolveId(member); if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'member', 'UserResolvable'); - await this.client.rest.delete(Routes.threadMembers(this.thread.id, id), { reason }); + await this.client.rest.delete(Routes.threadMembers(this.thread.id, id)); return id; } diff --git a/packages/discord.js/src/structures/ThreadMember.js b/packages/discord.js/src/structures/ThreadMember.js index 1df5f7071f6f..e2c58683bd46 100644 --- a/packages/discord.js/src/structures/ThreadMember.js +++ b/packages/discord.js/src/structures/ThreadMember.js @@ -101,11 +101,10 @@ class ThreadMember extends Base { /** * Removes this member from the thread. - * @param {string} [reason] Reason for removing the member * @returns {Promise} */ - async remove(reason) { - await this.thread.members.remove(this.id, reason); + async remove() { + await this.thread.members.remove(this.id); return this; } } diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 7c69d4fd3635..51c0f07fca94 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -3396,7 +3396,7 @@ export class ThreadMember extends Base public thread: AnyThreadChannel; public get user(): User | null; public get partial(): false; - public remove(reason?: string): Promise; + public remove(): Promise; } export type ThreadMemberFlagsString = keyof typeof ThreadMemberFlags; @@ -4668,7 +4668,7 @@ export class ThreadMemberManager extends CachedManager); public thread: AnyThreadChannel; public get me(): ThreadMember | null; - public add(member: UserResolvable | '@me', reason?: string): Promise; + public add(member: UserResolvable | '@me'): Promise; public fetch( options: ThreadMember | ((FetchThreadMemberOptions & { withMember: true }) | { member: ThreadMember }), @@ -4682,7 +4682,7 @@ export class ThreadMemberManager extends CachedManager>; public fetchMe(options?: BaseFetchOptions): Promise; - public remove(member: UserResolvable | '@me', reason?: string): Promise; + public remove(member: UserResolvable | '@me'): Promise; } export class UserManager extends CachedManager { From c1b849fa5a6c7b541c4f1a196bca08aae353e867 Mon Sep 17 00:00:00 2001 From: Superchupu <53496941+SuperchupuDev@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:10:46 +0200 Subject: [PATCH 52/65] docs(discord.js): remove `utf-8-validate` (#10531) --- packages/discord.js/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/discord.js/README.md b/packages/discord.js/README.md index d334efbb4bf6..04661319bb1e 100644 --- a/packages/discord.js/README.md +++ b/packages/discord.js/README.md @@ -42,7 +42,6 @@ bun add discord.js - [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`) - [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`) -- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`) - [@discordjs/voice](https://www.npmjs.com/package/@discordjs/voice) for interacting with the Discord Voice API (`npm install @discordjs/voice`) ## Example usage From 12e510671b67c11e1a659d344bb3f4c939e2d44c Mon Sep 17 00:00:00 2001 From: Denis Cristea Date: Fri, 4 Oct 2024 14:17:34 +0300 Subject: [PATCH 53/65] chore!: remove all deprecated features/props (#10421) BREAKING CHANGE: Removed `Client#fetchPremiumStickerPacks` method BREAKING CHANGE: Removed `Client#webhookUpdate` event BREAKING CHANGE: Removed various error codes BREAKING CHANGE: Removed `Formatters` namespace BREAKING CHANGE: Removed `InviteStageInstance` class BREAKING CHANGE: Removed `Invite#stageInstance` property BREAKING CHANGE: Removed `StageInstance#discoverable_disabled` property BREAKING CHANGE: Removed `SelectMenuBuilder` alias BREAKING CHANGE: Removed `SelectMenuComponent` alias BREAKING CHANGE: Removed `SelectMenuInteraction` alias BREAKING CHANGE: Removed `SelectMenuOptionBuilder` alias BREAKING CHANGE: Removed `BaseInteraction#isSelectMenu` alias BREAKING CHANGE: Removed `deleteMessageDays` option from `GuildBanManager#create` BREAKING CHANGE: Removed `ActionRow#from` method BREAKING CHANGE: Removed `Emoji#url` getter BREAKING CHANGE: Removed `TeamMember#permissions` property BREAKING CHANGE: Removed `User#avatarDecoration` property BREAKING CHANGE: Removed `InteractionResponses#sendPremiumRequired` method BREAKING CHANGE: Removed `DeletableMessageTypes` constant --- packages/discord.js/src/client/Client.js | 20 - .../src/client/actions/WebhooksUpdate.js | 19 +- packages/discord.js/src/errors/ErrorCodes.js | 85 +--- packages/discord.js/src/errors/Messages.js | 30 -- packages/discord.js/src/index.js | 6 - .../src/managers/GuildBanManager.js | 29 +- .../src/managers/GuildMemberManager.js | 2 +- .../discord.js/src/managers/RoleManager.js | 16 +- .../discord.js/src/structures/ActionRow.js | 15 - .../src/structures/BaseGuildEmoji.js | 10 - .../src/structures/BaseInteraction.js | 15 - .../src/structures/CommandInteraction.js | 1 - packages/discord.js/src/structures/Emoji.js | 18 - packages/discord.js/src/structures/Invite.js | 12 - .../src/structures/InviteStageInstance.js | 87 ---- .../structures/MessageComponentInteraction.js | 1 - .../src/structures/ModalSubmitInteraction.js | 1 - .../src/structures/SelectMenuBuilder.js | 26 -- .../src/structures/SelectMenuComponent.js | 26 -- .../src/structures/SelectMenuInteraction.js | 26 -- .../src/structures/SelectMenuOptionBuilder.js | 26 -- .../src/structures/StageInstance.js | 11 - .../src/structures/StringSelectMenuBuilder.js | 4 +- .../discord.js/src/structures/TeamMember.js | 9 - .../src/structures/ThreadChannel.js | 13 +- packages/discord.js/src/structures/User.js | 22 +- .../interfaces/InteractionResponses.js | 25 -- packages/discord.js/src/util/Constants.js | 50 --- packages/discord.js/src/util/Events.js | 4 +- packages/discord.js/src/util/Formatters.js | 413 ------------------ packages/discord.js/test/monetization.js | 14 +- packages/discord.js/test/tester2000.js | 4 +- packages/discord.js/typings/index.d.ts | 203 +-------- packages/discord.js/typings/index.test-d.ts | 27 +- packages/discord.js/typings/rawDataTypes.d.ts | 3 - 35 files changed, 45 insertions(+), 1228 deletions(-) delete mode 100644 packages/discord.js/src/structures/InviteStageInstance.js delete mode 100644 packages/discord.js/src/structures/SelectMenuBuilder.js delete mode 100644 packages/discord.js/src/structures/SelectMenuComponent.js delete mode 100644 packages/discord.js/src/structures/SelectMenuInteraction.js delete mode 100644 packages/discord.js/src/structures/SelectMenuOptionBuilder.js delete mode 100644 packages/discord.js/src/util/Formatters.js diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 1d378a4dcd3d..3a72b8414986 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -31,8 +31,6 @@ const PermissionsBitField = require('../util/PermissionsBitField'); const Status = require('../util/Status'); const Sweepers = require('../util/Sweepers'); -let deprecationEmittedForPremiumStickerPacks = false; - /** * The main hub for interacting with the Discord API, and the starting point for any bot. * @extends {BaseClient} @@ -372,24 +370,6 @@ class Client extends BaseClient { return new Collection(data.sticker_packs.map(stickerPack => [stickerPack.id, new StickerPack(this, stickerPack)])); } - /** - * Obtains the list of available sticker packs. - * @returns {Promise>} - * @deprecated Use {@link Client#fetchStickerPacks} instead. - */ - fetchPremiumStickerPacks() { - if (!deprecationEmittedForPremiumStickerPacks) { - process.emitWarning( - 'The Client#fetchPremiumStickerPacks() method is deprecated. Use Client#fetchStickerPacks() instead.', - 'DeprecationWarning', - ); - - deprecationEmittedForPremiumStickerPacks = true; - } - - return this.fetchStickerPacks(); - } - /** * Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds. * @param {GuildResolvable} guild The guild to fetch the preview for diff --git a/packages/discord.js/src/client/actions/WebhooksUpdate.js b/packages/discord.js/src/client/actions/WebhooksUpdate.js index 2f0394c19186..3e3c46affc3e 100644 --- a/packages/discord.js/src/client/actions/WebhooksUpdate.js +++ b/packages/discord.js/src/client/actions/WebhooksUpdate.js @@ -1,9 +1,7 @@ 'use strict'; -const process = require('node:process'); const Action = require('./Action'); - -let deprecationEmitted = false; +const Events = require('../../util/Events'); class WebhooksUpdate extends Action { handle(data) { @@ -11,26 +9,13 @@ class WebhooksUpdate extends Action { const channel = client.channels.cache.get(data.channel_id); if (!channel) return; - // TODO: change to Events.WebhooksUpdate in the next major version /** * Emitted whenever a channel has its webhooks changed. * @event Client#webhooksUpdate * @param {TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel} channel * The channel that had a webhook update */ - client.emit('webhooksUpdate', channel); - - /** - * Emitted whenever a channel has its webhooks changed. - * @event Client#webhookUpdate - * @param {TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel} channel - * The channel that had a webhook update - * @deprecated Use {@link Client#event:webhooksUpdate} instead. - */ - if (client.emit('webhookUpdate', channel) && !deprecationEmitted) { - deprecationEmitted = true; - process.emitWarning('The webhookUpdate event is deprecated. Use webhooksUpdate instead.', 'DeprecationWarning'); - } + client.emit(Events.WebhooksUpdate, channel); } } diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index c1552392aa90..7d7b0b99ec1b 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -12,25 +12,8 @@ * @property {'TokenMissing'} TokenMissing * @property {'ApplicationCommandPermissionsTokenMissing'} ApplicationCommandPermissionsTokenMissing - * @property {'WSCloseRequested'} WSCloseRequested - * This property is deprecated. - * @property {'WSConnectionExists'} WSConnectionExists - * This property is deprecated. - * @property {'WSNotOpen'} WSNotOpen - * This property is deprecated. - * @property {'ManagerDestroyed'} ManagerDestroyed - * This property is deprecated. - - * @property {'BitFieldInvalid'} BitFieldInvalid - - * @property {'ShardingInvalid'} ShardingInvalid - * This property is deprecated. - * @property {'ShardingRequired'} ShardingRequired - * This property is deprecated. - * @property {'InvalidIntents'} InvalidIntents - * This property is deprecated. - * @property {'DisallowedIntents'} DisallowedIntents - * This property is deprecated. +* @property {'BitFieldInvalid'} BitFieldInvalid + * @property {'ShardingNoShards'} ShardingNoShards * @property {'ShardingInProcess'} ShardingInProcess * @property {'ShardingInvalidEvalBroadcast'} ShardingInvalidEvalBroadcast @@ -49,30 +32,10 @@ * @property {'InviteOptionsMissingChannel'} InviteOptionsMissingChannel - * @property {'ButtonLabel'} ButtonLabel - * This property is deprecated. - * @property {'ButtonURL'} ButtonURL - * This property is deprecated. - * @property {'ButtonCustomId'} ButtonCustomId - * This property is deprecated. - - * @property {'SelectMenuCustomId'} SelectMenuCustomId - * This property is deprecated. - * @property {'SelectMenuPlaceholder'} SelectMenuPlaceholder - * This property is deprecated. - * @property {'SelectOptionLabel'} SelectOptionLabel - * This property is deprecated. - * @property {'SelectOptionValue'} SelectOptionValue - * This property is deprecated. - * @property {'SelectOptionDescription'} SelectOptionDescription - * This property is deprecated. - * @property {'InteractionCollectorError'} InteractionCollectorError * @property {'FileNotFound'} FileNotFound - * @property {'UserBannerNotFetched'} UserBannerNotFetched - * This property is deprecated. * @property {'UserNoDMChannel'} UserNoDMChannel * @property {'VoiceNotStageChannel'} VoiceNotStageChannel @@ -82,19 +45,11 @@ * @property {'ReqResourceType'} ReqResourceType - * @property {'ImageFormat'} ImageFormat - * This property is deprecated. - * @property {'ImageSize'} ImageSize - * This property is deprecated. - * @property {'MessageBulkDeleteType'} MessageBulkDeleteType * @property {'MessageContentType'} MessageContentType * @property {'MessageNonceRequired'} MessageNonceRequired * @property {'MessageNonceType'} MessageNonceType - * @property {'SplitMaxLen'} SplitMaxLen - * This property is deprecated. - * @property {'BanResolveId'} BanResolveId * @property {'FetchBanResolveId'} FetchBanResolveId @@ -128,16 +83,11 @@ * @property {'EmojiType'} EmojiType * @property {'EmojiManaged'} EmojiManaged * @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission - * @property {'MissingManageEmojisAndStickersPermission'} MissingManageEmojisAndStickersPermission - * This property is deprecated. Use `MissingManageGuildExpressionsPermission` instead. * * @property {'NotGuildSticker'} NotGuildSticker * @property {'ReactionResolveUser'} ReactionResolveUser - * @property {'VanityURL'} VanityURL - * This property is deprecated. - * @property {'InviteResolveCode'} InviteResolveCode * @property {'InviteNotFound'} InviteNotFound @@ -152,8 +102,6 @@ * @property {'InteractionAlreadyReplied'} InteractionAlreadyReplied * @property {'InteractionNotReplied'} InteractionNotReplied - * @property {'InteractionEphemeralReplied'} InteractionEphemeralReplied - * This property is deprecated. * @property {'CommandInteractionOptionNotFound'} CommandInteractionOptionNotFound * @property {'CommandInteractionOptionType'} CommandInteractionOptionType @@ -192,17 +140,8 @@ const keys = [ 'TokenMissing', 'ApplicationCommandPermissionsTokenMissing', - 'WSCloseRequested', - 'WSConnectionExists', - 'WSNotOpen', - 'ManagerDestroyed', - 'BitFieldInvalid', - 'ShardingInvalid', - 'ShardingRequired', - 'InvalidIntents', - 'DisallowedIntents', 'ShardingNoShards', 'ShardingInProcess', 'ShardingInvalidEvalBroadcast', @@ -221,21 +160,10 @@ const keys = [ 'InviteOptionsMissingChannel', - 'ButtonLabel', - 'ButtonURL', - 'ButtonCustomId', - - 'SelectMenuCustomId', - 'SelectMenuPlaceholder', - 'SelectOptionLabel', - 'SelectOptionValue', - 'SelectOptionDescription', - 'InteractionCollectorError', 'FileNotFound', - 'UserBannerNotFetched', 'UserNoDMChannel', 'VoiceNotStageChannel', @@ -245,16 +173,11 @@ const keys = [ 'ReqResourceType', - 'ImageFormat', - 'ImageSize', - 'MessageBulkDeleteType', 'MessageContentType', 'MessageNonceRequired', 'MessageNonceType', - 'SplitMaxLen', - 'BanResolveId', 'FetchBanResolveId', @@ -288,14 +211,11 @@ const keys = [ 'EmojiType', 'EmojiManaged', 'MissingManageGuildExpressionsPermission', - 'MissingManageEmojisAndStickersPermission', 'NotGuildSticker', 'ReactionResolveUser', - 'VanityURL', - 'InviteResolveCode', 'InviteNotFound', @@ -310,7 +230,6 @@ const keys = [ 'InteractionAlreadyReplied', 'InteractionNotReplied', - 'InteractionEphemeralReplied', 'CommandInteractionOptionNotFound', 'CommandInteractionOptionType', diff --git a/packages/discord.js/src/errors/Messages.js b/packages/discord.js/src/errors/Messages.js index 234718c50c79..41e81dd3402f 100644 --- a/packages/discord.js/src/errors/Messages.js +++ b/packages/discord.js/src/errors/Messages.js @@ -13,17 +13,8 @@ const Messages = { [DjsErrorCodes.ApplicationCommandPermissionsTokenMissing]: 'Editing application command permissions requires an OAuth2 bearer token, but none was provided.', - [DjsErrorCodes.WSCloseRequested]: 'WebSocket closed due to user request.', - [DjsErrorCodes.WSConnectionExists]: 'There is already an existing WebSocket connection.', - [DjsErrorCodes.WSNotOpen]: (data = 'data') => `WebSocket not open to send ${data}`, - [DjsErrorCodes.ManagerDestroyed]: 'Manager was destroyed.', - [DjsErrorCodes.BitFieldInvalid]: bit => `Invalid bitfield flag or number: ${bit}.`, - [DjsErrorCodes.ShardingInvalid]: 'Invalid shard settings were provided.', - [DjsErrorCodes.ShardingRequired]: 'This session would have handled too many guilds - Sharding is required.', - [DjsErrorCodes.InvalidIntents]: 'Invalid intent provided for WebSocket intents.', - [DjsErrorCodes.DisallowedIntents]: 'Privileged intent provided is not enabled or whitelisted.', [DjsErrorCodes.ShardingNoShards]: 'No shards have been spawned.', [DjsErrorCodes.ShardingInProcess]: 'Shards are still being spawned.', [DjsErrorCodes.ShardingInvalidEvalBroadcast]: 'Script to evaluate must be a function', @@ -44,22 +35,11 @@ const Messages = { [DjsErrorCodes.InviteOptionsMissingChannel]: 'A valid guild channel must be provided when GuildScheduledEvent is EXTERNAL.', - [DjsErrorCodes.ButtonLabel]: 'MessageButton label must be a string', - [DjsErrorCodes.ButtonURL]: 'MessageButton URL must be a string', - [DjsErrorCodes.ButtonCustomId]: 'MessageButton customId must be a string', - - [DjsErrorCodes.SelectMenuCustomId]: 'MessageSelectMenu customId must be a string', - [DjsErrorCodes.SelectMenuPlaceholder]: 'MessageSelectMenu placeholder must be a string', - [DjsErrorCodes.SelectOptionLabel]: 'MessageSelectOption label must be a string', - [DjsErrorCodes.SelectOptionValue]: 'MessageSelectOption value must be a string', - [DjsErrorCodes.SelectOptionDescription]: 'MessageSelectOption description must be a string', - [DjsErrorCodes.InteractionCollectorError]: reason => `Collector received no interactions before ending with reason: ${reason}`, [DjsErrorCodes.FileNotFound]: file => `File could not be found: ${file}`, - [DjsErrorCodes.UserBannerNotFetched]: "You must fetch this user's banner before trying to generate its URL!", [DjsErrorCodes.UserNoDMChannel]: 'No DM Channel exists!', [DjsErrorCodes.VoiceNotStageChannel]: 'You are only allowed to do this in stage channels.', @@ -70,16 +50,11 @@ const Messages = { [DjsErrorCodes.ReqResourceType]: 'The resource must be a string, Buffer or a valid file stream.', - [DjsErrorCodes.ImageFormat]: format => `Invalid image format: ${format}`, - [DjsErrorCodes.ImageSize]: size => `Invalid image size: ${size}`, - [DjsErrorCodes.MessageBulkDeleteType]: 'The messages must be an Array, Collection, or number.', [DjsErrorCodes.MessageContentType]: 'Message content must be a string.', [DjsErrorCodes.MessageNonceRequired]: 'Message nonce is required when enforceNonce is true.', [DjsErrorCodes.MessageNonceType]: 'Message nonce must be an integer or a string.', - [DjsErrorCodes.SplitMaxLen]: 'Chunk exceeds the max length and contains no split characters.', - [DjsErrorCodes.BanResolveId]: (ban = false) => `Couldn't resolve the user id to ${ban ? 'ban' : 'unban'}.`, [DjsErrorCodes.FetchBanResolveId]: "Couldn't resolve the user id to fetch the ban.", @@ -114,15 +89,11 @@ const Messages = { [DjsErrorCodes.EmojiManaged]: 'Emoji is managed and has no Author.', [DjsErrorCodes.MissingManageGuildExpressionsPermission]: guild => `Client must have Manage Guild Expressions permission in guild ${guild} to see emoji authors.`, - [DjsErrorCodes.MissingManageEmojisAndStickersPermission]: guild => - `Client must have Manage Emojis and Stickers permission in guild ${guild} to see emoji authors.`, [DjsErrorCodes.NotGuildSticker]: 'Sticker is a standard (non-guild) sticker and has no author.', [DjsErrorCodes.ReactionResolveUser]: "Couldn't resolve the user id to remove from the reaction.", - [DjsErrorCodes.VanityURL]: 'This guild does not have the vanity URL feature enabled.', - [DjsErrorCodes.InviteResolveCode]: 'Could not resolve the code to fetch the invite.', [DjsErrorCodes.InviteNotFound]: 'Could not find the requested invite.', @@ -140,7 +111,6 @@ const Messages = { [DjsErrorCodes.InteractionAlreadyReplied]: 'The reply to this interaction has already been sent or deferred.', [DjsErrorCodes.InteractionNotReplied]: 'The reply to this interaction has not been sent or deferred.', - [DjsErrorCodes.InteractionEphemeralReplied]: 'Ephemeral responses cannot be deleted.', [DjsErrorCodes.CommandInteractionOptionNotFound]: name => `Required option "${name}" not found.`, [DjsErrorCodes.CommandInteractionOptionType]: (name, type, expected) => diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 5b30a29bcdd6..45e8ac4f797c 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -31,7 +31,6 @@ exports.Constants = require('./util/Constants'); exports.Colors = require('./util/Colors'); __exportStar(require('./util/DataResolver.js'), exports); exports.Events = require('./util/Events'); -exports.Formatters = require('./util/Formatters'); exports.GuildMemberFlagsBitField = require('./util/GuildMemberFlagsBitField').GuildMemberFlagsBitField; exports.IntentsBitField = require('./util/IntentsBitField'); exports.LimitedCollection = require('./util/LimitedCollection'); @@ -151,7 +150,6 @@ exports.InteractionCollector = require('./structures/InteractionCollector'); exports.InteractionResponse = require('./structures/InteractionResponse'); exports.InteractionWebhook = require('./structures/InteractionWebhook'); exports.Invite = require('./structures/Invite'); -exports.InviteStageInstance = require('./structures/InviteStageInstance'); exports.InviteGuild = require('./structures/InviteGuild'); exports.Message = require('./structures/Message').Message; exports.Attachment = require('./structures/Attachment'); @@ -177,27 +175,23 @@ exports.ReactionCollector = require('./structures/ReactionCollector'); exports.ReactionEmoji = require('./structures/ReactionEmoji'); exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets; exports.Role = require('./structures/Role').Role; -exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder'); exports.ChannelSelectMenuBuilder = require('./structures/ChannelSelectMenuBuilder'); exports.MentionableSelectMenuBuilder = require('./structures/MentionableSelectMenuBuilder'); exports.RoleSelectMenuBuilder = require('./structures/RoleSelectMenuBuilder'); exports.StringSelectMenuBuilder = require('./structures/StringSelectMenuBuilder'); exports.UserSelectMenuBuilder = require('./structures/UserSelectMenuBuilder'); exports.BaseSelectMenuComponent = require('./structures/BaseSelectMenuComponent'); -exports.SelectMenuComponent = require('./structures/SelectMenuComponent'); exports.ChannelSelectMenuComponent = require('./structures/ChannelSelectMenuComponent'); exports.MentionableSelectMenuComponent = require('./structures/MentionableSelectMenuComponent'); exports.RoleSelectMenuComponent = require('./structures/RoleSelectMenuComponent'); exports.StringSelectMenuComponent = require('./structures/StringSelectMenuComponent'); exports.UserSelectMenuComponent = require('./structures/UserSelectMenuComponent'); -exports.SelectMenuInteraction = require('./structures/SelectMenuInteraction'); exports.ChannelSelectMenuInteraction = require('./structures/ChannelSelectMenuInteraction'); exports.MentionableSelectMenuInteraction = require('./structures/MentionableSelectMenuInteraction'); exports.MentionableSelectMenuInteraction = require('./structures/MentionableSelectMenuInteraction'); exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteraction'); exports.StringSelectMenuInteraction = require('./structures/StringSelectMenuInteraction'); exports.UserSelectMenuInteraction = require('./structures/UserSelectMenuInteraction'); -exports.SelectMenuOptionBuilder = require('./structures/SelectMenuOptionBuilder'); exports.SKU = require('./structures/SKU').SKU; exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder'); exports.StageChannel = require('./structures/StageChannel'); diff --git a/packages/discord.js/src/managers/GuildBanManager.js b/packages/discord.js/src/managers/GuildBanManager.js index 5bcbd368fb0c..53f5ca38f391 100644 --- a/packages/discord.js/src/managers/GuildBanManager.js +++ b/packages/discord.js/src/managers/GuildBanManager.js @@ -1,6 +1,5 @@ 'use strict'; -const process = require('node:process'); const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); const { Routes } = require('discord-api-types/v10'); @@ -9,8 +8,6 @@ const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../errors'); const GuildBan = require('../structures/GuildBan'); const { GuildMember } = require('../structures/GuildMember'); -let deprecationEmittedForDeleteMessageDays = false; - /** * Manages API methods for guild bans and stores their cache. * @extends {CachedManager} @@ -131,8 +128,6 @@ class GuildBanManager extends CachedManager { /** * Options used to ban a user from a guild. * @typedef {Object} BanOptions - * @property {number} [deleteMessageDays] Number of days of messages to delete, must be between 0 and 7, inclusive - * This property is deprecated. Use `deleteMessageSeconds` instead. * @property {number} [deleteMessageSeconds] Number of seconds of messages to delete, * must be between 0 and 604800 (7 days), inclusive * @property {string} [reason] The reason for the ban @@ -156,21 +151,9 @@ class GuildBanManager extends CachedManager { const id = this.client.users.resolveId(user); if (!id) throw new DiscordjsError(ErrorCodes.BanResolveId, true); - if (options.deleteMessageDays !== undefined && !deprecationEmittedForDeleteMessageDays) { - process.emitWarning( - // eslint-disable-next-line max-len - 'The deleteMessageDays option for GuildBanManager#create() is deprecated. Use the deleteMessageSeconds option instead.', - 'DeprecationWarning', - ); - - deprecationEmittedForDeleteMessageDays = true; - } - await this.client.rest.put(Routes.guildBan(this.guild.id, id), { body: { - delete_message_seconds: - options.deleteMessageSeconds ?? - (options.deleteMessageDays ? options.deleteMessageDays * 24 * 60 * 60 : undefined), + delete_message_seconds: options.deleteMessageSeconds, }, reason: options.reason, }); @@ -200,14 +183,6 @@ class GuildBanManager extends CachedManager { return this.client.users.resolve(user); } - /** - * Options used for bulk banning users from a guild. - * @typedef {Object} BulkBanOptions - * @property {number} [deleteMessageSeconds] Number of seconds of messages to delete, - * must be between 0 and 604800 (7 days), inclusive - * @property {string} [reason] The reason for the bans - */ - /** * Result of bulk banning users from a guild. * @typedef {Object} BulkBanResult @@ -218,7 +193,7 @@ class GuildBanManager extends CachedManager { /** * Bulk ban users from a guild, and optionally delete previous messages sent by them. * @param {Collection|UserResolvable[]} users The users to ban - * @param {BulkBanOptions} [options] The options for bulk banning users + * @param {BanOptions} [options] The options for bulk banning users * @returns {Promise} Returns an object with `bannedUsers` key containing the IDs of the banned users * and the key `failedUsers` with the IDs that could not be banned or were already banned. * @example diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index 909ac6213169..4b1b48e62d1d 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -504,7 +504,7 @@ class GuildMemberManager extends CachedManager { /** * Bulk ban users from a guild, and optionally delete previous messages sent by them. * @param {Collection|UserResolvable[]} users The users to ban - * @param {BulkBanOptions} [options] The options for bulk banning users + * @param {BanOptions} [options] The options for bulk banning users * @returns {Promise} Returns an object with `bannedUsers` key containing the IDs of the banned users * and the key `failedUsers` with the IDs that could not be banned or were already banned. * Internally calls the GuildBanManager#bulkCreate method. diff --git a/packages/discord.js/src/managers/RoleManager.js b/packages/discord.js/src/managers/RoleManager.js index a69018f54329..f1b2d29388c6 100644 --- a/packages/discord.js/src/managers/RoleManager.js +++ b/packages/discord.js/src/managers/RoleManager.js @@ -2,8 +2,7 @@ const process = require('node:process'); const { Collection } = require('@discordjs/collection'); -const { DiscordAPIError } = require('@discordjs/rest'); -const { RESTJSONErrorCodes, Routes } = require('discord-api-types/v10'); +const { Routes } = require('discord-api-types/v10'); const CachedManager = require('./CachedManager'); const { DiscordjsTypeError, ErrorCodes } = require('../errors'); const { Role } = require('../structures/Role'); @@ -74,17 +73,8 @@ class RoleManager extends CachedManager { if (existing) return existing; } - try { - const data = await this.client.rest.get(Routes.guildRole(this.guild.id, id)); - return this._add(data, cache); - } catch (error) { - // TODO: Remove this catch in the next major version - if (error instanceof DiscordAPIError && error.code === RESTJSONErrorCodes.UnknownRole) { - return null; - } - - throw error; - } + const data = await this.client.rest.get(Routes.guildRole(this.guild.id, id)); + return this._add(data, cache); } /** diff --git a/packages/discord.js/src/structures/ActionRow.js b/packages/discord.js/src/structures/ActionRow.js index 041224ee0c49..ad9a59e2c848 100644 --- a/packages/discord.js/src/structures/ActionRow.js +++ b/packages/discord.js/src/structures/ActionRow.js @@ -1,7 +1,5 @@ 'use strict'; -const { deprecate } = require('node:util'); -const { isJSONEncodable } = require('@discordjs/util'); const Component = require('./Component'); const { createComponent } = require('../util/Components'); @@ -21,19 +19,6 @@ class ActionRow extends Component { this.components = components.map(component => createComponent(component)); } - /** - * Creates a new action row builder from JSON data - * @method from - * @memberof ActionRow - * @param {ActionRowBuilder|ActionRow|APIActionRowComponent} other The other data - * @returns {ActionRowBuilder} - * @deprecated Use {@link ActionRowBuilder.from | ActionRowBuilder#from} instead. - */ - static from = deprecate( - other => new this(isJSONEncodable(other) ? other.toJSON() : other), - 'ActionRow.from() is deprecated. Use ActionRowBuilder.from() instead.', - ); - /** * Returns the API-compatible JSON for this component * @returns {APIActionRowComponent} diff --git a/packages/discord.js/src/structures/BaseGuildEmoji.js b/packages/discord.js/src/structures/BaseGuildEmoji.js index a5c2d5da1bf8..0eb29670d99b 100644 --- a/packages/discord.js/src/structures/BaseGuildEmoji.js +++ b/packages/discord.js/src/structures/BaseGuildEmoji.js @@ -62,14 +62,4 @@ class BaseGuildEmoji extends Emoji { * @returns {string} */ -/** - * Returns a URL for the emoji. - * @name url - * @memberof BaseGuildEmoji - * @instance - * @type {string} - * @readonly - * @deprecated Use {@link BaseGuildEmoji#imageURL} instead. - */ - module.exports = BaseGuildEmoji; diff --git a/packages/discord.js/src/structures/BaseInteraction.js b/packages/discord.js/src/structures/BaseInteraction.js index 28d1e4b35ad3..1386a75f9eb4 100644 --- a/packages/discord.js/src/structures/BaseInteraction.js +++ b/packages/discord.js/src/structures/BaseInteraction.js @@ -1,6 +1,5 @@ 'use strict'; -const { deprecate } = require('node:util'); const { Collection } = require('@discordjs/collection'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v10'); @@ -259,15 +258,6 @@ class BaseInteraction extends Base { return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.Button; } - /** - * Indicates whether this interaction is a {@link StringSelectMenuInteraction}. - * @returns {boolean} - * @deprecated Use {@link BaseInteraction#isStringSelectMenu} instead. - */ - isSelectMenu() { - return this.isStringSelectMenu(); - } - /** * Indicates whether this interaction is a select menu of any known type. * @returns {boolean} @@ -325,9 +315,4 @@ class BaseInteraction extends Base { } } -BaseInteraction.prototype.isSelectMenu = deprecate( - BaseInteraction.prototype.isSelectMenu, - 'BaseInteraction#isSelectMenu() is deprecated. Use BaseInteraction#isStringSelectMenu() instead.', -); - module.exports = BaseInteraction; diff --git a/packages/discord.js/src/structures/CommandInteraction.js b/packages/discord.js/src/structures/CommandInteraction.js index 0d435deeb446..88086f9605b2 100644 --- a/packages/discord.js/src/structures/CommandInteraction.js +++ b/packages/discord.js/src/structures/CommandInteraction.js @@ -153,7 +153,6 @@ class CommandInteraction extends BaseInteraction { deleteReply() {} followUp() {} showModal() {} - sendPremiumRequired() {} awaitModalSubmit() {} } diff --git a/packages/discord.js/src/structures/Emoji.js b/packages/discord.js/src/structures/Emoji.js index 9451fb043b0b..0dd368e5ddf6 100644 --- a/packages/discord.js/src/structures/Emoji.js +++ b/packages/discord.js/src/structures/Emoji.js @@ -1,12 +1,9 @@ 'use strict'; -const process = require('node:process'); const { formatEmoji } = require('@discordjs/formatters'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const Base = require('./Base'); -let deprecationEmittedForURL = false; - /** * Represents an emoji, see {@link ApplicationEmoji}, {@link GuildEmoji} and {@link ReactionEmoji}. * @extends {Base} @@ -52,21 +49,6 @@ class Emoji extends Base { return this.id && this.client.rest.cdn.emoji(this.id, options); } - /** - * Returns a URL for the emoji or `null` if this is not a custom emoji. - * @type {?string} - * @readonly - * @deprecated Use {@link Emoji#imageURL} instead. - */ - get url() { - if (!deprecationEmittedForURL) { - process.emitWarning('The Emoji#url getter is deprecated. Use Emoji#imageURL() instead.', 'DeprecationWarning'); - deprecationEmittedForURL = true; - } - - return this.imageURL({ extension: this.animated ? 'gif' : 'png' }); - } - /** * The timestamp the emoji was created at, or null if unicode * @type {?number} diff --git a/packages/discord.js/src/structures/Invite.js b/packages/discord.js/src/structures/Invite.js index 4f597d20063b..655d947c7961 100644 --- a/packages/discord.js/src/structures/Invite.js +++ b/packages/discord.js/src/structures/Invite.js @@ -4,7 +4,6 @@ const { RouteBases, Routes, PermissionFlagsBits } = require('discord-api-types/v const Base = require('./Base'); const { GuildScheduledEvent } = require('./GuildScheduledEvent'); const IntegrationApplication = require('./IntegrationApplication'); -const InviteStageInstance = require('./InviteStageInstance'); const { DiscordjsError, ErrorCodes } = require('../errors'); /** @@ -202,17 +201,6 @@ class Invite extends Base { this._expiresTimestamp ??= null; } - if ('stage_instance' in data) { - /** - * The stage instance data if there is a public {@link StageInstance} in the stage channel this invite is for - * @type {?InviteStageInstance} - * @deprecated - */ - this.stageInstance = new InviteStageInstance(this.client, data.stage_instance, this.channel.id, this.guild.id); - } else { - this.stageInstance ??= null; - } - if ('guild_scheduled_event' in data) { /** * The guild scheduled event data if there is a {@link GuildScheduledEvent} in the channel this invite is for diff --git a/packages/discord.js/src/structures/InviteStageInstance.js b/packages/discord.js/src/structures/InviteStageInstance.js deleted file mode 100644 index 21ede43a6c59..000000000000 --- a/packages/discord.js/src/structures/InviteStageInstance.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const Base = require('./Base'); - -/** - * Represents the data about a public {@link StageInstance} in an {@link Invite}. - * @extends {Base} - * @deprecated - */ -class InviteStageInstance extends Base { - constructor(client, data, channelId, guildId) { - super(client); - - /** - * The id of the stage channel this invite is for - * @type {Snowflake} - */ - this.channelId = channelId; - - /** - * The stage channel's guild id - * @type {Snowflake} - */ - this.guildId = guildId; - - /** - * The members speaking in the stage channel - * @type {Collection} - */ - this.members = new Collection(); - - this._patch(data); - } - - _patch(data) { - if ('topic' in data) { - /** - * The topic of the stage instance - * @type {string} - */ - this.topic = data.topic; - } - - if ('participant_count' in data) { - /** - * The number of users in the stage channel - * @type {number} - */ - this.participantCount = data.participant_count; - } - - if ('speaker_count' in data) { - /** - * The number of users speaking in the stage channel - * @type {number} - */ - this.speakerCount = data.speaker_count; - } - - this.members.clear(); - for (const rawMember of data.members) { - const member = this.guild.members._add(rawMember); - this.members.set(member.id, member); - } - } - - /** - * The stage channel this invite is for - * @type {?StageChannel} - * @readonly - */ - get channel() { - return this.client.channels.resolve(this.channelId); - } - - /** - * The guild of the stage channel this invite is for - * @type {?Guild} - * @readonly - */ - get guild() { - return this.client.guilds.resolve(this.guildId); - } -} - -module.exports = InviteStageInstance; diff --git a/packages/discord.js/src/structures/MessageComponentInteraction.js b/packages/discord.js/src/structures/MessageComponentInteraction.js index 2e6df11e5611..47b31e04c12c 100644 --- a/packages/discord.js/src/structures/MessageComponentInteraction.js +++ b/packages/discord.js/src/structures/MessageComponentInteraction.js @@ -99,7 +99,6 @@ class MessageComponentInteraction extends BaseInteraction { deferUpdate() {} update() {} showModal() {} - sendPremiumRequired() {} awaitModalSubmit() {} } diff --git a/packages/discord.js/src/structures/ModalSubmitInteraction.js b/packages/discord.js/src/structures/ModalSubmitInteraction.js index ba94190436da..559807bfa078 100644 --- a/packages/discord.js/src/structures/ModalSubmitInteraction.js +++ b/packages/discord.js/src/structures/ModalSubmitInteraction.js @@ -118,7 +118,6 @@ class ModalSubmitInteraction extends BaseInteraction { followUp() {} deferUpdate() {} update() {} - sendPremiumRequired() {} } InteractionResponses.applyToClass(ModalSubmitInteraction, 'showModal'); diff --git a/packages/discord.js/src/structures/SelectMenuBuilder.js b/packages/discord.js/src/structures/SelectMenuBuilder.js deleted file mode 100644 index a77937054777..000000000000 --- a/packages/discord.js/src/structures/SelectMenuBuilder.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const process = require('node:process'); -const StringSelectMenuBuilder = require('./StringSelectMenuBuilder'); - -let deprecationEmitted = false; - -/** - * @deprecated Use {@link StringSelectMenuBuilder} instead. - * @extends {StringSelectMenuBuilder} - */ -class SelectMenuBuilder extends StringSelectMenuBuilder { - constructor(...params) { - super(...params); - - if (!deprecationEmitted) { - process.emitWarning( - 'The SelectMenuBuilder class is deprecated. Use StringSelectMenuBuilder instead.', - 'DeprecationWarning', - ); - deprecationEmitted = true; - } - } -} - -module.exports = SelectMenuBuilder; diff --git a/packages/discord.js/src/structures/SelectMenuComponent.js b/packages/discord.js/src/structures/SelectMenuComponent.js deleted file mode 100644 index 2cd8097c95f3..000000000000 --- a/packages/discord.js/src/structures/SelectMenuComponent.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const process = require('node:process'); -const StringSelectMenuComponent = require('./StringSelectMenuComponent'); - -let deprecationEmitted = false; - -/** - * @deprecated Use {@link StringSelectMenuComponent} instead. - * @extends {StringSelectMenuComponent} - */ -class SelectMenuComponent extends StringSelectMenuComponent { - constructor(...params) { - super(...params); - - if (!deprecationEmitted) { - process.emitWarning( - 'The SelectMenuComponent class is deprecated. Use StringSelectMenuComponent instead.', - 'DeprecationWarning', - ); - deprecationEmitted = true; - } - } -} - -module.exports = SelectMenuComponent; diff --git a/packages/discord.js/src/structures/SelectMenuInteraction.js b/packages/discord.js/src/structures/SelectMenuInteraction.js deleted file mode 100644 index a09655958846..000000000000 --- a/packages/discord.js/src/structures/SelectMenuInteraction.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const process = require('node:process'); -const StringSelectMenuInteraction = require('./StringSelectMenuInteraction'); - -let deprecationEmitted = false; - -/** - * @deprecated Use {@link StringSelectMenuInteraction} instead. - * @extends {StringSelectMenuInteraction} - */ -class SelectMenuInteraction extends StringSelectMenuInteraction { - constructor(...params) { - super(...params); - - if (!deprecationEmitted) { - process.emitWarning( - 'The SelectMenuInteraction class is deprecated. Use StringSelectMenuInteraction instead.', - 'DeprecationWarning', - ); - deprecationEmitted = true; - } - } -} - -module.exports = SelectMenuInteraction; diff --git a/packages/discord.js/src/structures/SelectMenuOptionBuilder.js b/packages/discord.js/src/structures/SelectMenuOptionBuilder.js deleted file mode 100644 index 85309d1542bb..000000000000 --- a/packages/discord.js/src/structures/SelectMenuOptionBuilder.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const process = require('node:process'); -const StringSelectMenuOptionBuilder = require('./StringSelectMenuOptionBuilder'); - -let deprecationEmitted = false; - -/** - * @deprecated Use {@link StringSelectMenuOptionBuilder} instead. - * @extends {StringSelectMenuOptionBuilder} - */ -class SelectMenuOptionBuilder extends StringSelectMenuOptionBuilder { - constructor(...params) { - super(...params); - - if (!deprecationEmitted) { - process.emitWarning( - 'The SelectMenuOptionBuilder class is deprecated. Use StringSelectMenuOptionBuilder instead.', - 'DeprecationWarning', - ); - deprecationEmitted = true; - } - } -} - -module.exports = SelectMenuOptionBuilder; diff --git a/packages/discord.js/src/structures/StageInstance.js b/packages/discord.js/src/structures/StageInstance.js index 97f65df1d9a6..632f7b0c78d8 100644 --- a/packages/discord.js/src/structures/StageInstance.js +++ b/packages/discord.js/src/structures/StageInstance.js @@ -53,17 +53,6 @@ class StageInstance extends Base { this.privacyLevel = data.privacy_level; } - if ('discoverable_disabled' in data) { - /** - * Whether or not stage discovery is disabled - * @type {?boolean} - * @deprecated See https://github.com/discord/discord-api-docs/pull/4296 for more information - */ - this.discoverableDisabled = data.discoverable_disabled; - } else { - this.discoverableDisabled ??= null; - } - if ('guild_scheduled_event_id' in data) { /** * The associated guild scheduled event id of this stage instance diff --git a/packages/discord.js/src/structures/StringSelectMenuBuilder.js b/packages/discord.js/src/structures/StringSelectMenuBuilder.js index ac555e745401..bd123e4af8e6 100644 --- a/packages/discord.js/src/structures/StringSelectMenuBuilder.js +++ b/packages/discord.js/src/structures/StringSelectMenuBuilder.js @@ -24,8 +24,8 @@ class StringSelectMenuBuilder extends BuildersSelectMenu { /** * Normalizes a select menu option emoji - * @param {SelectMenuOptionData|APISelectMenuOption} selectMenuOption The option to normalize - * @returns {SelectMenuOptionBuilder|APISelectMenuOption} + * @param {SelectMenuComponentOptionData|APISelectMenuOption} selectMenuOption The option to normalize + * @returns {StringSelectMenuOptionBuilder|APISelectMenuOption} * @private */ static normalizeEmoji(selectMenuOption) { diff --git a/packages/discord.js/src/structures/TeamMember.js b/packages/discord.js/src/structures/TeamMember.js index b3c53987d943..b289ccd72677 100644 --- a/packages/discord.js/src/structures/TeamMember.js +++ b/packages/discord.js/src/structures/TeamMember.js @@ -20,15 +20,6 @@ class TeamMember extends Base { } _patch(data) { - if ('permissions' in data) { - /** - * The permissions this Team Member has with regard to the team - * @type {string[]} - * @deprecated Use {@link TeamMember#role} instead. - */ - this.permissions = data.permissions; - } - if ('membership_state' in data) { /** * The permissions this Team Member has with regard to the team diff --git a/packages/discord.js/src/structures/ThreadChannel.js b/packages/discord.js/src/structures/ThreadChannel.js index 074ff052ac59..a0d8c2b383af 100644 --- a/packages/discord.js/src/structures/ThreadChannel.js +++ b/packages/discord.js/src/structures/ThreadChannel.js @@ -1,8 +1,7 @@ 'use strict'; -const { DiscordAPIError } = require('@discordjs/rest'); const { lazy } = require('@discordjs/util'); -const { RESTJSONErrorCodes, ChannelFlags, ChannelType, PermissionFlagsBits, Routes } = require('discord-api-types/v10'); +const { ChannelFlags, ChannelType, PermissionFlagsBits, Routes } = require('discord-api-types/v10'); const { BaseChannel } = require('./BaseChannel'); const getThreadOnlyChannel = lazy(() => require('./ThreadOnlyChannel')); const TextBasedChannel = require('./interfaces/TextBasedChannel'); @@ -299,15 +298,7 @@ class ThreadChannel extends BaseChannel { throw new DiscordjsError(ErrorCodes.FetchOwnerId, 'thread'); } - // TODO: Remove that catch in the next major version - const member = await this.members._fetchSingle({ ...options, member: this.ownerId }).catch(error => { - if (error instanceof DiscordAPIError && error.code === RESTJSONErrorCodes.UnknownMember) { - return null; - } - - throw error; - }); - + const member = await this.members._fetchSingle({ ...options, member: this.ownerId }); return member; } diff --git a/packages/discord.js/src/structures/User.js b/packages/discord.js/src/structures/User.js index a7df27f42a9f..6025410c30f1 100644 --- a/packages/discord.js/src/structures/User.js +++ b/packages/discord.js/src/structures/User.js @@ -123,17 +123,6 @@ class User extends Base { this.flags = new UserFlagsBitField(data.public_flags); } - if ('avatar_decoration' in data) { - /** - * The user avatar decoration's hash - * @type {?string} - * @deprecated Use `avatarDecorationData` instead - */ - this.avatarDecoration = data.avatar_decoration; - } else { - this.avatarDecoration ??= null; - } - /** * @typedef {Object} AvatarDecorationData * @property {string} asset The avatar decoration hash @@ -192,15 +181,10 @@ class User extends Base { /** * A link to the user's avatar decoration. - * @param {BaseImageURLOptions} [options={}] Options for the image URL * @returns {?string} */ - avatarDecorationURL(options = {}) { - if (this.avatarDecorationData) { - return this.client.rest.cdn.avatarDecoration(this.avatarDecorationData.asset); - } - - return this.avatarDecoration && this.client.rest.cdn.avatarDecoration(this.id, this.avatarDecoration, options); + avatarDecorationURL() { + return this.avatarDecorationData ? this.client.rest.cdn.avatarDecoration(this.avatarDecorationData.asset) : null; } /** @@ -311,7 +295,6 @@ class User extends Base { this.flags?.bitfield === user.flags?.bitfield && this.banner === user.banner && this.accentColor === user.accentColor && - this.avatarDecoration === user.avatarDecoration && this.avatarDecorationData?.asset === user.avatarDecorationData?.asset && this.avatarDecorationData?.skuId === user.avatarDecorationData?.skuId ); @@ -334,7 +317,6 @@ class User extends Base { this.flags?.bitfield === user.public_flags && ('banner' in user ? this.banner === user.banner : true) && ('accent_color' in user ? this.accentColor === user.accent_color : true) && - ('avatar_decoration' in user ? this.avatarDecoration === user.avatar_decoration : true) && ('avatar_decoration_data' in user ? this.avatarDecorationData?.asset === user.avatar_decoration_data?.asset && this.avatarDecorationData?.skuId === user.avatar_decoration_data?.sku_id diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index 440242a34e56..5c8900530f61 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -1,6 +1,5 @@ 'use strict'; -const { deprecate } = require('node:util'); const { isJSONEncodable } = require('@discordjs/util'); const { InteractionResponseType, MessageFlags, Routes, InteractionType } = require('discord-api-types/v10'); const { DiscordjsError, ErrorCodes } = require('../../errors'); @@ -264,23 +263,6 @@ class InteractionResponses { this.replied = true; } - /** - * Responds to the interaction with an upgrade button. - * Only available for applications with monetization enabled. - * @deprecated Sending a premium-style button is the new Discord behaviour. - * @returns {Promise} - */ - async sendPremiumRequired() { - if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied); - await this.client.rest.post(Routes.interactionCallback(this.id, this.token), { - body: { - type: InteractionResponseType.PremiumRequired, - }, - auth: false, - }); - this.replied = true; - } - /** * An object containing the same properties as {@link CollectorOptions}, but a few less: * @typedef {Object} AwaitModalSubmitOptions @@ -324,7 +306,6 @@ class InteractionResponses { 'deferUpdate', 'update', 'showModal', - 'sendPremiumRequired', 'awaitModalSubmit', ]; @@ -339,10 +320,4 @@ class InteractionResponses { } } -InteractionResponses.prototype.sendPremiumRequired = deprecate( - InteractionResponses.prototype.sendPremiumRequired, - // eslint-disable-next-line max-len - 'InteractionResponses#sendPremiumRequired() is deprecated. Sending a premium-style button is the new Discord behaviour.', -); - module.exports = InteractionResponses; diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index 8babdfdbea68..0a2f48d0ef26 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -189,56 +189,6 @@ exports.UndeletableMessageTypes = [ MessageType.ThreadStarterMessage, ]; -/** - * The types of messages that can be deleted. The available types are: - * * {@link MessageType.AutoModerationAction} - * * {@link MessageType.ChannelFollowAdd} - * * {@link MessageType.ChannelPinnedMessage} - * * {@link MessageType.ChatInputCommand} - * * {@link MessageType.ContextMenuCommand} - * * {@link MessageType.Default} - * * {@link MessageType.GuildBoost} - * * {@link MessageType.GuildBoostTier1} - * * {@link MessageType.GuildBoostTier2} - * * {@link MessageType.GuildBoostTier3} - * * {@link MessageType.GuildInviteReminder} - * * {@link MessageType.InteractionPremiumUpsell} - * * {@link MessageType.Reply} - * * {@link MessageType.RoleSubscriptionPurchase} - * * {@link MessageType.StageEnd} - * * {@link MessageType.StageRaiseHand} - * * {@link MessageType.StageSpeaker} - * * {@link MessageType.StageStart} - * * {@link MessageType.StageTopic} - * * {@link MessageType.ThreadCreated} - * * {@link MessageType.UserJoin} - * @typedef {MessageType[]} DeletableMessageTypes - * @deprecated This list will no longer be updated. Use {@link UndeletableMessageTypes} instead. - */ -exports.DeletableMessageTypes = [ - MessageType.AutoModerationAction, - MessageType.ChannelFollowAdd, - MessageType.ChannelPinnedMessage, - MessageType.ChatInputCommand, - MessageType.ContextMenuCommand, - MessageType.Default, - MessageType.GuildBoost, - MessageType.GuildBoostTier1, - MessageType.GuildBoostTier2, - MessageType.GuildBoostTier3, - MessageType.GuildInviteReminder, - MessageType.InteractionPremiumUpsell, - MessageType.Reply, - MessageType.RoleSubscriptionPurchase, - MessageType.StageEnd, - MessageType.StageRaiseHand, - MessageType.StageSpeaker, - MessageType.StageStart, - MessageType.StageTopic, - MessageType.ThreadCreated, - MessageType.UserJoin, -]; - /** * A mapping between sticker formats and their respective image formats. * * {@link StickerFormatType.PNG} -> {@link ImageFormat.PNG} diff --git a/packages/discord.js/src/util/Events.js b/packages/discord.js/src/util/Events.js index a8ee63f66a56..a2de59453452 100644 --- a/packages/discord.js/src/util/Events.js +++ b/packages/discord.js/src/util/Events.js @@ -80,7 +80,7 @@ * @property {string} VoiceServerUpdate voiceServerUpdate * @property {string} VoiceStateUpdate voiceStateUpdate * @property {string} Warn warn - * @property {string} WebhooksUpdate webhookUpdate + * @property {string} WebhooksUpdate webhooksUpdate */ // JSDoc for IntelliSense purposes @@ -168,5 +168,5 @@ module.exports = { VoiceServerUpdate: 'voiceServerUpdate', VoiceStateUpdate: 'voiceStateUpdate', Warn: 'warn', - WebhooksUpdate: 'webhookUpdate', + WebhooksUpdate: 'webhooksUpdate', }; diff --git a/packages/discord.js/src/util/Formatters.js b/packages/discord.js/src/util/Formatters.js deleted file mode 100644 index 2dd0d6245f1c..000000000000 --- a/packages/discord.js/src/util/Formatters.js +++ /dev/null @@ -1,413 +0,0 @@ -'use strict'; - -const { deprecate } = require('node:util'); -const { - blockQuote, - bold, - channelMention, - codeBlock, - formatEmoji, - hideLinkEmbed, - hyperlink, - inlineCode, - italic, - quote, - roleMention, - spoiler, - strikethrough, - time, - TimestampStyles, - underscore, - userMention, -} = require('@discordjs/formatters'); - -/** - * Formats an application command name and id into an application command mention. - * @method chatInputApplicationCommandMention - * @param {string} commandName The name of the application command - * @param {string|Snowflake} subcommandGroupOrSubOrId - * The subcommand group name, subcommand name, or application command id - * @param {string|Snowflake} [subcommandNameOrId] The subcommand name or application command id - * @param {string} [commandId] The id of the application command - * @returns {string} - */ - -/** - * Wraps the content inside a code block with an optional language. - * @method codeBlock - * @param {string} contentOrLanguage The language to use or content if a second parameter isn't provided - * @param {string} [content] The content to wrap - * @returns {string} - */ - -/** - * Wraps the content inside \`backticks\`, which formats it as inline code. - * @method inlineCode - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Formats the content into italic text. - * @method italic - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Formats the content into bold text. - * @method bold - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Formats the content into underscored text. - * @method underscore - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Formats the content into strike-through text. - * @method strikethrough - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Formats the content into a quote. - * This needs to be at the start of the line for Discord to format it. - * @method quote - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Formats the content into a block quote. - * This needs to be at the start of the line for Discord to format it. - * @method blockQuote - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Wraps the URL into `<>`, which stops it from embedding. - * @method hideLinkEmbed - * @param {string} content The content to wrap - * @returns {string} - */ - -/** - * Formats the content and the URL into a masked URL with an optional title. - * @method hyperlink - * @param {string} content The content to display - * @param {string} url The URL the content links to - * @param {string} [title] The title shown when hovering on the masked link - * @returns {string} - */ - -/** - * Formats the content into spoiler text. - * @method spoiler - * @param {string} content The content to spoiler - * @returns {string} - */ - -/** - * Formats a user id into a user mention. - * @method userMention - * @param {Snowflake} userId The user id to format - * @returns {string} - */ - -/** - * Formats a channel id into a channel mention. - * @method channelMention - * @param {Snowflake} channelId The channel id to format - * @returns {string} - */ - -/** - * Formats a role id into a role mention. - * @method roleMention - * @param {Snowflake} roleId The role id to format - * @returns {string} - */ - -/** - * Formats an emoji id into a fully qualified emoji identifier. - * @method formatEmoji - * @param {Snowflake} emojiId The emoji id to format - * @param {boolean} [animated=false] Whether the emoji is animated - * @returns {string} - */ - -/** - * Formats a channel link for a channel. - * @method channelLink - * @param {Snowflake} channelId The id of the channel - * @param {Snowflake} [guildId] The id of the guild - * @returns {string} - */ - -/** - * Formats a message link for a channel. - * @method messageLink - * @param {Snowflake} channelId The id of the channel - * @param {Snowflake} messageId The id of the message - * @param {Snowflake} [guildId] The id of the guild - * @returns {string} - */ - -/** - * A message formatting timestamp style, as defined in - * [here](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles). - * * `t` Short time format, consisting of hours and minutes, e.g. 16:20. - * * `T` Long time format, consisting of hours, minutes, and seconds, e.g. 16:20:30. - * * `d` Short date format, consisting of day, month, and year, e.g. 20/04/2021. - * * `D` Long date format, consisting of day, month, and year, e.g. 20 April 2021. - * * `f` Short date-time format, consisting of short date and short time formats, e.g. 20 April 2021 16:20. - * * `F` Long date-time format, consisting of long date and short time formats, e.g. Tuesday, 20 April 2021 16:20. - * * `R` Relative time format, consisting of a relative duration format, e.g. 2 months ago. - * @typedef {string} TimestampStylesString - */ - -/** - * Formats a date into a short date-time string. - * @method time - * @param {number|Date} [date] The date to format - * @param {TimestampStylesString} [style] The style to use - * @returns {string} - */ - -/** - * Contains various Discord-specific functions for formatting messages. - * @deprecated This class is redundant as all methods of the class can be imported from discord.js directly. - */ -class Formatters extends null { - /** - * Formats the content into a block quote. - * This needs to be at the start of the line for Discord to format it. - * @method blockQuote - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static blockQuote = deprecate( - blockQuote, - 'Formatters.blockQuote() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats the content into bold text. - * @method bold - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static bold = deprecate( - bold, - 'Formatters.bold() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats a channel id into a channel mention. - * @method channelMention - * @memberof Formatters - * @param {Snowflake} channelId The channel id to format - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static channelMention = deprecate( - channelMention, - 'Formatters.channelMention() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Wraps the content inside a code block with an optional language. - * @method codeBlock - * @memberof Formatters - * @param {string} contentOrLanguage The language to use or content if a second parameter isn't provided - * @param {string} [content] The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static codeBlock = deprecate( - codeBlock, - 'Formatters.codeBlock() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats an emoji id into a fully qualified emoji identifier. - * @method formatEmoji - * @memberof Formatters - * @param {string} emojiId The emoji id to format - * @param {boolean} [animated=false] Whether the emoji is animated - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static formatEmoji = deprecate( - formatEmoji, - 'Formatters.formatEmoji() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Wraps the URL into `<>`, which stops it from embedding. - * @method hideLinkEmbed - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static hideLinkEmbed = deprecate( - hideLinkEmbed, - 'Formatters.hideLinkEmbed() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats the content and the URL into a masked URL with an optional title. - * @method hyperlink - * @memberof Formatters - * @param {string} content The content to display - * @param {string} url The URL the content links to - * @param {string} [title] The title shown when hovering on the masked link - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static hyperlink = deprecate( - hyperlink, - 'Formatters.hyperlink() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Wraps the content inside \`backticks\`, which formats it as inline code. - * @method inlineCode - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static inlineCode = deprecate( - inlineCode, - 'Formatters.inlineCode() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats the content into italic text. - * @method italic - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static italic = deprecate( - italic, - 'Formatters.italic() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats the content into a quote. This needs to be at the start of the line for Discord to format it. - * @method quote - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static quote = deprecate( - quote, - 'Formatters.quote() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats a role id into a role mention. - * @method roleMention - * @memberof Formatters - * @param {Snowflake} roleId The role id to format - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static roleMention = deprecate( - roleMention, - 'Formatters.roleMention() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats the content into spoiler text. - * @method spoiler - * @memberof Formatters - * @param {string} content The content to spoiler - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static spoiler = deprecate( - spoiler, - 'Formatters.spoiler() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats the content into strike-through text. - * @method strikethrough - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static strikethrough = deprecate( - strikethrough, - 'Formatters.strikethrough() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats a date into a short date-time string. - * @method time - * @memberof Formatters - * @param {number|Date} [date] The date to format - * @param {TimestampStylesString} [style] The style to use - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static time = deprecate( - time, - 'Formatters.time() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * The message formatting timestamp - * [styles](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles) supported by Discord. - * @type {Object} - * @memberof Formatters - * @deprecated Import this property directly from discord.js instead. - */ - static TimestampStyles = TimestampStyles; - - /** - * Formats the content into underscored text. - * @method underscore - * @memberof Formatters - * @param {string} content The content to wrap - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static underscore = deprecate( - underscore, - 'Formatters.underscore() is deprecated. Import this method directly from discord.js instead.', - ); - - /** - * Formats a user id into a user mention. - * @method userMention - * @memberof Formatters - * @param {Snowflake} userId The user id to format - * @returns {string} - * @deprecated Import this method directly from discord.js instead. - */ - static userMention = deprecate( - userMention, - 'Formatters.userMention() is deprecated. Import this method directly from discord.js instead.', - ); -} - -module.exports = Formatters; diff --git a/packages/discord.js/test/monetization.js b/packages/discord.js/test/monetization.js index 375c7a48f6e7..5628c41cf468 100644 --- a/packages/discord.js/test/monetization.js +++ b/packages/discord.js/test/monetization.js @@ -1,7 +1,8 @@ 'use strict'; -const { token, owner } = require('./auth.js'); -const { Client, Events, codeBlock, GatewayIntentBits } = require('../src'); +const { token, owner, skuId } = require('./auth.js'); +const { Client, Events, codeBlock, GatewayIntentBits, ActionRowBuilder, ButtonBuilder } = require('../src'); +const { ButtonStyle } = require('discord-api-types/v10'); const client = new Client({ intents: GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages }); @@ -35,7 +36,14 @@ client.on(Events.InteractionCreate, async interaction => { console.log('interaction.entitlements', interaction.entitlements); if (interaction.commandName === 'test') { - await interaction.sendPremiumRequired(); + await interaction.reply({ + content: ':3:3:3', + components: [ + new ActionRowBuilder().setComponents( + new ButtonBuilder().setCustomId('test').setLabel('test').setStyle(ButtonStyle.Premium).setSKUId(skuId), + ), + ], + }); } }); diff --git a/packages/discord.js/test/tester2000.js b/packages/discord.js/test/tester2000.js index 548299802be1..a7e2c3c758e0 100644 --- a/packages/discord.js/test/tester2000.js +++ b/packages/discord.js/test/tester2000.js @@ -3,7 +3,7 @@ const process = require('node:process'); const { GatewayIntentBits } = require('discord-api-types/v10'); const { token, prefix, owner } = require('./auth.js'); -const { Client, Options, Formatters } = require('../src'); +const { Client, Options, codeBlock } = require('../src'); // eslint-disable-next-line no-console const log = (...args) => console.log(process.uptime().toFixed(3), ...args); @@ -44,7 +44,7 @@ const commands = { console.error(err.stack); res = err.message; } - message.channel.send(Formatters.codeBlock(res)); + message.channel.send(codeBlock(res)); }, ping: message => message.channel.send('pong'), }; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 51c0f07fca94..40fa9b21552b 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -17,25 +17,6 @@ import { type RestOrArray, ApplicationCommandOptionAllowedChannelTypes, } from '@discordjs/builders'; -import { - blockQuote, - bold, - channelMention, - codeBlock, - formatEmoji, - hideLinkEmbed, - hyperlink, - inlineCode, - italic, - quote, - roleMention, - spoiler, - strikethrough, - time, - TimestampStyles, - underscore, - userMention, -} from '@discordjs/formatters'; import { Awaitable, JSONEncodable } from '@discordjs/util'; import { Collection, ReadonlyCollection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; @@ -220,7 +201,6 @@ import { RawInteractionData, RawInviteData, RawInviteGuildData, - RawInviteStageInstance, RawMessageButtonInteractionData, RawMessageComponentInteractionData, RawMessageData, @@ -602,8 +582,6 @@ export abstract class CommandInteraction e | ModalComponentData | APIModalInteractionResponseCallbackData, ): Promise; - /** @deprecated Sending a premium-style button is the new Discord behaviour. */ - public sendPremiumRequired(): Promise; public awaitModalSubmit( options: AwaitModalSubmitOptions, ): Promise>; @@ -784,13 +762,6 @@ export class StringSelectMenuBuilder extends BuilderStringSelectMenuComponent { ): StringSelectMenuBuilder; } -export { - /** @deprecated Use {@link StringSelectMenuBuilder} instead */ - StringSelectMenuBuilder as SelectMenuBuilder, - /** @deprecated Use {@link StringSelectMenuOptionBuilder} instead */ - StringSelectMenuOptionBuilder as SelectMenuOptionBuilder, -}; - export class UserSelectMenuBuilder extends BuilderUserSelectMenuComponent { public constructor(data?: Partial); public static from(other: JSONEncodable | APIUserSelectComponent): UserSelectMenuBuilder; @@ -851,11 +822,6 @@ export class StringSelectMenuComponent extends BaseSelectMenuComponent {} export class RoleSelectMenuComponent extends BaseSelectMenuComponent {} @@ -1033,8 +999,6 @@ export class Client extends BaseClient { public fetchSticker(id: Snowflake): Promise; public fetchStickerPacks(options: { packId: Snowflake }): Promise; public fetchStickerPacks(options?: StickerPackFetchOptions): Promise>; - /** @deprecated Use {@link Client.fetchStickerPacks} instead. */ - public fetchPremiumStickerPacks(): ReturnType; public fetchWebhook(id: Snowflake, token?: string): Promise; public fetchGuildWidget(guild: GuildResolvable): Promise; public generateInvite(options?: InviteGenerationOptions): string; @@ -1932,7 +1896,7 @@ export type Interaction = | ChatInputCommandInteraction | MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction - | AnySelectMenuInteraction + | SelectMenuInteraction | ButtonInteraction | AutocompleteInteraction | ModalSubmitInteraction; @@ -1984,9 +1948,7 @@ export class BaseInteraction extends Base public isMessageContextMenuCommand(): this is MessageContextMenuCommandInteraction; public isModalSubmit(): this is ModalSubmitInteraction; public isUserContextMenuCommand(): this is UserContextMenuCommandInteraction; - /** @deprecated Use {@link BaseInteraction.isStringSelectMenu} instead. */ - public isSelectMenu(): this is StringSelectMenuInteraction; - public isAnySelectMenu(): this is AnySelectMenuInteraction; + public isAnySelectMenu(): this is SelectMenuInteraction; public isStringSelectMenu(): this is StringSelectMenuInteraction; public isUserSelectMenu(): this is UserSelectMenuInteraction; public isRoleSelectMenu(): this is RoleSelectMenuInteraction; @@ -2075,24 +2037,9 @@ export class Invite extends Base { public toJSON(): unknown; public toString(): string; public static InvitesPattern: RegExp; - /** @deprecated Public Stage Instances don't exist anymore */ - public stageInstance: InviteStageInstance | null; public guildScheduledEvent: GuildScheduledEvent | null; } -/** @deprecated Public Stage Instances don't exist anymore */ -export class InviteStageInstance extends Base { - private constructor(client: Client, data: RawInviteStageInstance, channelId: Snowflake, guildId: Snowflake); - public channelId: Snowflake; - public guildId: Snowflake; - public members: Collection; - public topic: string; - public participantCount: number; - public speakerCount: number; - public get channel(): StageChannel | null; - public get guild(): Guild | null; -} - export class InviteGuild extends AnonymousGuild { private constructor(client: Client, data: RawInviteGuildData); public welcomeScreen: WelcomeScreen | null; @@ -2352,8 +2299,6 @@ export class MessageComponentInteraction e | ModalComponentData | APIModalInteractionResponseCallbackData, ): Promise; - /** @deprecated Sending a premium-style button is the new Discord behaviour. */ - public sendPremiumRequired(): Promise; public awaitModalSubmit( options: AwaitModalSubmitOptions, ): Promise>; @@ -2559,8 +2504,6 @@ export class ModalSubmitInteraction extend options: InteractionDeferUpdateOptions & { fetchReply: true }, ): Promise>>; public deferUpdate(options?: InteractionDeferUpdateOptions): Promise>>; - /** @deprecated Sending a premium-style button is the new Discord behaviour. */ - public sendPremiumRequired(): Promise; public inGuild(): this is ModalSubmitInteraction<'raw' | 'cached'>; public inCachedGuild(): this is ModalSubmitInteraction<'cached'>; public inRawGuild(): this is ModalSubmitInteraction<'raw'>; @@ -2861,11 +2804,6 @@ export class StringSelectMenuInteraction< public inRawGuild(): this is StringSelectMenuInteraction<'raw'>; } -export { - /** @deprecated Use {@link StringSelectMenuInteraction} instead */ - StringSelectMenuInteraction as SelectMenuInteraction, -}; - export class UserSelectMenuInteraction< Cached extends CacheType = CacheType, > extends MessageComponentInteraction { @@ -2956,8 +2894,7 @@ export class ChannelSelectMenuInteraction< // Ideally this should be named SelectMenuInteraction, but that's the name of the "old" StringSelectMenuInteraction, meaning // the type name is reserved as a re-export to prevent a breaking change from being made, as such: -// TODO: Rename this to SelectMenuInteraction in the next major -export type AnySelectMenuInteraction = +export type SelectMenuInteraction = | StringSelectMenuInteraction | UserSelectMenuInteraction | RoleSelectMenuInteraction @@ -3142,8 +3079,6 @@ export class StageInstance extends Base { public channelId: Snowflake; public topic: string; public privacyLevel: StageInstancePrivacyLevel; - /** @deprecated See https://github.com/discord/discord-api-docs/pull/4296 for more information */ - public discoverableDisabled: boolean | null; public guildScheduledEventId?: Snowflake; public get channel(): StageChannel | null; public get guild(): Guild | null; @@ -3296,8 +3231,6 @@ export class TeamMember extends Base { private constructor(team: Team, data: RawTeamMemberData); public team: Team; public get id(): Snowflake; - /** @deprecated Use {@link TeamMember.role} instead. */ - public permissions: string[]; public membershipState: TeamMemberMembershipState; public user: User; public role: TeamMemberRole; @@ -3433,8 +3366,6 @@ export class User extends Base { public accentColor: number | null | undefined; public avatar: string | null; - /** @deprecated Use {@link User.avatarDecorationData} instead */ - public avatarDecoration: string | null; public avatarDecorationData: AvatarDecorationData | null; public banner: string | null | undefined; public bot: boolean; @@ -3588,44 +3519,6 @@ export function createComponentBuilder(data: Data): Data; export function createComponentBuilder(data: APIMessageComponent | ComponentBuilder): ComponentBuilder; -/** @deprecated This class is redundant as all methods of the class can be imported from discord.js directly. */ -export class Formatters extends null { - /** @deprecated Import this method directly from discord.js instead. */ - public static blockQuote: typeof blockQuote; - /** @deprecated Import this method directly from discord.js instead. */ - public static bold: typeof bold; - /** @deprecated Import this method directly from discord.js instead. */ - public static channelMention: typeof channelMention; - /** @deprecated Import this method directly from discord.js instead. */ - public static codeBlock: typeof codeBlock; - /** @deprecated Import this method directly from discord.js instead. */ - public static formatEmoji: typeof formatEmoji; - /** @deprecated Import this method directly from discord.js instead. */ - public static hideLinkEmbed: typeof hideLinkEmbed; - /** @deprecated Import this method directly from discord.js instead. */ - public static hyperlink: typeof hyperlink; - /** @deprecated Import this method directly from discord.js instead. */ - public static inlineCode: typeof inlineCode; - /** @deprecated Import this method directly from discord.js instead. */ - public static italic: typeof italic; - /** @deprecated Import this method directly from discord.js instead. */ - public static quote: typeof quote; - /** @deprecated Import this method directly from discord.js instead. */ - public static roleMention: typeof roleMention; - /** @deprecated Import this method directly from discord.js instead. */ - public static spoiler: typeof spoiler; - /** @deprecated Import this method directly from discord.js instead. */ - public static strikethrough: typeof strikethrough; - /** @deprecated Import this method directly from discord.js instead. */ - public static time: typeof time; - /** @deprecated Import this property directly from discord.js instead. */ - public static TimestampStyles: typeof TimestampStyles; - /** @deprecated Import this method directly from discord.js instead. */ - public static underscore: typeof underscore; - /** @deprecated Import this method directly from discord.js instead. */ - public static userMention: typeof userMention; -} - /** @internal */ export function resolveBase64(data: Base64Resolvable): string; /** @internal */ @@ -3869,30 +3762,6 @@ export type UndeletableMessageType = | MessageType.ChannelIconChange | MessageType.ThreadStarterMessage; -/** @deprecated This type will no longer be updated. Use {@link UndeletableMessageType} instead. */ -export type DeletableMessageType = - | MessageType.AutoModerationAction - | MessageType.ChannelFollowAdd - | MessageType.ChannelPinnedMessage - | MessageType.ChatInputCommand - | MessageType.ContextMenuCommand - | MessageType.Default - | MessageType.GuildBoost - | MessageType.GuildBoostTier1 - | MessageType.GuildBoostTier2 - | MessageType.GuildBoostTier3 - | MessageType.GuildInviteReminder - | MessageType.InteractionPremiumUpsell - | MessageType.Reply - | MessageType.RoleSubscriptionPurchase - | MessageType.StageEnd - | MessageType.StageRaiseHand - | MessageType.StageSpeaker - | MessageType.StageStart - | MessageType.StageTopic - | MessageType.ThreadCreated - | MessageType.UserJoin; - export const Constants: { MaxBulkDeletableMessageAge: 1_209_600_000; SweeperKeys: SweeperKey[]; @@ -3904,8 +3773,6 @@ export const Constants: { VoiceBasedChannelTypes: VoiceBasedChannelTypes[]; SelectMenuTypes: SelectMenuType[]; UndeletableMessageTypes: UndeletableMessageType[]; - /** @deprecated This list will no longer be updated. Use {@link Constants.UndeletableMessageTypes} instead. */ - DeletableMessageTypes: DeletableMessageType[]; StickerFormatExtensionMap: Record; }; @@ -3924,25 +3791,8 @@ export enum DiscordjsErrorCodes { TokenMissing = 'TokenMissing', ApplicationCommandPermissionsTokenMissing = 'ApplicationCommandPermissionsTokenMissing', - /** @deprecated WebSocket errors are now handled in `@discordjs/ws` */ - WSCloseRequested = 'WSCloseRequested', - /** @deprecated WebSocket errors are now handled in `@discordjs/ws` */ - WSConnectionExists = 'WSConnectionExists', - /** @deprecated WebSocket errors are now handled in `@discordjs/ws` */ - WSNotOpen = 'WSNotOpen', - /** @deprecated No longer in use */ - ManagerDestroyed = 'ManagerDestroyed', - BitFieldInvalid = 'BitFieldInvalid', - /** @deprecated This error is now handled in `@discordjs/ws` */ - ShardingInvalid = 'ShardingInvalid', - /** @deprecated This error is now handled in `@discordjs/ws` */ - ShardingRequired = 'ShardingRequired', - /** @deprecated This error is now handled in `@discordjs/ws` */ - InvalidIntents = 'InvalidIntents', - /** @deprecated This error is now handled in `@discordjs/ws` */ - DisallowedIntents = 'DisallowedIntents', ShardingNoShards = 'ShardingNoShards', ShardingInProcess = 'ShardingInProcess', ShardingInvalidEvalBroadcast = 'ShardingInvalidEvalBroadcast', @@ -3961,30 +3811,10 @@ export enum DiscordjsErrorCodes { InviteOptionsMissingChannel = 'InviteOptionsMissingChannel', - /** @deprecated Button validation errors are now handled in `@discordjs/builders` */ - ButtonLabel = 'ButtonLabel', - /** @deprecated Button validation errors are now handled in `@discordjs/builders` */ - ButtonURL = 'ButtonURL', - /** @deprecated Button validation errors are now handled in `@discordjs/builders` */ - ButtonCustomId = 'ButtonCustomId', - - /** @deprecated Select Menu validation errors are now handled in `@discordjs/builders` */ - SelectMenuCustomId = 'SelectMenuCustomId', - /** @deprecated Select Menu validation errors are now handled in `@discordjs/builders` */ - SelectMenuPlaceholder = 'SelectMenuPlaceholder', - /** @deprecated Select Menu validation errors are now handled in `@discordjs/builders` */ - SelectOptionLabel = 'SelectOptionLabel', - /** @deprecated Select Menu validation errors are now handled in `@discordjs/builders` */ - SelectOptionValue = 'SelectOptionValue', - /** @deprecated Select Menu validation errors are now handled in `@discordjs/builders` */ - SelectOptionDescription = 'SelectOptionDescription', - InteractionCollectorError = 'InteractionCollectorError', FileNotFound = 'FileNotFound', - /** @deprecated No longer in use */ - UserBannerNotFetched = 'UserBannerNotFetched', UserNoDMChannel = 'UserNoDMChannel', VoiceNotStageChannel = 'VoiceNotStageChannel', @@ -3994,19 +3824,11 @@ export enum DiscordjsErrorCodes { ReqResourceType = 'ReqResourceType', - /** @deprecated This error is now handled in `@discordjs/rest` */ - ImageFormat = 'ImageFormat', - /** @deprecated This error is now handled in `@discordjs/rest` */ - ImageSize = 'ImageSize', - MessageBulkDeleteType = 'MessageBulkDeleteType', MessageContentType = 'MessageContentType', MessageNonceRequired = 'MessageNonceRequired', MessageNonceType = 'MessageNonceType', - /** @deprecated No longer in use */ - SplitMaxLen = 'SplitMaxLen', - BanResolveId = 'BanResolveId', FetchBanResolveId = 'FetchBanResolveId', @@ -4040,16 +3862,11 @@ export enum DiscordjsErrorCodes { EmojiType = 'EmojiType', EmojiManaged = 'EmojiManaged', MissingManageGuildExpressionsPermission = 'MissingManageGuildExpressionsPermission', - /** @deprecated Use {@link DiscordjsErrorCodes.MissingManageGuildExpressionsPermission} instead. */ - MissingManageEmojisAndStickersPermission = 'MissingManageEmojisAndStickersPermission', NotGuildSticker = 'NotGuildSticker', ReactionResolveUser = 'ReactionResolveUser', - /** @deprecated Not used anymore since the introduction of `GUILD_WEB_PAGE_VANITY_URL` feature */ - VanityURL = 'VanityURL', - InviteResolveCode = 'InviteResolveCode', InviteNotFound = 'InviteNotFound', @@ -4064,8 +3881,6 @@ export enum DiscordjsErrorCodes { InteractionAlreadyReplied = 'InteractionAlreadyReplied', InteractionNotReplied = 'InteractionNotReplied', - /** @deprecated Not used anymore since ephemeral replies can now be deleted */ - InteractionEphemeralReplied = 'InteractionEphemeralReplied', CommandInteractionOptionNotFound = 'CommandInteractionOptionNotFound', CommandInteractionOptionType = 'CommandInteractionOptionType', @@ -4417,7 +4232,7 @@ export class GuildMemberManager extends CachedManager; public bulkBan( users: ReadonlyCollection | readonly UserResolvable[], - options?: BulkBanOptions, + options?: BanOptions, ): Promise; public edit(user: UserResolvable, options: GuildMemberEditOptions): Promise; public fetch( @@ -4444,7 +4259,7 @@ export class GuildBanManager extends CachedManager; public bulkCreate( users: ReadonlyCollection | readonly UserResolvable[], - options?: BulkBanOptions, + options?: BanOptions, ): Promise; } @@ -5147,14 +4962,10 @@ export interface AwaitReactionsOptions extends ReactionCollectorOptions { } export interface BanOptions { - /** @deprecated Use {@link BanOptions.deleteMessageSeconds} instead. */ - deleteMessageDays?: number; deleteMessageSeconds?: number; reason?: string; } -export interface BulkBanOptions extends Omit {} - export interface BulkBanResult { bannedUsers: readonly Snowflake[]; failedUsers: readonly Snowflake[]; @@ -5390,8 +5201,6 @@ export interface ClientEvents { typingStart: [typing: Typing]; userUpdate: [oldUser: User | PartialUser, newUser: User]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; - /** @deprecated Use {@link ClientEvents.webhooksUpdate} instead. */ - webhookUpdate: ClientEvents['webhooksUpdate']; webhooksUpdate: [channel: TextChannel | NewsChannel | VoiceChannel | ForumChannel | MediaChannel]; interactionCreate: [interaction: Interaction]; shardDisconnect: [closeEvent: CloseEvent, shardId: number]; @@ -5603,7 +5412,7 @@ export enum Events { VoiceServerUpdate = 'voiceServerUpdate', VoiceStateUpdate = 'voiceStateUpdate', TypingStart = 'typingStart', - WebhooksUpdate = 'webhookUpdate', + WebhooksUpdate = 'webhooksUpdate', InteractionCreate = 'interactionCreate', Error = 'error', Warn = 'warn', diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 1f1b34f3a0b6..df99d3f29a8f 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -108,7 +108,7 @@ import { StageInstance, ActionRowBuilder, ButtonComponent, - SelectMenuComponent, + StringSelectMenuComponent, RepliableInteraction, ThreadChannelType, Events, @@ -152,9 +152,8 @@ import { ChannelFlagsBitField, GuildForumThreadManager, GuildTextThreadManager, - AnySelectMenuInteraction, + SelectMenuInteraction, StringSelectMenuInteraction, - StringSelectMenuComponent, UserSelectMenuInteraction, RoleSelectMenuInteraction, ChannelSelectMenuInteraction, @@ -213,7 +212,7 @@ import { SendableChannels, PollData, } from '.'; -import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; +import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; import { ReadonlyCollection } from '@discordjs/collection'; @@ -1780,10 +1779,9 @@ if (interaction.inGuild()) { client.on('interactionCreate', async interaction => { if (interaction.type === InteractionType.MessageComponent) { - expectType(interaction); + expectType(interaction); expectType(interaction.component); expectType(interaction.message); - expectDeprecated(interaction.sendPremiumRequired()); if (interaction.inCachedGuild()) { expectAssignable(interaction); expectType(interaction.component); @@ -1948,7 +1946,7 @@ client.on('interactionCreate', async interaction => { expectType(interaction.message); if (interaction.inCachedGuild()) { expectAssignable(interaction); - expectType(interaction.component); + expectType(interaction.component); expectType>(interaction.message); expectType(interaction.guild); expectType>>(interaction.reply({ fetchReply: true })); @@ -1960,7 +1958,7 @@ client.on('interactionCreate', async interaction => { expectType>>(interaction.reply({ fetchReply: true })); } else if (interaction.inGuild()) { expectAssignable(interaction); - expectType(interaction.component); + expectType(interaction.component); expectType(interaction.message); expectType(interaction.guild); expectType>(interaction.reply({ fetchReply: true })); @@ -1971,7 +1969,6 @@ client.on('interactionCreate', async interaction => { interaction.type === InteractionType.ApplicationCommand && interaction.commandType === ApplicationCommandType.ChatInput ) { - expectDeprecated(interaction.sendPremiumRequired()); if (interaction.inRawGuild()) { expectNotAssignable>(interaction); expectAssignable(interaction); @@ -2095,10 +2092,6 @@ client.on('interactionCreate', async interaction => { expectType>(interaction.followUp({ content: 'a' })); } } - - if (interaction.isModalSubmit()) { - expectDeprecated(interaction.sendPremiumRequired()); - } }); declare const shard: Shard; @@ -2422,10 +2415,10 @@ expectType(partialGroupDMChannel.flags); // Select menu type narrowing if (interaction.isAnySelectMenu()) { - expectType(interaction); + expectType(interaction); } -declare const anySelectMenu: AnySelectMenuInteraction; +declare const anySelectMenu: SelectMenuInteraction; if (anySelectMenu.isStringSelectMenu()) { expectType(anySelectMenu); @@ -2559,10 +2552,6 @@ declare const sku: SKU; client.on(Events.InteractionCreate, async interaction => { expectType>(interaction.entitlements); - - if (interaction.isRepliable()) { - await interaction.sendPremiumRequired(); - } }); } diff --git a/packages/discord.js/typings/rawDataTypes.d.ts b/packages/discord.js/typings/rawDataTypes.d.ts index 794daafa4069..b6c29feb7992 100644 --- a/packages/discord.js/typings/rawDataTypes.d.ts +++ b/packages/discord.js/typings/rawDataTypes.d.ts @@ -25,7 +25,6 @@ import { APIInteractionDataResolvedGuildMember, APIInteractionGuildMember, APIInvite, - APIInviteStageInstance, APIMessage, APIMessageButtonInteractionData, APIMessageComponentInteraction, @@ -148,8 +147,6 @@ export type RawInviteData = | (GatewayInviteCreateDispatchData & { channel: GuildChannel; guild: Guild }) | (GatewayInviteDeleteDispatchData & { channel: GuildChannel; guild: Guild }); -export type RawInviteStageInstance = APIInviteStageInstance; - export type RawMessageData = APIMessage; export type RawPartialMessageData = GatewayMessageUpdateDispatchData; From 04df3c4130629863cdc7b116f55bb8973badc92e Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Sun, 6 Oct 2024 16:19:50 +0200 Subject: [PATCH 54/65] feat: add linked roles formatters (#10461) * feat: add linked roles formatters * docs: requested changes Co-authored-by: Almeida * docs: remove locale --------- Co-authored-by: Almeida Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/formatters/__tests__/formatters.test.ts | 7 +++++++ packages/formatters/src/formatters.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/formatters/__tests__/formatters.test.ts b/packages/formatters/__tests__/formatters.test.ts index 32cb8b3985fa..1b745439943d 100644 --- a/packages/formatters/__tests__/formatters.test.ts +++ b/packages/formatters/__tests__/formatters.test.ts @@ -17,6 +17,7 @@ import { hyperlink, inlineCode, italic, + linkedRoleMention, messageLink, orderedList, quote, @@ -145,6 +146,12 @@ describe('Message formatters', () => { }); }); + describe('linkedRoleMention', () => { + test('GIVEN roleId THEN returns ""', () => { + expect(linkedRoleMention('815434166602170409')).toEqual(''); + }); + }); + describe('chatInputApplicationCommandMention', () => { test('GIVEN commandName and commandId THEN returns ""', () => { expect(chatInputApplicationCommandMention('airhorn', '815434166602170409')).toEqual( diff --git a/packages/formatters/src/formatters.ts b/packages/formatters/src/formatters.ts index 5ffa2a9e20a4..9c4e11932949 100644 --- a/packages/formatters/src/formatters.ts +++ b/packages/formatters/src/formatters.ts @@ -225,6 +225,16 @@ export function roleMention(roleId: RoleId): `<@&${Rol return `<@&${roleId}>`; } +/** + * Formats a role id into a linked role mention. + * + * @typeParam RoleId - This is inferred by the supplied role id + * @param roleId - The role id to format + */ +export function linkedRoleMention(roleId: RoleId): `` { + return ``; +} + /** * Formats an application command name, subcommand group name, subcommand name, and id into an application command mention. * @@ -754,4 +764,8 @@ export enum GuildNavigationMentions { * {@link https://support.discord.com/hc/articles/13497665141655 | Server Guide} tab. */ Guide = '', + /** + * {@link https://support.discord.com/hc/articles/10388356626711 | Linked Roles} tab. + */ + LinkedRoles = '', } From bb04e09f8b478bf95d56ed04a7e495bc887ed5f3 Mon Sep 17 00:00:00 2001 From: Amgelo563 <61554601+Amgelo563@users.noreply.github.com> Date: Sun, 6 Oct 2024 09:23:44 -0500 Subject: [PATCH 55/65] types: remove newMessage partial on messageUpdate event typing (#10526) * types: remove newMessage partial on messageUpdate event typing * types: omit partial group DM for newMessage on messageUpdate * types: omit partial group DM for oldMessage on messageUpdate --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/discord.js/typings/index.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 40fa9b21552b..42614f56a126 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -5181,7 +5181,10 @@ export interface ClientEvents { user: User | PartialUser, details: MessageReactionEventDetails, ]; - messageUpdate: [oldMessage: Message | PartialMessage, newMessage: Message | PartialMessage]; + messageUpdate: [ + oldMessage: OmitPartialGroupDMChannel, + newMessage: OmitPartialGroupDMChannel, + ]; presenceUpdate: [oldPresence: Presence | null, newPresence: Presence]; ready: [client: Client]; invalidated: []; From 24128a3c459ed0c3eb0932308f03ecc55e3c60f1 Mon Sep 17 00:00:00 2001 From: pat <73502164+nyapat@users.noreply.github.com> Date: Mon, 7 Oct 2024 01:26:53 +1100 Subject: [PATCH 56/65] test: replace jest with vitest (#10472) * chore: vitest config * feat: vitest * fix: do not actually create ws * chore: config * chore: lockfile * chore: revert downgrade, up node * chore: package - 'git add -A' * chore: delete mock-socket * chore: delete mock-socket * fix: lockfile --------- Co-authored-by: almeidx Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/voice/__mocks__/ws.js | 1 - packages/voice/__tests__/AudioPlayer.test.ts | 94 +++++++++------- .../__tests__/AudioReceiveStream.test.ts | 1 + .../voice/__tests__/AudioResource.test.ts | 12 ++- packages/voice/__tests__/DataStore.test.ts | 21 ++-- packages/voice/__tests__/SSRCMap.test.ts | 1 + packages/voice/__tests__/Secretbox.test.ts | 7 +- packages/voice/__tests__/SpeakingMap.test.ts | 9 +- .../voice/__tests__/TransformerGraph.test.ts | 1 + .../voice/__tests__/VoiceConnection.test.ts | 99 +++++++++-------- .../voice/__tests__/VoiceReceiver.test.ts | 35 +++--- .../voice/__tests__/VoiceUDPSocket.test.ts | 25 ++--- .../voice/__tests__/VoiceWebSocket.test.ts | 3 +- packages/voice/__tests__/abortAfter.test.ts | 7 +- packages/voice/__tests__/demuxProbe.test.ts | 11 +- packages/voice/__tests__/entersState.test.ts | 9 +- .../voice/__tests__/joinVoiceChannel.test.ts | 5 +- packages/voice/babel.config.js | 17 --- packages/voice/jest.config.js | 11 -- packages/voice/package.json | 17 ++- pnpm-lock.yaml | 100 ++++++++++++------ 21 files changed, 267 insertions(+), 219 deletions(-) delete mode 100644 packages/voice/__mocks__/ws.js delete mode 100644 packages/voice/babel.config.js delete mode 100644 packages/voice/jest.config.js diff --git a/packages/voice/__mocks__/ws.js b/packages/voice/__mocks__/ws.js deleted file mode 100644 index be3a438230e9..000000000000 --- a/packages/voice/__mocks__/ws.js +++ /dev/null @@ -1 +0,0 @@ -export { WebSocket as default } from 'mock-socket'; diff --git a/packages/voice/__tests__/AudioPlayer.test.ts b/packages/voice/__tests__/AudioPlayer.test.ts index 6b254ad48516..7d417d8b82a4 100644 --- a/packages/voice/__tests__/AudioPlayer.test.ts +++ b/packages/voice/__tests__/AudioPlayer.test.ts @@ -5,6 +5,7 @@ import { Buffer } from 'node:buffer'; import { once } from 'node:events'; import process from 'node:process'; import { Readable } from 'node:stream'; +import { describe, test, expect, vitest, type Mock, beforeEach, afterEach } from 'vitest'; import { addAudioPlayer, deleteAudioPlayer } from '../src/DataStore'; import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection'; import type { AudioPlayer } from '../src/audio/AudioPlayer'; @@ -13,14 +14,31 @@ import { AudioPlayerError } from '../src/audio/AudioPlayerError'; import { AudioResource } from '../src/audio/AudioResource'; import { NoSubscriberBehavior } from '../src/index'; -jest.mock('../src/DataStore'); -jest.mock('../src/VoiceConnection'); -jest.mock('../src/audio/AudioPlayerError'); +vitest.mock('../src/DataStore', () => { + return { + addAudioPlayer: vitest.fn(), + deleteAudioPlayer: vitest.fn(), + }; +}); -const addAudioPlayerMock = addAudioPlayer as unknown as jest.Mock; -const deleteAudioPlayerMock = deleteAudioPlayer as unknown as jest.Mock; -const AudioPlayerErrorMock = AudioPlayerError as unknown as jest.Mock; -const VoiceConnectionMock = VoiceConnection as unknown as jest.Mock; +vitest.mock('../src/VoiceConnection', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal(); + const VoiceConnection = vitest.fn(); + VoiceConnection.prototype.setSpeaking = vitest.fn(); + VoiceConnection.prototype.dispatchAudio = vitest.fn(); + VoiceConnection.prototype.prepareAudioPacket = vitest.fn(); + return { + ...actual, + VoiceConnection, + }; +}); + +vitest.mock('../src/audio/AudioPlayerError', () => { + return { + AudioPlayerError: vitest.fn(), + }; +}); function* silence() { while (true) { @@ -29,15 +47,15 @@ function* silence() { } function createVoiceConnectionMock() { - const connection = new VoiceConnectionMock(); + const connection = new VoiceConnection({} as any, {} as any); connection.state = { status: VoiceConnectionStatus.Signalling, adapter: { - sendPayload: jest.fn(), - destroy: jest.fn(), + sendPayload: vitest.fn(), + destroy: vitest.fn(), }, }; - connection.subscribe = jest.fn((player) => player['subscribe'](connection)); + connection.subscribe = vitest.fn((player) => player['subscribe'](connection)); return connection; } @@ -57,10 +75,7 @@ async function started(resource: AudioResource) { let player: AudioPlayer | undefined; beforeEach(() => { - AudioPlayerErrorMock.mockReset(); - VoiceConnectionMock.mockReset(); - addAudioPlayerMock.mockReset(); - deleteAudioPlayerMock.mockReset(); + vitest.resetAllMocks(); }); afterEach(() => { @@ -71,8 +86,8 @@ describe('State transitions', () => { test('Starts in Idle state', () => { player = createAudioPlayer(); expect(player.state.status).toEqual(AudioPlayerStatus.Idle); - expect(addAudioPlayerMock).toBeCalledTimes(0); - expect(deleteAudioPlayerMock).toBeCalledTimes(0); + expect(addAudioPlayer).toBeCalledTimes(0); + expect(deleteAudioPlayer).toBeCalledTimes(0); }); test('Playing resource with pausing and resuming', async () => { @@ -86,11 +101,11 @@ describe('State transitions', () => { expect(player.state.status).toEqual(AudioPlayerStatus.Idle); expect(player.unpause()).toEqual(false); expect(player.state.status).toEqual(AudioPlayerStatus.Idle); - expect(addAudioPlayerMock).toBeCalledTimes(0); + expect(addAudioPlayer).toBeCalledTimes(0); player.play(resource); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - expect(addAudioPlayerMock).toBeCalledTimes(1); + expect(addAudioPlayer).toBeCalledTimes(1); // Expect pause() to return true and transition to paused state expect(player.pause()).toEqual(true); @@ -109,7 +124,7 @@ describe('State transitions', () => { expect(player.state.status).toEqual(AudioPlayerStatus.Playing); // The audio player should not have been deleted throughout these changes - expect(deleteAudioPlayerMock).toBeCalledTimes(0); + expect(deleteAudioPlayer).toBeCalledTimes(0); }); test('Playing to Stopping', async () => { @@ -122,13 +137,13 @@ describe('State transitions', () => { player.play(resource); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - expect(addAudioPlayerMock).toBeCalledTimes(1); - expect(deleteAudioPlayerMock).toBeCalledTimes(0); + expect(addAudioPlayer).toBeCalledTimes(1); + expect(deleteAudioPlayer).toBeCalledTimes(0); expect(player.stop()).toEqual(true); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - expect(addAudioPlayerMock).toBeCalledTimes(1); - expect(deleteAudioPlayerMock).toBeCalledTimes(0); + expect(addAudioPlayer).toBeCalledTimes(1); + expect(deleteAudioPlayer).toBeCalledTimes(0); expect(resource.silenceRemaining).toEqual(5); }); @@ -142,8 +157,8 @@ describe('State transitions', () => { await started(resource); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - expect(addAudioPlayerMock).toHaveBeenCalled(); - expect(deleteAudioPlayerMock).not.toHaveBeenCalled(); + expect(addAudioPlayer).toHaveBeenCalled(); + expect(deleteAudioPlayer).not.toHaveBeenCalled(); }); describe('NoSubscriberBehavior transitions', () => { @@ -188,11 +203,11 @@ describe('State transitions', () => { player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Stop } }); player.play(resource); - expect(addAudioPlayerMock).toBeCalledTimes(1); + expect(addAudioPlayer).toBeCalledTimes(1); expect(player.checkPlayable()).toEqual(true); player['_stepPrepare'](); expect(player.state.status).toEqual(AudioPlayerStatus.Idle); - expect(deleteAudioPlayerMock).toBeCalledTimes(1); + expect(deleteAudioPlayer).toBeCalledTimes(1); }); }); @@ -217,7 +232,7 @@ describe('State transitions', () => { player.play(resource); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - expect(addAudioPlayerMock).toBeCalledTimes(1); + expect(addAudioPlayer).toBeCalledTimes(1); expect(player.checkPlayable()).toEqual(true); // Run through a few packet cycles @@ -241,7 +256,8 @@ describe('State transitions', () => { expect(connection.dispatchAudio).toHaveBeenCalledTimes(6); await wait(); player['_stepPrepare'](); - const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock< + const prepareAudioPacket = connection.prepareAudioPacket as unknown as Mock< + [Buffer], typeof connection.prepareAudioPacket >; expect(prepareAudioPacket).toHaveBeenCalledTimes(6); @@ -251,7 +267,7 @@ describe('State transitions', () => { expect(player.state.status).toEqual(AudioPlayerStatus.Idle); expect(connection.setSpeaking).toBeCalledTimes(1); expect(connection.setSpeaking).toHaveBeenLastCalledWith(false); - expect(deleteAudioPlayerMock).toHaveBeenCalledTimes(1); + expect(deleteAudioPlayer).toHaveBeenCalledTimes(1); }); test('stop() causes resource to use silence padding frames', async () => { @@ -275,7 +291,7 @@ describe('State transitions', () => { player.play(resource); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - expect(addAudioPlayerMock).toBeCalledTimes(1); + expect(addAudioPlayer).toBeCalledTimes(1); expect(player.checkPlayable()).toEqual(true); player.stop(); @@ -298,7 +314,8 @@ describe('State transitions', () => { await wait(); expect(player.checkPlayable()).toEqual(false); - const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock< + const prepareAudioPacket = connection.prepareAudioPacket as unknown as Mock< + [Buffer], typeof connection.prepareAudioPacket >; expect(prepareAudioPacket).toHaveBeenCalledTimes(5); @@ -306,7 +323,7 @@ describe('State transitions', () => { expect(player.state.status).toEqual(AudioPlayerStatus.Idle); expect(connection.setSpeaking).toBeCalledTimes(1); expect(connection.setSpeaking).toHaveBeenLastCalledWith(false); - expect(deleteAudioPlayerMock).toHaveBeenCalledTimes(1); + expect(deleteAudioPlayer).toHaveBeenCalledTimes(1); }); test('Plays silence 5 times for unreadable stream before quitting', async () => { @@ -328,10 +345,11 @@ describe('State transitions', () => { player.play(resource); expect(player.state.status).toEqual(AudioPlayerStatus.Playing); - expect(addAudioPlayerMock).toBeCalledTimes(1); + expect(addAudioPlayer).toBeCalledTimes(1); expect(player.checkPlayable()).toEqual(true); - const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock< + const prepareAudioPacket = connection.prepareAudioPacket as unknown as Mock< + [Buffer], typeof connection.prepareAudioPacket >; @@ -351,7 +369,7 @@ describe('State transitions', () => { expect(player.state.status).toEqual(AudioPlayerStatus.Idle); expect(connection.setSpeaking).toBeCalledTimes(1); expect(connection.setSpeaking).toHaveBeenLastCalledWith(false); - expect(deleteAudioPlayerMock).toHaveBeenCalledTimes(1); + expect(deleteAudioPlayer).toHaveBeenCalledTimes(1); }); test('checkPlayable() transitions to Idle for unreadable stream', async () => { @@ -397,6 +415,6 @@ test('Propagates errors from streams', async () => { const res = await once(player, 'error'); const playerError = res[0] as AudioPlayerError; expect(playerError).toBeInstanceOf(AudioPlayerError); - expect(AudioPlayerErrorMock).toHaveBeenCalledWith(error, resource); + expect(AudioPlayerError).toHaveBeenCalledWith(error, resource); expect(player.state.status).toEqual(AudioPlayerStatus.Idle); }); diff --git a/packages/voice/__tests__/AudioReceiveStream.test.ts b/packages/voice/__tests__/AudioReceiveStream.test.ts index 415ec1bf2b69..8823ff641c21 100644 --- a/packages/voice/__tests__/AudioReceiveStream.test.ts +++ b/packages/voice/__tests__/AudioReceiveStream.test.ts @@ -1,5 +1,6 @@ /* eslint-disable no-promise-executor-return */ import { Buffer } from 'node:buffer'; +import { describe, test, expect } from 'vitest'; import { SILENCE_FRAME } from '../src/audio/AudioPlayer'; import { AudioReceiveStream, EndBehaviorType } from '../src/receive/AudioReceiveStream'; diff --git a/packages/voice/__tests__/AudioResource.test.ts b/packages/voice/__tests__/AudioResource.test.ts index 383195eac3b6..21043c5e1708 100644 --- a/packages/voice/__tests__/AudioResource.test.ts +++ b/packages/voice/__tests__/AudioResource.test.ts @@ -2,12 +2,12 @@ import { Buffer } from 'node:buffer'; import process from 'node:process'; import { PassThrough, Readable } from 'node:stream'; import { opus, VolumeTransformer } from 'prism-media'; +import { describe, test, expect, vitest, type MockedFunction, beforeAll, beforeEach } from 'vitest'; import { SILENCE_FRAME } from '../src/audio/AudioPlayer'; import { AudioResource, createAudioResource, NO_CONSTRAINT, VOLUME_CONSTRAINT } from '../src/audio/AudioResource'; import { findPipeline as _findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph'; -jest.mock('prism-media'); -jest.mock('../src/audio/TransformerGraph'); +vitest.mock('../src/audio/TransformerGraph'); async function wait() { // eslint-disable-next-line no-promise-executor-return @@ -22,7 +22,7 @@ async function started(resource: AudioResource) { return resource; } -const findPipeline = _findPipeline as unknown as jest.MockedFunction; +const findPipeline = _findPipeline as unknown as MockedFunction; beforeAll(() => { // @ts-expect-error: No type @@ -37,7 +37,8 @@ beforeAll(() => { if (constraint === VOLUME_CONSTRAINT) { base.push({ cost: 1, - transformer: () => new VolumeTransformer({} as any), + // Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer + transformer: () => new VolumeTransformer({ type: 's16le' } as any), type: TransformerType.InlineVolume, }); } @@ -96,7 +97,8 @@ describe('createAudioResource', () => { }); test('Infers from VolumeTransformer', () => { - const stream = new VolumeTransformer({} as any); + // Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer + const stream = new VolumeTransformer({ type: 's16le' } as any); const resource = createAudioResource(stream, { inlineVolume: true }); expect(findPipeline).toHaveBeenCalledWith(StreamType.Raw, NO_CONSTRAINT); expect(resource.volume).toEqual(stream); diff --git a/packages/voice/__tests__/DataStore.test.ts b/packages/voice/__tests__/DataStore.test.ts index 5ed445ab89cb..38aeec7fd0c1 100644 --- a/packages/voice/__tests__/DataStore.test.ts +++ b/packages/voice/__tests__/DataStore.test.ts @@ -1,13 +1,14 @@ /* eslint-disable @typescript-eslint/dot-notation */ import { GatewayOpcodes } from 'discord-api-types/v10'; +import { describe, test, expect, vitest, type Mocked, beforeEach } from 'vitest'; import * as DataStore from '../src/DataStore'; import type { VoiceConnection } from '../src/VoiceConnection'; import * as _AudioPlayer from '../src/audio/AudioPlayer'; -jest.mock('../src/VoiceConnection'); -jest.mock('../src/audio/AudioPlayer'); +vitest.mock('../src/VoiceConnection'); +vitest.mock('../src/audio/AudioPlayer'); -const AudioPlayer = _AudioPlayer as unknown as jest.Mocked; +const AudioPlayer = _AudioPlayer as unknown as Mocked; function createVoiceConnection(joinConfig: Pick): VoiceConnection { return { @@ -71,8 +72,8 @@ describe('DataStore', () => { }); test('Managing Audio Players', async () => { const player = DataStore.addAudioPlayer(new AudioPlayer.AudioPlayer()); - const dispatchSpy = jest.spyOn(player as any, '_stepDispatch'); - const prepareSpy = jest.spyOn(player as any, '_stepPrepare'); + const dispatchSpy = vitest.spyOn(player as any, '_stepDispatch'); + const prepareSpy = vitest.spyOn(player as any, '_stepPrepare'); expect(DataStore.hasAudioPlayer(player)).toEqual(true); expect(DataStore.addAudioPlayer(player)).toEqual(player); DataStore.deleteAudioPlayer(player); @@ -87,12 +88,12 @@ describe('DataStore', () => { test('Preparing Audio Frames', async () => { // Test functional player const player2 = DataStore.addAudioPlayer(new AudioPlayer.AudioPlayer()); - player2['checkPlayable'] = jest.fn(() => true); + player2['checkPlayable'] = vitest.fn(() => true); const player3 = DataStore.addAudioPlayer(new AudioPlayer.AudioPlayer()); - const dispatchSpy2 = jest.spyOn(player2 as any, '_stepDispatch'); - const prepareSpy2 = jest.spyOn(player2 as any, '_stepPrepare'); - const dispatchSpy3 = jest.spyOn(player3 as any, '_stepDispatch'); - const prepareSpy3 = jest.spyOn(player3 as any, '_stepPrepare'); + const dispatchSpy2 = vitest.spyOn(player2 as any, '_stepDispatch'); + const prepareSpy2 = vitest.spyOn(player2 as any, '_stepPrepare'); + const dispatchSpy3 = vitest.spyOn(player3 as any, '_stepDispatch'); + const prepareSpy3 = vitest.spyOn(player3 as any, '_stepPrepare'); await waitForEventLoop(); DataStore.deleteAudioPlayer(player2); await waitForEventLoop(); diff --git a/packages/voice/__tests__/SSRCMap.test.ts b/packages/voice/__tests__/SSRCMap.test.ts index 252445f4643a..4927ee091f12 100644 --- a/packages/voice/__tests__/SSRCMap.test.ts +++ b/packages/voice/__tests__/SSRCMap.test.ts @@ -1,5 +1,6 @@ import { type EventEmitter, once } from 'node:events'; import process from 'node:process'; +import { describe, test, expect } from 'vitest'; import { SSRCMap, type VoiceUserData } from '../src/receive/SSRCMap'; async function onceOrThrow(target: Emitter, event: string, after: number) { diff --git a/packages/voice/__tests__/Secretbox.test.ts b/packages/voice/__tests__/Secretbox.test.ts index a01dd2f3bf69..0f523d90d096 100644 --- a/packages/voice/__tests__/Secretbox.test.ts +++ b/packages/voice/__tests__/Secretbox.test.ts @@ -1,8 +1,9 @@ +import { test, expect, vitest } from 'vitest'; import { methods } from '../src/util/Secretbox'; -jest.mock('tweetnacl'); +vitest.mock('tweetnacl'); test('Does not throw error with a package installed', () => { - // @ts-expect-error: Unknown type - expect(() => methods.open()).not.toThrowError(); + // @ts-expect-error We are testing + expect(() => methods.open()).toThrow(TypeError); }); diff --git a/packages/voice/__tests__/SpeakingMap.test.ts b/packages/voice/__tests__/SpeakingMap.test.ts index 1a7bb9cc90fc..e0a668b81182 100644 --- a/packages/voice/__tests__/SpeakingMap.test.ts +++ b/packages/voice/__tests__/SpeakingMap.test.ts @@ -1,7 +1,8 @@ +import { describe, test, expect, vitest } from 'vitest'; import { SpeakingMap } from '../src/receive/SpeakingMap'; import { noop } from '../src/util/util'; -jest.useFakeTimers(); +vitest.useFakeTimers(); describe('SpeakingMap', () => { test('Emits start and end', () => { @@ -17,17 +18,17 @@ describe('SpeakingMap', () => { for (let index = 0; index < 10; index++) { speaking.onPacket(userId); setTimeout(noop, SpeakingMap.DELAY / 2); - jest.advanceTimersToNextTimer(); + vitest.advanceTimersToNextTimer(); expect(starts).toEqual([userId]); expect(ends).toEqual([]); } - jest.advanceTimersToNextTimer(); + vitest.advanceTimersToNextTimer(); expect(ends).toEqual([userId]); speaking.onPacket(userId); - jest.advanceTimersToNextTimer(); + vitest.advanceTimersToNextTimer(); expect(starts).toEqual([userId, userId]); }); }); diff --git a/packages/voice/__tests__/TransformerGraph.test.ts b/packages/voice/__tests__/TransformerGraph.test.ts index ac3567328f85..48fcb4c852d7 100644 --- a/packages/voice/__tests__/TransformerGraph.test.ts +++ b/packages/voice/__tests__/TransformerGraph.test.ts @@ -1,4 +1,5 @@ // @ts-nocheck +import { describe, test, expect } from 'vitest'; import { findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph'; const noConstraint = () => true; diff --git a/packages/voice/__tests__/VoiceConnection.test.ts b/packages/voice/__tests__/VoiceConnection.test.ts index 43ca2d59bfd4..7e20facb6050 100644 --- a/packages/voice/__tests__/VoiceConnection.test.ts +++ b/packages/voice/__tests__/VoiceConnection.test.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/dot-notation */ // @ts-nocheck import { EventEmitter } from 'node:events'; +import { vitest, describe, test, expect, beforeEach } from 'vitest'; import * as _DataStore from '../src/DataStore'; import { createVoiceConnection, @@ -14,34 +15,42 @@ import { } from '../src/VoiceConnection'; import * as _AudioPlayer from '../src/audio/AudioPlayer'; import { PlayerSubscription as _PlayerSubscription } from '../src/audio/PlayerSubscription'; -import * as _Networking from '../src/networking/Networking'; +import * as Networking from '../src/networking/Networking'; import type { DiscordGatewayAdapterLibraryMethods } from '../src/util/adapter'; -jest.mock('../src/audio/AudioPlayer'); -jest.mock('../src/audio/PlayerSubscription'); -jest.mock('../src/DataStore'); -jest.mock('../src/networking/Networking'); +vitest.mock('../src/audio/AudioPlayer'); +vitest.mock('../src/audio/PlayerSubscription'); +vitest.mock('../src/DataStore'); +vitest.mock('../src/networking/Networking', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal(); + const Networking = actual.Networking; + Networking.prototype.createWebSocket = vitest.fn(); + return { + ...actual, + Networking, + }; +}); -const DataStore = _DataStore as unknown as jest.Mocked; -const Networking = _Networking as unknown as jest.Mocked; -const AudioPlayer = _AudioPlayer as unknown as jest.Mocked; -const PlayerSubscription = _PlayerSubscription as unknown as jest.Mock<_PlayerSubscription>; +const DataStore = _DataStore as unknown as vitest.Mocked; +const AudioPlayer = _AudioPlayer as unknown as vitest.Mocked; +const PlayerSubscription = _PlayerSubscription as unknown as vitest.Mock<_PlayerSubscription>; -Networking.Networking.mockImplementation(function mockedConstructor() { - this.state = {}; - return this; +const _NetworkingClass = Networking.Networking; +vitest.spyOn(Networking, 'Networking').mockImplementation((...args) => { + return new _NetworkingClass(...args); }); function createFakeAdapter() { - const sendPayload = jest.fn(); + const sendPayload = vitest.fn(); sendPayload.mockReturnValue(true); - const destroy = jest.fn(); + const destroy = vitest.fn(); const libMethods: Partial = {}; return { sendPayload, destroy, libMethods, - creator: jest.fn((methods) => { + creator: vitest.fn((methods) => { Object.assign(libMethods, methods); return { sendPayload, @@ -124,7 +133,7 @@ describe('createVoiceConnection', () => { adapterCreator: existingAdapter.creator, }); - const stateSetter = jest.spyOn(existingVoiceConnection, 'state', 'set'); + const stateSetter = vitest.spyOn(existingVoiceConnection, 'state', 'set'); // @ts-expect-error: We're testing DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') => @@ -163,7 +172,7 @@ describe('createVoiceConnection', () => { reason: VoiceConnectionDisconnectReason.EndpointRemoved, }; - const rejoinSpy = jest.spyOn(existingVoiceConnection, 'rejoin'); + const rejoinSpy = vitest.spyOn(existingVoiceConnection, 'rejoin'); // @ts-expect-error: We're testing DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') => @@ -222,7 +231,7 @@ describe('createVoiceConnection', () => { describe('VoiceConnection#addServerPacket', () => { test('Stores the packet and attempts to configure networking', () => { const { voiceConnection } = createFakeVoiceConnection(); - voiceConnection.configureNetworking = jest.fn(); + voiceConnection.configureNetworking = vitest.fn(); const dummy = { endpoint: 'discord.com', guild_id: 123, @@ -236,7 +245,7 @@ describe('VoiceConnection#addServerPacket', () => { test('Overwrites existing packet', () => { const { voiceConnection } = createFakeVoiceConnection(); voiceConnection['packets'].server = Symbol('old') as any; - voiceConnection.configureNetworking = jest.fn(); + voiceConnection.configureNetworking = vitest.fn(); const dummy = { endpoint: 'discord.com', guild_id: 123, @@ -250,7 +259,7 @@ describe('VoiceConnection#addServerPacket', () => { test('Disconnects when given a null endpoint', () => { const { voiceConnection } = createFakeVoiceConnection(); voiceConnection['packets'].server = Symbol('old') as any; - voiceConnection.configureNetworking = jest.fn(); + voiceConnection.configureNetworking = vitest.fn(); const dummy = { endpoint: null, guild_id: 123, @@ -344,7 +353,7 @@ describe('VoiceConnection#configureNetworking', () => { adapter, }); expect((voiceConnection.state as unknown as VoiceConnectionConnectingState).networking).toBeInstanceOf( - Networking.Networking, + _NetworkingClass, ); }); }); @@ -399,24 +408,24 @@ describe('VoiceConnection#onNetworkingClose', () => { describe('VoiceConnection#onNetworkingStateChange', () => { test('Does nothing when status code identical', () => { const { voiceConnection } = createFakeVoiceConnection(); - const stateSetter = jest.spyOn(voiceConnection, 'state', 'set'); + const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set'); voiceConnection['onNetworkingStateChange']( - { code: _Networking.NetworkingStatusCode.Ready } as any, - { code: _Networking.NetworkingStatusCode.Ready } as any, + { code: Networking.NetworkingStatusCode.Ready } as any, + { code: Networking.NetworkingStatusCode.Ready } as any, ); voiceConnection['onNetworkingStateChange']( - { code: _Networking.NetworkingStatusCode.Closed } as any, - { code: _Networking.NetworkingStatusCode.Closed } as any, + { code: Networking.NetworkingStatusCode.Closed } as any, + { code: Networking.NetworkingStatusCode.Closed } as any, ); expect(stateSetter).not.toHaveBeenCalled(); }); test('Does nothing when not in Ready or Connecting states', () => { const { voiceConnection } = createFakeVoiceConnection(); - const stateSetter = jest.spyOn(voiceConnection, 'state', 'set'); + const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set'); const call = [ - { code: _Networking.NetworkingStatusCode.Ready } as any, - { code: _Networking.NetworkingStatusCode.Closed } as any, + { code: Networking.NetworkingStatusCode.Ready } as any, + { code: Networking.NetworkingStatusCode.Closed } as any, ]; voiceConnection['_state'] = { status: VoiceConnectionStatus.Signalling } as any; voiceConnection['onNetworkingStateChange'](call[0], call[1]); @@ -429,7 +438,7 @@ describe('VoiceConnection#onNetworkingStateChange', () => { test('Transitions to Ready', () => { const { voiceConnection } = createFakeVoiceConnection(); - const stateSetter = jest.spyOn(voiceConnection, 'state', 'set'); + const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set'); voiceConnection['_state'] = { ...(voiceConnection.state as VoiceConnectionSignallingState), status: VoiceConnectionStatus.Connecting, @@ -437,8 +446,8 @@ describe('VoiceConnection#onNetworkingStateChange', () => { }; voiceConnection['onNetworkingStateChange']( - { code: _Networking.NetworkingStatusCode.Closed } as any, - { code: _Networking.NetworkingStatusCode.Ready } as any, + { code: Networking.NetworkingStatusCode.Closed } as any, + { code: Networking.NetworkingStatusCode.Ready } as any, ); expect(stateSetter).toHaveBeenCalledTimes(1); @@ -447,7 +456,7 @@ describe('VoiceConnection#onNetworkingStateChange', () => { test('Transitions to Connecting', () => { const { voiceConnection } = createFakeVoiceConnection(); - const stateSetter = jest.spyOn(voiceConnection, 'state', 'set'); + const stateSetter = vitest.spyOn(voiceConnection, 'state', 'set'); voiceConnection['_state'] = { ...(voiceConnection.state as VoiceConnectionSignallingState), status: VoiceConnectionStatus.Connecting, @@ -455,8 +464,8 @@ describe('VoiceConnection#onNetworkingStateChange', () => { }; voiceConnection['onNetworkingStateChange']( - { code: _Networking.NetworkingStatusCode.Ready } as any, - { code: _Networking.NetworkingStatusCode.Identifying } as any, + { code: Networking.NetworkingStatusCode.Ready } as any, + { code: Networking.NetworkingStatusCode.Identifying } as any, ); expect(stateSetter).toHaveBeenCalledTimes(1); @@ -598,7 +607,7 @@ describe('VoiceConnection#subscribe', () => { test('Does nothing in Destroyed state', () => { const { voiceConnection } = createFakeVoiceConnection(); const player = new AudioPlayer.AudioPlayer(); - player['subscribe'] = jest.fn(); + player['subscribe'] = vitest.fn(); voiceConnection.state = { status: VoiceConnectionStatus.Destroyed }; expect(voiceConnection.subscribe(player)).toBeUndefined(); expect(player['subscribe']).not.toHaveBeenCalled(); @@ -610,7 +619,7 @@ describe('VoiceConnection#subscribe', () => { const adapter = (voiceConnection.state as VoiceConnectionSignallingState).adapter; const player = new AudioPlayer.AudioPlayer(); const dummy = Symbol('dummy'); - player['subscribe'] = jest.fn().mockImplementation(() => dummy); + player['subscribe'] = vitest.fn().mockImplementation(() => dummy); expect(voiceConnection.subscribe(player)).toEqual(dummy); expect(player['subscribe']).toHaveBeenCalledWith(voiceConnection); expect(voiceConnection.state).toMatchObject({ @@ -624,7 +633,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => { test('Does nothing in Destroyed state', () => { const { voiceConnection } = createFakeVoiceConnection(); const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer()); - subscription.unsubscribe = jest.fn(); + subscription.unsubscribe = vitest.fn(); voiceConnection.state = { status: VoiceConnectionStatus.Destroyed }; voiceConnection['onSubscriptionRemoved'](subscription); @@ -635,7 +644,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => { test('Does nothing when subscription is not the same as the stored one', () => { const { voiceConnection } = createFakeVoiceConnection(); const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer()); - subscription.unsubscribe = jest.fn(); + subscription.unsubscribe = vitest.fn(); voiceConnection.state = { ...(voiceConnection.state as VoiceConnectionSignallingState), subscription }; voiceConnection['onSubscriptionRemoved'](Symbol('new subscription') as any); @@ -649,7 +658,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => { test('Unsubscribes in a live state with matching subscription', () => { const { voiceConnection } = createFakeVoiceConnection(); const subscription = new PlayerSubscription(voiceConnection, new AudioPlayer.AudioPlayer()); - subscription.unsubscribe = jest.fn(); + subscription.unsubscribe = vitest.fn(); voiceConnection.state = { ...(voiceConnection.state as VoiceConnectionSignallingState), subscription }; voiceConnection['onSubscriptionRemoved'](subscription); @@ -667,7 +676,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => { const oldNetworking = new Networking.Networking({} as any, false); oldNetworking.state = { - code: _Networking.NetworkingStatusCode.Ready, + code: Networking.NetworkingStatusCode.Ready, connectionData: {} as any, connectionOptions: {} as any, udp: new EventEmitter() as any, @@ -697,7 +706,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => { const oldNetworking = new Networking.Networking({} as any, false); oldNetworking.state = { - code: _Networking.NetworkingStatusCode.Ready, + code: Networking.NetworkingStatusCode.Ready, connectionData: {} as any, connectionOptions: {} as any, udp, @@ -726,7 +735,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => { const newNetworking = new Networking.Networking({} as any, false); newNetworking.state = { - code: _Networking.NetworkingStatusCode.Ready, + code: Networking.NetworkingStatusCode.Ready, connectionData: {} as any, connectionOptions: {} as any, udp: new EventEmitter() as any, @@ -749,7 +758,7 @@ describe('VoiceConnection#onSubscriptionRemoved', () => { describe('Adapter', () => { test('onVoiceServerUpdate', () => { const { adapter, voiceConnection } = createFakeVoiceConnection(); - voiceConnection['addServerPacket'] = jest.fn(); + voiceConnection['addServerPacket'] = vitest.fn(); const dummy = Symbol('dummy') as any; adapter.libMethods.onVoiceServerUpdate!(dummy); expect(voiceConnection['addServerPacket']).toHaveBeenCalledWith(dummy); @@ -757,7 +766,7 @@ describe('Adapter', () => { test('onVoiceStateUpdate', () => { const { adapter, voiceConnection } = createFakeVoiceConnection(); - voiceConnection['addStatePacket'] = jest.fn(); + voiceConnection['addStatePacket'] = vitest.fn(); const dummy = Symbol('dummy') as any; adapter.libMethods.onVoiceStateUpdate!(dummy); expect(voiceConnection['addStatePacket']).toHaveBeenCalledWith(dummy); diff --git a/packages/voice/__tests__/VoiceReceiver.test.ts b/packages/voice/__tests__/VoiceReceiver.test.ts index f19c868c586c..6cb2ec717d11 100644 --- a/packages/voice/__tests__/VoiceReceiver.test.ts +++ b/packages/voice/__tests__/VoiceReceiver.test.ts @@ -5,19 +5,26 @@ import { Buffer } from 'node:buffer'; import { once } from 'node:events'; import process from 'node:process'; import { VoiceOpcodes } from 'discord-api-types/voice/v4'; +import { describe, test, expect, vitest, beforeEach } from 'vitest'; import { RTP_PACKET_DESKTOP, RTP_PACKET_CHROME, RTP_PACKET_ANDROID } from '../__mocks__/rtp'; -import { VoiceConnection as _VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection'; +import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection'; import { VoiceReceiver } from '../src/receive/VoiceReceiver'; import { methods } from '../src/util/Secretbox'; -jest.mock('../src/VoiceConnection'); -jest.mock('../src/receive/SSRCMap'); +vitest.mock('../src/VoiceConnection', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal(); + return { + ...actual, + VoiceConnection: vitest.fn(), + }; +}); -const openSpy = jest.spyOn(methods, 'open'); +vitest.mock('../src/receive/SSRCMap'); -openSpy.mockImplementation((buffer) => buffer); +const openSpy = vitest.spyOn(methods, 'open'); -const VoiceConnection = _VoiceConnection as unknown as jest.Mocked; +openSpy.mockImplementation((buffer) => buffer); async function nextTick() { // eslint-disable-next-line no-promise-executor-return @@ -56,9 +63,9 @@ describe('VoiceReceiver', () => { ['RTP Packet Chrome', RTP_PACKET_CHROME], ['RTP Packet Android', RTP_PACKET_ANDROID], ])('onUdpMessage: %s', async (testName, RTP_PACKET) => { - receiver['decrypt'] = jest.fn().mockImplementationOnce(() => RTP_PACKET.decrypted); + receiver['decrypt'] = vitest.fn().mockImplementationOnce(() => RTP_PACKET.decrypted); - const spy = jest.spyOn(receiver.ssrcMap, 'get'); + const spy = vitest.spyOn(receiver.ssrcMap, 'get'); spy.mockImplementation(() => ({ audioSSRC: RTP_PACKET.ssrc, userId: '123', @@ -76,9 +83,9 @@ describe('VoiceReceiver', () => { }); test('onUdpMessage: destroys stream on decrypt failure', async () => { - receiver['decrypt'] = jest.fn().mockImplementationOnce(() => null); + receiver['decrypt'] = vitest.fn().mockImplementationOnce(() => null); - const spy = jest.spyOn(receiver.ssrcMap, 'get'); + const spy = vitest.spyOn(receiver.ssrcMap, 'get'); spy.mockImplementation(() => ({ audioSSRC: RTP_PACKET_DESKTOP.ssrc, userId: '123', @@ -95,7 +102,7 @@ describe('VoiceReceiver', () => { }); test('subscribe: only allows one subscribe stream per SSRC', () => { - const spy = jest.spyOn(receiver.ssrcMap, 'get'); + const spy = vitest.spyOn(receiver.ssrcMap, 'get'); spy.mockImplementation(() => ({ audioSSRC: RTP_PACKET_DESKTOP.ssrc, userId: '123', @@ -107,7 +114,7 @@ describe('VoiceReceiver', () => { describe('onWsPacket', () => { test('CLIENT_DISCONNECT packet', () => { - const spy = jest.spyOn(receiver.ssrcMap, 'delete'); + const spy = vitest.spyOn(receiver.ssrcMap, 'delete'); receiver['onWsPacket']({ op: VoiceOpcodes.ClientDisconnect, d: { @@ -118,7 +125,7 @@ describe('VoiceReceiver', () => { }); test('SPEAKING packet', () => { - const spy = jest.spyOn(receiver.ssrcMap, 'update'); + const spy = vitest.spyOn(receiver.ssrcMap, 'update'); receiver['onWsPacket']({ op: VoiceOpcodes.Speaking, d: { @@ -134,7 +141,7 @@ describe('VoiceReceiver', () => { }); test('CLIENT_CONNECT packet', () => { - const spy = jest.spyOn(receiver.ssrcMap, 'update'); + const spy = vitest.spyOn(receiver.ssrcMap, 'update'); receiver['onWsPacket']({ op: VoiceOpcodes.ClientConnect, d: { diff --git a/packages/voice/__tests__/VoiceUDPSocket.test.ts b/packages/voice/__tests__/VoiceUDPSocket.test.ts index 0e65ad7f61c7..62e7b57def48 100644 --- a/packages/voice/__tests__/VoiceUDPSocket.test.ts +++ b/packages/voice/__tests__/VoiceUDPSocket.test.ts @@ -2,12 +2,13 @@ import { Buffer } from 'node:buffer'; import { createSocket as _createSocket } from 'node:dgram'; import { EventEmitter } from 'node:events'; +import { describe, test, expect, vitest, beforeEach, afterEach } from 'vitest'; import { VoiceUDPSocket } from '../src/networking/VoiceUDPSocket'; -jest.mock('node:dgram'); -jest.useFakeTimers(); +vitest.mock('node:dgram'); +vitest.useFakeTimers(); -const createSocket = _createSocket as unknown as jest.Mock; +const createSocket = _createSocket as unknown as vitest.Mock; beforeEach(() => { createSocket.mockReset(); @@ -32,7 +33,7 @@ const VALID_RESPONSE = Buffer.from([ async function wait() { return new Promise((resolve) => { setImmediate(resolve); - jest.advanceTimersToNextTimer(); + vitest.advanceTimersToNextTimer(); }); } @@ -48,7 +49,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { */ test('Resolves and cleans up with a successful flow', async () => { const fake = new FakeSocket(); - fake.send = jest.fn().mockImplementation((buffer: Buffer, port: number, address: string) => { + fake.send = vitest.fn().mockImplementation((buffer: Buffer, port: number, address: string) => { fake.emit('message', VALID_RESPONSE); }); createSocket.mockImplementation((type) => fake as any); @@ -71,7 +72,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { test('Waits for a valid response in an unexpected flow', async () => { const fake = new FakeSocket(); const fakeResponse = Buffer.from([1, 2, 3, 4, 5]); - fake.send = jest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => { + fake.send = vitest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => { fake.emit('message', fakeResponse); await wait(); fake.emit('message', VALID_RESPONSE); @@ -91,7 +92,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { test('Rejects if socket closes before IP discovery can be completed', async () => { const fake = new FakeSocket(); - fake.send = jest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => { + fake.send = vitest.fn().mockImplementation(async (buffer: Buffer, port: number, address: string) => { await wait(); fake.close(); }); @@ -104,7 +105,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { test('Stays alive when messages are echoed back', async () => { const fake = new FakeSocket(); - fake.send = jest.fn().mockImplementation(async (buffer: Buffer) => { + fake.send = vitest.fn().mockImplementation(async (buffer: Buffer) => { await wait(); fake.emit('message', buffer); }); @@ -115,7 +116,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { socket.on('close', () => (closed = true)); for (let index = 0; index < 30; index++) { - jest.advanceTimersToNextTimer(); + vitest.advanceTimersToNextTimer(); await wait(); } @@ -124,7 +125,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { test('Recovers from intermittent responses', async () => { const fake = new FakeSocket(); - const fakeSend = jest.fn(); + const fakeSend = vitest.fn(); fake.send = fakeSend; createSocket.mockImplementation(() => fake as any); socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 }); @@ -134,7 +135,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { socket.on('close', () => (closed = true)); for (let index = 0; index < 10; index++) { - jest.advanceTimersToNextTimer(); + vitest.advanceTimersToNextTimer(); await wait(); } @@ -144,7 +145,7 @@ describe('VoiceUDPSocket#performIPDiscovery', () => { }); expect(closed).toEqual(false); for (let index = 0; index < 30; index++) { - jest.advanceTimersToNextTimer(); + vitest.advanceTimersToNextTimer(); await wait(); } diff --git a/packages/voice/__tests__/VoiceWebSocket.test.ts b/packages/voice/__tests__/VoiceWebSocket.test.ts index 402139c480e1..87fc72ecb07e 100644 --- a/packages/voice/__tests__/VoiceWebSocket.test.ts +++ b/packages/voice/__tests__/VoiceWebSocket.test.ts @@ -1,6 +1,7 @@ import { type EventEmitter, once } from 'node:events'; import { VoiceOpcodes } from 'discord-api-types/voice/v4'; -import WS from 'jest-websocket-mock'; +import { describe, test, expect, beforeEach } from 'vitest'; +import WS from 'vitest-websocket-mock'; import { VoiceWebSocket } from '../src/networking/VoiceWebSocket'; beforeEach(() => { diff --git a/packages/voice/__tests__/abortAfter.test.ts b/packages/voice/__tests__/abortAfter.test.ts index be98b4e5eee7..9c7b6eaeaa03 100644 --- a/packages/voice/__tests__/abortAfter.test.ts +++ b/packages/voice/__tests__/abortAfter.test.ts @@ -1,15 +1,16 @@ +import { describe, test, expect, vitest } from 'vitest'; import { abortAfter } from '../src/util/abortAfter'; -jest.useFakeTimers(); +vitest.useFakeTimers(); -const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); +const clearTimeoutSpy = vitest.spyOn(global, 'clearTimeout'); describe('abortAfter', () => { test('Aborts after the given delay', () => { const [ac, signal] = abortAfter(100); expect(ac.signal).toEqual(signal); expect(signal.aborted).toEqual(false); - jest.runAllTimers(); + vitest.runAllTimers(); expect(signal.aborted).toEqual(true); }); diff --git a/packages/voice/__tests__/demuxProbe.test.ts b/packages/voice/__tests__/demuxProbe.test.ts index d9c65a00484d..cec2107c5c27 100644 --- a/packages/voice/__tests__/demuxProbe.test.ts +++ b/packages/voice/__tests__/demuxProbe.test.ts @@ -4,13 +4,14 @@ import EventEmitter, { once } from 'node:events'; import process from 'node:process'; import { Readable } from 'node:stream'; import { opus as _opus } from 'prism-media'; +import { describe, test, expect, vitest, type Mock, beforeAll, beforeEach } from 'vitest'; import { StreamType } from '../src/audio/index'; import { demuxProbe } from '../src/util/demuxProbe'; -jest.mock('prism-media'); +vitest.mock('prism-media'); -const WebmDemuxer = _opus.WebmDemuxer as unknown as jest.Mock<_opus.WebmDemuxer>; -const OggDemuxer = _opus.OggDemuxer as unknown as jest.Mock<_opus.OggDemuxer>; +const WebmDemuxer = _opus.WebmDemuxer as unknown as Mock<_opus.WebmDemuxer>; +const OggDemuxer = _opus.OggDemuxer as unknown as Mock<_opus.OggDemuxer>; async function nextTick() { // eslint-disable-next-line no-promise-executor-return @@ -47,8 +48,8 @@ async function collectStream(stream: Readable): Promise { } describe('demuxProbe', () => { - const webmWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn(); - const oggWrite: jest.Mock<(buffer: Buffer) => void> = jest.fn(); + const webmWrite: Mock<(buffer: Buffer) => void> = vitest.fn(); + const oggWrite: Mock<(buffer: Buffer) => void> = vitest.fn(); beforeAll(() => { WebmDemuxer.prototype = { diff --git a/packages/voice/__tests__/entersState.test.ts b/packages/voice/__tests__/entersState.test.ts index 3b8cb626fd13..b95c68e4f563 100644 --- a/packages/voice/__tests__/entersState.test.ts +++ b/packages/voice/__tests__/entersState.test.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'node:events'; import process from 'node:process'; +import { describe, test, expect, vitest, beforeEach } from 'vitest'; import { VoiceConnectionStatus, type VoiceConnection } from '../src/VoiceConnection'; import { entersState } from '../src/util/entersState'; @@ -10,12 +11,12 @@ function createFakeVoiceConnection(status = VoiceConnectionStatus.Signalling) { } beforeEach(() => { - jest.useFakeTimers(); + vitest.useFakeTimers(); }); describe('entersState', () => { test('Returns the target once the state has been entered before timeout', async () => { - jest.useRealTimers(); + vitest.useRealTimers(); const vc = createFakeVoiceConnection(); process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any)); const result = await entersState(vc, VoiceConnectionStatus.Ready, 1_000); @@ -25,12 +26,12 @@ describe('entersState', () => { test('Rejects once the timeout is exceeded', async () => { const vc = createFakeVoiceConnection(); const promise = entersState(vc, VoiceConnectionStatus.Ready, 1_000); - jest.runAllTimers(); + vitest.runAllTimers(); await expect(promise).rejects.toThrowError(); }); test('Returns the target once the state has been entered before signal is aborted', async () => { - jest.useRealTimers(); + vitest.useRealTimers(); const vc = createFakeVoiceConnection(); const ac = new AbortController(); process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any)); diff --git a/packages/voice/__tests__/joinVoiceChannel.test.ts b/packages/voice/__tests__/joinVoiceChannel.test.ts index 5f1e9490268a..1a6bff329fb8 100644 --- a/packages/voice/__tests__/joinVoiceChannel.test.ts +++ b/packages/voice/__tests__/joinVoiceChannel.test.ts @@ -1,9 +1,10 @@ // @ts-nocheck +import { describe, test, expect, vitest, beforeAll, beforeEach } from 'vitest'; import * as VoiceConnection from '../src/VoiceConnection'; import { joinVoiceChannel } from '../src/joinVoiceChannel'; -const adapterCreator = () => ({ destroy: jest.fn(), send: jest.fn() }) as any; -const createVoiceConnection = jest.spyOn(VoiceConnection, 'createVoiceConnection'); +const adapterCreator = () => ({ destroy: vitest.fn(), send: vitest.fn() }) as any; +const createVoiceConnection = vitest.spyOn(VoiceConnection, 'createVoiceConnection'); beforeAll(() => { createVoiceConnection.mockImplementation(() => null as any); diff --git a/packages/voice/babel.config.js b/packages/voice/babel.config.js deleted file mode 100644 index bbaa0e8cb0e7..000000000000 --- a/packages/voice/babel.config.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @type {import('@babel/core').TransformOptions} - */ -module.exports = { - parserOpts: { strictMode: true }, - sourceMaps: 'inline', - presets: [ - [ - '@babel/preset-env', - { - targets: { node: 'current' }, - modules: 'commonjs', - }, - ], - '@babel/preset-typescript', - ], -}; diff --git a/packages/voice/jest.config.js b/packages/voice/jest.config.js deleted file mode 100644 index dd29a6258ffd..000000000000 --- a/packages/voice/jest.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @type {import('@jest/types').Config.InitialOptions} - */ -module.exports = { - testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], - testEnvironment: 'node', - collectCoverage: true, - collectCoverageFrom: ['src/**/*.ts'], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'cobertura'], -}; diff --git a/packages/voice/package.json b/packages/voice/package.json index 101961ae7f6f..f33cee6492be 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "tsc --noEmit && tsup && node scripts/postbuild.mjs", "build:docs": "tsc -p tsconfig.docs.json", - "test": "jest --coverage", + "test": "vitest run", "lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src __tests__", "format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src __tests__", "fmt": "pnpm run format", @@ -70,27 +70,24 @@ "ws": "^8.18.0" }, "devDependencies": { - "@babel/core": "^7.24.6", - "@babel/preset-env": "^7.24.6", - "@babel/preset-typescript": "^7.24.6", "@discordjs/api-extractor": "workspace:^", + "@discordjs/opus": "^0.9.0", "@discordjs/scripts": "workspace:^", "@favware/cliff-jumper": "^4.1.0", - "@types/jest": "^29.5.12", - "@types/node": "^16.18.105", + "@types/node": "18.19.45", + "@vitest/coverage-v8": "2.0.5", "cross-env": "^7.0.3", "esbuild-plugin-version-injector": "^1.2.1", "eslint": "^8.57.0", "eslint-config-neon": "^0.1.62", "eslint-formatter-pretty": "^6.0.1", - "jest": "^29.7.0", - "jest-websocket-mock": "^2.5.0", - "mock-socket": "^9.3.1", "prettier": "^3.3.3", "tsup": "^8.2.4", "turbo": "^2.0.14", "tweetnacl": "^1.0.3", - "typescript": "~5.5.4" + "typescript": "~5.5.4", + "vitest": "^2.0.5", + "vitest-websocket-mock": "^0.3.0" }, "engines": { "node": ">=18" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3665411b2dd7..42abf3613b92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1602,7 +1602,7 @@ importers: version: 0.37.101 prism-media: specifier: ^1.3.5 - version: 1.3.5 + version: 1.3.5(@discordjs/opus@0.9.0(encoding@0.1.13)) tslib: specifier: ^2.6.3 version: 2.6.3 @@ -1610,30 +1610,24 @@ importers: specifier: ^8.18.0 version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) devDependencies: - '@babel/core': - specifier: ^7.24.6 - version: 7.25.2 - '@babel/preset-env': - specifier: ^7.24.6 - version: 7.25.4(@babel/core@7.25.2) - '@babel/preset-typescript': - specifier: ^7.24.6 - version: 7.24.7(@babel/core@7.25.2) '@discordjs/api-extractor': specifier: workspace:^ version: link:../api-extractor + '@discordjs/opus': + specifier: ^0.9.0 + version: 0.9.0(encoding@0.1.13) '@discordjs/scripts': specifier: workspace:^ version: link:../scripts '@favware/cliff-jumper': specifier: ^4.1.0 version: 4.1.0 - '@types/jest': - specifier: ^29.5.12 - version: 29.5.12 '@types/node': - specifier: ^16.18.105 - version: 16.18.105 + specifier: 18.19.45 + version: 18.19.45 + '@vitest/coverage-v8': + specifier: 2.0.5 + version: 2.0.5(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -1649,21 +1643,12 @@ importers: eslint-formatter-pretty: specifier: ^6.0.1 version: 6.0.1 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@16.18.105)(ts-node@10.9.2(@types/node@16.18.105)(typescript@5.5.4)) - jest-websocket-mock: - specifier: ^2.5.0 - version: 2.5.0 - mock-socket: - specifier: ^9.3.1 - version: 9.3.1 prettier: specifier: ^3.3.3 version: 3.3.3 tsup: specifier: ^8.2.4 - version: 8.2.4(@microsoft/api-extractor@7.43.0(@types/node@16.18.105))(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0) + version: 8.2.4(@microsoft/api-extractor@7.43.0(@types/node@18.19.45))(jiti@1.21.6)(postcss@8.4.41)(typescript@5.5.4)(yaml@2.5.0) turbo: specifier: ^2.0.14 version: 2.0.14 @@ -1673,6 +1658,12 @@ importers: typescript: specifier: ~5.5.4 version: 5.5.4 + vitest: + specifier: ^2.0.5 + version: 2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6) + vitest-websocket-mock: + specifier: ^0.3.0 + version: 0.3.0(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)) packages/ws: dependencies: @@ -2623,6 +2614,14 @@ packages: resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==} engines: {node: '>=18'} + '@discordjs/node-pre-gyp@0.4.5': + resolution: {integrity: sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==} + hasBin: true + + '@discordjs/opus@0.9.0': + resolution: {integrity: sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==} + engines: {node: '>=12.0.0'} + '@discordjs/rest@2.3.0': resolution: {integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==} engines: {node: '>=16.11.0'} @@ -9553,9 +9552,6 @@ packages: resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-websocket-mock@2.5.0: - resolution: {integrity: sha512-a+UJGfowNIWvtIKIQBHoEWIUqRxxQHFx4CXT+R5KxxKBtEQ5rS3pPOV/5299sHzqbmeCzxxY5qE4+yfXePePig==} - jest-worker@29.7.0: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -10665,6 +10661,9 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + node-dir@0.1.17: resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} engines: {node: '>= 0.10.5'} @@ -13375,6 +13374,11 @@ packages: terser: optional: true + vitest-websocket-mock@0.3.0: + resolution: {integrity: sha512-kTEFtfHIUDiiiEBj/CR6WajugqObjnuNdolGRJA3vo3Xt+fmfd1Ghwe+NpJytG6OE57noHOCUXzs2R9XUF0cwg==} + peerDependencies: + vitest: '>=1 <2' + vitest@2.0.5: resolution: {integrity: sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -14850,6 +14854,29 @@ snapshots: dependencies: discord-api-types: 0.37.97 + '@discordjs/node-pre-gyp@0.4.5(encoding@0.1.13)': + dependencies: + detect-libc: 2.0.3 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.5.4 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@discordjs/opus@0.9.0(encoding@0.1.13)': + dependencies: + '@discordjs/node-pre-gyp': 0.4.5(encoding@0.1.13) + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + '@discordjs/rest@2.3.0': dependencies: '@discordjs/collection': 2.1.0 @@ -24231,11 +24258,6 @@ snapshots: jest-util: 29.7.0 string-length: 4.0.2 - jest-websocket-mock@2.5.0: - dependencies: - jest-diff: 29.7.0 - mock-socket: 9.3.1 - jest-worker@29.7.0: dependencies: '@types/node': 18.19.45 @@ -25954,6 +25976,8 @@ snapshots: lower-case: 2.0.2 tslib: 2.6.3 + node-addon-api@5.1.0: {} + node-dir@0.1.17: dependencies: minimatch: 3.1.2 @@ -26667,7 +26691,9 @@ snapshots: dependencies: parse-ms: 4.0.0 - prism-media@1.3.5: {} + prism-media@1.3.5(@discordjs/opus@0.9.0(encoding@0.1.13)): + optionalDependencies: + '@discordjs/opus': 0.9.0(encoding@0.1.13) proc-log@3.0.0: {} @@ -29328,6 +29354,12 @@ snapshots: fsevents: 2.3.3 terser: 5.31.6 + vitest-websocket-mock@0.3.0(vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6)): + dependencies: + jest-diff: 29.7.0 + mock-socket: 9.3.1 + vitest: 2.0.5(@edge-runtime/vm@3.2.0)(@types/node@18.19.45)(happy-dom@14.12.3)(terser@5.31.6) + vitest@2.0.5(@edge-runtime/vm@3.2.0)(@types/node@16.18.105)(happy-dom@14.12.3)(terser@5.31.6): dependencies: '@ampproject/remapping': 2.3.0 From 8ab4124ef9818920970d17071cc4cb7dbe63bb61 Mon Sep 17 00:00:00 2001 From: Denis Cristea Date: Sun, 6 Oct 2024 17:43:06 +0300 Subject: [PATCH 57/65] feat: implement zod-validation-error (#10534) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/builders/package.json | 3 +- packages/builders/src/components/ActionRow.ts | 6 +-- .../builders/src/components/button/Button.ts | 7 +--- .../selectMenu/ChannelSelectMenu.ts | 7 +--- .../selectMenu/MentionableSelectMenu.ts | 7 +--- .../components/selectMenu/RoleSelectMenu.ts | 7 +--- .../components/selectMenu/StringSelectMenu.ts | 6 +-- .../selectMenu/StringSelectMenuOption.ts | 7 +--- .../components/selectMenu/UserSelectMenu.ts | 7 +--- .../src/components/textInput/TextInput.ts | 7 +--- .../commands/chatInput/ChatInputCommand.ts | 6 +-- .../chatInput/ChatInputCommandSubcommands.ts | 10 ++--- .../options/ApplicationCommandOptionBase.ts | 7 +--- .../commands/contextMenu/MessageCommand.ts | 7 +--- .../commands/contextMenu/UserCommand.ts | 7 +--- .../builders/src/interactions/modals/Modal.ts | 6 +-- packages/builders/src/messages/embed/Embed.ts | 6 +-- .../src/messages/embed/EmbedAuthor.ts | 7 +--- .../builders/src/messages/embed/EmbedField.ts | 7 +--- .../src/messages/embed/EmbedFooter.ts | 7 +--- packages/builders/src/util/validation.ts | 38 +++++++++++++++++-- pnpm-lock.yaml | 13 +++++++ 22 files changed, 88 insertions(+), 97 deletions(-) diff --git a/packages/builders/package.json b/packages/builders/package.json index d0e288c7cfa2..f678a01e0ab7 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -69,7 +69,8 @@ "discord-api-types": "^0.37.101", "ts-mixer": "^6.0.4", "tslib": "^2.6.3", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-validation-error": "^3.4.0" }, "devDependencies": { "@discordjs/api-extractor": "workspace:^", diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 84d7268ddcf0..9d099356fd84 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -16,7 +16,7 @@ import type { import { ComponentType } from 'discord-api-types/v10'; import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js'; import { resolveBuilder } from '../util/resolveBuilder.js'; -import { isValidationEnabled } from '../util/validation.js'; +import { validate } from '../util/validation.js'; import { actionRowPredicate } from './Assertions.js'; import { ComponentBuilder } from './Component.js'; import type { AnyActionRowComponentBuilder } from './Components.js'; @@ -336,9 +336,7 @@ export class ActionRowBuilder extends ComponentBuilder component.toJSON(validationOverride)), }; - if (validationOverride ?? isValidationEnabled()) { - actionRowPredicate.parse(data); - } + validate(actionRowPredicate, data, validationOverride); return data as APIActionRowComponent; } diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index 448059ddd941..94737fa4af91 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -1,5 +1,5 @@ import type { APIButtonComponent } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { buttonPredicate } from '../Assertions.js'; import { ComponentBuilder } from '../Component.js'; @@ -24,10 +24,7 @@ export abstract class BaseButtonBuilder e */ public override toJSON(validationOverride?: boolean): ButtonData { const clone = structuredClone(this.data); - - if (validationOverride ?? isValidationEnabled()) { - buttonPredicate.parse(clone); - } + validate(buttonPredicate, clone, validationOverride); return clone as ButtonData; } diff --git a/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts b/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts index 913d61592e4e..3f795605239b 100644 --- a/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts @@ -6,7 +6,7 @@ import { SelectMenuDefaultValueType, } from 'discord-api-types/v10'; import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { selectMenuChannelPredicate } from '../Assertions.js'; import { BaseSelectMenuBuilder } from './BaseSelectMenu.js'; @@ -108,10 +108,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder option.toJSON(false)), }; - if (validationOverride ?? isValidationEnabled()) { - selectMenuStringPredicate.parse(data); - } + validate(selectMenuStringPredicate, data, validationOverride); return data as APIStringSelectComponent; } diff --git a/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts b/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts index c2faa5361934..39723d74ecb8 100644 --- a/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts @@ -1,6 +1,6 @@ import type { JSONEncodable } from '@discordjs/util'; import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { selectMenuStringOptionPredicate } from '../Assertions.js'; /** @@ -106,10 +106,7 @@ export class StringSelectMenuOptionBuilder implements JSONEncodable { */ public toJSON(validationOverride?: boolean): APITextInputComponent { const clone = structuredClone(this.data); - - if (validationOverride ?? isValidationEnabled()) { - textInputPredicate.parse(clone); - } + validate(textInputPredicate, clone, validationOverride); return clone as APITextInputComponent; } diff --git a/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts index 422b5d9371ae..14a600dac072 100644 --- a/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts +++ b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts @@ -1,6 +1,6 @@ import { ApplicationCommandType, type RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord-api-types/v10'; import { Mixin } from 'ts-mixer'; -import { isValidationEnabled } from '../../../util/validation.js'; +import { validate } from '../../../util/validation.js'; import { CommandBuilder } from '../Command.js'; import { SharedNameAndDescription } from '../SharedNameAndDescription.js'; import { chatInputCommandPredicate } from './Assertions.js'; @@ -28,9 +28,7 @@ export class ChatInputCommandBuilder extends Mixin( options: options?.map((option) => option.toJSON(validationOverride)), }; - if (validationOverride ?? isValidationEnabled()) { - chatInputCommandPredicate.parse(data); - } + validate(chatInputCommandPredicate, data, validationOverride); return data; } diff --git a/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts b/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts index 7ecfd6a641b9..bf350d1dec36 100644 --- a/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts +++ b/packages/builders/src/interactions/commands/chatInput/ChatInputCommandSubcommands.ts @@ -7,7 +7,7 @@ import { ApplicationCommandOptionType } from 'discord-api-types/v10'; import { Mixin } from 'ts-mixer'; import { normalizeArray, type RestOrArray } from '../../../util/normalizeArray.js'; import { resolveBuilder } from '../../../util/resolveBuilder.js'; -import { isValidationEnabled } from '../../../util/validation.js'; +import { validate } from '../../../util/validation.js'; import type { SharedNameAndDescriptionData } from '../SharedNameAndDescription.js'; import { SharedNameAndDescription } from '../SharedNameAndDescription.js'; import { chatInputCommandSubcommandGroupPredicate, chatInputCommandSubcommandPredicate } from './Assertions.js'; @@ -69,9 +69,7 @@ export class ChatInputCommandSubcommandGroupBuilder options: options?.map((option) => option.toJSON(validationOverride)) ?? [], }; - if (validationOverride ?? isValidationEnabled()) { - chatInputCommandSubcommandGroupPredicate.parse(data); - } + validate(chatInputCommandSubcommandGroupPredicate, data, validationOverride); return data; } @@ -102,9 +100,7 @@ export class ChatInputCommandSubcommandBuilder options: options?.map((option) => option.toJSON(validationOverride)) ?? [], }; - if (validationOverride ?? isValidationEnabled()) { - chatInputCommandSubcommandPredicate.parse(data); - } + validate(chatInputCommandSubcommandPredicate, data, validationOverride); return data; } diff --git a/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts b/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts index 7016083e6acf..cb14ef9dc7ba 100644 --- a/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts +++ b/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts @@ -5,7 +5,7 @@ import type { ApplicationCommandOptionType, } from 'discord-api-types/v10'; import type { z } from 'zod'; -import { isValidationEnabled } from '../../../../util/validation.js'; +import { validate } from '../../../../util/validation.js'; import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js'; import { SharedNameAndDescription } from '../../SharedNameAndDescription.js'; import { basicOptionPredicate } from '../Assertions.js'; @@ -49,10 +49,7 @@ export abstract class ApplicationCommandOptionBase */ public toJSON(validationOverride?: boolean): APIApplicationCommandBasicOption { const clone = structuredClone(this.data); - - if (validationOverride ?? isValidationEnabled()) { - (this.constructor as typeof ApplicationCommandOptionBase).predicate.parse(clone); - } + validate((this.constructor as typeof ApplicationCommandOptionBase).predicate, clone, validationOverride); return clone as APIApplicationCommandBasicOption; } diff --git a/packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts b/packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts index ccaab7bc33eb..4ea63edd96a1 100644 --- a/packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts +++ b/packages/builders/src/interactions/commands/contextMenu/MessageCommand.ts @@ -1,5 +1,5 @@ import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../../util/validation.js'; +import { validate } from '../../../util/validation.js'; import { messageCommandPredicate } from './Assertions.js'; import { ContextMenuCommandBuilder } from './ContextMenuCommand.js'; @@ -9,10 +9,7 @@ export class MessageContextCommandBuilder extends ContextMenuCommandBuilder { */ public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody { const data = { ...structuredClone(this.data), type: ApplicationCommandType.Message }; - - if (validationOverride ?? isValidationEnabled()) { - messageCommandPredicate.parse(data); - } + validate(messageCommandPredicate, data, validationOverride); return data as RESTPostAPIContextMenuApplicationCommandsJSONBody; } diff --git a/packages/builders/src/interactions/commands/contextMenu/UserCommand.ts b/packages/builders/src/interactions/commands/contextMenu/UserCommand.ts index b911fb11f387..69279701f5b9 100644 --- a/packages/builders/src/interactions/commands/contextMenu/UserCommand.ts +++ b/packages/builders/src/interactions/commands/contextMenu/UserCommand.ts @@ -1,5 +1,5 @@ import { ApplicationCommandType, type RESTPostAPIContextMenuApplicationCommandsJSONBody } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../../util/validation.js'; +import { validate } from '../../../util/validation.js'; import { userCommandPredicate } from './Assertions.js'; import { ContextMenuCommandBuilder } from './ContextMenuCommand.js'; @@ -9,10 +9,7 @@ export class UserContextCommandBuilder extends ContextMenuCommandBuilder { */ public override toJSON(validationOverride?: boolean): RESTPostAPIContextMenuApplicationCommandsJSONBody { const data = { ...structuredClone(this.data), type: ApplicationCommandType.User }; - - if (validationOverride ?? isValidationEnabled()) { - userCommandPredicate.parse(data); - } + validate(userCommandPredicate, data, validationOverride); return data as RESTPostAPIContextMenuApplicationCommandsJSONBody; } diff --git a/packages/builders/src/interactions/modals/Modal.ts b/packages/builders/src/interactions/modals/Modal.ts index 6191f1d9197d..3eb42daabad1 100644 --- a/packages/builders/src/interactions/modals/Modal.ts +++ b/packages/builders/src/interactions/modals/Modal.ts @@ -10,7 +10,7 @@ import { ActionRowBuilder } from '../../components/ActionRow.js'; import { createComponentBuilder } from '../../components/Components.js'; import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; import { resolveBuilder } from '../../util/resolveBuilder.js'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { modalPredicate } from './Assertions.js'; export interface ModalBuilderData extends Partial> { @@ -162,9 +162,7 @@ export class ModalBuilder implements JSONEncodable component.toJSON(validationOverride)), }; - if (validationOverride ?? isValidationEnabled()) { - modalPredicate.parse(data); - } + validate(modalPredicate, data, validationOverride); return data as APIModalInteractionResponseCallbackData; } diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index 25e408189120..75bddc37540a 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -3,7 +3,7 @@ import type { APIEmbed, APIEmbedAuthor, APIEmbedField, APIEmbedFooter } from 'di import type { RestOrArray } from '../../util/normalizeArray.js'; import { normalizeArray } from '../../util/normalizeArray.js'; import { resolveBuilder } from '../../util/resolveBuilder.js'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { embedPredicate } from './Assertions.js'; import { EmbedAuthorBuilder } from './EmbedAuthor.js'; import { EmbedFieldBuilder } from './EmbedField.js'; @@ -343,9 +343,7 @@ export class EmbedBuilder implements JSONEncodable { footer: this.data.footer?.toJSON(false), }; - if (validationOverride ?? isValidationEnabled()) { - embedPredicate.parse(data); - } + validate(embedPredicate, data, validationOverride); return data; } diff --git a/packages/builders/src/messages/embed/EmbedAuthor.ts b/packages/builders/src/messages/embed/EmbedAuthor.ts index 0c3d0b6fb776..5eb9df58cdee 100644 --- a/packages/builders/src/messages/embed/EmbedAuthor.ts +++ b/packages/builders/src/messages/embed/EmbedAuthor.ts @@ -1,5 +1,5 @@ import type { APIEmbedAuthor } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { embedAuthorPredicate } from './Assertions.js'; /** @@ -72,10 +72,7 @@ export class EmbedAuthorBuilder { */ public toJSON(validationOverride?: boolean): APIEmbedAuthor { const clone = structuredClone(this.data); - - if (validationOverride ?? isValidationEnabled()) { - embedAuthorPredicate.parse(clone); - } + validate(embedAuthorPredicate, clone, validationOverride); return clone as APIEmbedAuthor; } diff --git a/packages/builders/src/messages/embed/EmbedField.ts b/packages/builders/src/messages/embed/EmbedField.ts index e385fad3ec14..5025fec0ecf5 100644 --- a/packages/builders/src/messages/embed/EmbedField.ts +++ b/packages/builders/src/messages/embed/EmbedField.ts @@ -1,5 +1,5 @@ import type { APIEmbedField } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { embedFieldPredicate } from './Assertions.js'; /** @@ -56,10 +56,7 @@ export class EmbedFieldBuilder { */ public toJSON(validationOverride?: boolean): APIEmbedField { const clone = structuredClone(this.data); - - if (validationOverride ?? isValidationEnabled()) { - embedFieldPredicate.parse(clone); - } + validate(embedFieldPredicate, clone, validationOverride); return clone as APIEmbedField; } diff --git a/packages/builders/src/messages/embed/EmbedFooter.ts b/packages/builders/src/messages/embed/EmbedFooter.ts index 5b3e0c0f8543..8d75b77f6df8 100644 --- a/packages/builders/src/messages/embed/EmbedFooter.ts +++ b/packages/builders/src/messages/embed/EmbedFooter.ts @@ -1,5 +1,5 @@ import type { APIEmbedFooter } from 'discord-api-types/v10'; -import { isValidationEnabled } from '../../util/validation.js'; +import { validate } from '../../util/validation.js'; import { embedFooterPredicate } from './Assertions.js'; /** @@ -54,10 +54,7 @@ export class EmbedFooterBuilder { */ public toJSON(validationOverride?: boolean): APIEmbedFooter { const clone = structuredClone(this.data); - - if (validationOverride ?? isValidationEnabled()) { - embedFooterPredicate.parse(clone); - } + validate(embedFooterPredicate, clone, validationOverride); return clone as APIEmbedFooter; } diff --git a/packages/builders/src/util/validation.ts b/packages/builders/src/util/validation.ts index 37e5c224bc6e..ce31bbdaa7e9 100644 --- a/packages/builders/src/util/validation.ts +++ b/packages/builders/src/util/validation.ts @@ -1,4 +1,7 @@ -let validate = true; +import type { z } from 'zod'; +import { fromZodError } from 'zod-validation-error'; + +let validationEnabled = true; /** * Enables validators. @@ -6,7 +9,7 @@ let validate = true; * @returns Whether validation is occurring. */ export function enableValidators() { - return (validate = true); + return (validationEnabled = true); } /** @@ -15,12 +18,39 @@ export function enableValidators() { * @returns Whether validation is occurring. */ export function disableValidators() { - return (validate = false); + return (validationEnabled = false); } /** * Checks whether validation is occurring. */ export function isValidationEnabled() { - return validate; + return validationEnabled; +} + +/** + * Parses a value with a given validator, accounting for wether validation is enabled. + * + * @param validator - The zod validator to use + * @param value - The value to parse + * @param validationOverride - Force validation to run/not run regardless of your global preference + * @returns The result from parsing + * @internal + */ +export function validate( + validator: Validator, + value: unknown, + validationOverride?: boolean, +): z.output { + if (validationOverride === false || !isValidationEnabled()) { + return value; + } + + const result = validator.safeParse(value); + + if (!result.success) { + throw fromZodError(result.error); + } + + return result.data; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42abf3613b92..2340a8c5d1c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -685,6 +685,9 @@ importers: zod: specifier: ^3.23.8 version: 3.23.8 + zod-validation-error: + specifier: ^3.4.0 + version: 3.4.0(zod@3.23.8) devDependencies: '@discordjs/api-extractor': specifier: workspace:^ @@ -13661,6 +13664,12 @@ packages: peerDependencies: zod: ^3.18.0 + zod-validation-error@3.4.0: + resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} @@ -29775,6 +29784,10 @@ snapshots: dependencies: zod: 3.23.8 + zod-validation-error@3.4.0(zod@3.23.8): + dependencies: + zod: 3.23.8 + zod@3.23.8: {} zwitch@2.0.4: {} From a65c76295065df303f42b90d874630248a91cb2a Mon Sep 17 00:00:00 2001 From: Qjuh <76154676+Qjuh@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:41:25 +0200 Subject: [PATCH 58/65] refactor!: fully integrate /ws into mainlib (#10420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: `Client#ws` is now a `@discordjs/ws#WebSocketManager` BREAKING CHANGE: `WebSocketManager` and `WebSocketShard` are now re-exports from `@discordjs/ws` BREAKING CHANGE: Removed the `WebSocketShardEvents` enum BREAKING CHANGE: Renamed the `Client#ready` event to `Client#clientReady` event to not confuse it with the gateway `READY` event BREAKING CHANGE: Added `Client#ping` to replace the old `WebSocketManager#ping` BREAKING CHANGE: Removed the `Shard#reconnecting` event which wasn’t emitted anymore since 14.8.0 anyway BREAKING CHANGE: Removed `ShardClientUtil#ids` and `ShardClientUtil#count` in favor of `Client#ws#getShardIds()` and `Client#ws#getShardCount()` BREAKING CHANGE: `ClientUser#setPresence()` and `ClientPresence#set()` now return a Promise which resolves when the gateway call was sent successfully BREAKING CHANGE: Removed `Guild#shard` as `WebSocketShard`s are now handled by `@discordjs/ws` BREAKING CHANGE: Removed the following deprecated `Client` events: `raw`, `shardDisconnect`, `shardError`, `shardReady`, `shardReconnecting`, `shardResume` in favor of events from `@discordjs/ws#WebSocketManager` BREAKING CHANGE: Removed `ClientOptions#shards` and `ClientOptions#shardCount` in favor of `ClientOptions#ws#shardIds` and `ClientOptions#ws#shardCount` --- packages/discord.js/package.json | 2 +- packages/discord.js/src/client/Client.js | 304 +++++++++++--- .../src/client/actions/GuildMemberRemove.js | 5 +- .../src/client/actions/GuildMemberUpdate.js | 5 +- .../src/client/voice/ClientVoiceManager.js | 12 +- .../src/client/websocket/WebSocketManager.js | 387 ------------------ .../src/client/websocket/WebSocketShard.js | 234 ----------- .../client/websocket/handlers/GUILD_CREATE.js | 6 +- .../websocket/handlers/GUILD_MEMBER_ADD.js | 17 +- .../websocket/handlers/GUILD_MEMBER_REMOVE.js | 4 +- .../websocket/handlers/GUILD_MEMBER_UPDATE.js | 4 +- .../src/client/websocket/handlers/READY.js | 6 +- .../src/client/websocket/handlers/RESUMED.js | 14 - .../src/client/websocket/handlers/index.js | 1 - packages/discord.js/src/errors/ErrorCodes.js | 85 +++- packages/discord.js/src/index.js | 3 - .../discord.js/src/managers/GuildManager.js | 2 +- .../src/managers/GuildMemberManager.js | 2 +- packages/discord.js/src/sharding/Shard.js | 17 +- .../src/sharding/ShardClientUtil.js | 37 +- .../src/structures/ClientPresence.js | 14 +- .../discord.js/src/structures/ClientUser.js | 8 +- packages/discord.js/src/structures/Guild.js | 13 +- packages/discord.js/src/util/APITypes.js | 4 + packages/discord.js/src/util/Events.js | 13 +- packages/discord.js/src/util/Options.js | 51 +-- packages/discord.js/src/util/Status.js | 18 +- .../src/util/WebSocketShardEvents.js | 25 -- packages/discord.js/typings/index.d.ts | 124 +----- packages/discord.js/typings/index.test-d.ts | 16 +- pnpm-lock.yaml | 62 +-- 31 files changed, 409 insertions(+), 1086 deletions(-) delete mode 100644 packages/discord.js/src/client/websocket/WebSocketManager.js delete mode 100644 packages/discord.js/src/client/websocket/WebSocketShard.js delete mode 100644 packages/discord.js/src/client/websocket/handlers/RESUMED.js delete mode 100644 packages/discord.js/src/util/WebSocketShardEvents.js diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 5aac93e7735f..e64d85856d0b 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -70,7 +70,7 @@ "@discordjs/formatters": "workspace:^", "@discordjs/rest": "workspace:^", "@discordjs/util": "workspace:^", - "@discordjs/ws": "1.1.1", + "@discordjs/ws": "workspace:^", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.37.101", "fast-deep-equal": "3.1.3", diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 3a72b8414986..048cfb32d322 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -1,14 +1,16 @@ 'use strict'; const process = require('node:process'); +const { clearTimeout, setImmediate, setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); -const { OAuth2Scopes, Routes } = require('discord-api-types/v10'); +const { WebSocketManager, WebSocketShardEvents, WebSocketShardStatus } = require('@discordjs/ws'); +const { GatewayDispatchEvents, GatewayIntentBits, OAuth2Scopes, Routes } = require('discord-api-types/v10'); const BaseClient = require('./BaseClient'); const ActionsManager = require('./actions/ActionsManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); -const WebSocketManager = require('./websocket/WebSocketManager'); -const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); +const PacketHandlers = require('./websocket/handlers'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const ChannelManager = require('../managers/ChannelManager'); const GuildManager = require('../managers/GuildManager'); @@ -31,6 +33,17 @@ const PermissionsBitField = require('../util/PermissionsBitField'); const Status = require('../util/Status'); const Sweepers = require('../util/Sweepers'); +const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; +const BeforeReadyWhitelist = [ + GatewayDispatchEvents.Ready, + GatewayDispatchEvents.Resumed, + GatewayDispatchEvents.GuildCreate, + GatewayDispatchEvents.GuildDelete, + GatewayDispatchEvents.GuildMembersChunk, + GatewayDispatchEvents.GuildMemberAdd, + GatewayDispatchEvents.GuildMemberRemove, +]; + /** * The main hub for interacting with the Discord API, and the starting point for any bot. * @extends {BaseClient} @@ -45,43 +58,45 @@ class Client extends BaseClient { const data = require('node:worker_threads').workerData ?? process.env; const defaults = Options.createDefault(); - if (this.options.shards === defaults.shards) { - if ('SHARDS' in data) { - this.options.shards = JSON.parse(data.SHARDS); - } + if (this.options.ws.shardIds === defaults.ws.shardIds && 'SHARDS' in data) { + this.options.ws.shardIds = JSON.parse(data.SHARDS); } - if (this.options.shardCount === defaults.shardCount) { - if ('SHARD_COUNT' in data) { - this.options.shardCount = Number(data.SHARD_COUNT); - } else if (Array.isArray(this.options.shards)) { - this.options.shardCount = this.options.shards.length; - } + if (this.options.ws.shardCount === defaults.ws.shardCount && 'SHARD_COUNT' in data) { + this.options.ws.shardCount = Number(data.SHARD_COUNT); } - const typeofShards = typeof this.options.shards; - - if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { - this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); - } + /** + * The presence of the Client + * @private + * @type {ClientPresence} + */ + this.presence = new ClientPresence(this, this.options.ws.initialPresence ?? this.options.presence); - if (typeofShards === 'number') this.options.shards = [this.options.shards]; + this._validateOptions(); - if (Array.isArray(this.options.shards)) { - this.options.shards = [ - ...new Set( - this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)), - ), - ]; - } + /** + * The current status of this Client + * @type {Status} + * @private + */ + this.status = Status.Idle; - this._validateOptions(); + /** + * A set of guild ids this Client expects to receive + * @name Client#expectedGuilds + * @type {Set} + * @private + */ + Object.defineProperty(this, 'expectedGuilds', { value: new Set(), writable: true }); /** - * The WebSocket manager of the client - * @type {WebSocketManager} + * The ready timeout + * @name Client#readyTimeout + * @type {?NodeJS.Timeout} + * @private */ - this.ws = new WebSocketManager(this); + Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); /** * The action manager of the client @@ -90,12 +105,6 @@ class Client extends BaseClient { */ this.actions = new ActionsManager(this); - /** - * The voice manager of the client - * @type {ClientVoiceManager} - */ - this.voice = new ClientVoiceManager(this); - /** * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * @type {?ShardClientUtil} @@ -119,7 +128,7 @@ class Client extends BaseClient { /** * All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids - - * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot + * as long as no sharding manager is being used, this will be *every* channel in *every* guild the bot * is a member of. Note that DM channels will not be initially cached, and thus not be present * in the Manager without their explicit fetching or use. * @type {ChannelManager} @@ -132,13 +141,6 @@ class Client extends BaseClient { */ this.sweepers = new Sweepers(this, this.options.sweepers); - /** - * The presence of the Client - * @private - * @type {ClientPresence} - */ - this.presence = new ClientPresence(this, this.options.presence); - Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'DISCORD_TOKEN' in process.env) { /** @@ -148,10 +150,31 @@ class Client extends BaseClient { * @type {?string} */ this.token = process.env.DISCORD_TOKEN; + } else if (this.options.ws.token) { + this.token = this.options.ws.token; } else { this.token = null; } + const wsOptions = { + ...this.options.ws, + intents: this.options.intents.bitfield, + rest: this.rest, + token: this.token, + }; + + /** + * The WebSocket manager of the client + * @type {WebSocketManager} + */ + this.ws = new WebSocketManager(wsOptions); + + /** + * The voice manager of the client + * @type {ClientVoiceManager} + */ + this.voice = new ClientVoiceManager(this); + /** * User that the client is logged in as * @type {?ClientUser} @@ -164,11 +187,33 @@ class Client extends BaseClient { */ this.application = null; + /** + * The latencies of the WebSocketShard connections + * @type {Collection} + */ + this.pings = new Collection(); + + /** + * The last time a ping was sent (a timestamp) for each WebSocketShard connection + * @type {Collection} + */ + this.lastPingTimestamps = new Collection(); + /** * Timestamp of the time the client was last {@link Status.Ready} at * @type {?number} */ this.readyTimestamp = null; + + /** + * An array of queued events before this Client became ready + * @type {Object[]} + * @private + * @name Client#incomingPacketQueue + */ + Object.defineProperty(this, 'incomingPacketQueue', { value: [] }); + + this._attachEvents(); } /** @@ -215,13 +260,10 @@ class Client extends BaseClient { this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); this.rest.setToken(token); this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); - - if (this.options.presence) { - this.options.ws.presence = this.presence._parse(this.options.presence); - } - this.emit(Events.Debug, 'Preparing to connect to the gateway...'); + this.ws.setToken(this.token); + try { await this.ws.connect(); return this.token; @@ -231,13 +273,150 @@ class Client extends BaseClient { } } + /** + * Checks if the client can be marked as ready + * @private + */ + async _checkReady() { + // Step 0. Clear the ready timeout, if it exists + if (this.readyTimeout) { + clearTimeout(this.readyTimeout); + this.readyTimeout = null; + } + // Step 1. If we don't have any other guilds pending, we are ready + if ( + !this.expectedGuilds.size && + (await this.ws.fetchStatus()).every(status => status === WebSocketShardStatus.Ready) + ) { + this.emit(Events.Debug, 'Client received all its guilds. Marking as fully ready.'); + this.status = Status.Ready; + + this._triggerClientReady(); + return; + } + const hasGuildsIntent = this.options.intents.has(GatewayIntentBits.Guilds); + // Step 2. Create a timeout that will mark the client as ready if there are still unavailable guilds + // * The timeout is 15 seconds by default + // * This can be optionally changed in the client options via the `waitGuildTimeout` option + // * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds. + + this.readyTimeout = setTimeout( + () => { + this.emit( + Events.Debug, + `${ + hasGuildsIntent + ? `Client did not receive any guild packets in ${this.options.waitGuildTimeout} ms.` + : 'Client will not receive anymore guild packets.' + }\nUnavailable guild count: ${this.expectedGuilds.size}`, + ); + + this.readyTimeout = null; + this.status = Status.Ready; + + this._triggerClientReady(); + }, + hasGuildsIntent ? this.options.waitGuildTimeout : 0, + ).unref(); + } + + /** + * Attaches event handlers to the WebSocketShardManager from `@discordjs/ws`. + * @private + */ + _attachEvents() { + this.ws.on(WebSocketShardEvents.Debug, (message, shardId) => + this.emit(Events.Debug, `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`), + ); + this.ws.on(WebSocketShardEvents.Dispatch, this._handlePacket.bind(this)); + + this.ws.on(WebSocketShardEvents.Ready, data => { + for (const guild of data.guilds) { + this.expectedGuilds.add(guild.id); + } + this.status = Status.WaitingForGuilds; + this._checkReady(); + }); + + this.ws.on(WebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency }, shardId) => { + this.emit(Events.Debug, `[WS => Shard ${shardId}] Heartbeat acknowledged, latency of ${latency}ms.`); + this.lastPingTimestamps.set(shardId, heartbeatAt); + this.pings.set(shardId, latency); + }); + } + + /** + * Processes a packet and queues it if this WebSocketManager is not ready. + * @param {GatewayDispatchPayload} packet The packet to be handled + * @param {number} shardId The shardId that received this packet + * @private + */ + _handlePacket(packet, shardId) { + if (this.status !== Status.Ready && !BeforeReadyWhitelist.includes(packet.t)) { + this.incomingPacketQueue.push({ packet, shardId }); + } else { + if (this.incomingPacketQueue.length) { + const item = this.incomingPacketQueue.shift(); + setImmediate(() => { + this._handlePacket(item.packet, item.shardId); + }).unref(); + } + + if (PacketHandlers[packet.t]) { + PacketHandlers[packet.t](this, packet, shardId); + } + + if (this.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(packet.t)) { + this.expectedGuilds.delete(packet.d.id); + this._checkReady(); + } + } + } + + /** + * Broadcasts a packet to every shard of this client handles. + * @param {Object} packet The packet to send + * @private + */ + async _broadcast(packet) { + const shardIds = await this.ws.getShardIds(); + return Promise.all(shardIds.map(shardId => this.ws.send(shardId, packet))); + } + + /** + * Causes the client to be marked as ready and emits the ready event. + * @private + */ + _triggerClientReady() { + this.status = Status.Ready; + + this.readyTimestamp = Date.now(); + + /** + * Emitted when the client becomes ready to start working. + * @event Client#clientReady + * @param {Client} client The client + */ + this.emit(Events.ClientReady, this); + } + /** * Returns whether the client has logged in, indicative of being able to access * properties such as `user` and `application`. * @returns {boolean} */ isReady() { - return !this.ws.destroyed && this.ws.status === Status.Ready; + return this.status === Status.Ready; + } + + /** + * The average ping of all WebSocketShards + * @type {number} + * @readonly + */ + get ping() { + const sum = this.pings.reduce((a, b) => a + b, 0); + return sum / this.pings.size; } /** @@ -505,20 +684,10 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { - if (options.intents === undefined) { + if (options.intents === undefined && options.ws?.intents === undefined) { throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents); } else { - options.intents = new IntentsBitField(options.intents).freeze(); - } - if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shardCount', 'a number greater than or equal to 1'); - } - if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shards', "'auto', a number or array of numbers"); - } - if (options.shards && !options.shards.length) throw new DiscordjsRangeError(ErrorCodes.ClientInvalidProvidedShards); - if (typeof options.makeCache !== 'function') { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'makeCache', 'a function'); + options.intents = new IntentsBitField(options.intents ?? options.ws.intents).freeze(); } if (typeof options.sweepers !== 'object' || options.sweepers === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'sweepers', 'an object'); @@ -541,12 +710,17 @@ class Client extends BaseClient { ) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'allowedMentions', 'an object'); } - if (typeof options.presence !== 'object' || options.presence === null) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); - } if (typeof options.ws !== 'object' || options.ws === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'ws', 'an object'); } + if ( + (typeof options.presence !== 'object' || options.presence === null) && + options.ws.initialPresence === undefined + ) { + throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); + } else { + options.ws.initialPresence = options.ws.initialPresence ?? this.presence._parse(this.options.presence); + } if (typeof options.rest !== 'object' || options.rest === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'rest', 'an object'); } diff --git a/packages/discord.js/src/client/actions/GuildMemberRemove.js b/packages/discord.js/src/client/actions/GuildMemberRemove.js index 45eb6c41931f..e7cefee67a11 100644 --- a/packages/discord.js/src/client/actions/GuildMemberRemove.js +++ b/packages/discord.js/src/client/actions/GuildMemberRemove.js @@ -2,10 +2,9 @@ const Action = require('./Action'); const Events = require('../../util/Events'); -const Status = require('../../util/Status'); class GuildMemberRemoveAction extends Action { - handle(data, shard) { + handle(data) { const client = this.client; const guild = client.guilds.cache.get(data.guild_id); let member = null; @@ -19,7 +18,7 @@ class GuildMemberRemoveAction extends Action { * @event Client#guildMemberRemove * @param {GuildMember} member The member that has left/been kicked from the guild */ - if (shard.status === Status.Ready) client.emit(Events.GuildMemberRemove, member); + client.emit(Events.GuildMemberRemove, member); } guild.presences.cache.delete(data.user.id); guild.voiceStates.cache.delete(data.user.id); diff --git a/packages/discord.js/src/client/actions/GuildMemberUpdate.js b/packages/discord.js/src/client/actions/GuildMemberUpdate.js index 491b36181e0a..9561ab82abb2 100644 --- a/packages/discord.js/src/client/actions/GuildMemberUpdate.js +++ b/packages/discord.js/src/client/actions/GuildMemberUpdate.js @@ -2,10 +2,9 @@ const Action = require('./Action'); const Events = require('../../util/Events'); -const Status = require('../../util/Status'); class GuildMemberUpdateAction extends Action { - handle(data, shard) { + handle(data) { const { client } = this; if (data.user.username) { const user = client.users.cache.get(data.user.id); @@ -27,7 +26,7 @@ class GuildMemberUpdateAction extends Action { * @param {GuildMember} oldMember The member before the update * @param {GuildMember} newMember The member after the update */ - if (shard.status === Status.Ready && !member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); + if (!member.equals(old)) client.emit(Events.GuildMemberUpdate, old, member); } else { const newMember = guild.members._add(data); /** diff --git a/packages/discord.js/src/client/voice/ClientVoiceManager.js b/packages/discord.js/src/client/voice/ClientVoiceManager.js index 55b0d830ed38..520dd40cfb30 100644 --- a/packages/discord.js/src/client/voice/ClientVoiceManager.js +++ b/packages/discord.js/src/client/voice/ClientVoiceManager.js @@ -1,6 +1,6 @@ 'use strict'; -const Events = require('../../util/Events'); +const { WebSocketShardEvents, CloseCodes } = require('@discordjs/ws'); /** * Manages voice connections for the client @@ -21,10 +21,12 @@ class ClientVoiceManager { */ this.adapters = new Map(); - client.on(Events.ShardDisconnect, (_, shardId) => { - for (const [guildId, adapter] of this.adapters.entries()) { - if (client.guilds.cache.get(guildId)?.shardId === shardId) { - adapter.destroy(); + client.ws.on(WebSocketShardEvents.Closed, (code, shardId) => { + if (code === CloseCodes.Normal) { + for (const [guildId, adapter] of this.adapters.entries()) { + if (client.guilds.cache.get(guildId)?.shardId === shardId) { + adapter.destroy(); + } } } }); diff --git a/packages/discord.js/src/client/websocket/WebSocketManager.js b/packages/discord.js/src/client/websocket/WebSocketManager.js deleted file mode 100644 index 7a1831ad6aea..000000000000 --- a/packages/discord.js/src/client/websocket/WebSocketManager.js +++ /dev/null @@ -1,387 +0,0 @@ -'use strict'; - -const EventEmitter = require('node:events'); -const process = require('node:process'); -const { setImmediate } = require('node:timers'); -const { Collection } = require('@discordjs/collection'); -const { - WebSocketManager: WSWebSocketManager, - WebSocketShardEvents: WSWebSocketShardEvents, - CompressionMethod, - CloseCodes, -} = require('@discordjs/ws'); -const { GatewayCloseCodes, GatewayDispatchEvents } = require('discord-api-types/v10'); -const WebSocketShard = require('./WebSocketShard'); -const PacketHandlers = require('./handlers'); -const { DiscordjsError, ErrorCodes } = require('../../errors'); -const Events = require('../../util/Events'); -const Status = require('../../util/Status'); -const WebSocketShardEvents = require('../../util/WebSocketShardEvents'); - -let zlib; - -try { - zlib = require('zlib-sync'); -} catch {} // eslint-disable-line no-empty - -const BeforeReadyWhitelist = [ - GatewayDispatchEvents.Ready, - GatewayDispatchEvents.Resumed, - GatewayDispatchEvents.GuildCreate, - GatewayDispatchEvents.GuildDelete, - GatewayDispatchEvents.GuildMembersChunk, - GatewayDispatchEvents.GuildMemberAdd, - GatewayDispatchEvents.GuildMemberRemove, -]; - -const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; - -const UNRESUMABLE_CLOSE_CODES = [ - CloseCodes.Normal, - GatewayCloseCodes.AlreadyAuthenticated, - GatewayCloseCodes.InvalidSeq, -]; - -const reasonIsDeprecated = 'the reason property is deprecated, use the code property to determine the reason'; -let deprecationEmittedForInvalidSessionEvent = false; -let deprecationEmittedForDestroyedEvent = false; - -/** - * The WebSocket manager for this client. - * This class forwards raw dispatch events, - * read more about it here {@link https://discord.com/developers/docs/topics/gateway} - * @extends {EventEmitter} - */ -class WebSocketManager extends EventEmitter { - constructor(client) { - super(); - - /** - * The client that instantiated this WebSocketManager - * @type {Client} - * @readonly - * @name WebSocketManager#client - */ - Object.defineProperty(this, 'client', { value: client }); - - /** - * The gateway this manager uses - * @type {?string} - */ - this.gateway = null; - - /** - * A collection of all shards this manager handles - * @type {Collection} - */ - this.shards = new Collection(); - - /** - * An array of queued events before this WebSocketManager became ready - * @type {Object[]} - * @private - * @name WebSocketManager#packetQueue - */ - Object.defineProperty(this, 'packetQueue', { value: [] }); - - /** - * The current status of this WebSocketManager - * @type {Status} - */ - this.status = Status.Idle; - - /** - * If this manager was destroyed. It will prevent shards from reconnecting - * @type {boolean} - * @private - */ - this.destroyed = false; - - /** - * The internal WebSocketManager from `@discordjs/ws`. - * @type {WSWebSocketManager} - * @private - */ - this._ws = null; - } - - /** - * The average ping of all WebSocketShards - * @type {number} - * @readonly - */ - get ping() { - const sum = this.shards.reduce((a, b) => a + b.ping, 0); - return sum / this.shards.size; - } - - /** - * Emits a debug message. - * @param {string[]} messages The debug message - * @param {?number} [shardId] The id of the shard that emitted this message, if any - * @private - */ - debug(messages, shardId) { - this.client.emit( - Events.Debug, - `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${messages.join('\n\t')}`, - ); - } - - /** - * Connects this manager to the gateway. - * @private - */ - async connect() { - const invalidToken = new DiscordjsError(ErrorCodes.TokenInvalid); - const { shards, shardCount, intents, ws } = this.client.options; - if (this._ws && this._ws.options.token !== this.client.token) { - await this._ws.destroy({ code: CloseCodes.Normal, reason: 'Login with differing token requested' }); - this._ws = null; - } - if (!this._ws) { - const wsOptions = { - intents: intents.bitfield, - rest: this.client.rest, - token: this.client.token, - largeThreshold: ws.large_threshold, - version: ws.version, - shardIds: shards === 'auto' ? null : shards, - shardCount: shards === 'auto' ? null : shardCount, - initialPresence: ws.presence, - retrieveSessionInfo: shardId => this.shards.get(shardId).sessionInfo, - updateSessionInfo: (shardId, sessionInfo) => { - this.shards.get(shardId).sessionInfo = sessionInfo; - }, - compression: zlib ? CompressionMethod.ZlibStream : null, - }; - if (ws.buildIdentifyThrottler) wsOptions.buildIdentifyThrottler = ws.buildIdentifyThrottler; - if (ws.buildStrategy) wsOptions.buildStrategy = ws.buildStrategy; - this._ws = new WSWebSocketManager(wsOptions); - this.attachEvents(); - } - - const { - url: gatewayURL, - shards: recommendedShards, - session_start_limit: sessionStartLimit, - } = await this._ws.fetchGatewayInformation().catch(error => { - throw error.status === 401 ? invalidToken : error; - }); - - const { total, remaining } = sessionStartLimit; - this.debug(['Fetched Gateway Information', `URL: ${gatewayURL}`, `Recommended Shards: ${recommendedShards}`]); - this.debug(['Session Limit Information', `Total: ${total}`, `Remaining: ${remaining}`]); - this.gateway = `${gatewayURL}/`; - - this.client.options.shardCount = await this._ws.getShardCount(); - this.client.options.shards = await this._ws.getShardIds(); - this.totalShards = this.client.options.shards.length; - for (const id of this.client.options.shards) { - if (!this.shards.has(id)) { - const shard = new WebSocketShard(this, id); - this.shards.set(id, shard); - - shard.on(WebSocketShardEvents.AllReady, unavailableGuilds => { - /** - * Emitted when a shard turns ready. - * @event Client#shardReady - * @param {number} id The shard id that turned ready - * @param {?Set} unavailableGuilds Set of unavailable guild ids, if any - */ - this.client.emit(Events.ShardReady, shard.id, unavailableGuilds); - - this.checkShardsReady(); - }); - shard.status = Status.Connecting; - } - } - - await this._ws.connect(); - - this.shards.forEach(shard => { - if (shard.listenerCount(WebSocketShardEvents.InvalidSession) > 0 && !deprecationEmittedForInvalidSessionEvent) { - process.emitWarning( - 'The WebSocketShard#invalidSession event is deprecated and will never emit.', - 'DeprecationWarning', - ); - - deprecationEmittedForInvalidSessionEvent = true; - } - if (shard.listenerCount(WebSocketShardEvents.Destroyed) > 0 && !deprecationEmittedForDestroyedEvent) { - process.emitWarning( - 'The WebSocketShard#destroyed event is deprecated and will never emit.', - 'DeprecationWarning', - ); - - deprecationEmittedForDestroyedEvent = true; - } - }); - } - - /** - * Attaches event handlers to the internal WebSocketShardManager from `@discordjs/ws`. - * @private - */ - attachEvents() { - this._ws.on(WSWebSocketShardEvents.Debug, ({ message, shardId }) => this.debug([message], shardId)); - this._ws.on(WSWebSocketShardEvents.Dispatch, ({ data, shardId }) => { - this.client.emit(Events.Raw, data, shardId); - this.emit(data.t, data.d, shardId); - const shard = this.shards.get(shardId); - this.handlePacket(data, shard); - if (shard.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(data.t)) { - shard.gotGuild(data.d.id); - } - }); - - this._ws.on(WSWebSocketShardEvents.Ready, ({ data, shardId }) => { - this.shards.get(shardId).onReadyPacket(data); - }); - - this._ws.on(WSWebSocketShardEvents.Closed, ({ code, shardId }) => { - const shard = this.shards.get(shardId); - shard.emit(WebSocketShardEvents.Close, { code, reason: reasonIsDeprecated, wasClean: true }); - if (UNRESUMABLE_CLOSE_CODES.includes(code) && this.destroyed) { - shard.status = Status.Disconnected; - /** - * Emitted when a shard's WebSocket disconnects and will no longer reconnect. - * @event Client#shardDisconnect - * @param {CloseEvent} event The WebSocket close event - * @param {number} id The shard id that disconnected - */ - this.client.emit(Events.ShardDisconnect, { code, reason: reasonIsDeprecated, wasClean: true }, shardId); - this.debug([`Shard not resumable: ${code} (${GatewayCloseCodes[code] ?? CloseCodes[code]})`], shardId); - return; - } - - this.shards.get(shardId).status = Status.Connecting; - /** - * Emitted when a shard is attempting to reconnect or re-identify. - * @event Client#shardReconnecting - * @param {number} id The shard id that is attempting to reconnect - */ - this.client.emit(Events.ShardReconnecting, shardId); - }); - this._ws.on(WSWebSocketShardEvents.Hello, ({ shardId }) => { - const shard = this.shards.get(shardId); - if (shard.sessionInfo) { - shard.closeSequence = shard.sessionInfo.sequence; - shard.status = Status.Resuming; - } else { - shard.status = Status.Identifying; - } - }); - - this._ws.on(WSWebSocketShardEvents.Resumed, ({ shardId }) => { - const shard = this.shards.get(shardId); - shard.status = Status.Ready; - /** - * Emitted when the shard resumes successfully - * @event WebSocketShard#resumed - */ - shard.emit(WebSocketShardEvents.Resumed); - }); - - this._ws.on(WSWebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency, shardId }) => { - this.debug([`Heartbeat acknowledged, latency of ${latency}ms.`], shardId); - const shard = this.shards.get(shardId); - shard.lastPingTimestamp = heartbeatAt; - shard.ping = latency; - }); - - this._ws.on(WSWebSocketShardEvents.Error, ({ error, shardId }) => { - /** - * Emitted whenever a shard's WebSocket encounters a connection error. - * @event Client#shardError - * @param {Error} error The encountered error - * @param {number} shardId The shard that encountered this error - */ - this.client.emit(Events.ShardError, error, shardId); - }); - } - - /** - * Broadcasts a packet to every shard this manager handles. - * @param {Object} packet The packet to send - * @private - */ - broadcast(packet) { - for (const shardId of this.shards.keys()) this._ws.send(shardId, packet); - } - - /** - * Destroys this manager and all its shards. - * @private - */ - async destroy() { - if (this.destroyed) return; - // TODO: Make a util for getting a stack - this.debug([Object.assign(new Error(), { name: 'Manager was destroyed:' }).stack]); - this.destroyed = true; - await this._ws?.destroy({ code: CloseCodes.Normal, reason: 'Manager was destroyed' }); - } - - /** - * Processes a packet and queues it if this WebSocketManager is not ready. - * @param {Object} [packet] The packet to be handled - * @param {WebSocketShard} [shard] The shard that will handle this packet - * @returns {boolean} - * @private - */ - handlePacket(packet, shard) { - if (packet && this.status !== Status.Ready) { - if (!BeforeReadyWhitelist.includes(packet.t)) { - this.packetQueue.push({ packet, shard }); - return false; - } - } - - if (this.packetQueue.length) { - const item = this.packetQueue.shift(); - setImmediate(() => { - this.handlePacket(item.packet, item.shard); - }).unref(); - } - - if (packet && PacketHandlers[packet.t]) { - PacketHandlers[packet.t](this.client, packet, shard); - } - - return true; - } - - /** - * Checks whether the client is ready to be marked as ready. - * @private - */ - checkShardsReady() { - if (this.status === Status.Ready) return; - if (this.shards.size !== this.totalShards || this.shards.some(shard => shard.status !== Status.Ready)) { - return; - } - - this.triggerClientReady(); - } - - /** - * Causes the client to be marked as ready and emits the ready event. - * @private - */ - triggerClientReady() { - this.status = Status.Ready; - - this.client.readyTimestamp = Date.now(); - - /** - * Emitted when the client becomes ready to start working. - * @event Client#ready - * @param {Client} client The client - */ - this.client.emit(Events.ClientReady, this.client); - - this.handlePacket(); - } -} - -module.exports = WebSocketManager; diff --git a/packages/discord.js/src/client/websocket/WebSocketShard.js b/packages/discord.js/src/client/websocket/WebSocketShard.js deleted file mode 100644 index d3d8167f928a..000000000000 --- a/packages/discord.js/src/client/websocket/WebSocketShard.js +++ /dev/null @@ -1,234 +0,0 @@ -'use strict'; - -const EventEmitter = require('node:events'); -const process = require('node:process'); -const { setTimeout, clearTimeout } = require('node:timers'); -const { GatewayIntentBits } = require('discord-api-types/v10'); -const Status = require('../../util/Status'); -const WebSocketShardEvents = require('../../util/WebSocketShardEvents'); - -let deprecationEmittedForImportant = false; -/** - * Represents a Shard's WebSocket connection - * @extends {EventEmitter} - */ -class WebSocketShard extends EventEmitter { - constructor(manager, id) { - super(); - - /** - * The WebSocketManager of the shard - * @type {WebSocketManager} - */ - this.manager = manager; - - /** - * The shard's id - * @type {number} - */ - this.id = id; - - /** - * The current status of the shard - * @type {Status} - */ - this.status = Status.Idle; - - /** - * The sequence of the shard after close - * @type {number} - * @private - */ - this.closeSequence = 0; - - /** - * The previous heartbeat ping of the shard - * @type {number} - */ - this.ping = -1; - - /** - * The last time a ping was sent (a timestamp) - * @type {number} - */ - this.lastPingTimestamp = -1; - - /** - * A set of guild ids this shard expects to receive - * @name WebSocketShard#expectedGuilds - * @type {?Set} - * @private - */ - Object.defineProperty(this, 'expectedGuilds', { value: null, writable: true }); - - /** - * The ready timeout - * @name WebSocketShard#readyTimeout - * @type {?NodeJS.Timeout} - * @private - */ - Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); - - /** - * @external SessionInfo - * @see {@link https://discord.js.org/docs/packages/ws/stable/SessionInfo:Interface} - */ - - /** - * The session info used by `@discordjs/ws` package. - * @name WebSocketShard#sessionInfo - * @type {?SessionInfo} - * @private - */ - Object.defineProperty(this, 'sessionInfo', { value: null, writable: true }); - } - - /** - * Emits a debug event. - * @param {string[]} messages The debug message - * @private - */ - debug(messages) { - this.manager.debug(messages, this.id); - } - - /** - * @external CloseEvent - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent} - */ - - /** - * This method is responsible to emit close event for this shard. - * This method helps the shard reconnect. - * @param {CloseEvent} [event] Close event that was received - * @deprecated - */ - emitClose( - event = { - code: 1011, - reason: 'INTERNAL_ERROR', - wasClean: false, - }, - ) { - this.debug([ - '[CLOSE]', - `Event Code: ${event.code}`, - `Clean : ${event.wasClean}`, - `Reason : ${event.reason ?? 'No reason received'}`, - ]); - - /** - * Emitted when a shard's WebSocket closes. - * @private - * @event WebSocketShard#close - * @param {CloseEvent} event The received event - */ - this.emit(WebSocketShardEvents.Close, event); - } - - /** - * Called when the shard receives the READY payload. - * @param {Object} packet The received packet - * @private - */ - onReadyPacket(packet) { - if (!packet) { - this.debug([`Received broken packet: '${packet}'.`]); - return; - } - - /** - * Emitted when the shard receives the READY payload and is now waiting for guilds - * @event WebSocketShard#ready - */ - this.emit(WebSocketShardEvents.Ready); - - this.expectedGuilds = new Set(packet.guilds.map(guild => guild.id)); - this.status = Status.WaitingForGuilds; - } - - /** - * Called when a GuildCreate or GuildDelete for this shard was sent after READY payload was received, - * but before we emitted the READY event. - * @param {Snowflake} guildId the id of the Guild sent in the payload - * @private - */ - gotGuild(guildId) { - this.expectedGuilds.delete(guildId); - this.checkReady(); - } - - /** - * Checks if the shard can be marked as ready - * @private - */ - checkReady() { - // Step 0. Clear the ready timeout, if it exists - if (this.readyTimeout) { - clearTimeout(this.readyTimeout); - this.readyTimeout = null; - } - // Step 1. If we don't have any other guilds pending, we are ready - if (!this.expectedGuilds.size) { - this.debug(['Shard received all its guilds. Marking as fully ready.']); - this.status = Status.Ready; - - /** - * Emitted when the shard is fully ready. - * This event is emitted if: - * * all guilds were received by this shard - * * the ready timeout expired, and some guilds are unavailable - * @event WebSocketShard#allReady - * @param {?Set} unavailableGuilds Set of unavailable guilds, if any - */ - this.emit(WebSocketShardEvents.AllReady); - return; - } - const hasGuildsIntent = this.manager.client.options.intents.has(GatewayIntentBits.Guilds); - // Step 2. Create a timeout that will mark the shard as ready if there are still unavailable guilds - // * The timeout is 15 seconds by default - // * This can be optionally changed in the client options via the `waitGuildTimeout` option - // * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds. - - const { waitGuildTimeout } = this.manager.client.options; - - this.readyTimeout = setTimeout( - () => { - this.debug([ - hasGuildsIntent - ? `Shard did not receive any guild packets in ${waitGuildTimeout} ms.` - : 'Shard will not receive anymore guild packets.', - `Unavailable guild count: ${this.expectedGuilds.size}`, - ]); - - this.readyTimeout = null; - this.status = Status.Ready; - - this.emit(WebSocketShardEvents.AllReady, this.expectedGuilds); - }, - hasGuildsIntent ? waitGuildTimeout : 0, - ).unref(); - } - - /** - * Adds a packet to the queue to be sent to the gateway. - * If you use this method, make sure you understand that you need to provide - * a full [Payload](https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-commands). - * Do not use this method if you don't know what you're doing. - * @param {Object} data The full packet to send - * @param {boolean} [important=false] If this packet should be added first in queue - * This parameter is **deprecated**. Important payloads are determined by their opcode instead. - */ - send(data, important = false) { - if (important && !deprecationEmittedForImportant) { - process.emitWarning( - 'Sending important payloads explicitly is deprecated. They are determined by their opcode implicitly now.', - 'DeprecationWarning', - ); - deprecationEmittedForImportant = true; - } - this.manager._ws.send(this.id, data); - } -} - -module.exports = WebSocketShard; diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js b/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js index 141f0abe9e77..87d724b585c9 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_CREATE.js @@ -3,7 +3,7 @@ const Events = require('../../../util/Events'); const Status = require('../../../util/Status'); -module.exports = (client, { d: data }, shard) => { +module.exports = (client, { d: data }, shardId) => { let guild = client.guilds.cache.get(data.id); if (guild) { if (!guild.available && !data.unavailable) { @@ -19,9 +19,9 @@ module.exports = (client, { d: data }, shard) => { } } else { // A new guild - data.shardId = shard.id; + data.shardId = shardId; guild = client.guilds._add(data); - if (client.ws.status === Status.Ready) { + if (client.status === Status.Ready) { /** * Emitted whenever the client joins a guild. * @event Client#guildCreate diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js index fece5d76f456..53faae51fd5e 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_ADD.js @@ -1,20 +1,17 @@ 'use strict'; const Events = require('../../../util/Events'); -const Status = require('../../../util/Status'); -module.exports = (client, { d: data }, shard) => { +module.exports = (client, { d: data }) => { const guild = client.guilds.cache.get(data.guild_id); if (guild) { guild.memberCount++; const member = guild.members._add(data); - if (shard.status === Status.Ready) { - /** - * Emitted whenever a user joins a guild. - * @event Client#guildMemberAdd - * @param {GuildMember} member The member that has joined a guild - */ - client.emit(Events.GuildMemberAdd, member); - } + /** + * Emitted whenever a user joins a guild. + * @event Client#guildMemberAdd + * @param {GuildMember} member The member that has joined a guild + */ + client.emit(Events.GuildMemberAdd, member); } }; diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js index 72432af11bfd..81f67201669f 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_REMOVE.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = (client, packet, shard) => { - client.actions.GuildMemberRemove.handle(packet.d, shard); +module.exports = (client, packet) => { + client.actions.GuildMemberRemove.handle(packet.d); }; diff --git a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js index cafc6bd59a39..5dab27e8d8ca 100644 --- a/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js +++ b/packages/discord.js/src/client/websocket/handlers/GUILD_MEMBER_UPDATE.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = (client, packet, shard) => { - client.actions.GuildMemberUpdate.handle(packet.d, shard); +module.exports = (client, packet) => { + client.actions.GuildMemberUpdate.handle(packet.d); }; diff --git a/packages/discord.js/src/client/websocket/handlers/READY.js b/packages/discord.js/src/client/websocket/handlers/READY.js index 82da01cf7952..5ff890248072 100644 --- a/packages/discord.js/src/client/websocket/handlers/READY.js +++ b/packages/discord.js/src/client/websocket/handlers/READY.js @@ -3,7 +3,7 @@ const ClientApplication = require('../../../structures/ClientApplication'); let ClientUser; -module.exports = (client, { d: data }, shard) => { +module.exports = (client, { d: data }, shardId) => { if (client.user) { client.user._patch(data.user); } else { @@ -13,7 +13,7 @@ module.exports = (client, { d: data }, shard) => { } for (const guild of data.guilds) { - guild.shardId = shard.id; + guild.shardId = shardId; client.guilds._add(guild); } @@ -22,6 +22,4 @@ module.exports = (client, { d: data }, shard) => { } else { client.application = new ClientApplication(client, data.application); } - - shard.checkReady(); }; diff --git a/packages/discord.js/src/client/websocket/handlers/RESUMED.js b/packages/discord.js/src/client/websocket/handlers/RESUMED.js deleted file mode 100644 index 27ed7ddc5df3..000000000000 --- a/packages/discord.js/src/client/websocket/handlers/RESUMED.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -const Events = require('../../../util/Events'); - -module.exports = (client, packet, shard) => { - const replayed = shard.sessionInfo.sequence - shard.closeSequence; - /** - * Emitted when a shard resumes successfully. - * @event Client#shardResume - * @param {number} id The shard id that resumed - * @param {number} replayedEvents The amount of replayed events - */ - client.emit(Events.ShardResume, shard.id, replayed); -}; diff --git a/packages/discord.js/src/client/websocket/handlers/index.js b/packages/discord.js/src/client/websocket/handlers/index.js index e18b0196e1ec..4af0714a12bf 100644 --- a/packages/discord.js/src/client/websocket/handlers/index.js +++ b/packages/discord.js/src/client/websocket/handlers/index.js @@ -49,7 +49,6 @@ const handlers = Object.fromEntries([ ['MESSAGE_UPDATE', require('./MESSAGE_UPDATE')], ['PRESENCE_UPDATE', require('./PRESENCE_UPDATE')], ['READY', require('./READY')], - ['RESUMED', require('./RESUMED')], ['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')], ['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')], ['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')], diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index 7d7b0b99ec1b..c1552392aa90 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -12,8 +12,25 @@ * @property {'TokenMissing'} TokenMissing * @property {'ApplicationCommandPermissionsTokenMissing'} ApplicationCommandPermissionsTokenMissing -* @property {'BitFieldInvalid'} BitFieldInvalid - + * @property {'WSCloseRequested'} WSCloseRequested + * This property is deprecated. + * @property {'WSConnectionExists'} WSConnectionExists + * This property is deprecated. + * @property {'WSNotOpen'} WSNotOpen + * This property is deprecated. + * @property {'ManagerDestroyed'} ManagerDestroyed + * This property is deprecated. + + * @property {'BitFieldInvalid'} BitFieldInvalid + + * @property {'ShardingInvalid'} ShardingInvalid + * This property is deprecated. + * @property {'ShardingRequired'} ShardingRequired + * This property is deprecated. + * @property {'InvalidIntents'} InvalidIntents + * This property is deprecated. + * @property {'DisallowedIntents'} DisallowedIntents + * This property is deprecated. * @property {'ShardingNoShards'} ShardingNoShards * @property {'ShardingInProcess'} ShardingInProcess * @property {'ShardingInvalidEvalBroadcast'} ShardingInvalidEvalBroadcast @@ -32,10 +49,30 @@ * @property {'InviteOptionsMissingChannel'} InviteOptionsMissingChannel + * @property {'ButtonLabel'} ButtonLabel + * This property is deprecated. + * @property {'ButtonURL'} ButtonURL + * This property is deprecated. + * @property {'ButtonCustomId'} ButtonCustomId + * This property is deprecated. + + * @property {'SelectMenuCustomId'} SelectMenuCustomId + * This property is deprecated. + * @property {'SelectMenuPlaceholder'} SelectMenuPlaceholder + * This property is deprecated. + * @property {'SelectOptionLabel'} SelectOptionLabel + * This property is deprecated. + * @property {'SelectOptionValue'} SelectOptionValue + * This property is deprecated. + * @property {'SelectOptionDescription'} SelectOptionDescription + * This property is deprecated. + * @property {'InteractionCollectorError'} InteractionCollectorError * @property {'FileNotFound'} FileNotFound + * @property {'UserBannerNotFetched'} UserBannerNotFetched + * This property is deprecated. * @property {'UserNoDMChannel'} UserNoDMChannel * @property {'VoiceNotStageChannel'} VoiceNotStageChannel @@ -45,11 +82,19 @@ * @property {'ReqResourceType'} ReqResourceType + * @property {'ImageFormat'} ImageFormat + * This property is deprecated. + * @property {'ImageSize'} ImageSize + * This property is deprecated. + * @property {'MessageBulkDeleteType'} MessageBulkDeleteType * @property {'MessageContentType'} MessageContentType * @property {'MessageNonceRequired'} MessageNonceRequired * @property {'MessageNonceType'} MessageNonceType + * @property {'SplitMaxLen'} SplitMaxLen + * This property is deprecated. + * @property {'BanResolveId'} BanResolveId * @property {'FetchBanResolveId'} FetchBanResolveId @@ -83,11 +128,16 @@ * @property {'EmojiType'} EmojiType * @property {'EmojiManaged'} EmojiManaged * @property {'MissingManageGuildExpressionsPermission'} MissingManageGuildExpressionsPermission + * @property {'MissingManageEmojisAndStickersPermission'} MissingManageEmojisAndStickersPermission + * This property is deprecated. Use `MissingManageGuildExpressionsPermission` instead. * * @property {'NotGuildSticker'} NotGuildSticker * @property {'ReactionResolveUser'} ReactionResolveUser + * @property {'VanityURL'} VanityURL + * This property is deprecated. + * @property {'InviteResolveCode'} InviteResolveCode * @property {'InviteNotFound'} InviteNotFound @@ -102,6 +152,8 @@ * @property {'InteractionAlreadyReplied'} InteractionAlreadyReplied * @property {'InteractionNotReplied'} InteractionNotReplied + * @property {'InteractionEphemeralReplied'} InteractionEphemeralReplied + * This property is deprecated. * @property {'CommandInteractionOptionNotFound'} CommandInteractionOptionNotFound * @property {'CommandInteractionOptionType'} CommandInteractionOptionType @@ -140,8 +192,17 @@ const keys = [ 'TokenMissing', 'ApplicationCommandPermissionsTokenMissing', + 'WSCloseRequested', + 'WSConnectionExists', + 'WSNotOpen', + 'ManagerDestroyed', + 'BitFieldInvalid', + 'ShardingInvalid', + 'ShardingRequired', + 'InvalidIntents', + 'DisallowedIntents', 'ShardingNoShards', 'ShardingInProcess', 'ShardingInvalidEvalBroadcast', @@ -160,10 +221,21 @@ const keys = [ 'InviteOptionsMissingChannel', + 'ButtonLabel', + 'ButtonURL', + 'ButtonCustomId', + + 'SelectMenuCustomId', + 'SelectMenuPlaceholder', + 'SelectOptionLabel', + 'SelectOptionValue', + 'SelectOptionDescription', + 'InteractionCollectorError', 'FileNotFound', + 'UserBannerNotFetched', 'UserNoDMChannel', 'VoiceNotStageChannel', @@ -173,11 +245,16 @@ const keys = [ 'ReqResourceType', + 'ImageFormat', + 'ImageSize', + 'MessageBulkDeleteType', 'MessageContentType', 'MessageNonceRequired', 'MessageNonceType', + 'SplitMaxLen', + 'BanResolveId', 'FetchBanResolveId', @@ -211,11 +288,14 @@ const keys = [ 'EmojiType', 'EmojiManaged', 'MissingManageGuildExpressionsPermission', + 'MissingManageEmojisAndStickersPermission', 'NotGuildSticker', 'ReactionResolveUser', + 'VanityURL', + 'InviteResolveCode', 'InviteNotFound', @@ -230,6 +310,7 @@ const keys = [ 'InteractionAlreadyReplied', 'InteractionNotReplied', + 'InteractionEphemeralReplied', 'CommandInteractionOptionNotFound', 'CommandInteractionOptionType', diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 45e8ac4f797c..4297f221b269 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -48,7 +48,6 @@ exports.SystemChannelFlagsBitField = require('./util/SystemChannelFlagsBitField' exports.ThreadMemberFlagsBitField = require('./util/ThreadMemberFlagsBitField'); exports.UserFlagsBitField = require('./util/UserFlagsBitField'); __exportStar(require('./util/Util.js'), exports); -exports.WebSocketShardEvents = require('./util/WebSocketShardEvents'); exports.version = require('../package.json').version; // Managers @@ -88,8 +87,6 @@ exports.ThreadManager = require('./managers/ThreadManager'); exports.ThreadMemberManager = require('./managers/ThreadMemberManager'); exports.UserManager = require('./managers/UserManager'); exports.VoiceStateManager = require('./managers/VoiceStateManager'); -exports.WebSocketManager = require('./client/websocket/WebSocketManager'); -exports.WebSocketShard = require('./client/websocket/WebSocketShard'); // Structures exports.ActionRow = require('./structures/ActionRow'); diff --git a/packages/discord.js/src/managers/GuildManager.js b/packages/discord.js/src/managers/GuildManager.js index 1ab9090bd1ee..e02cd5e82e1a 100644 --- a/packages/discord.js/src/managers/GuildManager.js +++ b/packages/discord.js/src/managers/GuildManager.js @@ -273,7 +273,7 @@ class GuildManager extends CachedManager { const data = await this.client.rest.get(Routes.guild(id), { query: makeURLSearchParams({ with_counts: options.withCounts ?? true }), }); - data.shardId = ShardClientUtil.shardIdForGuildId(id, this.client.options.shardCount); + data.shardId = ShardClientUtil.shardIdForGuildId(id, await this.client.ws.fetchShardCount()); return this._add(data, options.cache); } diff --git a/packages/discord.js/src/managers/GuildMemberManager.js b/packages/discord.js/src/managers/GuildMemberManager.js index 4b1b48e62d1d..fb471328447f 100644 --- a/packages/discord.js/src/managers/GuildMemberManager.js +++ b/packages/discord.js/src/managers/GuildMemberManager.js @@ -235,7 +235,7 @@ class GuildMemberManager extends CachedManager { return new Promise((resolve, reject) => { if (!query && !users) query = ''; - this.guild.shard.send({ + this.guild.client.ws.send(this.guild.shardId, { op: GatewayOpcodes.RequestGuildMembers, d: { guild_id: this.guild.id, diff --git a/packages/discord.js/src/sharding/Shard.js b/packages/discord.js/src/sharding/Shard.js index 9d9da67d518e..da1d57b59e50 100644 --- a/packages/discord.js/src/sharding/Shard.js +++ b/packages/discord.js/src/sharding/Shard.js @@ -352,7 +352,7 @@ class Shard extends EventEmitter { if (message._ready) { this.ready = true; /** - * Emitted upon the shard's {@link Client#event:shardReady} event. + * Emitted upon the shard's {@link Client#event:clientReady} event. * @event Shard#ready */ this.emit(ShardEvents.Ready); @@ -363,29 +363,18 @@ class Shard extends EventEmitter { if (message._disconnect) { this.ready = false; /** - * Emitted upon the shard's {@link Client#event:shardDisconnect} event. + * Emitted upon the shard's {@link WebSocketShardEvents#Closed} event. * @event Shard#disconnect */ this.emit(ShardEvents.Disconnect); return; } - // Shard is attempting to reconnect - if (message._reconnecting) { - this.ready = false; - /** - * Emitted upon the shard's {@link Client#event:shardReconnecting} event. - * @event Shard#reconnecting - */ - this.emit(ShardEvents.Reconnecting); - return; - } - // Shard has resumed if (message._resume) { this.ready = true; /** - * Emitted upon the shard's {@link Client#event:shardResume} event. + * Emitted upon the shard's {@link WebSocketShardEvents#Resumed} event. * @event Shard#resume */ this.emit(ShardEvents.Resume); diff --git a/packages/discord.js/src/sharding/ShardClientUtil.js b/packages/discord.js/src/sharding/ShardClientUtil.js index c1bd4a800571..bec0aba52fcb 100644 --- a/packages/discord.js/src/sharding/ShardClientUtil.js +++ b/packages/discord.js/src/sharding/ShardClientUtil.js @@ -2,6 +2,7 @@ const process = require('node:process'); const { calculateShardId } = require('@discordjs/util'); +const { WebSocketShardEvents } = require('@discordjs/ws'); const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); const Events = require('../util/Events'); const { makeError, makePlainError } = require('../util/Util'); @@ -33,56 +34,32 @@ class ShardClientUtil { switch (mode) { case 'process': process.on('message', this._handleMessage.bind(this)); - client.on(Events.ShardReady, () => { + client.on(Events.ClientReady, () => { process.send({ _ready: true }); }); - client.on(Events.ShardDisconnect, () => { + client.ws.on(WebSocketShardEvents.Closed, () => { process.send({ _disconnect: true }); }); - client.on(Events.ShardReconnecting, () => { - process.send({ _reconnecting: true }); - }); - client.on(Events.ShardResume, () => { + client.ws.on(WebSocketShardEvents.Resumed, () => { process.send({ _resume: true }); }); break; case 'worker': this.parentPort = require('node:worker_threads').parentPort; this.parentPort.on('message', this._handleMessage.bind(this)); - client.on(Events.ShardReady, () => { + client.on(Events.ClientReady, () => { this.parentPort.postMessage({ _ready: true }); }); - client.on(Events.ShardDisconnect, () => { + client.ws.on(WebSocketShardEvents.Closed, () => { this.parentPort.postMessage({ _disconnect: true }); }); - client.on(Events.ShardReconnecting, () => { - this.parentPort.postMessage({ _reconnecting: true }); - }); - client.on(Events.ShardResume, () => { + client.ws.on(WebSocketShardEvents.Resumed, () => { this.parentPort.postMessage({ _resume: true }); }); break; } } - /** - * Array of shard ids of this client - * @type {number[]} - * @readonly - */ - get ids() { - return this.client.options.shards; - } - - /** - * Total number of shards - * @type {number} - * @readonly - */ - get count() { - return this.client.options.shardCount; - } - /** * Sends a message to the master process. * @param {*} message Message to send diff --git a/packages/discord.js/src/structures/ClientPresence.js b/packages/discord.js/src/structures/ClientPresence.js index f8e45af916ab..bec6ab169ef6 100644 --- a/packages/discord.js/src/structures/ClientPresence.js +++ b/packages/discord.js/src/structures/ClientPresence.js @@ -16,19 +16,19 @@ class ClientPresence extends Presence { /** * Sets the client's presence * @param {PresenceData} presence The data to set the presence to - * @returns {ClientPresence} + * @returns {Promise} */ - set(presence) { + async set(presence) { const packet = this._parse(presence); this._patch(packet); if (presence.shardId === undefined) { - this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + await this.client._broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet }); } else if (Array.isArray(presence.shardId)) { - for (const shardId of presence.shardId) { - this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); - } + await Promise.all( + presence.shardId.map(shardId => this.client.ws.send(shardId, { op: GatewayOpcodes.PresenceUpdate, d: packet })), + ); } else { - this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + await this.client.ws.send(presence.shardId, { op: GatewayOpcodes.PresenceUpdate, d: packet }); } return this; } diff --git a/packages/discord.js/src/structures/ClientUser.js b/packages/discord.js/src/structures/ClientUser.js index 3da3a52794cb..98e795a70348 100644 --- a/packages/discord.js/src/structures/ClientUser.js +++ b/packages/discord.js/src/structures/ClientUser.js @@ -135,7 +135,7 @@ class ClientUser extends User { /** * Sets the full presence of the client user. * @param {PresenceData} data Data for the presence - * @returns {ClientPresence} + * @returns {Promise} * @example * // Set the client user's presence * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' }); @@ -157,7 +157,7 @@ class ClientUser extends User { * Sets the status of the client user. * @param {PresenceStatusData} status Status to change to * @param {number|number[]} [shardId] Shard id(s) to have the activity set on - * @returns {ClientPresence} + * @returns {Promise} * @example * // Set the client user's status * client.user.setStatus('idle'); @@ -180,7 +180,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 {ActivityOptions} [options] Options for setting the activity - * @returns {ClientPresence} + * @returns {Promise} * @example * // Set the client user's activity * client.user.setActivity('discord.js', { type: ActivityType.Watching }); @@ -196,7 +196,7 @@ class ClientUser extends User { * Sets/removes the AFK flag for the client user. * @param {boolean} [afk=true] Whether or not the user is AFK * @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on - * @returns {ClientPresence} + * @returns {Promise} */ setAFK(afk = true, shardId) { return this.setPresence({ afk, shardId }); diff --git a/packages/discord.js/src/structures/Guild.js b/packages/discord.js/src/structures/Guild.js index 6fc78f07a8ca..733b6094b17d 100644 --- a/packages/discord.js/src/structures/Guild.js +++ b/packages/discord.js/src/structures/Guild.js @@ -27,7 +27,6 @@ const RoleManager = require('../managers/RoleManager'); const StageInstanceManager = require('../managers/StageInstanceManager'); const VoiceStateManager = require('../managers/VoiceStateManager'); const { resolveImage } = require('../util/DataResolver'); -const Status = require('../util/Status'); const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); const { discordSort, getSortableGroupTypes, resolvePartialEmoji } = require('../util/Util'); @@ -126,15 +125,6 @@ class Guild extends AnonymousGuild { this.shardId = data.shardId; } - /** - * The Shard this Guild belongs to. - * @type {WebSocketShard} - * @readonly - */ - get shard() { - return this.client.ws.shards.get(this.shardId); - } - _patch(data) { super._patch(data); this.id = data.id; @@ -1418,8 +1408,7 @@ class Guild extends AnonymousGuild { this.client.voice.adapters.set(this.id, methods); return { sendPayload: data => { - if (this.shard.status !== Status.Ready) return false; - this.shard.send(data); + this.client.ws.send(this.shardId, data); return true; }, destroy: () => { diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 43a8bcf81ada..8737623d62e6 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -325,6 +325,10 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayDispatchEvents} */ +/** + * @external GatewayDispatchPayload + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#GatewayDispatchPayload} + */ /** * @external GatewayIntentBits * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GatewayIntentBits} diff --git a/packages/discord.js/src/util/Events.js b/packages/discord.js/src/util/Events.js index a2de59453452..1ab65a13d8f5 100644 --- a/packages/discord.js/src/util/Events.js +++ b/packages/discord.js/src/util/Events.js @@ -61,11 +61,6 @@ * @property {string} MessageReactionRemoveEmoji messageReactionRemoveEmoji * @property {string} MessageUpdate messageUpdate * @property {string} PresenceUpdate presenceUpdate - * @property {string} ShardDisconnect shardDisconnect - * @property {string} ShardError shardError - * @property {string} ShardReady shardReady - * @property {string} ShardReconnecting shardReconnecting - * @property {string} ShardResume shardResume * @property {string} StageInstanceCreate stageInstanceCreate * @property {string} StageInstanceDelete stageInstanceDelete * @property {string} StageInstanceUpdate stageInstanceUpdate @@ -99,7 +94,7 @@ module.exports = { ChannelDelete: 'channelDelete', ChannelPinsUpdate: 'channelPinsUpdate', ChannelUpdate: 'channelUpdate', - ClientReady: 'ready', + ClientReady: 'clientReady', Debug: 'debug', EntitlementCreate: 'entitlementCreate', EntitlementUpdate: 'entitlementUpdate', @@ -148,12 +143,6 @@ module.exports = { MessageReactionRemoveEmoji: 'messageReactionRemoveEmoji', MessageUpdate: 'messageUpdate', PresenceUpdate: 'presenceUpdate', - Raw: 'raw', - ShardDisconnect: 'shardDisconnect', - ShardError: 'shardError', - ShardReady: 'shardReady', - ShardReconnecting: 'shardReconnecting', - ShardResume: 'shardResume', StageInstanceCreate: 'stageInstanceCreate', StageInstanceDelete: 'stageInstanceDelete', StageInstanceUpdate: 'stageInstanceUpdate', diff --git a/packages/discord.js/src/util/Options.js b/packages/discord.js/src/util/Options.js index 04ec75840c5e..14af76ea692c 100644 --- a/packages/discord.js/src/util/Options.js +++ b/packages/discord.js/src/util/Options.js @@ -1,6 +1,7 @@ 'use strict'; const { DefaultRestOptions, DefaultUserAgentAppendix } = require('@discordjs/rest'); +const { DefaultWebSocketManagerOptions } = require('@discordjs/ws'); const { toSnakeCase } = require('./Transformers'); const { version } = require('../../package.json'); @@ -16,13 +17,8 @@ const { version } = require('../../package.json'); /** * Options for a client. * @typedef {Object} ClientOptions - * @property {number|number[]|string} [shards] The shard's id to run, or an array of shard ids. If not specified, - * the client will spawn {@link ClientOptions#shardCount} shards. If set to `auto`, it will fetch the - * recommended amount of shards from Discord and spawn that amount * @property {number} [closeTimeout=5_000] The amount of time in milliseconds to wait for the close frame to be received * from the WebSocket. Don't have this too high/low. It's best to have it between 2_000-6_000 ms. - * @property {number} [shardCount=1] The total amount of shards used by all processes of this bot - * (e.g. recommended shard count, shard count of the ShardingManager) * @property {CacheFactory} [makeCache] Function to create a cache. * You can use your own function, or the {@link Options} class to customize the Collection used for the cache. * Overriding the cache used in `GuildManager`, `ChannelManager`, `GuildChannelManager`, `RoleManager`, @@ -33,12 +29,12 @@ const { version } = require('../../package.json'); * [guide](https://discordjs.guide/popular-topics/partials.html) for some * important usage information, as partials require you to put checks in place when handling data. * @property {boolean} [failIfNotExists=true] The default value for {@link MessageReplyOptions#failIfNotExists} - * @property {PresenceData} [presence={}] Presence data to use upon login + * @property {PresenceData} [presence] Presence data to use upon login * @property {IntentsResolvable} intents Intents to enable for this connection * @property {number} [waitGuildTimeout=15_000] Time in milliseconds that clients with the * {@link GatewayIntentBits.Guilds} gateway intent should wait for missing guilds to be received before being ready. * @property {SweeperOptions} [sweepers=this.DefaultSweeperSettings] Options for cache sweeping - * @property {WebsocketOptions} [ws] Options for the WebSocket + * @property {WebSocketManagerOptions} [ws] Options for the WebSocketManager * @property {RESTOptions} [rest] Options for the REST manager * @property {Function} [jsonTransformer] A function used to transform outgoing json data * @property {boolean} [enforceNonce=false] The default value for {@link MessageReplyOptions#enforceNonce} @@ -60,40 +56,6 @@ const { version } = require('../../package.json'); * This property is optional when the key is `invites`, `messages`, or `threads` and `lifetime` is set */ -/** - * A function to determine what strategy to use for sharding internally. - * ```js - * (manager) => new WorkerShardingStrategy(manager, { shardsPerWorker: 2 }) - * ``` - * @typedef {Function} BuildStrategyFunction - * @param {WSWebSocketManager} manager The WebSocketManager that is going to initiate the sharding - * @returns {IShardingStrategy} The strategy to use for sharding - */ - -/** - * A function to change the concurrency handling for shard identifies of this manager - * ```js - * async (manager) => { - * const gateway = await manager.fetchGatewayInformation(); - * return new SimpleIdentifyThrottler(gateway.session_start_limit.max_concurrency); - * } - * ``` - * @typedef {Function} IdentifyThrottlerFunction - * @param {WSWebSocketManager} manager The WebSocketManager that is going to initiate the sharding - * @returns {Awaitable} The identify throttler that this ws manager will use - */ - -/** - * WebSocket options (these are left as snake_case to match the API) - * @typedef {Object} WebsocketOptions - * @property {number} [large_threshold=50] Number of members in a guild after which offline users will no longer be - * sent in the initial guild member list, must be between 50 and 250 - * @property {number} [version=10] The Discord gateway version to use Changing this can break the library; - * only set this if you know what you are doing - * @property {BuildStrategyFunction} [buildStrategy] Builds the strategy to use for sharding - * @property {IdentifyThrottlerFunction} [buildIdentifyThrottler] Builds the identify throttler to use for sharding - */ - /** * Contains various utilities for client options. */ @@ -114,15 +76,14 @@ class Options extends null { return { closeTimeout: 5_000, waitGuildTimeout: 15_000, - shardCount: 1, makeCache: this.cacheWithLimits(this.DefaultMakeCacheSettings), partials: [], failIfNotExists: true, enforceNonce: false, - presence: {}, sweepers: this.DefaultSweeperSettings, ws: { - large_threshold: 50, + ...DefaultWebSocketManagerOptions, + largeThreshold: 50, version: 10, }, rest: { @@ -224,7 +185,7 @@ module.exports = Options; */ /** - * @external WSWebSocketManager + * @external WebSocketManager * @see {@link https://discord.js.org/docs/packages/ws/stable/WebSocketManager:Class} */ diff --git a/packages/discord.js/src/util/Status.js b/packages/discord.js/src/util/Status.js index e5241971c36e..c9daddc27c9e 100644 --- a/packages/discord.js/src/util/Status.js +++ b/packages/discord.js/src/util/Status.js @@ -5,14 +5,8 @@ const { createEnum } = require('./Enums'); /** * @typedef {Object} Status * @property {number} Ready - * @property {number} Connecting - * @property {number} Reconnecting * @property {number} Idle - * @property {number} Nearly - * @property {number} Disconnected * @property {number} WaitingForGuilds - * @property {number} Identifying - * @property {number} Resuming */ // JSDoc for IntelliSense purposes @@ -20,14 +14,4 @@ const { createEnum } = require('./Enums'); * @type {Status} * @ignore */ -module.exports = createEnum([ - 'Ready', - 'Connecting', - 'Reconnecting', - 'Idle', - 'Nearly', - 'Disconnected', - 'WaitingForGuilds', - 'Identifying', - 'Resuming', -]); +module.exports = createEnum(['Ready', 'Idle', 'WaitingForGuilds']); diff --git a/packages/discord.js/src/util/WebSocketShardEvents.js b/packages/discord.js/src/util/WebSocketShardEvents.js deleted file mode 100644 index 81e05f2c405a..000000000000 --- a/packages/discord.js/src/util/WebSocketShardEvents.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -/** - * @typedef {Object} WebSocketShardEvents - * @property {string} Close close - * @property {string} Destroyed destroyed - * @property {string} InvalidSession invalidSession - * @property {string} Ready ready - * @property {string} Resumed resumed - * @property {string} AllReady allReady - */ - -// JSDoc for IntelliSense purposes -/** - * @type {WebSocketShardEvents} - * @ignore - */ -module.exports = { - Close: 'close', - Destroyed: 'destroyed', - InvalidSession: 'invalidSession', - Ready: 'ready', - Resumed: 'resumed', - AllReady: 'allReady', -}; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 42614f56a126..fa08c5f54099 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -20,12 +20,7 @@ import { import { Awaitable, JSONEncodable } from '@discordjs/util'; import { Collection, ReadonlyCollection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; -import { - WebSocketManager as WSWebSocketManager, - IShardingStrategy, - IIdentifyThrottler, - SessionInfo, -} from '@discordjs/ws'; +import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws'; import { APIActionRowComponent, APIApplicationCommandInteractionData, @@ -50,7 +45,6 @@ import { ButtonStyle, ChannelType, ComponentType, - GatewayDispatchEvents, GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData, GuildFeature, @@ -170,6 +164,8 @@ import { GuildScheduledEventRecurrenceRuleWeekday, GuildScheduledEventRecurrenceRuleMonth, GuildScheduledEventRecurrenceRuleFrequency, + GatewaySendPayload, + GatewayDispatchPayload, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -956,8 +952,16 @@ export type If = Value ex export class Client extends BaseClient { public constructor(options: ClientOptions); private actions: unknown; + private expectedGuilds: Set; + private readonly packetQueue: unknown[]; private presence: ClientPresence; + private pings: Collection; + private readyTimeout: NodeJS.Timeout | null; + private _broadcast(packet: GatewaySendPayload): void; private _eval(script: string): unknown; + private _handlePacket(packet?: GatewayDispatchPayload, shardId?: number): boolean; + private _checkReady(): void; + private _triggerClientReady(): void; private _validateOptions(options: ClientOptions): void; private get _censoredToken(): string | null; // This a technique used to brand the ready state. Or else we'll get `never` errors on typeguard checks. @@ -979,17 +983,21 @@ export class Client extends BaseClient { public channels: ChannelManager; public get emojis(): BaseGuildEmojiManager; public guilds: GuildManager; + public lastPingTimestamp: number; public options: Omit & { intents: IntentsBitField }; + public get ping(): number; public get readyAt(): If; public readyTimestamp: If; public sweepers: Sweepers; public shard: ShardClientUtil | null; + public status: Status; public token: If; public get uptime(): If; public user: If; public users: UserManager; public voice: ClientVoiceManager; public ws: WebSocketManager; + public destroy(): Promise; public deleteWebhook(id: Snowflake, options?: WebhookDeleteOptions): Promise; public fetchGuildPreview(guild: GuildResolvable): Promise; @@ -1431,7 +1439,6 @@ export class Guild extends AnonymousGuild { public get safetyAlertsChannel(): TextChannel | null; public safetyAlertsChannelId: Snowflake | null; public scheduledEvents: GuildScheduledEventManager; - public get shard(): WebSocketShard; public shardId: number; public stageInstances: StageInstanceManager; public stickers: GuildStickerManager; @@ -3632,70 +3639,6 @@ export class WebhookClient extends BaseClient { public send(options: string | MessagePayload | WebhookMessageCreateOptions): Promise; } -export class WebSocketManager extends EventEmitter { - private constructor(client: Client); - private readonly packetQueue: unknown[]; - private destroyed: boolean; - - public readonly client: Client; - public gateway: string | null; - public shards: Collection; - public status: Status; - public get ping(): number; - - public on(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this; - public once(event: GatewayDispatchEvents, listener: (data: any, shardId: number) => void): this; - - private debug(messages: readonly string[], shardId?: number): void; - private connect(): Promise; - private broadcast(packet: unknown): void; - private destroy(): Promise; - private handlePacket(packet?: unknown, shard?: WebSocketShard): boolean; - private checkShardsReady(): void; - private triggerClientReady(): void; -} - -export interface WebSocketShardEventTypes { - ready: []; - resumed: []; - invalidSession: []; - destroyed: []; - close: [event: CloseEvent]; - allReady: [unavailableGuilds?: Set]; -} - -export class WebSocketShard extends EventEmitter { - private constructor(manager: WebSocketManager, id: number); - private closeSequence: number; - private sessionInfo: SessionInfo | null; - public lastPingTimestamp: number; - private expectedGuilds: Set | null; - private readyTimeout: NodeJS.Timeout | null; - - public manager: WebSocketManager; - public id: number; - public status: Status; - public ping: number; - - private debug(messages: readonly string[]): void; - private onReadyPacket(packet: unknown): void; - private gotGuild(guildId: Snowflake): void; - private checkReady(): void; - private emitClose(event?: CloseEvent): void; - - public send(data: unknown, important?: boolean): void; - - public on( - event: Event, - listener: (...args: WebSocketShardEventTypes[Event]) => void, - ): this; - - public once( - event: Event, - listener: (...args: WebSocketShardEventTypes[Event]) => void, - ): this; -} - export class Widget extends Base { private constructor(client: Client, data: RawWidgetData); private _patch(data: RawWidgetData): void; @@ -5133,6 +5076,7 @@ export interface ClientEvents { oldChannel: DMChannel | NonThreadGuildBasedChannel, newChannel: DMChannel | NonThreadGuildBasedChannel, ]; + clientReady: [client: Client]; debug: [message: string]; warn: [message: string]; emojiCreate: [emoji: GuildEmoji]; @@ -5186,7 +5130,6 @@ export interface ClientEvents { newMessage: OmitPartialGroupDMChannel, ]; presenceUpdate: [oldPresence: Presence | null, newPresence: Presence]; - ready: [client: Client]; invalidated: []; roleCreate: [role: Role]; roleDelete: [role: Role]; @@ -5206,11 +5149,6 @@ export interface ClientEvents { voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; webhooksUpdate: [channel: TextChannel | NewsChannel | VoiceChannel | ForumChannel | MediaChannel]; interactionCreate: [interaction: Interaction]; - shardDisconnect: [closeEvent: CloseEvent, shardId: number]; - shardError: [error: Error, shardId: number]; - shardReady: [shardId: number, unavailableGuilds: Set | undefined]; - shardReconnecting: [shardId: number]; - shardResume: [shardId: number, replayedEvents: number]; stageInstanceCreate: [stageInstance: StageInstance]; stageInstanceUpdate: [oldStageInstance: StageInstance | null, newStageInstance: StageInstance]; stageInstanceDelete: [stageInstance: StageInstance]; @@ -5232,8 +5170,6 @@ export interface ClientFetchInviteOptions { } export interface ClientOptions { - shards?: number | readonly number[] | 'auto'; - shardCount?: number; closeTimeout?: number; makeCache?: CacheFactory; allowedMentions?: MessageMentionOptions; @@ -5243,7 +5179,7 @@ export interface ClientOptions { intents: BitFieldResolvable; waitGuildTimeout?: number; sweepers?: SweeperOptions; - ws?: WebSocketOptions; + ws?: Partial; rest?: Partial; jsonTransformer?: (obj: unknown) => unknown; enforceNonce?: boolean; @@ -5263,14 +5199,6 @@ export interface ClientUserEditOptions { banner?: BufferResolvable | Base64Resolvable | null; } -export interface CloseEvent { - /** @deprecated Not used anymore since using {@link @discordjs/ws#(WebSocketManager:class)} internally */ - wasClean: boolean; - code: number; - /** @deprecated Not used anymore since using {@link @discordjs/ws#(WebSocketManager:class)} internally */ - reason: string; -} - export type CollectorFilter = (...args: Arguments) => Awaitable; export interface CollectorOptions { @@ -5364,7 +5292,7 @@ export enum Events { AutoModerationRuleCreate = 'autoModerationRuleCreate', AutoModerationRuleDelete = 'autoModerationRuleDelete', AutoModerationRuleUpdate = 'autoModerationRuleUpdate', - ClientReady = 'ready', + ClientReady = 'clientReady', EntitlementCreate = 'entitlementCreate', EntitlementDelete = 'entitlementDelete', EntitlementUpdate = 'entitlementUpdate', @@ -5452,15 +5380,6 @@ export enum ShardEvents { Spawn = 'spawn', } -export enum WebSocketShardEvents { - Close = 'close', - Destroyed = 'destroyed', - InvalidSession = 'invalidSession', - Ready = 'ready', - Resumed = 'resumed', - AllReady = 'allReady', -} - export enum Status { Ready = 0, Connecting = 1, @@ -6879,13 +6798,6 @@ export interface WebhookMessageCreateOptions extends Omit; -} - export interface WidgetActivity { name: string; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index df99d3f29a8f..9fc56a9812d8 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -49,7 +49,6 @@ import { Client, ClientApplication, ClientUser, - CloseEvent, Collection, ChatInputCommandInteraction, CommandInteractionOption, @@ -100,7 +99,6 @@ import { User, VoiceChannel, Shard, - WebSocketShard, Collector, GuildAuditLogsEntry, GuildAuditLogs, @@ -112,7 +110,6 @@ import { RepliableInteraction, ThreadChannelType, Events, - WebSocketShardEvents, Status, CategoryChannelChildManager, ActionRowData, @@ -677,7 +674,7 @@ client.on('presenceUpdate', (oldPresence, { client }) => { declare const slashCommandBuilder: SlashCommandBuilder; declare const contextMenuCommandBuilder: ContextMenuCommandBuilder; -client.on('ready', async client => { +client.on('clientReady', async client => { expectType>(client); console.log(`Client is logged in as ${client.user.tag} and ready!`); @@ -1305,8 +1302,8 @@ client.on('guildCreate', async g => { }); // EventEmitter static method overrides -expectType]>>(Client.once(client, 'ready')); -expectType]>>(Client.on(client, 'ready')); +expectType]>>(Client.once(client, 'clientReady')); +expectType]>>(Client.on(client, 'clientReady')); client.login('absolutely-valid-token'); @@ -1426,7 +1423,6 @@ reactionCollector.on('dispose', (...args) => { // Make sure the properties are typed correctly, and that no backwards properties // (K -> V and V -> K) exist: expectAssignable<'messageCreate'>(Events.MessageCreate); -expectAssignable<'close'>(WebSocketShardEvents.Close); expectAssignable<'death'>(ShardEvents.Death); expectAssignable<1>(Status.Connecting); @@ -2100,12 +2096,6 @@ shard.on('death', process => { expectType(process); }); -declare const webSocketShard: WebSocketShard; - -webSocketShard.on('close', event => { - expectType(event); -}); - declare const collector: Collector; collector.on('collect', (collected, ...other) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2340a8c5d1c6..1ff2a3ee1619 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -932,8 +932,8 @@ importers: specifier: workspace:^ version: link:../util '@discordjs/ws': - specifier: 1.1.1 - version: 1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) + specifier: workspace:^ + version: link:../ws '@sapphire/snowflake': specifier: 3.5.3 version: 3.5.3 @@ -2609,10 +2609,6 @@ packages: resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} engines: {node: '>=16.11.0'} - '@discordjs/collection@2.1.0': - resolution: {integrity: sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==} - engines: {node: '>=18'} - '@discordjs/formatters@0.5.0': resolution: {integrity: sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==} engines: {node: '>=18'} @@ -2625,22 +2621,10 @@ packages: resolution: {integrity: sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==} engines: {node: '>=12.0.0'} - '@discordjs/rest@2.3.0': - resolution: {integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==} - engines: {node: '>=16.11.0'} - - '@discordjs/util@1.1.0': - resolution: {integrity: sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==} - engines: {node: '>=16.11.0'} - '@discordjs/util@1.1.1': resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} engines: {node: '>=18'} - '@discordjs/ws@1.1.1': - resolution: {integrity: sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==} - engines: {node: '>=16.11.0'} - '@edge-runtime/format@2.2.1': resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} engines: {node: '>=16'} @@ -7631,9 +7615,6 @@ packages: discord-api-types@0.37.101: resolution: {integrity: sha512-2wizd94t7G3A8U5Phr3AiuL4gSvhqistDwWnlk1VLTit8BI1jWUncFqFQNdPbHqS3661+Nx/iEyIwtVjPuBP3w==} - discord-api-types@0.37.83: - resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} - discord-api-types@0.37.97: resolution: {integrity: sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==} @@ -13035,10 +13016,6 @@ packages: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} - undici@6.13.0: - resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==} - engines: {node: '>=18.0'} - undici@6.19.8: resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} engines: {node: '>=18.17'} @@ -14857,8 +14834,6 @@ snapshots: '@discordjs/collection@1.5.3': {} - '@discordjs/collection@2.1.0': {} - '@discordjs/formatters@0.5.0': dependencies: discord-api-types: 0.37.97 @@ -14886,37 +14861,8 @@ snapshots: - encoding - supports-color - '@discordjs/rest@2.3.0': - dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.3 - '@sapphire/snowflake': 3.5.3 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.83 - magic-bytes.js: 1.10.0 - tslib: 2.6.3 - undici: 6.13.0 - - '@discordjs/util@1.1.0': {} - '@discordjs/util@1.1.1': {} - '@discordjs/ws@1.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)': - dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/rest': 2.3.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.3 - '@types/ws': 8.5.12 - '@vladfrangu/async_event_emitter': 2.4.6 - discord-api-types: 0.37.83 - tslib: 2.6.3 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@edge-runtime/format@2.2.1': {} '@edge-runtime/node-utils@2.3.0': {} @@ -21518,8 +21464,6 @@ snapshots: discord-api-types@0.37.101: {} - discord-api-types@0.37.83: {} - discord-api-types@0.37.97: {} dlv@1.1.3: {} @@ -28791,8 +28735,6 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - undici@6.13.0: {} - undici@6.19.8: {} unicode-canonical-property-names-ecmascript@2.0.0: {} From c8ef899a68d0746709c15d944e4758393e2b3019 Mon Sep 17 00:00:00 2001 From: Naiyar <137700126+imnaiyar@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:35:12 +0600 Subject: [PATCH 59/65] refactor(NewsChannel)!: rename NewsChannel to AnnouncementChannel (#10532) BREAKING CHANGE: The `NewsChannel` class was renamed to `AnnouncementChannel`, in line with the type name change --- .../src/client/actions/ThreadListSync.js | 2 +- .../src/client/actions/WebhooksUpdate.js | 2 +- packages/discord.js/src/errors/Messages.js | 2 +- packages/discord.js/src/index.js | 2 +- .../src/managers/GuildChannelManager.js | 14 ++--- .../src/managers/GuildInviteManager.js | 4 +- .../src/managers/GuildTextThreadManager.js | 4 +- .../discord.js/src/managers/ThreadManager.js | 2 +- ...{NewsChannel.js => AnnouncementChannel.js} | 8 +-- .../src/structures/BaseGuildTextChannel.js | 2 +- packages/discord.js/src/structures/Guild.js | 14 ++--- .../discord.js/src/structures/GuildChannel.js | 2 +- .../src/structures/ThreadChannel.js | 2 +- packages/discord.js/src/structures/Webhook.js | 4 +- .../src/structures/WelcomeChannel.js | 2 +- packages/discord.js/src/util/Channels.js | 4 +- packages/discord.js/src/util/Constants.js | 4 +- packages/discord.js/typings/index.d.ts | 63 +++++++++++-------- packages/discord.js/typings/index.test-d.ts | 36 ++++++----- 19 files changed, 94 insertions(+), 79 deletions(-) rename packages/discord.js/src/structures/{NewsChannel.js => AnnouncementChannel.js} (83%) diff --git a/packages/discord.js/src/client/actions/ThreadListSync.js b/packages/discord.js/src/client/actions/ThreadListSync.js index b16fb85a5c0f..51e6ff292a57 100644 --- a/packages/discord.js/src/client/actions/ThreadListSync.js +++ b/packages/discord.js/src/client/actions/ThreadListSync.js @@ -36,7 +36,7 @@ class ThreadListSyncAction extends Action { } /** - * Emitted whenever the client user gains access to a text or news channel that contains threads + * Emitted whenever the client user gains access to a text or announcement channel that contains threads * @event Client#threadListSync * @param {Collection} threads The threads that were synced * @param {Guild} guild The guild that the threads were synced in diff --git a/packages/discord.js/src/client/actions/WebhooksUpdate.js b/packages/discord.js/src/client/actions/WebhooksUpdate.js index 3e3c46affc3e..53dbfc1757a4 100644 --- a/packages/discord.js/src/client/actions/WebhooksUpdate.js +++ b/packages/discord.js/src/client/actions/WebhooksUpdate.js @@ -12,7 +12,7 @@ class WebhooksUpdate extends Action { /** * Emitted whenever a channel has its webhooks changed. * @event Client#webhooksUpdate - * @param {TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel} channel + * @param {TextChannel|AnnouncementChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel} channel * The channel that had a webhook update */ client.emit(Events.WebhooksUpdate, channel); diff --git a/packages/discord.js/src/errors/Messages.js b/packages/discord.js/src/errors/Messages.js index 41e81dd3402f..2ee1ab3405e4 100644 --- a/packages/discord.js/src/errors/Messages.js +++ b/packages/discord.js/src/errors/Messages.js @@ -75,7 +75,7 @@ const Messages = { [DjsErrorCodes.InvalidType]: (name, expected, an = false) => `Supplied ${name} is not a${an ? 'n' : ''} ${expected}.`, [DjsErrorCodes.InvalidElement]: (type, name, elem) => `Supplied ${type} ${name} includes an invalid element: ${elem}`, - [DjsErrorCodes.MessageThreadParent]: 'The message was not sent in a guild text or news channel', + [DjsErrorCodes.MessageThreadParent]: 'The message was not sent in a guild text or announcement channel', [DjsErrorCodes.MessageExistingThread]: 'The message already has a thread', [DjsErrorCodes.ThreadInvitableType]: type => `Invitable cannot be edited on ${type}`, diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 4297f221b269..cdedc8aa45c0 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -161,7 +161,7 @@ exports.MessagePayload = require('./structures/MessagePayload'); exports.MessageReaction = require('./structures/MessageReaction'); exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction'); exports.ModalSubmitFields = require('./structures/ModalSubmitFields'); -exports.NewsChannel = require('./structures/NewsChannel'); +exports.AnnouncementChannel = require('./structures/AnnouncementChannel.js'); exports.OAuth2Guild = require('./structures/OAuth2Guild'); exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel'); exports.PermissionOverwrites = require('./structures/PermissionOverwrites'); diff --git a/packages/discord.js/src/managers/GuildChannelManager.js b/packages/discord.js/src/managers/GuildChannelManager.js index 61209f39d547..b981801b601f 100644 --- a/packages/discord.js/src/managers/GuildChannelManager.js +++ b/packages/discord.js/src/managers/GuildChannelManager.js @@ -99,15 +99,15 @@ class GuildChannelManager extends CachedManager { } /** - * Data that can be resolved to a News Channel object. This can be: - * * A NewsChannel object + * Data that can be resolved to an Announcement Channel object. This can be: + * * An Announcement Channel object * * A Snowflake - * @typedef {NewsChannel|Snowflake} NewsChannelResolvable + * @typedef {AnnouncementChannel|Snowflake} AnnouncementChannelResolvable */ /** * Adds the target channel to a channel's followers. - * @param {NewsChannelResolvable} channel The channel to follow + * @param {AnnouncementChannelResolvable} channel The channel to follow * @param {TextChannelResolvable} targetChannel The channel where published announcements will be posted at * @param {string} [reason] Reason for creating the webhook * @returns {Promise} Returns created target webhook id. @@ -115,7 +115,7 @@ class GuildChannelManager extends CachedManager { async addFollower(channel, targetChannel, reason) { const channelId = this.resolveId(channel); if (!channelId) { - throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'NewsChannelResolvable'); + throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'AnnouncementChannelResolvable'); } const targetChannelId = this.resolveId(targetChannel); if (!targetChannelId) { @@ -208,7 +208,7 @@ class GuildChannelManager extends CachedManager { /** * @typedef {ChannelWebhookCreateOptions} WebhookCreateOptions - * @property {TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel|Snowflake} channel + * @property {TextChannel|AnnouncementChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel|Snowflake} channel * The channel to create the webhook for */ @@ -247,7 +247,7 @@ class GuildChannelManager extends CachedManager { * Options used to edit a guild channel. * @typedef {Object} GuildChannelEditOptions * @property {string} [name] The name of the channel - * @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported) + * @property {ChannelType} [type] The type of the channel (only conversion between text and announcement is supported) * @property {number} [position] The position of the channel * @property {?string} [topic] The topic of the text channel * @property {boolean} [nsfw] Whether the channel is NSFW diff --git a/packages/discord.js/src/managers/GuildInviteManager.js b/packages/discord.js/src/managers/GuildInviteManager.js index 93b0c973a28e..641cf53ff8a8 100644 --- a/packages/discord.js/src/managers/GuildInviteManager.js +++ b/packages/discord.js/src/managers/GuildInviteManager.js @@ -43,12 +43,12 @@ class GuildInviteManager extends CachedManager { * Data that can be resolved to a channel that an invite can be created on. This can be: * * TextChannel * * VoiceChannel - * * NewsChannel + * * AnnouncementChannel * * StageChannel * * ForumChannel * * MediaChannel * * Snowflake - * @typedef {TextChannel|VoiceChannel|NewsChannel|StageChannel|ForumChannel|MediaChannel|Snowflake} + * @typedef {TextChannel|VoiceChannel|AnnouncementChannel|StageChannel|ForumChannel|MediaChannel|Snowflake} * GuildInvitableChannelResolvable */ diff --git a/packages/discord.js/src/managers/GuildTextThreadManager.js b/packages/discord.js/src/managers/GuildTextThreadManager.js index 9fdcd7141c52..84c6a341997d 100644 --- a/packages/discord.js/src/managers/GuildTextThreadManager.js +++ b/packages/discord.js/src/managers/GuildTextThreadManager.js @@ -12,7 +12,7 @@ class GuildTextThreadManager extends ThreadManager { /** * The channel this Manager belongs to * @name GuildTextThreadManager#channel - * @type {TextChannel|NewsChannel} + * @type {TextChannel|AnnouncementChannel} */ /** @@ -22,7 +22,7 @@ class GuildTextThreadManager extends ThreadManager { * If this is defined, then the `type` of thread gets inferred automatically and cannot be changed. * @property {ThreadChannelTypes} [type] The type of thread to create. * Defaults to {@link ChannelType.PublicThread} if created in a {@link TextChannel} - * When creating threads in a {@link NewsChannel}, this is ignored and is always + * When creating threads in a {@link AnnouncementChannel}, this is ignored and is always * {@link ChannelType.AnnouncementThread} * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread * Can only be set when type will be {@link ChannelType.PrivateThread} diff --git a/packages/discord.js/src/managers/ThreadManager.js b/packages/discord.js/src/managers/ThreadManager.js index ab32cc495dc3..a8bf2a67e83c 100644 --- a/packages/discord.js/src/managers/ThreadManager.js +++ b/packages/discord.js/src/managers/ThreadManager.js @@ -20,7 +20,7 @@ class ThreadManager extends CachedManager { /** * The channel this Manager belongs to - * @type {TextChannel|NewsChannel|ForumChannel|MediaChannel} + * @type {TextChannel|AnnouncementChannel|ForumChannel|MediaChannel} */ this.channel = channel; } diff --git a/packages/discord.js/src/structures/NewsChannel.js b/packages/discord.js/src/structures/AnnouncementChannel.js similarity index 83% rename from packages/discord.js/src/structures/NewsChannel.js rename to packages/discord.js/src/structures/AnnouncementChannel.js index 3f5aff3ac2bd..a23f8b26b67f 100644 --- a/packages/discord.js/src/structures/NewsChannel.js +++ b/packages/discord.js/src/structures/AnnouncementChannel.js @@ -5,15 +5,15 @@ const BaseGuildTextChannel = require('./BaseGuildTextChannel'); const { DiscordjsError, ErrorCodes } = require('../errors'); /** - * Represents a guild news channel on Discord. + * Represents a guild announcement channel on Discord. * @extends {BaseGuildTextChannel} */ -class NewsChannel extends BaseGuildTextChannel { +class AnnouncementChannel extends BaseGuildTextChannel { /** * Adds the target to this channel's followers. * @param {TextChannelResolvable} channel The channel where the webhook should be created * @param {string} [reason] Reason for creating the webhook - * @returns {Promise} + * @returns {Promise} * @example * if (channel.type === ChannelType.GuildAnnouncement) { * channel.addFollower('222197033908436994', 'Important announcements') @@ -29,4 +29,4 @@ class NewsChannel extends BaseGuildTextChannel { } } -module.exports = NewsChannel; +module.exports = AnnouncementChannel; diff --git a/packages/discord.js/src/structures/BaseGuildTextChannel.js b/packages/discord.js/src/structures/BaseGuildTextChannel.js index 9b8490ecc995..c271565d750a 100644 --- a/packages/discord.js/src/structures/BaseGuildTextChannel.js +++ b/packages/discord.js/src/structures/BaseGuildTextChannel.js @@ -101,7 +101,7 @@ class BaseGuildTextChannel extends GuildChannel { /** * Sets the type of this channel. - * Only conversion between {@link TextChannel} and {@link NewsChannel} is supported. + * Only conversion between {@link TextChannel} and {@link AnnouncementChannel} is supported. * @param {ChannelType.GuildText|ChannelType.GuildAnnouncement} type The new channel type * @param {string} [reason] Reason for changing the channel's type * @returns {Promise} diff --git a/packages/discord.js/src/structures/Guild.js b/packages/discord.js/src/structures/Guild.js index 733b6094b17d..fa11307fca39 100644 --- a/packages/discord.js/src/structures/Guild.js +++ b/packages/discord.js/src/structures/Guild.js @@ -514,7 +514,7 @@ class Guild extends AnonymousGuild { /** * Widget channel for this guild - * @type {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel)} + * @type {?(TextChannel|AnnouncementChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel)} * @readonly */ get widgetChannel() { @@ -687,7 +687,7 @@ class Guild extends AnonymousGuild { * Data for the Guild Widget Settings object * @typedef {Object} GuildWidgetSettings * @property {boolean} enabled Whether the widget is enabled - * @property {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel)} channel + * @property {?(TextChannel|AnnouncementChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel)} channel * The widget invite channel */ @@ -695,8 +695,8 @@ class Guild extends AnonymousGuild { * The Guild Widget Settings object * @typedef {Object} GuildWidgetSettingsData * @property {boolean} enabled Whether the widget is enabled - * @property {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|MediaChannel|Snowflake)} channel - * The widget invite channel + * @property {?(TextChannel|AnnouncementChannel|VoiceChannel|StageChannel|ForumChannel| + * MediaChannel|Snowflake)} channel The widget invite channel */ /** @@ -958,7 +958,7 @@ class Guild extends AnonymousGuild { * Welcome channel data * @typedef {Object} WelcomeChannelData * @property {string} description The description to show for this welcome channel - * @property {TextChannel|NewsChannel|ForumChannel|MediaChannel|Snowflake} channel + * @property {TextChannel|AnnouncementChannel|ForumChannel|MediaChannel|Snowflake} channel * The channel to link for this welcome channel * @property {EmojiIdentifierResolvable} [emoji] The emoji to display for this welcome channel */ @@ -974,9 +974,9 @@ class Guild extends AnonymousGuild { /** * Data that can be resolved to a GuildTextChannel object. This can be: * * A TextChannel - * * A NewsChannel + * * A AnnouncementChannel * * A Snowflake - * @typedef {TextChannel|NewsChannel|Snowflake} GuildTextChannelResolvable + * @typedef {TextChannel|AnnouncementChannel|Snowflake} GuildTextChannelResolvable */ /** diff --git a/packages/discord.js/src/structures/GuildChannel.js b/packages/discord.js/src/structures/GuildChannel.js index a25434a48300..e0e9ee027e11 100644 --- a/packages/discord.js/src/structures/GuildChannel.js +++ b/packages/discord.js/src/structures/GuildChannel.js @@ -14,7 +14,7 @@ const { getSortableGroupTypes } = require('../util/Util'); * - {@link TextChannel} * - {@link VoiceChannel} * - {@link CategoryChannel} - * - {@link NewsChannel} + * - {@link AnnouncementChannel} * - {@link StageChannel} * - {@link ForumChannel} * - {@link MediaChannel} diff --git a/packages/discord.js/src/structures/ThreadChannel.js b/packages/discord.js/src/structures/ThreadChannel.js index a0d8c2b383af..129fa3ae3ed1 100644 --- a/packages/discord.js/src/structures/ThreadChannel.js +++ b/packages/discord.js/src/structures/ThreadChannel.js @@ -250,7 +250,7 @@ class ThreadChannel extends BaseChannel { /** * The parent channel of this thread - * @type {?(NewsChannel|TextChannel|ForumChannel|MediaChannel)} + * @type {?(AnnouncementChannel|TextChannel|ForumChannel|MediaChannel)} * @readonly */ get parent() { diff --git a/packages/discord.js/src/structures/Webhook.js b/packages/discord.js/src/structures/Webhook.js index e52ec14040ae..7151b17cd112 100644 --- a/packages/discord.js/src/structures/Webhook.js +++ b/packages/discord.js/src/structures/Webhook.js @@ -116,7 +116,7 @@ class Webhook { if ('source_channel' in data) { /** * The source channel of the webhook - * @type {?(NewsChannel|APIChannel)} + * @type {?(AnnouncementChannel|APIChannel)} */ this.sourceChannel = this.client.channels?.resolve(data.source_channel?.id) ?? data.source_channel; } else { @@ -149,7 +149,7 @@ class Webhook { /** * The channel the webhook belongs to - * @type {?(TextChannel|VoiceChannel|StageChannel|NewsChannel|ForumChannel|MediaChannel)} + * @type {?(TextChannel|VoiceChannel|StageChannel|AnnouncementChannel|ForumChannel|MediaChannel)} * @readonly */ get channel() { diff --git a/packages/discord.js/src/structures/WelcomeChannel.js b/packages/discord.js/src/structures/WelcomeChannel.js index 3ca99a156c48..4b2c69ac7af4 100644 --- a/packages/discord.js/src/structures/WelcomeChannel.js +++ b/packages/discord.js/src/structures/WelcomeChannel.js @@ -42,7 +42,7 @@ class WelcomeChannel extends Base { /** * The channel of this welcome channel - * @type {?(TextChannel|NewsChannel|ForumChannel|MediaChannel)} + * @type {?(TextChannel|AnnouncementChannel|ForumChannel|MediaChannel)} */ get channel() { return this.client.channels.resolve(this.channelId); diff --git a/packages/discord.js/src/util/Channels.js b/packages/discord.js/src/util/Channels.js index 93918504528e..9c5310e5a21b 100644 --- a/packages/discord.js/src/util/Channels.js +++ b/packages/discord.js/src/util/Channels.js @@ -5,7 +5,7 @@ const { ChannelType } = require('discord-api-types/v10'); const getCategoryChannel = lazy(() => require('../structures/CategoryChannel')); const getDMChannel = lazy(() => require('../structures/DMChannel')); -const getNewsChannel = lazy(() => require('../structures/NewsChannel')); +const getAnnouncementChannel = lazy(() => require('../structures/AnnouncementChannel')); const getStageChannel = lazy(() => require('../structures/StageChannel')); const getTextChannel = lazy(() => require('../structures/TextChannel')); const getThreadChannel = lazy(() => require('../structures/ThreadChannel')); @@ -57,7 +57,7 @@ function createChannel(client, data, guild, { allowUnknownGuild } = {}) { break; } case ChannelType.GuildAnnouncement: { - channel = new (getNewsChannel())(guild, data, client); + channel = new (getAnnouncementChannel())(guild, data, client); break; } case ChannelType.GuildStageVoice: { diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index 0a2f48d0ef26..c4209cb3c4bc 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -65,11 +65,11 @@ exports.NonSystemMessageTypes = [ /** * The guild channels that are text-based. * * TextChannel - * * NewsChannel + * * AnnouncementChannel * * ThreadChannel * * VoiceChannel * * StageChannel - * @typedef {TextChannel|NewsChannel|ThreadChannel|VoiceChannel|StageChannel} GuildTextBasedChannel + * @typedef {TextChannel|AnnouncementChannel|ThreadChannel|VoiceChannel|StageChannel} GuildTextBasedChannel */ /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index fa08c5f54099..7867472f2903 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -642,7 +642,7 @@ export class BaseGuildTextChannel extends GuildChannel { public defaultThreadRateLimitPerUser: number | null; public rateLimitPerUser: number | null; public nsfw: boolean; - public threads: GuildTextThreadManager; + public threads: GuildTextThreadManager; public topic: string | null; public createInvite(options?: InviteCreateOptions): Promise; public fetchInvites(cache?: boolean): Promise>; @@ -652,7 +652,7 @@ export class BaseGuildTextChannel extends GuildChannel { ): Promise; public setTopic(topic: string | null, reason?: string): Promise; public setType(type: ChannelType.GuildText, reason?: string): Promise; - public setType(type: ChannelType.GuildAnnouncement, reason?: string): Promise; + public setType(type: ChannelType.GuildAnnouncement, reason?: string): Promise; } // tslint:disable-next-line no-empty-interface @@ -886,7 +886,7 @@ export class Embed { } export interface MappedChannelCategoryTypes { - [ChannelType.GuildAnnouncement]: NewsChannel; + [ChannelType.GuildAnnouncement]: AnnouncementChannel; [ChannelType.GuildVoice]: VoiceChannel; [ChannelType.GuildText]: TextChannel; [ChannelType.GuildStageVoice]: StageChannel; @@ -1448,7 +1448,13 @@ export class Guild extends AnonymousGuild { public vanityURLUses: number | null; public get voiceAdapterCreator(): InternalDiscordGatewayAdapterCreator; public voiceStates: VoiceStateManager; - public get widgetChannel(): TextChannel | NewsChannel | VoiceBasedChannel | ForumChannel | MediaChannel | null; + public get widgetChannel(): + | TextChannel + | AnnouncementChannel + | VoiceBasedChannel + | ForumChannel + | MediaChannel + | null; public widgetChannelId: Snowflake | null; public widgetEnabled: boolean | null; public get maximumBitrate(): number; @@ -2517,13 +2523,13 @@ export class ModalSubmitInteraction extend public isFromMessage(): this is ModalMessageModalSubmitInteraction; } -export class NewsChannel extends BaseGuildTextChannel { - public threads: GuildTextThreadManager; +export class AnnouncementChannel extends BaseGuildTextChannel { + public threads: GuildTextThreadManager; public type: ChannelType.GuildAnnouncement; - public addFollower(channel: TextChannelResolvable, reason?: string): Promise; + public addFollower(channel: TextChannelResolvable, reason?: string): Promise; } -export type NewsChannelResolvable = NewsChannel | Snowflake; +export type AnnouncementChannelResolvable = AnnouncementChannel | Snowflake; export class OAuth2Guild extends BaseGuild { private constructor(client: Client, data: RawOAuth2GuildData); @@ -3296,7 +3302,7 @@ export class ThreadChannel extends BaseCha public members: ThreadMemberManager; public name: string; public ownerId: Snowflake | null; - public get parent(): If | null; + public get parent(): If | null; public parentId: Snowflake | null; public rateLimitPerUser: number | null; public type: ThreadChannelType; @@ -3355,7 +3361,7 @@ export class Typing extends Base { public get guild(): Guild | null; public get member(): GuildMember | null; public inGuild(): this is this & { - channel: TextChannel | NewsChannel | ThreadChannel; + channel: TextChannel | AnnouncementChannel | ThreadChannel; get guild(): Guild; }; } @@ -3600,7 +3606,7 @@ export class Webhook { public name: string; public owner: Type extends WebhookType.Incoming ? User | APIUser | null : User | APIUser; public sourceGuild: Type extends WebhookType.ChannelFollower ? Guild | APIPartialGuild : null; - public sourceChannel: Type extends WebhookType.ChannelFollower ? NewsChannel | APIPartialChannel : null; + public sourceChannel: Type extends WebhookType.ChannelFollower ? AnnouncementChannel | APIPartialChannel : null; public token: Type extends WebhookType.Incoming ? string : Type extends WebhookType.ChannelFollower @@ -3608,7 +3614,14 @@ export class Webhook { : string | null; public type: Type; public applicationId: Type extends WebhookType.Application ? Snowflake : null; - public get channel(): TextChannel | VoiceChannel | NewsChannel | StageChannel | ForumChannel | MediaChannel | null; + public get channel(): + | TextChannel + | VoiceChannel + | AnnouncementChannel + | StageChannel + | ForumChannel + | MediaChannel + | null; public isUserCreated(): this is Webhook & { owner: User | APIUser; }; @@ -3675,7 +3688,7 @@ export class WelcomeChannel extends Base { public channelId: Snowflake; public guild: Guild | InviteGuild; public description: string; - public get channel(): TextChannel | NewsChannel | ForumChannel | MediaChannel | null; + public get channel(): TextChannel | AnnouncementChannel | ForumChannel | MediaChannel | null; public get emoji(): GuildEmoji | Emoji; } @@ -4096,7 +4109,7 @@ export class GuildChannelManager extends CachedManager; @@ -4395,10 +4408,10 @@ export class ThreadManager extends CachedM ThreadChannelResolvable > { protected constructor( - channel: TextChannel | NewsChannel | ForumChannel | MediaChannel, + channel: TextChannel | AnnouncementChannel | ForumChannel | MediaChannel, iterable?: Iterable, ); - public channel: If; + public channel: If; public fetch( options: ThreadChannelResolvable, cacheOptions?: BaseFetchOptions, @@ -4555,7 +4568,7 @@ export type AllowedPartial = | GuildScheduledEvent | ThreadMember; -export type AllowedThreadTypeForNewsChannel = ChannelType.AnnouncementThread; +export type AllowedThreadTypeForAnnouncementChannel = ChannelType.AnnouncementThread; export type AllowedThreadTypeForTextChannel = ChannelType.PublicThread | ChannelType.PrivateThread; @@ -5035,7 +5048,7 @@ export interface ChannelPosition { position?: number; } -export type GuildTextChannelResolvable = TextChannel | NewsChannel | Snowflake; +export type GuildTextChannelResolvable = TextChannel | AnnouncementChannel | Snowflake; export type ChannelResolvable = Channel | Snowflake; export interface ChannelWebhookCreateOptions { @@ -5045,7 +5058,7 @@ export interface ChannelWebhookCreateOptions { } export interface WebhookCreateOptions extends ChannelWebhookCreateOptions { - channel: TextChannel | NewsChannel | VoiceChannel | StageChannel | ForumChannel | MediaChannel | Snowflake; + channel: TextChannel | AnnouncementChannel | VoiceChannel | StageChannel | ForumChannel | MediaChannel | Snowflake; } export interface GuildMembersChunk { @@ -5147,7 +5160,7 @@ export interface ClientEvents { typingStart: [typing: Typing]; userUpdate: [oldUser: User | PartialUser, newUser: User]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; - webhooksUpdate: [channel: TextChannel | NewsChannel | VoiceChannel | ForumChannel | MediaChannel]; + webhooksUpdate: [channel: TextChannel | AnnouncementChannel | VoiceChannel | ForumChannel | MediaChannel]; interactionCreate: [interaction: Interaction]; stageInstanceCreate: [stageInstance: StageInstance]; stageInstanceUpdate: [oldStageInstance: StageInstance | null, newStageInstance: StageInstance]; @@ -5818,7 +5831,7 @@ export interface GuildCreateOptions { export interface GuildWidgetSettings { enabled: boolean; - channel: TextChannel | NewsChannel | VoiceBasedChannel | ForumChannel | MediaChannel | null; + channel: TextChannel | AnnouncementChannel | VoiceBasedChannel | ForumChannel | MediaChannel | null; } export interface GuildEditOptions { @@ -5898,7 +5911,7 @@ export interface GuildPruneMembersOptions { export interface GuildWidgetSettingsData { enabled: boolean; - channel: TextChannel | NewsChannel | VoiceBasedChannel | ForumChannel | MediaChannel | Snowflake | null; + channel: TextChannel | AnnouncementChannel | VoiceBasedChannel | ForumChannel | MediaChannel | Snowflake | null; } export interface GuildSearchMembersOptions { @@ -6090,7 +6103,7 @@ export interface InviteGenerationOptions { export type GuildInvitableChannelResolvable = | TextChannel | VoiceChannel - | NewsChannel + | AnnouncementChannel | StageChannel | ForumChannel | MediaChannel @@ -6669,7 +6682,7 @@ export type Channel = | DMChannel | PartialDMChannel | PartialGroupDMChannel - | NewsChannel + | AnnouncementChannel | StageChannel | TextChannel | PublicThreadChannel @@ -6810,7 +6823,7 @@ export interface WidgetChannel { export interface WelcomeChannelData { description: string; - channel: TextChannel | NewsChannel | ForumChannel | MediaChannel | Snowflake; + channel: TextChannel | AnnouncementChannel | ForumChannel | MediaChannel | Snowflake; emoji?: EmojiIdentifierResolvable; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 9fc56a9812d8..390d698223f6 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -72,7 +72,7 @@ import { MessageComponentInteraction, MessageReaction, ModalBuilder, - NewsChannel, + AnnouncementChannel, Options, PartialTextBasedChannelFields, PartialUser, @@ -546,7 +546,7 @@ client.on('messageCreate', async message => { if (webhook.isChannelFollower()) { expectAssignable(webhook.sourceGuild); - expectAssignable(webhook.sourceChannel); + expectAssignable(webhook.sourceChannel); expectType>(webhook); } else if (webhook.isIncoming()) { expectType(webhook.token); @@ -554,7 +554,7 @@ client.on('messageCreate', async message => { } expectNotType(webhook.sourceGuild); - expectNotType(webhook.sourceChannel); + expectNotType(webhook.sourceChannel); expectNotType(webhook.token); channel.awaitMessageComponent({ @@ -1362,7 +1362,7 @@ declare const dmChannel: DMChannel; declare const threadChannel: ThreadChannel; declare const threadChannelFromForum: ThreadChannel; declare const threadChannelNotFromForum: ThreadChannel; -declare const newsChannel: NewsChannel; +declare const announcementChannel: AnnouncementChannel; declare const textChannel: TextChannel; declare const voiceChannel: VoiceChannel; declare const guild: Guild; @@ -1370,25 +1370,25 @@ declare const user: User; declare const guildMember: GuildMember; // Test thread channels' parent inference -expectType(threadChannel.parent); +expectType(threadChannel.parent); expectType(threadChannelFromForum.parent); -expectType(threadChannelNotFromForum.parent); +expectType(threadChannelNotFromForum.parent); // Test whether the structures implement send expectType['send']>(dmChannel.send); expectType['send']>(threadChannel.send); -expectType['send']>(newsChannel.send); +expectType['send']>(announcementChannel.send); expectType['send']>(textChannel.send); expectType['send']>(voiceChannel.send); expectAssignable(user); expectAssignable(guildMember); -expectType>(textChannel.setType(ChannelType.GuildAnnouncement)); -expectType>(newsChannel.setType(ChannelType.GuildText)); +expectType>(textChannel.setType(ChannelType.GuildAnnouncement)); +expectType>(announcementChannel.setType(ChannelType.GuildText)); expectType(dmChannel.lastMessage); expectType(threadChannel.lastMessage); -expectType(newsChannel.lastMessage); +expectType(announcementChannel.lastMessage); expectType(textChannel.lastMessage); expectType(voiceChannel.lastMessage); @@ -1569,7 +1569,7 @@ declare const categoryChannelChildManager: CategoryChannelChildManager; { expectType>(categoryChannelChildManager.create({ name: 'name', type: ChannelType.GuildVoice })); expectType>(categoryChannelChildManager.create({ name: 'name', type: ChannelType.GuildText })); - expectType>( + expectType>( categoryChannelChildManager.create({ name: 'name', type: ChannelType.GuildAnnouncement }), ); expectType>( @@ -1586,7 +1586,9 @@ declare const guildChannelManager: GuildChannelManager; expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildVoice })); expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildCategory })); expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildText })); - expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildAnnouncement })); + expectType>( + guildChannelManager.create({ name: 'name', type: ChannelType.GuildAnnouncement }), + ); expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildStageVoice })); expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildForum })); expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildMedia })); @@ -1654,7 +1656,7 @@ expectType(guildForumThreadManager.channel); declare const guildTextThreadManager: GuildTextThreadManager< ChannelType.PublicThread | ChannelType.PrivateThread | ChannelType.AnnouncementThread >; -expectType(guildTextThreadManager.channel); +expectType(guildTextThreadManager.channel); declare const guildMemberManager: GuildMemberManager; { @@ -2203,9 +2205,9 @@ expectType< >(TextBasedChannelTypes); expectType(VoiceBasedChannel); expectType(GuildBasedChannel); -expectType( - NonThreadGuildBasedChannel, -); +expectType< + CategoryChannel | AnnouncementChannel | StageChannel | TextChannel | VoiceChannel | ForumChannel | MediaChannel +>(NonThreadGuildBasedChannel); expectType(GuildTextBasedChannel); const button = new ButtonBuilder({ @@ -2397,7 +2399,7 @@ expectType>(stageChannel.flags); expectType>(forumChannel.flags); expectType>(dmChannel.flags); expectType>(categoryChannel.flags); -expectType>(newsChannel.flags); +expectType>(announcementChannel.flags); expectType>(categoryChannel.flags); expectType>(threadChannel.flags); From c36728a8144a7522da950b6436d33aa305281a73 Mon Sep 17 00:00:00 2001 From: Denis Cristea Date: Wed, 9 Oct 2024 13:49:27 +0300 Subject: [PATCH 60/65] fix(Client): never pass token in ws constructor (#10544) * fix(Client): never pass token in ws constructor * chore: don't reassign parameter Co-authored-by: Almeida --------- Co-authored-by: Almeida --- packages/discord.js/src/client/Client.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 048cfb32d322..7d528b37c6ca 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -160,7 +160,8 @@ class Client extends BaseClient { ...this.options.ws, intents: this.options.intents.bitfield, rest: this.rest, - token: this.token, + // Explicitly nulled to always be set using `setToken` in `login` + token: null, }; /** @@ -257,8 +258,10 @@ class Client extends BaseClient { */ async login(token = this.token) { if (!token || typeof token !== 'string') throw new DiscordjsError(ErrorCodes.TokenInvalid); - this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); - this.rest.setToken(token); + this.token = token.replace(/^(Bot|Bearer)\s*/i, ''); + + this.rest.setToken(this.token); + this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); this.emit(Events.Debug, 'Preparing to connect to the gateway...'); From 1925c11a4869a436a77c7e14bfd7af3a4070c95f Mon Sep 17 00:00:00 2001 From: Eejit <76887639+Eejit43@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:24:08 -0400 Subject: [PATCH 61/65] fix(GuildScheduledEvent): handle null recurrence_rule (#10543) * fix(GuildScheduledEvent): handle null recurrence_rule * refactor: consistency * feat: implement suggested logic change * fix: correct data.recurrence_rule check --------- Co-authored-by: Almeida --- packages/discord.js/src/structures/GuildScheduledEvent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/discord.js/src/structures/GuildScheduledEvent.js b/packages/discord.js/src/structures/GuildScheduledEvent.js index 9f6124f0198c..c1d4cbc1beb0 100644 --- a/packages/discord.js/src/structures/GuildScheduledEvent.js +++ b/packages/discord.js/src/structures/GuildScheduledEvent.js @@ -218,7 +218,7 @@ class GuildScheduledEvent extends Base { * The recurrence rule for this scheduled event * @type {?GuildScheduledEventRecurrenceRule} */ - this.recurrenceRule = { + this.recurrenceRule = data.recurrence_rule && { startTimestamp: Date.parse(data.recurrence_rule.start), get startAt() { return new Date(this.startTimestamp); From 79423c80b41a4e857e9239889e8cfeb7a7fb90cb Mon Sep 17 00:00:00 2001 From: Naiyar <137700126+imnaiyar@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:44:57 +0600 Subject: [PATCH 62/65] refactor!: exclude removed events from their enum (#10547) BREAKING CHANGE: Removed the following members from `Events` enum: `Raw`, `ShardResume`, `ShardError`, `ShardReady`, `ShardReconnecting`, `ShardResume`, `ShardDisconnect` BREAKING CHANGE: Removed `Reconnecting` from `ShardEvents` enum --- packages/discord.js/src/util/ShardEvents.js | 2 -- packages/discord.js/typings/index.d.ts | 7 ------- 2 files changed, 9 deletions(-) diff --git a/packages/discord.js/src/util/ShardEvents.js b/packages/discord.js/src/util/ShardEvents.js index f5ba9616849c..175a111d2aab 100644 --- a/packages/discord.js/src/util/ShardEvents.js +++ b/packages/discord.js/src/util/ShardEvents.js @@ -7,7 +7,6 @@ * @property {string} Error error * @property {string} Message message * @property {string} Ready ready - * @property {string} Reconnecting reconnecting * @property {string} Resume resume * @property {string} Spawn spawn */ @@ -23,7 +22,6 @@ module.exports = { Error: 'error', Message: 'message', Ready: 'ready', - Reconnecting: 'reconnecting', Resume: 'resume', Spawn: 'spawn', }; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 7867472f2903..2ac75da47a11 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -5362,13 +5362,7 @@ export enum Events { Warn = 'warn', Debug = 'debug', CacheSweep = 'cacheSweep', - ShardDisconnect = 'shardDisconnect', - ShardError = 'shardError', - ShardReconnecting = 'shardReconnecting', - ShardReady = 'shardReady', - ShardResume = 'shardResume', Invalidated = 'invalidated', - Raw = 'raw', StageInstanceCreate = 'stageInstanceCreate', StageInstanceUpdate = 'stageInstanceUpdate', StageInstanceDelete = 'stageInstanceDelete', @@ -5388,7 +5382,6 @@ export enum ShardEvents { Error = 'error', Message = 'message', Ready = 'ready', - Reconnecting = 'reconnecting', Resume = 'resume', Spawn = 'spawn', } From eded459335700cd74fba148847d02ec8288427d4 Mon Sep 17 00:00:00 2001 From: Luna <84203950+Wolvinny@users.noreply.github.com> Date: Fri, 11 Oct 2024 22:54:55 +0200 Subject: [PATCH 63/65] docs(Client): fix incorrect managers descriptions (#10519) * Edit manager descriptions Some managers had incorrect descriptions, which applied only to the cache of the manager * Update Client.js * remove trailing space --- packages/discord.js/src/client/Client.js | 335 ++++++----------------- 1 file changed, 89 insertions(+), 246 deletions(-) diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 7d528b37c6ca..8a7b69b58306 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -1,16 +1,14 @@ 'use strict'; const process = require('node:process'); -const { clearTimeout, setImmediate, setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); -const { WebSocketManager, WebSocketShardEvents, WebSocketShardStatus } = require('@discordjs/ws'); -const { GatewayDispatchEvents, GatewayIntentBits, OAuth2Scopes, Routes } = require('discord-api-types/v10'); +const { OAuth2Scopes, Routes } = require('discord-api-types/v10'); const BaseClient = require('./BaseClient'); const ActionsManager = require('./actions/ActionsManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); -const PacketHandlers = require('./websocket/handlers'); -const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); +const WebSocketManager = require('./websocket/WebSocketManager'); +const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const ChannelManager = require('../managers/ChannelManager'); const GuildManager = require('../managers/GuildManager'); @@ -33,16 +31,7 @@ const PermissionsBitField = require('../util/PermissionsBitField'); const Status = require('../util/Status'); const Sweepers = require('../util/Sweepers'); -const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; -const BeforeReadyWhitelist = [ - GatewayDispatchEvents.Ready, - GatewayDispatchEvents.Resumed, - GatewayDispatchEvents.GuildCreate, - GatewayDispatchEvents.GuildDelete, - GatewayDispatchEvents.GuildMembersChunk, - GatewayDispatchEvents.GuildMemberAdd, - GatewayDispatchEvents.GuildMemberRemove, -]; +let deprecationEmittedForPremiumStickerPacks = false; /** * The main hub for interacting with the Discord API, and the starting point for any bot. @@ -58,45 +47,43 @@ class Client extends BaseClient { const data = require('node:worker_threads').workerData ?? process.env; const defaults = Options.createDefault(); - if (this.options.ws.shardIds === defaults.ws.shardIds && 'SHARDS' in data) { - this.options.ws.shardIds = JSON.parse(data.SHARDS); + if (this.options.shards === defaults.shards) { + if ('SHARDS' in data) { + this.options.shards = JSON.parse(data.SHARDS); + } } - if (this.options.ws.shardCount === defaults.ws.shardCount && 'SHARD_COUNT' in data) { - this.options.ws.shardCount = Number(data.SHARD_COUNT); + if (this.options.shardCount === defaults.shardCount) { + if ('SHARD_COUNT' in data) { + this.options.shardCount = Number(data.SHARD_COUNT); + } else if (Array.isArray(this.options.shards)) { + this.options.shardCount = this.options.shards.length; + } } - /** - * The presence of the Client - * @private - * @type {ClientPresence} - */ - this.presence = new ClientPresence(this, this.options.ws.initialPresence ?? this.options.presence); + const typeofShards = typeof this.options.shards; - this._validateOptions(); + if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { + this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); + } - /** - * The current status of this Client - * @type {Status} - * @private - */ - this.status = Status.Idle; + if (typeofShards === 'number') this.options.shards = [this.options.shards]; - /** - * A set of guild ids this Client expects to receive - * @name Client#expectedGuilds - * @type {Set} - * @private - */ - Object.defineProperty(this, 'expectedGuilds', { value: new Set(), writable: true }); + if (Array.isArray(this.options.shards)) { + this.options.shards = [ + ...new Set( + this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)), + ), + ]; + } + + this._validateOptions(); /** - * The ready timeout - * @name Client#readyTimeout - * @type {?NodeJS.Timeout} - * @private + * The WebSocket manager of the client + * @type {WebSocketManager} */ - Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); + this.ws = new WebSocketManager(this); /** * The action manager of the client @@ -105,6 +92,12 @@ class Client extends BaseClient { */ this.actions = new ActionsManager(this); + /** + * The voice manager of the client + * @type {ClientVoiceManager} + */ + this.voice = new ClientVoiceManager(this); + /** * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * @type {?ShardClientUtil} @@ -114,21 +107,21 @@ class Client extends BaseClient { : null; /** - * All of the {@link User} objects that have been cached at any point, mapped by their ids + * The user manager of this client * @type {UserManager} */ this.users = new UserManager(this); /** - * All of the guilds the client is currently handling, mapped by their ids - + * A manager of all the guilds the client is currently handling - * as long as sharding isn't being used, this will be *every* guild the bot is a member of * @type {GuildManager} */ this.guilds = new GuildManager(this); /** - * All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids - - * as long as no sharding manager is being used, this will be *every* channel in *every* guild the bot + * A manager of all the {@link BaseChannel}s that the client is currently handling - + * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot * is a member of. Note that DM channels will not be initially cached, and thus not be present * in the Manager without their explicit fetching or use. * @type {ChannelManager} @@ -141,6 +134,13 @@ class Client extends BaseClient { */ this.sweepers = new Sweepers(this, this.options.sweepers); + /** + * The presence of the Client + * @private + * @type {ClientPresence} + */ + this.presence = new ClientPresence(this, this.options.presence); + Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'DISCORD_TOKEN' in process.env) { /** @@ -150,32 +150,10 @@ class Client extends BaseClient { * @type {?string} */ this.token = process.env.DISCORD_TOKEN; - } else if (this.options.ws.token) { - this.token = this.options.ws.token; } else { this.token = null; } - const wsOptions = { - ...this.options.ws, - intents: this.options.intents.bitfield, - rest: this.rest, - // Explicitly nulled to always be set using `setToken` in `login` - token: null, - }; - - /** - * The WebSocket manager of the client - * @type {WebSocketManager} - */ - this.ws = new WebSocketManager(wsOptions); - - /** - * The voice manager of the client - * @type {ClientVoiceManager} - */ - this.voice = new ClientVoiceManager(this); - /** * User that the client is logged in as * @type {?ClientUser} @@ -188,37 +166,15 @@ class Client extends BaseClient { */ this.application = null; - /** - * The latencies of the WebSocketShard connections - * @type {Collection} - */ - this.pings = new Collection(); - - /** - * The last time a ping was sent (a timestamp) for each WebSocketShard connection - * @type {Collection} - */ - this.lastPingTimestamps = new Collection(); - /** * Timestamp of the time the client was last {@link Status.Ready} at * @type {?number} */ this.readyTimestamp = null; - - /** - * An array of queued events before this Client became ready - * @type {Object[]} - * @private - * @name Client#incomingPacketQueue - */ - Object.defineProperty(this, 'incomingPacketQueue', { value: [] }); - - this._attachEvents(); } /** - * All custom emojis that the client has access to, mapped by their ids + * A manager of all the custom emojis that the client has access to * @type {BaseGuildEmojiManager} * @readonly */ @@ -258,15 +214,16 @@ class Client extends BaseClient { */ async login(token = this.token) { if (!token || typeof token !== 'string') throw new DiscordjsError(ErrorCodes.TokenInvalid); - this.token = token.replace(/^(Bot|Bearer)\s*/i, ''); + this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); + this.rest.setToken(token); + this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); - this.rest.setToken(this.token); + if (this.options.presence) { + this.options.ws.presence = this.presence._parse(this.options.presence); + } - this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); this.emit(Events.Debug, 'Preparing to connect to the gateway...'); - this.ws.setToken(this.token); - try { await this.ws.connect(); return this.token; @@ -276,150 +233,13 @@ class Client extends BaseClient { } } - /** - * Checks if the client can be marked as ready - * @private - */ - async _checkReady() { - // Step 0. Clear the ready timeout, if it exists - if (this.readyTimeout) { - clearTimeout(this.readyTimeout); - this.readyTimeout = null; - } - // Step 1. If we don't have any other guilds pending, we are ready - if ( - !this.expectedGuilds.size && - (await this.ws.fetchStatus()).every(status => status === WebSocketShardStatus.Ready) - ) { - this.emit(Events.Debug, 'Client received all its guilds. Marking as fully ready.'); - this.status = Status.Ready; - - this._triggerClientReady(); - return; - } - const hasGuildsIntent = this.options.intents.has(GatewayIntentBits.Guilds); - // Step 2. Create a timeout that will mark the client as ready if there are still unavailable guilds - // * The timeout is 15 seconds by default - // * This can be optionally changed in the client options via the `waitGuildTimeout` option - // * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds. - - this.readyTimeout = setTimeout( - () => { - this.emit( - Events.Debug, - `${ - hasGuildsIntent - ? `Client did not receive any guild packets in ${this.options.waitGuildTimeout} ms.` - : 'Client will not receive anymore guild packets.' - }\nUnavailable guild count: ${this.expectedGuilds.size}`, - ); - - this.readyTimeout = null; - this.status = Status.Ready; - - this._triggerClientReady(); - }, - hasGuildsIntent ? this.options.waitGuildTimeout : 0, - ).unref(); - } - - /** - * Attaches event handlers to the WebSocketShardManager from `@discordjs/ws`. - * @private - */ - _attachEvents() { - this.ws.on(WebSocketShardEvents.Debug, (message, shardId) => - this.emit(Events.Debug, `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`), - ); - this.ws.on(WebSocketShardEvents.Dispatch, this._handlePacket.bind(this)); - - this.ws.on(WebSocketShardEvents.Ready, data => { - for (const guild of data.guilds) { - this.expectedGuilds.add(guild.id); - } - this.status = Status.WaitingForGuilds; - this._checkReady(); - }); - - this.ws.on(WebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency }, shardId) => { - this.emit(Events.Debug, `[WS => Shard ${shardId}] Heartbeat acknowledged, latency of ${latency}ms.`); - this.lastPingTimestamps.set(shardId, heartbeatAt); - this.pings.set(shardId, latency); - }); - } - - /** - * Processes a packet and queues it if this WebSocketManager is not ready. - * @param {GatewayDispatchPayload} packet The packet to be handled - * @param {number} shardId The shardId that received this packet - * @private - */ - _handlePacket(packet, shardId) { - if (this.status !== Status.Ready && !BeforeReadyWhitelist.includes(packet.t)) { - this.incomingPacketQueue.push({ packet, shardId }); - } else { - if (this.incomingPacketQueue.length) { - const item = this.incomingPacketQueue.shift(); - setImmediate(() => { - this._handlePacket(item.packet, item.shardId); - }).unref(); - } - - if (PacketHandlers[packet.t]) { - PacketHandlers[packet.t](this, packet, shardId); - } - - if (this.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(packet.t)) { - this.expectedGuilds.delete(packet.d.id); - this._checkReady(); - } - } - } - - /** - * Broadcasts a packet to every shard of this client handles. - * @param {Object} packet The packet to send - * @private - */ - async _broadcast(packet) { - const shardIds = await this.ws.getShardIds(); - return Promise.all(shardIds.map(shardId => this.ws.send(shardId, packet))); - } - - /** - * Causes the client to be marked as ready and emits the ready event. - * @private - */ - _triggerClientReady() { - this.status = Status.Ready; - - this.readyTimestamp = Date.now(); - - /** - * Emitted when the client becomes ready to start working. - * @event Client#clientReady - * @param {Client} client The client - */ - this.emit(Events.ClientReady, this); - } - /** * Returns whether the client has logged in, indicative of being able to access * properties such as `user` and `application`. * @returns {boolean} */ isReady() { - return this.status === Status.Ready; - } - - /** - * The average ping of all WebSocketShards - * @type {number} - * @readonly - */ - get ping() { - const sum = this.pings.reduce((a, b) => a + b, 0); - return sum / this.pings.size; + return !this.ws.destroyed && this.ws.status === Status.Ready; } /** @@ -552,6 +372,24 @@ class Client extends BaseClient { return new Collection(data.sticker_packs.map(stickerPack => [stickerPack.id, new StickerPack(this, stickerPack)])); } + /** + * Obtains the list of available sticker packs. + * @returns {Promise>} + * @deprecated Use {@link Client#fetchStickerPacks} instead. + */ + fetchPremiumStickerPacks() { + if (!deprecationEmittedForPremiumStickerPacks) { + process.emitWarning( + 'The Client#fetchPremiumStickerPacks() method is deprecated. Use Client#fetchStickerPacks() instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForPremiumStickerPacks = true; + } + + return this.fetchStickerPacks(); + } + /** * Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds. * @param {GuildResolvable} guild The guild to fetch the preview for @@ -687,10 +525,20 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { - if (options.intents === undefined && options.ws?.intents === undefined) { + if (options.intents === undefined) { throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents); } else { - options.intents = new IntentsBitField(options.intents ?? options.ws.intents).freeze(); + options.intents = new IntentsBitField(options.intents).freeze(); + } + if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { + throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shardCount', 'a number greater than or equal to 1'); + } + if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) { + throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shards', "'auto', a number or array of numbers"); + } + if (options.shards && !options.shards.length) throw new DiscordjsRangeError(ErrorCodes.ClientInvalidProvidedShards); + if (typeof options.makeCache !== 'function') { + throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'makeCache', 'a function'); } if (typeof options.sweepers !== 'object' || options.sweepers === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'sweepers', 'an object'); @@ -713,17 +561,12 @@ class Client extends BaseClient { ) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'allowedMentions', 'an object'); } + if (typeof options.presence !== 'object' || options.presence === null) { + throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); + } if (typeof options.ws !== 'object' || options.ws === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'ws', 'an object'); } - if ( - (typeof options.presence !== 'object' || options.presence === null) && - options.ws.initialPresence === undefined - ) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); - } else { - options.ws.initialPresence = options.ws.initialPresence ?? this.presence._parse(this.options.presence); - } if (typeof options.rest !== 'object' || options.rest === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'rest', 'an object'); } From b16d851770b7b11492ff88680945623f7fb8c444 Mon Sep 17 00:00:00 2001 From: almeidx Date: Fri, 11 Oct 2024 22:07:49 +0100 Subject: [PATCH 64/65] revert: docs: fix incorrect managers descriptions (#10519) This reverts commit eded459335700cd74fba148847d02ec8288427d4. --- packages/discord.js/src/client/Client.js | 335 +++++++++++++++++------ 1 file changed, 246 insertions(+), 89 deletions(-) diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 8a7b69b58306..7d528b37c6ca 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -1,14 +1,16 @@ 'use strict'; const process = require('node:process'); +const { clearTimeout, setImmediate, setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); -const { OAuth2Scopes, Routes } = require('discord-api-types/v10'); +const { WebSocketManager, WebSocketShardEvents, WebSocketShardStatus } = require('@discordjs/ws'); +const { GatewayDispatchEvents, GatewayIntentBits, OAuth2Scopes, Routes } = require('discord-api-types/v10'); const BaseClient = require('./BaseClient'); const ActionsManager = require('./actions/ActionsManager'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); -const WebSocketManager = require('./websocket/WebSocketManager'); -const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); +const PacketHandlers = require('./websocket/handlers'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager'); const ChannelManager = require('../managers/ChannelManager'); const GuildManager = require('../managers/GuildManager'); @@ -31,7 +33,16 @@ const PermissionsBitField = require('../util/PermissionsBitField'); const Status = require('../util/Status'); const Sweepers = require('../util/Sweepers'); -let deprecationEmittedForPremiumStickerPacks = false; +const WaitingForGuildEvents = [GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.GuildDelete]; +const BeforeReadyWhitelist = [ + GatewayDispatchEvents.Ready, + GatewayDispatchEvents.Resumed, + GatewayDispatchEvents.GuildCreate, + GatewayDispatchEvents.GuildDelete, + GatewayDispatchEvents.GuildMembersChunk, + GatewayDispatchEvents.GuildMemberAdd, + GatewayDispatchEvents.GuildMemberRemove, +]; /** * The main hub for interacting with the Discord API, and the starting point for any bot. @@ -47,43 +58,45 @@ class Client extends BaseClient { const data = require('node:worker_threads').workerData ?? process.env; const defaults = Options.createDefault(); - if (this.options.shards === defaults.shards) { - if ('SHARDS' in data) { - this.options.shards = JSON.parse(data.SHARDS); - } + if (this.options.ws.shardIds === defaults.ws.shardIds && 'SHARDS' in data) { + this.options.ws.shardIds = JSON.parse(data.SHARDS); } - if (this.options.shardCount === defaults.shardCount) { - if ('SHARD_COUNT' in data) { - this.options.shardCount = Number(data.SHARD_COUNT); - } else if (Array.isArray(this.options.shards)) { - this.options.shardCount = this.options.shards.length; - } + if (this.options.ws.shardCount === defaults.ws.shardCount && 'SHARD_COUNT' in data) { + this.options.ws.shardCount = Number(data.SHARD_COUNT); } - const typeofShards = typeof this.options.shards; - - if (typeofShards === 'undefined' && typeof this.options.shardCount === 'number') { - this.options.shards = Array.from({ length: this.options.shardCount }, (_, i) => i); - } + /** + * The presence of the Client + * @private + * @type {ClientPresence} + */ + this.presence = new ClientPresence(this, this.options.ws.initialPresence ?? this.options.presence); - if (typeofShards === 'number') this.options.shards = [this.options.shards]; + this._validateOptions(); - if (Array.isArray(this.options.shards)) { - this.options.shards = [ - ...new Set( - this.options.shards.filter(item => !isNaN(item) && item >= 0 && item < Infinity && item === (item | 0)), - ), - ]; - } + /** + * The current status of this Client + * @type {Status} + * @private + */ + this.status = Status.Idle; - this._validateOptions(); + /** + * A set of guild ids this Client expects to receive + * @name Client#expectedGuilds + * @type {Set} + * @private + */ + Object.defineProperty(this, 'expectedGuilds', { value: new Set(), writable: true }); /** - * The WebSocket manager of the client - * @type {WebSocketManager} + * The ready timeout + * @name Client#readyTimeout + * @type {?NodeJS.Timeout} + * @private */ - this.ws = new WebSocketManager(this); + Object.defineProperty(this, 'readyTimeout', { value: null, writable: true }); /** * The action manager of the client @@ -92,12 +105,6 @@ class Client extends BaseClient { */ this.actions = new ActionsManager(this); - /** - * The voice manager of the client - * @type {ClientVoiceManager} - */ - this.voice = new ClientVoiceManager(this); - /** * Shard helpers for the client (only if the process was spawned from a {@link ShardingManager}) * @type {?ShardClientUtil} @@ -107,21 +114,21 @@ class Client extends BaseClient { : null; /** - * The user manager of this client + * All of the {@link User} objects that have been cached at any point, mapped by their ids * @type {UserManager} */ this.users = new UserManager(this); /** - * A manager of all the guilds the client is currently handling - + * All of the guilds the client is currently handling, mapped by their ids - * as long as sharding isn't being used, this will be *every* guild the bot is a member of * @type {GuildManager} */ this.guilds = new GuildManager(this); /** - * A manager of all the {@link BaseChannel}s that the client is currently handling - - * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot + * All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids - + * as long as no sharding manager is being used, this will be *every* channel in *every* guild the bot * is a member of. Note that DM channels will not be initially cached, and thus not be present * in the Manager without their explicit fetching or use. * @type {ChannelManager} @@ -134,13 +141,6 @@ class Client extends BaseClient { */ this.sweepers = new Sweepers(this, this.options.sweepers); - /** - * The presence of the Client - * @private - * @type {ClientPresence} - */ - this.presence = new ClientPresence(this, this.options.presence); - Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'DISCORD_TOKEN' in process.env) { /** @@ -150,10 +150,32 @@ class Client extends BaseClient { * @type {?string} */ this.token = process.env.DISCORD_TOKEN; + } else if (this.options.ws.token) { + this.token = this.options.ws.token; } else { this.token = null; } + const wsOptions = { + ...this.options.ws, + intents: this.options.intents.bitfield, + rest: this.rest, + // Explicitly nulled to always be set using `setToken` in `login` + token: null, + }; + + /** + * The WebSocket manager of the client + * @type {WebSocketManager} + */ + this.ws = new WebSocketManager(wsOptions); + + /** + * The voice manager of the client + * @type {ClientVoiceManager} + */ + this.voice = new ClientVoiceManager(this); + /** * User that the client is logged in as * @type {?ClientUser} @@ -166,15 +188,37 @@ class Client extends BaseClient { */ this.application = null; + /** + * The latencies of the WebSocketShard connections + * @type {Collection} + */ + this.pings = new Collection(); + + /** + * The last time a ping was sent (a timestamp) for each WebSocketShard connection + * @type {Collection} + */ + this.lastPingTimestamps = new Collection(); + /** * Timestamp of the time the client was last {@link Status.Ready} at * @type {?number} */ this.readyTimestamp = null; + + /** + * An array of queued events before this Client became ready + * @type {Object[]} + * @private + * @name Client#incomingPacketQueue + */ + Object.defineProperty(this, 'incomingPacketQueue', { value: [] }); + + this._attachEvents(); } /** - * A manager of all the custom emojis that the client has access to + * All custom emojis that the client has access to, mapped by their ids * @type {BaseGuildEmojiManager} * @readonly */ @@ -214,16 +258,15 @@ class Client extends BaseClient { */ async login(token = this.token) { if (!token || typeof token !== 'string') throw new DiscordjsError(ErrorCodes.TokenInvalid); - this.token = token = token.replace(/^(Bot|Bearer)\s*/i, ''); - this.rest.setToken(token); - this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); + this.token = token.replace(/^(Bot|Bearer)\s*/i, ''); - if (this.options.presence) { - this.options.ws.presence = this.presence._parse(this.options.presence); - } + this.rest.setToken(this.token); + this.emit(Events.Debug, `Provided token: ${this._censoredToken}`); this.emit(Events.Debug, 'Preparing to connect to the gateway...'); + this.ws.setToken(this.token); + try { await this.ws.connect(); return this.token; @@ -233,13 +276,150 @@ class Client extends BaseClient { } } + /** + * Checks if the client can be marked as ready + * @private + */ + async _checkReady() { + // Step 0. Clear the ready timeout, if it exists + if (this.readyTimeout) { + clearTimeout(this.readyTimeout); + this.readyTimeout = null; + } + // Step 1. If we don't have any other guilds pending, we are ready + if ( + !this.expectedGuilds.size && + (await this.ws.fetchStatus()).every(status => status === WebSocketShardStatus.Ready) + ) { + this.emit(Events.Debug, 'Client received all its guilds. Marking as fully ready.'); + this.status = Status.Ready; + + this._triggerClientReady(); + return; + } + const hasGuildsIntent = this.options.intents.has(GatewayIntentBits.Guilds); + // Step 2. Create a timeout that will mark the client as ready if there are still unavailable guilds + // * The timeout is 15 seconds by default + // * This can be optionally changed in the client options via the `waitGuildTimeout` option + // * a timeout time of zero will skip this timeout, which potentially could cause the Client to miss guilds. + + this.readyTimeout = setTimeout( + () => { + this.emit( + Events.Debug, + `${ + hasGuildsIntent + ? `Client did not receive any guild packets in ${this.options.waitGuildTimeout} ms.` + : 'Client will not receive anymore guild packets.' + }\nUnavailable guild count: ${this.expectedGuilds.size}`, + ); + + this.readyTimeout = null; + this.status = Status.Ready; + + this._triggerClientReady(); + }, + hasGuildsIntent ? this.options.waitGuildTimeout : 0, + ).unref(); + } + + /** + * Attaches event handlers to the WebSocketShardManager from `@discordjs/ws`. + * @private + */ + _attachEvents() { + this.ws.on(WebSocketShardEvents.Debug, (message, shardId) => + this.emit(Events.Debug, `[WS => ${typeof shardId === 'number' ? `Shard ${shardId}` : 'Manager'}] ${message}`), + ); + this.ws.on(WebSocketShardEvents.Dispatch, this._handlePacket.bind(this)); + + this.ws.on(WebSocketShardEvents.Ready, data => { + for (const guild of data.guilds) { + this.expectedGuilds.add(guild.id); + } + this.status = Status.WaitingForGuilds; + this._checkReady(); + }); + + this.ws.on(WebSocketShardEvents.HeartbeatComplete, ({ heartbeatAt, latency }, shardId) => { + this.emit(Events.Debug, `[WS => Shard ${shardId}] Heartbeat acknowledged, latency of ${latency}ms.`); + this.lastPingTimestamps.set(shardId, heartbeatAt); + this.pings.set(shardId, latency); + }); + } + + /** + * Processes a packet and queues it if this WebSocketManager is not ready. + * @param {GatewayDispatchPayload} packet The packet to be handled + * @param {number} shardId The shardId that received this packet + * @private + */ + _handlePacket(packet, shardId) { + if (this.status !== Status.Ready && !BeforeReadyWhitelist.includes(packet.t)) { + this.incomingPacketQueue.push({ packet, shardId }); + } else { + if (this.incomingPacketQueue.length) { + const item = this.incomingPacketQueue.shift(); + setImmediate(() => { + this._handlePacket(item.packet, item.shardId); + }).unref(); + } + + if (PacketHandlers[packet.t]) { + PacketHandlers[packet.t](this, packet, shardId); + } + + if (this.status === Status.WaitingForGuilds && WaitingForGuildEvents.includes(packet.t)) { + this.expectedGuilds.delete(packet.d.id); + this._checkReady(); + } + } + } + + /** + * Broadcasts a packet to every shard of this client handles. + * @param {Object} packet The packet to send + * @private + */ + async _broadcast(packet) { + const shardIds = await this.ws.getShardIds(); + return Promise.all(shardIds.map(shardId => this.ws.send(shardId, packet))); + } + + /** + * Causes the client to be marked as ready and emits the ready event. + * @private + */ + _triggerClientReady() { + this.status = Status.Ready; + + this.readyTimestamp = Date.now(); + + /** + * Emitted when the client becomes ready to start working. + * @event Client#clientReady + * @param {Client} client The client + */ + this.emit(Events.ClientReady, this); + } + /** * Returns whether the client has logged in, indicative of being able to access * properties such as `user` and `application`. * @returns {boolean} */ isReady() { - return !this.ws.destroyed && this.ws.status === Status.Ready; + return this.status === Status.Ready; + } + + /** + * The average ping of all WebSocketShards + * @type {number} + * @readonly + */ + get ping() { + const sum = this.pings.reduce((a, b) => a + b, 0); + return sum / this.pings.size; } /** @@ -372,24 +552,6 @@ class Client extends BaseClient { return new Collection(data.sticker_packs.map(stickerPack => [stickerPack.id, new StickerPack(this, stickerPack)])); } - /** - * Obtains the list of available sticker packs. - * @returns {Promise>} - * @deprecated Use {@link Client#fetchStickerPacks} instead. - */ - fetchPremiumStickerPacks() { - if (!deprecationEmittedForPremiumStickerPacks) { - process.emitWarning( - 'The Client#fetchPremiumStickerPacks() method is deprecated. Use Client#fetchStickerPacks() instead.', - 'DeprecationWarning', - ); - - deprecationEmittedForPremiumStickerPacks = true; - } - - return this.fetchStickerPacks(); - } - /** * Obtains a guild preview from Discord, available for all guilds the bot is in and all Discoverable guilds. * @param {GuildResolvable} guild The guild to fetch the preview for @@ -525,20 +687,10 @@ class Client extends BaseClient { * @private */ _validateOptions(options = this.options) { - if (options.intents === undefined) { + if (options.intents === undefined && options.ws?.intents === undefined) { throw new DiscordjsTypeError(ErrorCodes.ClientMissingIntents); } else { - options.intents = new IntentsBitField(options.intents).freeze(); - } - if (typeof options.shardCount !== 'number' || isNaN(options.shardCount) || options.shardCount < 1) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shardCount', 'a number greater than or equal to 1'); - } - if (options.shards && !(options.shards === 'auto' || Array.isArray(options.shards))) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'shards', "'auto', a number or array of numbers"); - } - if (options.shards && !options.shards.length) throw new DiscordjsRangeError(ErrorCodes.ClientInvalidProvidedShards); - if (typeof options.makeCache !== 'function') { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'makeCache', 'a function'); + options.intents = new IntentsBitField(options.intents ?? options.ws.intents).freeze(); } if (typeof options.sweepers !== 'object' || options.sweepers === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'sweepers', 'an object'); @@ -561,12 +713,17 @@ class Client extends BaseClient { ) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'allowedMentions', 'an object'); } - if (typeof options.presence !== 'object' || options.presence === null) { - throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); - } if (typeof options.ws !== 'object' || options.ws === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'ws', 'an object'); } + if ( + (typeof options.presence !== 'object' || options.presence === null) && + options.ws.initialPresence === undefined + ) { + throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'presence', 'an object'); + } else { + options.ws.initialPresence = options.ws.initialPresence ?? this.presence._parse(this.options.presence); + } if (typeof options.rest !== 'object' || options.rest === null) { throw new DiscordjsTypeError(ErrorCodes.ClientInvalidOption, 'rest', 'an object'); } From 960a80dbaeac1d900e82d86539bab3a1fff9b8cd Mon Sep 17 00:00:00 2001 From: almeidx Date: Fri, 11 Oct 2024 22:10:48 +0100 Subject: [PATCH 65/65] docs(Client): fix incorrect managers descriptions Co-authored-by: Luna <84203950+Wolvinny@users.noreply.github.com> --- packages/discord.js/src/client/Client.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/discord.js/src/client/Client.js b/packages/discord.js/src/client/Client.js index 7d528b37c6ca..cec6ef95ab89 100644 --- a/packages/discord.js/src/client/Client.js +++ b/packages/discord.js/src/client/Client.js @@ -114,21 +114,21 @@ class Client extends BaseClient { : null; /** - * All of the {@link User} objects that have been cached at any point, mapped by their ids + * The user manager of this client * @type {UserManager} */ this.users = new UserManager(this); /** - * All of the guilds the client is currently handling, mapped by their ids - + * A manager of all the guilds the client is currently handling - * as long as sharding isn't being used, this will be *every* guild the bot is a member of * @type {GuildManager} */ this.guilds = new GuildManager(this); /** - * All of the {@link BaseChannel}s that the client is currently handling, mapped by their ids - - * as long as no sharding manager is being used, this will be *every* channel in *every* guild the bot + * All of the {@link BaseChannel}s that the client is currently handling - + * as long as sharding isn't being used, this will be *every* channel in *every* guild the bot * is a member of. Note that DM channels will not be initially cached, and thus not be present * in the Manager without their explicit fetching or use. * @type {ChannelManager} @@ -218,7 +218,7 @@ class Client extends BaseClient { } /** - * All custom emojis that the client has access to, mapped by their ids + * A manager of all the custom emojis that the client has access to * @type {BaseGuildEmojiManager} * @readonly */