From 5152abf7285581abf7689e9050fdc56c4abb1e2b Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 1 Nov 2022 19:36:05 +0200 Subject: [PATCH] feat: new select menus (#8793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(builders): new select menus * chore: better re-exporting of deprecated classes * feat: new select menus * chore: typings * chore: add missing todo comment * chore: finish updating tests * chore: add runtime deprecation warnings * chore: format deprecation warning * feat(BaseInteraction): isAnySelectMenu * chore: requested changes * fix: deprecation comments * chore: update @deprecated comments in typings * chore: add tests for select menu type narrowing * fix: bad auto imports Co-authored-by: Julian Vennen * fix: properly handle resolved members * fix: collectors * chore: suggested changes Co-authored-by: Almeida * fix(typings): bad class extends * feat(ChannelSelectMenuBuilder): validation * chore: update todo comment * refactor(ChannelSelectMenu): better handling of channel_types state * chore: style nit * chore: suggested nits Co-authored-by: Aura Román Co-authored-by: Julian Vennen Co-authored-by: Almeida Co-authored-by: Aura Román Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../__tests__/components/actionRow.test.ts | 20 +- .../__tests__/components/components.test.ts | 10 +- .../__tests__/components/selectMenu.test.ts | 16 +- packages/builders/package.json | 2 +- packages/builders/src/components/ActionRow.ts | 14 +- .../builders/src/components/Assertions.ts | 10 +- .../builders/src/components/Components.ts | 24 +- .../components/selectMenu/BaseSelectMenu.ts | 64 ++++ .../selectMenu/ChannelSelectMenu.ts | 63 ++++ .../selectMenu/MentionableSelectMenu.ts | 31 ++ .../components/selectMenu/RoleSelectMenu.ts | 31 ++ .../src/components/selectMenu/SelectMenu.ts | 163 ---------- .../components/selectMenu/StringSelectMenu.ts | 106 +++++++ ...enuOption.ts => StringSelectMenuOption.ts} | 12 +- .../components/selectMenu/UserSelectMenu.ts | 31 ++ packages/builders/src/index.ts | 23 +- packages/discord.js/package.json | 2 +- .../src/client/actions/InteractionCreate.js | 22 +- packages/discord.js/src/index.js | 18 ++ .../src/structures/BaseInteraction.js | 62 +++- .../src/structures/BaseSelectMenuComponent.js | 56 ++++ .../structures/ChannelSelectMenuBuilder.js | 33 ++ .../structures/ChannelSelectMenuComponent.js | 20 ++ .../ChannelSelectMenuInteraction.js | 25 ++ .../src/structures/InteractionCollector.js | 9 +- .../MentionableSelectMenuBuilder.js | 33 ++ .../MentionableSelectMenuComponent.js | 11 + .../MentionableSelectMenuInteraction.js | 65 ++++ .../src/structures/RoleSelectMenuBuilder.js | 33 ++ .../src/structures/RoleSelectMenuComponent.js | 11 + .../structures/RoleSelectMenuInteraction.js | 25 ++ .../src/structures/SelectMenuBuilder.js | 83 +---- .../src/structures/SelectMenuComponent.js | 72 +---- .../src/structures/SelectMenuInteraction.js | 26 +- .../src/structures/SelectMenuOptionBuilder.js | 53 +--- .../src/structures/StringSelectMenuBuilder.js | 78 +++++ .../structures/StringSelectMenuComponent.js | 20 ++ .../structures/StringSelectMenuInteraction.js | 21 ++ .../StringSelectMenuOptionBuilder.js | 51 ++++ .../src/structures/UserSelectMenuBuilder.js | 33 ++ .../src/structures/UserSelectMenuComponent.js | 11 + .../structures/UserSelectMenuInteraction.js | 49 +++ packages/discord.js/src/util/Components.js | 36 ++- packages/discord.js/src/util/Constants.js | 20 +- packages/discord.js/typings/index.d.ts | 286 +++++++++++++++--- packages/discord.js/typings/index.test-d.ts | 93 ++++-- packages/rest/package.json | 2 +- packages/voice/package.json | 2 +- packages/ws/package.json | 2 +- yarn.lock | 18 +- 50 files changed, 1530 insertions(+), 471 deletions(-) create mode 100644 packages/builders/src/components/selectMenu/BaseSelectMenu.ts create mode 100644 packages/builders/src/components/selectMenu/ChannelSelectMenu.ts create mode 100644 packages/builders/src/components/selectMenu/MentionableSelectMenu.ts create mode 100644 packages/builders/src/components/selectMenu/RoleSelectMenu.ts delete mode 100644 packages/builders/src/components/selectMenu/SelectMenu.ts create mode 100644 packages/builders/src/components/selectMenu/StringSelectMenu.ts rename packages/builders/src/components/selectMenu/{SelectMenuOption.ts => StringSelectMenuOption.ts} (83%) create mode 100644 packages/builders/src/components/selectMenu/UserSelectMenu.ts create mode 100644 packages/discord.js/src/structures/BaseSelectMenuComponent.js create mode 100644 packages/discord.js/src/structures/ChannelSelectMenuBuilder.js create mode 100644 packages/discord.js/src/structures/ChannelSelectMenuComponent.js create mode 100644 packages/discord.js/src/structures/ChannelSelectMenuInteraction.js create mode 100644 packages/discord.js/src/structures/MentionableSelectMenuBuilder.js create mode 100644 packages/discord.js/src/structures/MentionableSelectMenuComponent.js create mode 100644 packages/discord.js/src/structures/MentionableSelectMenuInteraction.js create mode 100644 packages/discord.js/src/structures/RoleSelectMenuBuilder.js create mode 100644 packages/discord.js/src/structures/RoleSelectMenuComponent.js create mode 100644 packages/discord.js/src/structures/RoleSelectMenuInteraction.js create mode 100644 packages/discord.js/src/structures/StringSelectMenuBuilder.js create mode 100644 packages/discord.js/src/structures/StringSelectMenuComponent.js create mode 100644 packages/discord.js/src/structures/StringSelectMenuInteraction.js create mode 100644 packages/discord.js/src/structures/StringSelectMenuOptionBuilder.js create mode 100644 packages/discord.js/src/structures/UserSelectMenuBuilder.js create mode 100644 packages/discord.js/src/structures/UserSelectMenuComponent.js create mode 100644 packages/discord.js/src/structures/UserSelectMenuInteraction.js diff --git a/packages/builders/__tests__/components/actionRow.test.ts b/packages/builders/__tests__/components/actionRow.test.ts index d0bb9f7b584f..b9f63b501529 100644 --- a/packages/builders/__tests__/components/actionRow.test.ts +++ b/packages/builders/__tests__/components/actionRow.test.ts @@ -9,8 +9,8 @@ import { ActionRowBuilder, ButtonBuilder, createComponentBuilder, - SelectMenuBuilder, - SelectMenuOptionBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, } from '../../src/index.js'; const rowWithButtonData: APIActionRowComponent = { @@ -29,7 +29,7 @@ const rowWithSelectMenuData: APIActionRowComponent type: ComponentType.ActionRow, components: [ { - type: ComponentType.SelectMenu, + type: ComponentType.StringSelect, custom_id: '1234', options: [ { @@ -73,7 +73,7 @@ describe('Action Row Components', () => { url: 'https://google.com', }, { - type: ComponentType.SelectMenu, + type: ComponentType.StringSelect, placeholder: 'test', custom_id: 'test', options: [ @@ -108,7 +108,7 @@ describe('Action Row Components', () => { type: ComponentType.ActionRow, components: [ { - type: ComponentType.SelectMenu, + type: ComponentType.StringSelect, custom_id: '1234', options: [ { @@ -134,17 +134,17 @@ describe('Action Row Components', () => { test('GIVEN valid builder options THEN valid JSON output is given 2', () => { const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); - const selectMenu = new SelectMenuBuilder() + const selectMenu = new StringSelectMenuBuilder() .setCustomId('1234') .setMaxValues(10) .setMinValues(12) .setOptions( - new SelectMenuOptionBuilder().setLabel('one').setValue('one'), - new SelectMenuOptionBuilder().setLabel('two').setValue('two'), + new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'), + new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'), ) .setOptions([ - new SelectMenuOptionBuilder().setLabel('one').setValue('one'), - new SelectMenuOptionBuilder().setLabel('two').setValue('two'), + new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'), + new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'), ]); expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData); diff --git a/packages/builders/__tests__/components/components.test.ts b/packages/builders/__tests__/components/components.test.ts index 520244d438eb..fa0bd4607f65 100644 --- a/packages/builders/__tests__/components/components.test.ts +++ b/packages/builders/__tests__/components/components.test.ts @@ -13,12 +13,12 @@ import { ActionRowBuilder, ButtonBuilder, createComponentBuilder, - SelectMenuBuilder, + StringSelectMenuBuilder, TextInputBuilder, } from '../../src/index.js'; describe('createComponentBuilder', () => { - test.each([ButtonBuilder, SelectMenuBuilder, TextInputBuilder])( + test.each([ButtonBuilder, StringSelectMenuBuilder, TextInputBuilder])( 'passing an instance of %j should return itself', (Builder) => { const builder = new Builder(); @@ -45,14 +45,14 @@ describe('createComponentBuilder', () => { expect(createComponentBuilder(button)).toBeInstanceOf(ButtonBuilder); }); - test('GIVEN a select menu component THEN returns a SelectMenuBuilder', () => { + test('GIVEN a select menu component THEN returns a StringSelectMenuBuilder', () => { const selectMenu: APISelectMenuComponent = { custom_id: 'abc', options: [], - type: ComponentType.SelectMenu, + type: ComponentType.StringSelect, }; - expect(createComponentBuilder(selectMenu)).toBeInstanceOf(SelectMenuBuilder); + expect(createComponentBuilder(selectMenu)).toBeInstanceOf(StringSelectMenuBuilder); }); test('GIVEN a text input component THEN returns a TextInputBuilder', () => { diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index c5e10c53ccde..f7ab28144918 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -1,9 +1,9 @@ import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10'; import { describe, test, expect } from 'vitest'; -import { SelectMenuBuilder, SelectMenuOptionBuilder } from '../../src/index.js'; +import { StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from '../../src/index.js'; -const selectMenu = () => new SelectMenuBuilder(); -const selectMenuOption = () => new SelectMenuOptionBuilder(); +const selectMenu = () => new StringSelectMenuBuilder(); +const selectMenuOption = () => new StringSelectMenuOptionBuilder(); const longStr = 'a'.repeat(256); @@ -165,16 +165,16 @@ describe('Select Menu Components', () => { test('GIVEN valid JSON input THEN valid JSON history is correct', () => { expect( - new SelectMenuBuilder(selectMenuDataWithoutOptions) - .addOptions(new SelectMenuOptionBuilder(selectMenuOptionData)) + new StringSelectMenuBuilder(selectMenuDataWithoutOptions) + .addOptions(new StringSelectMenuOptionBuilder(selectMenuOptionData)) .toJSON(), ).toEqual(selectMenuData); expect( - new SelectMenuBuilder(selectMenuDataWithoutOptions) - .addOptions([new SelectMenuOptionBuilder(selectMenuOptionData)]) + new StringSelectMenuBuilder(selectMenuDataWithoutOptions) + .addOptions([new StringSelectMenuOptionBuilder(selectMenuOptionData)]) .toJSON(), ).toEqual(selectMenuData); - expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); + expect(new StringSelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); }); }); }); diff --git a/packages/builders/package.json b/packages/builders/package.json index 7639a07f2dbb..fd481908da84 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -56,7 +56,7 @@ "dependencies": { "@discordjs/util": "workspace:^", "@sapphire/shapeshift": "^3.7.0", - "discord-api-types": "^0.37.14", + "discord-api-types": "^0.37.15", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.1", "tslib": "^2.4.0" diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 1f10ddc85e8c..90dda30cae9e 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -11,14 +11,24 @@ import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js'; import { ComponentBuilder } from './Component.js'; import { createComponentBuilder } from './Components.js'; import type { ButtonBuilder } from './button/Button.js'; -import type { SelectMenuBuilder } from './selectMenu/SelectMenu.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'; export type MessageComponentBuilder = | ActionRowBuilder | MessageActionRowComponentBuilder; export type ModalComponentBuilder = ActionRowBuilder | ModalActionRowComponentBuilder; -export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder; +export type MessageActionRowComponentBuilder = + | ButtonBuilder + | ChannelSelectMenuBuilder + | MentionableSelectMenuBuilder + | RoleSelectMenuBuilder + | StringSelectMenuBuilder + | UserSelectMenuBuilder; export type ModalActionRowComponentBuilder = TextInputBuilder; export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder; diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index faae51dd16b7..960efd706c7c 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -1,7 +1,7 @@ import { s } from '@sapphire/shapeshift'; -import { ButtonStyle, type APIMessageComponentEmoji } from 'discord-api-types/v10'; +import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10'; import { isValidationEnabled } from '../util/validation.js'; -import { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption.js'; +import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js'; export const customIdValidator = s.string .lengthGreaterThanOrEqual(1) @@ -46,7 +46,7 @@ export const jsonOptionValidator = s }) .setValidationEnabled(isValidationEnabled); -export const optionValidator = s.instance(SelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled); +export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled); export const optionsValidator = optionValidator.array .lengthGreaterThanOrEqual(0) @@ -56,7 +56,7 @@ export const optionsLengthValidator = s.number.int .lessThanOrEqual(25) .setValidationEnabled(isValidationEnabled); -export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) { +export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) { customIdValidator.parse(customId); optionsValidator.parse(options); } @@ -68,6 +68,8 @@ export function validateRequiredSelectMenuOptionParameters(label?: string, value labelValueDescriptionValidator.parse(value); } +export const channelTypesValidator = s.nativeEnum(ChannelType).array.setValidationEnabled(isValidationEnabled); + export const urlValidator = s.string .url({ allowedProtocols: ['http:', 'https:', 'discord:'], diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index ea13e013de12..d3e635ece957 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -7,14 +7,22 @@ import { } from './ActionRow.js'; import { ComponentBuilder } from './Component.js'; import { ButtonBuilder } from './button/Button.js'; -import { SelectMenuBuilder } from './selectMenu/SelectMenu.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'; export interface MappedComponentTypes { [ComponentType.ActionRow]: ActionRowBuilder; [ComponentType.Button]: ButtonBuilder; - [ComponentType.SelectMenu]: SelectMenuBuilder; + [ComponentType.StringSelect]: StringSelectMenuBuilder; [ComponentType.TextInput]: TextInputBuilder; + [ComponentType.UserSelect]: UserSelectMenuBuilder; + [ComponentType.RoleSelect]: RoleSelectMenuBuilder; + [ComponentType.MentionableSelect]: MentionableSelectMenuBuilder; + [ComponentType.ChannelSelect]: ChannelSelectMenuBuilder; } /** @@ -39,10 +47,18 @@ export function createComponentBuilder( return new ActionRowBuilder(data); case ComponentType.Button: return new ButtonBuilder(data); - case ComponentType.SelectMenu: - return new SelectMenuBuilder(data); + case ComponentType.StringSelect: + return new StringSelectMenuBuilder(data); case ComponentType.TextInput: return new TextInputBuilder(data); + case ComponentType.UserSelect: + return new UserSelectMenuBuilder(data); + case ComponentType.RoleSelect: + return new RoleSelectMenuBuilder(data); + case ComponentType.MentionableSelect: + return new MentionableSelectMenuBuilder(data); + case ComponentType.ChannelSelect: + return new ChannelSelectMenuBuilder(data); default: // @ts-expect-error: This case can still occur if we get a newer unsupported component type throw new Error(`Cannot properly serialize component type: ${data.type}`); diff --git a/packages/builders/src/components/selectMenu/BaseSelectMenu.ts b/packages/builders/src/components/selectMenu/BaseSelectMenu.ts new file mode 100644 index 000000000000..cd1a306ac8f8 --- /dev/null +++ b/packages/builders/src/components/selectMenu/BaseSelectMenu.ts @@ -0,0 +1,64 @@ +import type { APISelectMenuComponent } from 'discord-api-types/v10'; +import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js'; +import { ComponentBuilder } from '../Component.js'; + +export class BaseSelectMenuBuilder< + SelectMenuType extends APISelectMenuComponent, +> extends ComponentBuilder { + /** + * Sets the placeholder for this select menu + * + * @param placeholder - The placeholder to use for this select menu + */ + public setPlaceholder(placeholder: string) { + this.data.placeholder = placeholderValidator.parse(placeholder); + return this; + } + + /** + * Sets the minimum values that must be selected in the select menu + * + * @param minValues - The minimum values that must be selected + */ + public setMinValues(minValues: number) { + this.data.min_values = minMaxValidator.parse(minValues); + return this; + } + + /** + * Sets the maximum values that must be selected in the select menu + * + * @param maxValues - The maximum values that must be selected + */ + public setMaxValues(maxValues: number) { + this.data.max_values = minMaxValidator.parse(maxValues); + return this; + } + + /** + * Sets the custom id for this select menu + * + * @param customId - The custom id to use for this select menu + */ + public setCustomId(customId: string) { + this.data.custom_id = customIdValidator.parse(customId); + return this; + } + + /** + * Sets whether this select menu is disabled + * + * @param disabled - Whether this select menu is disabled + */ + public setDisabled(disabled = true) { + this.data.disabled = disabledValidator.parse(disabled); + return this; + } + + 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 new file mode 100644 index 000000000000..a2d46f35e65c --- /dev/null +++ b/packages/builders/src/components/selectMenu/ChannelSelectMenu.ts @@ -0,0 +1,63 @@ +import type { APIChannelSelectComponent, ChannelType } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; +import { channelTypesValidator, customIdValidator } from '../Assertions.js'; +import { BaseSelectMenuBuilder } from './BaseSelectMenu.js'; + +export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder { + /** + * Creates a new select menu from API data + * + * @param data - The API data to create this select menu with + * @example + * Creating a select menu from an API data object + * ```ts + * const selectMenu = new ChannelSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * placeholder: 'select an option', + * max_values: 2, + * }); + * ``` + * @example + * Creating a select menu using setters and API data + * ```ts + * const selectMenu = new ChannelSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * }) + * .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement) + * .setMinValues(2) + * ``` + */ + public constructor(data?: Partial) { + super({ ...data, type: ComponentType.ChannelSelect }); + } + + public addChannelTypes(...types: RestOrArray) { + // eslint-disable-next-line no-param-reassign + types = normalizeArray(types); + + this.data.channel_types ??= []; + this.data.channel_types.push(...channelTypesValidator.parse(types)); + return this; + } + + public setChannelTypes(...types: RestOrArray) { + // eslint-disable-next-line no-param-reassign + types = normalizeArray(types); + + this.data.channel_types ??= []; + this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(types)); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public override toJSON(): APIChannelSelectComponent { + customIdValidator.parse(this.data.custom_id); + + return { + ...this.data, + } as APIChannelSelectComponent; + } +} diff --git a/packages/builders/src/components/selectMenu/MentionableSelectMenu.ts b/packages/builders/src/components/selectMenu/MentionableSelectMenu.ts new file mode 100644 index 000000000000..c996e2b4776d --- /dev/null +++ b/packages/builders/src/components/selectMenu/MentionableSelectMenu.ts @@ -0,0 +1,31 @@ +import type { APIMentionableSelectComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { BaseSelectMenuBuilder } from './BaseSelectMenu.js'; + +export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder { + /** + * Creates a new select menu from API data + * + * @param data - The API data to create this select menu with + * @example + * Creating a select menu from an API data object + * ```ts + * const selectMenu = new MentionableSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * placeholder: 'select an option', + * max_values: 2, + * }); + * ``` + * @example + * Creating a select menu using setters and API data + * ```ts + * const selectMenu = new MentionableSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * }) + * .setMinValues(1) + * ``` + */ + public constructor(data?: Partial) { + super({ ...data, type: ComponentType.MentionableSelect }); + } +} diff --git a/packages/builders/src/components/selectMenu/RoleSelectMenu.ts b/packages/builders/src/components/selectMenu/RoleSelectMenu.ts new file mode 100644 index 000000000000..818ef5b7763f --- /dev/null +++ b/packages/builders/src/components/selectMenu/RoleSelectMenu.ts @@ -0,0 +1,31 @@ +import type { APIRoleSelectComponent } from 'discord-api-types/v10'; +import { ComponentType } from 'discord-api-types/v10'; +import { BaseSelectMenuBuilder } from './BaseSelectMenu.js'; + +export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder { + /** + * Creates a new select menu from API data + * + * @param data - The API data to create this select menu with + * @example + * Creating a select menu from an API data object + * ```ts + * const selectMenu = new RoleSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * placeholder: 'select an option', + * max_values: 2, + * }); + * ``` + * @example + * Creating a select menu using setters and API data + * ```ts + * const selectMenu = new RoleSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * }) + * .setMinValues(1) + * ``` + */ + public constructor(data?: Partial) { + super({ ...data, type: ComponentType.RoleSelect }); + } +} diff --git a/packages/builders/src/components/selectMenu/SelectMenu.ts b/packages/builders/src/components/selectMenu/SelectMenu.ts deleted file mode 100644 index 496138a020ac..000000000000 --- a/packages/builders/src/components/selectMenu/SelectMenu.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10'; -import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; -import { - customIdValidator, - disabledValidator, - jsonOptionValidator, - minMaxValidator, - optionsLengthValidator, - placeholderValidator, - validateRequiredSelectMenuParameters, -} from '../Assertions.js'; -import { ComponentBuilder } from '../Component.js'; -import { SelectMenuOptionBuilder } from './SelectMenuOption.js'; - -/** - * Represents a select menu component - */ -export class SelectMenuBuilder extends ComponentBuilder { - /** - * The options within this select menu - */ - public readonly options: SelectMenuOptionBuilder[]; - - /** - * Creates a new select menu from API data - * - * @param data - The API data to create this select menu with - * @example - * Creating a select menu from an API data object - * ```ts - * const selectMenu = new SelectMenuBuilder({ - * custom_id: 'a cool select menu', - * placeholder: 'select an option', - * max_values: 2, - * options: [ - * { label: 'option 1', value: '1' }, - * { label: 'option 2', value: '2' }, - * { label: 'option 3', value: '3' }, - * ], - * }); - * ``` - * @example - * Creating a select menu using setters and API data - * ```ts - * const selectMenu = new SelectMenuBuilder({ - * custom_id: 'a cool select menu', - * }) - * .setMinValues(1) - * .addOptions({ - * label: 'Catchy', - * value: 'catch', - * }); - * ``` - */ - public constructor(data?: Partial) { - const { options, ...initData } = data ?? {}; - super({ type: ComponentType.SelectMenu, ...initData }); - this.options = options?.map((option) => new SelectMenuOptionBuilder(option)) ?? []; - } - - /** - * Sets the placeholder for this select menu - * - * @param placeholder - The placeholder to use for this select menu - */ - public setPlaceholder(placeholder: string) { - this.data.placeholder = placeholderValidator.parse(placeholder); - return this; - } - - /** - * Sets the minimum values that must be selected in the select menu - * - * @param minValues - The minimum values that must be selected - */ - public setMinValues(minValues: number) { - this.data.min_values = minMaxValidator.parse(minValues); - return this; - } - - /** - * Sets the maximum values that must be selected in the select menu - * - * @param maxValues - The maximum values that must be selected - */ - public setMaxValues(maxValues: number) { - this.data.max_values = minMaxValidator.parse(maxValues); - return this; - } - - /** - * Sets the custom id for this select menu - * - * @param customId - The custom id to use for this select menu - */ - public setCustomId(customId: string) { - this.data.custom_id = customIdValidator.parse(customId); - return this; - } - - /** - * Sets whether this select menu is disabled - * - * @param disabled - Whether this select menu is disabled - */ - public setDisabled(disabled = true) { - this.data.disabled = disabledValidator.parse(disabled); - return this; - } - - /** - * Adds options to this select menu - * - * @param options - The options to add to this select menu - * @returns - */ - public addOptions(...options: RestOrArray) { - // eslint-disable-next-line no-param-reassign - options = normalizeArray(options); - optionsLengthValidator.parse(this.options.length + options.length); - this.options.push( - ...options.map((option) => - option instanceof SelectMenuOptionBuilder - ? option - : new SelectMenuOptionBuilder(jsonOptionValidator.parse(option)), - ), - ); - return this; - } - - /** - * Sets the options on this select menu - * - * @param options - The options to set on this select menu - */ - public setOptions(...options: RestOrArray) { - // eslint-disable-next-line no-param-reassign - options = normalizeArray(options); - optionsLengthValidator.parse(options.length); - this.options.splice( - 0, - this.options.length, - ...options.map((option) => - option instanceof SelectMenuOptionBuilder - ? option - : new SelectMenuOptionBuilder(jsonOptionValidator.parse(option)), - ), - ); - return this; - } - - /** - * {@inheritDoc ComponentBuilder.toJSON} - */ - public toJSON(): APISelectMenuComponent { - validateRequiredSelectMenuParameters(this.options, this.data.custom_id); - - return { - ...this.data, - options: this.options.map((option) => option.toJSON()), - } as APISelectMenuComponent; - } -} diff --git a/packages/builders/src/components/selectMenu/StringSelectMenu.ts b/packages/builders/src/components/selectMenu/StringSelectMenu.ts new file mode 100644 index 000000000000..ed5f928c144b --- /dev/null +++ b/packages/builders/src/components/selectMenu/StringSelectMenu.ts @@ -0,0 +1,106 @@ +import type { APIStringSelectComponent } from 'discord-api-types/v10'; +import { ComponentType, type APISelectMenuOption } from 'discord-api-types/v10'; +import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js'; +import { jsonOptionValidator, optionsLengthValidator, validateRequiredSelectMenuParameters } from '../Assertions.js'; +import { BaseSelectMenuBuilder } from './BaseSelectMenu.js'; +import { StringSelectMenuOptionBuilder } from './StringSelectMenuOption.js'; + +/** + * Represents a string select menu component + */ +export class StringSelectMenuBuilder extends BaseSelectMenuBuilder { + /** + * The options within this select menu + */ + public readonly options: StringSelectMenuOptionBuilder[]; + + /** + * Creates a new select menu from API data + * + * @param data - The API data to create this select menu with + * @example + * Creating a select menu from an API data object + * ```ts + * const selectMenu = new StringSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * placeholder: 'select an option', + * max_values: 2, + * options: [ + * { label: 'option 1', value: '1' }, + * { label: 'option 2', value: '2' }, + * { label: 'option 3', value: '3' }, + * ], + * }); + * ``` + * @example + * Creating a select menu using setters and API data + * ```ts + * const selectMenu = new StringSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * }) + * .setMinValues(1) + * .addOptions({ + * label: 'Catchy', + * value: 'catch', + * }); + * ``` + */ + public constructor(data?: Partial) { + const { options, ...initData } = data ?? {}; + super({ ...initData, type: ComponentType.StringSelect }); + this.options = options?.map((option: APISelectMenuOption) => new StringSelectMenuOptionBuilder(option)) ?? []; + } + + /** + * Adds options to this select menu + * + * @param options - The options to add to this select menu + * @returns + */ + public addOptions(...options: RestOrArray) { + // eslint-disable-next-line no-param-reassign + options = normalizeArray(options); + optionsLengthValidator.parse(this.options.length + options.length); + this.options.push( + ...options.map((option) => + option instanceof StringSelectMenuOptionBuilder + ? option + : new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(option)), + ), + ); + return this; + } + + /** + * Sets the options on this select menu + * + * @param options - The options to set on this select menu + */ + public setOptions(...options: RestOrArray) { + // eslint-disable-next-line no-param-reassign + options = normalizeArray(options); + optionsLengthValidator.parse(options.length); + this.options.splice( + 0, + this.options.length, + ...options.map((option) => + option instanceof StringSelectMenuOptionBuilder + ? option + : new StringSelectMenuOptionBuilder(jsonOptionValidator.parse(option)), + ), + ); + return this; + } + + /** + * {@inheritDoc ComponentBuilder.toJSON} + */ + public override toJSON(): APIStringSelectComponent { + validateRequiredSelectMenuParameters(this.options, this.data.custom_id); + + return { + ...this.data, + options: this.options.map((option) => option.toJSON()), + } as APIStringSelectComponent; + } +} diff --git a/packages/builders/src/components/selectMenu/SelectMenuOption.ts b/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts similarity index 83% rename from packages/builders/src/components/selectMenu/SelectMenuOption.ts rename to packages/builders/src/components/selectMenu/StringSelectMenuOption.ts index 6654e7dba0dc..c43145463947 100644 --- a/packages/builders/src/components/selectMenu/SelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/StringSelectMenuOption.ts @@ -8,15 +8,15 @@ import { } from '../Assertions.js'; /** - * Represents a option within a select menu component + * Represents an option within a string select menu component */ -export class SelectMenuOptionBuilder implements JSONEncodable { +export class StringSelectMenuOptionBuilder implements JSONEncodable { /** - * Creates a new select menu option from API data + * Creates a new string select menu option from API data * - * @param data - The API data to create this select menu option with + * @param data - The API data to create this string select menu option with * @example - * Creating a select menu option from an API data object + * Creating a string select menu option from an API data object * ```ts * const selectMenuOption = new SelectMenuOptionBuilder({ * label: 'catchy label', @@ -24,7 +24,7 @@ export class SelectMenuOptionBuilder implements JSONEncodable { + /** + * Creates a new select menu from API data + * + * @param data - The API data to create this select menu with + * @example + * Creating a select menu from an API data object + * ```ts + * const selectMenu = new UserSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * placeholder: 'select an option', + * max_values: 2, + * }); + * ``` + * @example + * Creating a select menu using setters and API data + * ```ts + * const selectMenu = new UserSelectMenuBuilder({ + * custom_id: 'a cool select menu', + * }) + * .setMinValues(1) + * ``` + */ + public constructor(data?: Partial) { + super({ ...data, type: ComponentType.UserSelect }); + } +} diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts index 0db122bc6062..9f644d5b064c 100644 --- a/packages/builders/src/index.ts +++ b/packages/builders/src/index.ts @@ -11,8 +11,27 @@ 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/selectMenu/SelectMenu.js'; -export * from './components/selectMenu/SelectMenuOption.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'; diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 3e9e7dc32876..48f3461df95c 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -55,7 +55,7 @@ "@discordjs/util": "workspace:^", "@sapphire/snowflake": "^3.2.2", "@types/ws": "^8.5.3", - "discord-api-types": "^0.37.14", + "discord-api-types": "^0.37.15", "fast-deep-equal": "^3.1.3", "lodash.snakecase": "^4.1.1", "tslib": "^2.4.0", diff --git a/packages/discord.js/src/client/actions/InteractionCreate.js b/packages/discord.js/src/client/actions/InteractionCreate.js index 9d2b12dceb8f..c079121c7f6f 100644 --- a/packages/discord.js/src/client/actions/InteractionCreate.js +++ b/packages/discord.js/src/client/actions/InteractionCreate.js @@ -4,11 +4,15 @@ const { InteractionType, ComponentType, ApplicationCommandType } = require('disc const Action = require('./Action'); const AutocompleteInteraction = require('../../structures/AutocompleteInteraction'); const ButtonInteraction = require('../../structures/ButtonInteraction'); +const ChannelSelectMenuInteraction = require('../../structures/ChannelSelectMenuInteraction'); const ChatInputCommandInteraction = require('../../structures/ChatInputCommandInteraction'); +const MentionableSelectMenuInteraction = require('../../structures/MentionableSelectMenuInteraction'); const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction'); const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction'); -const SelectMenuInteraction = require('../../structures/SelectMenuInteraction'); +const RoleSelectMenuInteraction = require('../../structures/RoleSelectMenuInteraction'); +const StringSelectMenuInteraction = require('../../structures/StringSelectMenuInteraction'); const UserContextMenuCommandInteraction = require('../../structures/UserContextMenuCommandInteraction'); +const UserSelectMenuInteraction = require('../../structures/UserSelectMenuInteraction'); const Events = require('../../util/Events'); class InteractionCreateAction extends Action { @@ -49,8 +53,20 @@ class InteractionCreateAction extends Action { case ComponentType.Button: InteractionClass = ButtonInteraction; break; - case ComponentType.SelectMenu: - InteractionClass = SelectMenuInteraction; + case ComponentType.StringSelect: + InteractionClass = StringSelectMenuInteraction; + break; + case ComponentType.UserSelect: + InteractionClass = UserSelectMenuInteraction; + break; + case ComponentType.RoleSelect: + InteractionClass = RoleSelectMenuInteraction; + break; + case ComponentType.MentionableSelect: + InteractionClass = MentionableSelectMenuInteraction; + break; + case ComponentType.ChannelSelect: + InteractionClass = ChannelSelectMenuInteraction; break; default: client.emit( diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 1be0d3f7c0eb..61927e2b964c 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -154,9 +154,27 @@ 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.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder'); exports.StageChannel = require('./structures/StageChannel'); exports.StageInstance = require('./structures/StageInstance').StageInstance; exports.Sticker = require('./structures/Sticker').Sticker; diff --git a/packages/discord.js/src/structures/BaseInteraction.js b/packages/discord.js/src/structures/BaseInteraction.js index 5278c6781381..df98cf2e1c85 100644 --- a/packages/discord.js/src/structures/BaseInteraction.js +++ b/packages/discord.js/src/structures/BaseInteraction.js @@ -1,8 +1,10 @@ 'use strict'; +const { deprecate } = require('node:util'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v10'); const Base = require('./Base'); +const { SelectMenuTypes } = require('../util/Constants'); const PermissionsBitField = require('../util/PermissionsBitField'); /** @@ -268,12 +270,63 @@ class BaseInteraction extends Base { return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.Button; } + // TODO: Get rid of this in the next major /** - * Indicates whether this interaction is a {@link SelectMenuInteraction}. + * Indicates whether this interaction is a {@link StringSelectMenuInteraction}. * @returns {boolean} + * + * @deprecated Use {@link Interaction#isStringSelectMenu} instead */ isSelectMenu() { - return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.SelectMenu; + return this.isStringSelectMenu(); + } + + /** + * Indicates whether this interaction is a select menu of any known type. + * @returns {boolean} + */ + isAnySelectMenu() { + return this.type === InteractionType.MessageComponent && SelectMenuTypes.includes(this.componentType); + } + + /** + * Indicates whether this interaction is a {@link StringSelectMenuInteraction}. + * @returns {boolean} + */ + isStringSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.StringSelect; + } + + /** + * Indicates whether this interaction is a {@link UserSelectMenuInteraction} + * @returns {boolean} + */ + isUserSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.UserSelect; + } + + /** + * Indicates whether this interaction is a {@link RoleSelectMenuInteraction} + * @returns {boolean} + */ + isRoleSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.RoleSelect; + } + + /** + * Indicates whether this interaction is a {@link ChannelSelectMenuInteraction} + * @returns {boolean} + */ + isChannelSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.ChannelSelect; + } + + /** + * Indicates whether this interaction is a {@link MenionableSelectMenuInteraction} + * @returns {boolean} + */ + isMentionableSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.MentionableSelect; } /** @@ -285,4 +338,9 @@ 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/BaseSelectMenuComponent.js b/packages/discord.js/src/structures/BaseSelectMenuComponent.js new file mode 100644 index 000000000000..bb08087630a6 --- /dev/null +++ b/packages/discord.js/src/structures/BaseSelectMenuComponent.js @@ -0,0 +1,56 @@ +'use strict'; + +const Component = require('./Component'); + +/** + * Represents a select menu component + * @extends {Component} + */ +class BaseSelectMenuComponent extends Component { + /** + * The placeholder for this select menu + * @type {?string} + * @readonly + */ + get placeholder() { + return this.data.placeholder ?? null; + } + + /** + * The maximum amount of options that can be selected + * @type {?number} + * @readonly + */ + get maxValues() { + return this.data.max_values ?? null; + } + + /** + * The minimum amount of options that must be selected + * @type {?number} + * @readonly + */ + get minValues() { + return this.data.min_values ?? null; + } + + /** + * The custom id of this select menu + * @type {string} + * @readonly + */ + get customId() { + return this.data.custom_id; + } + + /** + * Whether this select menu is disabled + * @type {?boolean} + * @readonly + */ + get disabled() { + return this.data.disabled ?? null; + } +} + +module.exports = BaseSelectMenuComponent; diff --git a/packages/discord.js/src/structures/ChannelSelectMenuBuilder.js b/packages/discord.js/src/structures/ChannelSelectMenuBuilder.js new file mode 100644 index 000000000000..324f70b3e27f --- /dev/null +++ b/packages/discord.js/src/structures/ChannelSelectMenuBuilder.js @@ -0,0 +1,33 @@ +'use strict'; + +const { ChannelSelectMenuBuilder: BuildersChannelSelectMenu, isJSONEncodable } = require('@discordjs/builders'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersChannelSelectMenu} + */ +class ChannelSelectMenuBuilder extends BuildersChannelSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from json data + * @param {JSONEncodable | APISelectMenuComponent} other The other data + * @returns {ChannelSelectMenuBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = ChannelSelectMenuBuilder; + +/** + * @external BuildersChannelSelectMenu + * @see {@link https://discord.js.org/#/docs/builders/main/class/ChannelSelectMenuBuilder} + */ diff --git a/packages/discord.js/src/structures/ChannelSelectMenuComponent.js b/packages/discord.js/src/structures/ChannelSelectMenuComponent.js new file mode 100644 index 000000000000..90a706315fb0 --- /dev/null +++ b/packages/discord.js/src/structures/ChannelSelectMenuComponent.js @@ -0,0 +1,20 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a channel select menu component + * @extends {BaseSelectMenuComponent} + */ +class ChannelSelectMenuComponent extends BaseSelectMenuComponent { + /** + * The options in this select menu + * @type {?(ChannelType[])} + * @readonly + */ + get channelTypes() { + return this.data.channel_types ?? null; + } +} + +module.exports = ChannelSelectMenuComponent; diff --git a/packages/discord.js/src/structures/ChannelSelectMenuInteraction.js b/packages/discord.js/src/structures/ChannelSelectMenuInteraction.js new file mode 100644 index 000000000000..04d076e599f0 --- /dev/null +++ b/packages/discord.js/src/structures/ChannelSelectMenuInteraction.js @@ -0,0 +1,25 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a {@link ComponentType.ChannelSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class ChannelSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + + /** + * Collection of the selected channels + * @type {Collection} + */ + this.channels = new Collection(); + for (const channel of Object.values(data.data.resolved.channels)) { + this.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel); + } + } +} + +module.exports = ChannelSelectMenuInteraction; diff --git a/packages/discord.js/src/structures/InteractionCollector.js b/packages/discord.js/src/structures/InteractionCollector.js index 92f4d63a9ee4..0e63c7d1c20b 100644 --- a/packages/discord.js/src/structures/InteractionCollector.js +++ b/packages/discord.js/src/structures/InteractionCollector.js @@ -147,10 +147,17 @@ class InteractionCollector extends Collector { * @event InteractionCollector#collect * @param {BaseInteraction} interaction The interaction that was collected */ + if (this.interactionType && interaction.type !== this.interactionType) return null; if (this.componentType && interaction.componentType !== this.componentType) return null; if (this.messageId && interaction.message?.id !== this.messageId) return null; - if (this.messageInteractionId && interaction.message?.interaction?.id !== this.messageInteractionId) return null; + if ( + this.messageInteractionId && + interaction.message?.interaction?.id && + interaction.message.interaction.id !== this.messageInteractionId + ) { + return null; + } if (this.channelId && interaction.channelId !== this.channelId) return null; if (this.guildId && interaction.guildId !== this.guildId) return null; diff --git a/packages/discord.js/src/structures/MentionableSelectMenuBuilder.js b/packages/discord.js/src/structures/MentionableSelectMenuBuilder.js new file mode 100644 index 000000000000..d5673db6865e --- /dev/null +++ b/packages/discord.js/src/structures/MentionableSelectMenuBuilder.js @@ -0,0 +1,33 @@ +'use strict'; + +const { MentionableSelectMenuBuilder: BuildersMentionableSelectMenu, isJSONEncodable } = require('@discordjs/builders'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersMentionableSelectMenu} + */ +class MentionableSelectMenuBuilder extends BuildersMentionableSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from json data + * @param {JSONEncodable | APISelectMenuComponent} other The other data + * @returns {MentionableSelectMenuBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = MentionableSelectMenuBuilder; + +/** + * @external BuildersMentionableSelectMenu + * @see {@link https://discord.js.org/#/docs/builders/main/class/MentionableSelectMenuBuilder} + */ diff --git a/packages/discord.js/src/structures/MentionableSelectMenuComponent.js b/packages/discord.js/src/structures/MentionableSelectMenuComponent.js new file mode 100644 index 000000000000..d0f75c356e0c --- /dev/null +++ b/packages/discord.js/src/structures/MentionableSelectMenuComponent.js @@ -0,0 +1,11 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a mentionable select menu component + * @extends {BaseSelectMenuComponent} + */ +class MentionableSelectMenuComponent extends BaseSelectMenuComponent {} + +module.exports = MentionableSelectMenuComponent; diff --git a/packages/discord.js/src/structures/MentionableSelectMenuInteraction.js b/packages/discord.js/src/structures/MentionableSelectMenuInteraction.js new file mode 100644 index 000000000000..bd294a04a74a --- /dev/null +++ b/packages/discord.js/src/structures/MentionableSelectMenuInteraction.js @@ -0,0 +1,65 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); +const Events = require('../util/Events'); + +/** + * Represents a {@link ComponentType.MentionableSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class MentionableSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + + const { members, users, roles } = data.data.resolved ?? {}; + + /** + * Collection of the selected users + * @type {Collection} + */ + this.users = new Collection(); + + /** + * Collection of the selected users + * @type {Collection} + */ + this.members = new Collection(); + + /** + * Collection of the selected roles + * @type {Collection} + */ + this.roles = new Collection(); + + if (members) { + for (const [id, member] of Object.entries(members)) { + const user = users[id]; + if (!user) { + this.client.emit( + Events.Debug, + `[MentionableSelectMenuInteraction] Received a member without a user, skipping ${id}`, + ); + + continue; + } + + this.members.set(id, this.guild?.members._add({ user, ...member }) ?? { user, ...member }); + } + } + + if (users) { + for (const user of Object.values(users)) { + this.users.set(user.id, this.client.users._add(user)); + } + } + + if (roles) { + for (const role of Object.values(roles)) { + this.roles.set(role.id, this.guild?.roles._add(role) ?? role); + } + } + } +} + +module.exports = MentionableSelectMenuInteraction; diff --git a/packages/discord.js/src/structures/RoleSelectMenuBuilder.js b/packages/discord.js/src/structures/RoleSelectMenuBuilder.js new file mode 100644 index 000000000000..a42b436fa2c8 --- /dev/null +++ b/packages/discord.js/src/structures/RoleSelectMenuBuilder.js @@ -0,0 +1,33 @@ +'use strict'; + +const { RoleSelectMenuBuilder: BuildersRoleSelectMenu, isJSONEncodable } = require('@discordjs/builders'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersRoleSelectMenu} + */ +class RoleSelectMenuBuilder extends BuildersRoleSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from json data + * @param {JSONEncodable | APISelectMenuComponent} other The other data + * @returns {RoleSelectMenuBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = RoleSelectMenuBuilder; + +/** + * @external BuildersRoleSelectMenu + * @see {@link https://discord.js.org/#/docs/builders/main/class/RoleSelectMenuBuilder} + */ diff --git a/packages/discord.js/src/structures/RoleSelectMenuComponent.js b/packages/discord.js/src/structures/RoleSelectMenuComponent.js new file mode 100644 index 000000000000..1b279428d57c --- /dev/null +++ b/packages/discord.js/src/structures/RoleSelectMenuComponent.js @@ -0,0 +1,11 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a role select menu component + * @extends {BaseSelectMenuComponent} + */ +class RoleSelectMenuComponent extends BaseSelectMenuComponent {} + +module.exports = RoleSelectMenuComponent; diff --git a/packages/discord.js/src/structures/RoleSelectMenuInteraction.js b/packages/discord.js/src/structures/RoleSelectMenuInteraction.js new file mode 100644 index 000000000000..b45d356a2828 --- /dev/null +++ b/packages/discord.js/src/structures/RoleSelectMenuInteraction.js @@ -0,0 +1,25 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a {@link ComponentType.RoleSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class RoleSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + + /** + * Collection of the selected roles + * @type {Collection} + */ + this.roles = new Collection(); + for (const role of Object.values(data.data.resolved.roles)) { + this.roles.set(role.id, this.guild?.roles._add(role) ?? role); + } + } +} + +module.exports = RoleSelectMenuInteraction; diff --git a/packages/discord.js/src/structures/SelectMenuBuilder.js b/packages/discord.js/src/structures/SelectMenuBuilder.js index 738a18fe2950..caa971b468e9 100644 --- a/packages/discord.js/src/structures/SelectMenuBuilder.js +++ b/packages/discord.js/src/structures/SelectMenuBuilder.js @@ -1,78 +1,25 @@ 'use strict'; -const { SelectMenuBuilder: BuildersSelectMenu, isJSONEncodable, normalizeArray } = require('@discordjs/builders'); -const { toSnakeCase } = require('../util/Transformers'); -const { resolvePartialEmoji } = require('../util/Util'); +const process = require('node:process'); +const StringSelectMenuBuilder = require('./StringSelectMenuBuilder'); + +let deprecationEmitted = false; /** - * Class used to build select menu components to be sent through the API - * @extends {BuildersSelectMenu} + * @deprecated Use {@link StringSelectMenuBuilder} instead. */ -class SelectMenuBuilder extends BuildersSelectMenu { - constructor({ options, ...data } = {}) { - super( - toSnakeCase({ - ...data, - options: options?.map(({ emoji, ...option }) => ({ - ...option, - emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, - })), - }), - ); - } - - /** - * Normalizes a select menu option emoji - * @param {SelectMenuOptionData|JSONEncodable} selectMenuOption The option to normalize - * @returns {Array} - * @private - */ - static normalizeEmoji(selectMenuOption) { - if (isJSONEncodable(selectMenuOption)) { - return selectMenuOption; - } - - const { emoji, ...option } = selectMenuOption; - return { - ...option, - emoji: typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, - }; - } - - /** - * Adds options to this select menu - * @param {RestOrArray} options The options to add to this select menu - * @returns {SelectMenuBuilder} - */ - addOptions(...options) { - return super.addOptions(normalizeArray(options).map(option => SelectMenuBuilder.normalizeEmoji(option))); - } - - /** - * Sets the options on this select menu - * @param {RestOrArray} options The options to set on this select menu - * @returns {SelectMenuBuilder} - */ - setOptions(...options) { - return super.setOptions(normalizeArray(options).map(option => SelectMenuBuilder.normalizeEmoji(option))); - } - - /** - * Creates a new select menu builder from json data - * @param {JSONEncodable | APISelectMenuComponent} other The other data - * @returns {SelectMenuBuilder} - */ - static from(other) { - if (isJSONEncodable(other)) { - return new this(other.toJSON()); +class SelectMenuBuilder extends StringSelectMenuBuilder { + constructor(...params) { + super(...params); + + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuBuilder class is deprecated, use StringSelectMenuBuilder instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; } - return new this(other); } } module.exports = SelectMenuBuilder; - -/** - * @external BuildersSelectMenu - * @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuBuilder} - */ diff --git a/packages/discord.js/src/structures/SelectMenuComponent.js b/packages/discord.js/src/structures/SelectMenuComponent.js index 1d3e80bc77d0..850c985275c2 100644 --- a/packages/discord.js/src/structures/SelectMenuComponent.js +++ b/packages/discord.js/src/structures/SelectMenuComponent.js @@ -1,64 +1,24 @@ 'use strict'; -const Component = require('./Component'); +const process = require('node:process'); +const StringSelectMenuComponent = require('./StringSelectMenuComponent'); + +let deprecationEmitted = false; /** - * Represents a select menu component - * @extends {Component} + * @deprecated Use {@link StringSelectMenuComponent} instead. */ -class SelectMenuComponent extends Component { - /** - * The placeholder for this select menu - * @type {?string} - * @readonly - */ - get placeholder() { - return this.data.placeholder ?? null; - } - - /** - * The maximum amount of options that can be selected - * @type {?number} - * @readonly - */ - get maxValues() { - return this.data.max_values ?? null; - } - - /** - * The minimum amount of options that must be selected - * @type {?number} - * @readonly - */ - get minValues() { - return this.data.min_values ?? null; - } - - /** - * The custom id of this select menu - * @type {string} - * @readonly - */ - get customId() { - return this.data.custom_id; - } - - /** - * Whether this select menu is disabled - * @type {?boolean} - * @readonly - */ - get disabled() { - return this.data.disabled ?? null; - } - - /** - * The options in this select menu - * @type {APISelectMenuOption[]} - * @readonly - */ - get options() { - return this.data.options; +class SelectMenuComponent extends StringSelectMenuComponent { + constructor(...params) { + super(...params); + + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuComponent class is deprecated, use StringSelectMenuComponent instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; + } } } diff --git a/packages/discord.js/src/structures/SelectMenuInteraction.js b/packages/discord.js/src/structures/SelectMenuInteraction.js index 42ef0c1069ff..9ee092d008b1 100644 --- a/packages/discord.js/src/structures/SelectMenuInteraction.js +++ b/packages/discord.js/src/structures/SelectMenuInteraction.js @@ -1,20 +1,24 @@ 'use strict'; -const MessageComponentInteraction = require('./MessageComponentInteraction'); +const process = require('node:process'); +const StringSelectMenuInteraction = require('./StringSelectMenuInteraction'); + +let deprecationEmitted = false; /** - * Represents a select menu interaction. - * @extends {MessageComponentInteraction} + * @deprecated Use {@link StringSelectMenuInteraction} instead. */ -class SelectMenuInteraction extends MessageComponentInteraction { - constructor(client, data) { - super(client, data); +class SelectMenuInteraction extends StringSelectMenuInteraction { + constructor(...params) { + super(...params); - /** - * The values selected, if the component which was interacted with was a select menu - * @type {string[]} - */ - this.values = data.data.values ?? []; + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuInteraction class is deprecated, use StringSelectMenuInteraction instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; + } } } diff --git a/packages/discord.js/src/structures/SelectMenuOptionBuilder.js b/packages/discord.js/src/structures/SelectMenuOptionBuilder.js index 8c9d87a22779..ac4d265379d7 100644 --- a/packages/discord.js/src/structures/SelectMenuOptionBuilder.js +++ b/packages/discord.js/src/structures/SelectMenuOptionBuilder.js @@ -1,50 +1,25 @@ 'use strict'; -const { SelectMenuOptionBuilder: BuildersSelectMenuOption, isJSONEncodable } = require('@discordjs/builders'); -const { toSnakeCase } = require('../util/Transformers'); -const { resolvePartialEmoji } = require('../util/Util'); +const process = require('node:process'); +const StringSelectMenuOptionBuilder = require('./StringSelectMenuOptionBuilder'); + +let deprecationEmitted = false; /** - * Represents a select menu option builder. - * @extends {BuildersSelectMenuOption} + * @deprecated Use {@link StringSelectMenuOptionBuilder} instead. */ -class SelectMenuOptionBuilder extends BuildersSelectMenuOption { - constructor({ emoji, ...data } = {}) { - super( - toSnakeCase({ - ...data, - emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, - }), - ); - } - /** - * Sets the emoji to display on this option - * @param {ComponentEmojiResolvable} emoji The emoji to display on this option - * @returns {SelectMenuOptionBuilder} - */ - setEmoji(emoji) { - if (typeof emoji === 'string') { - return super.setEmoji(resolvePartialEmoji(emoji)); - } - return super.setEmoji(emoji); - } +class SelectMenuOptionBuilder extends StringSelectMenuOptionBuilder { + constructor(...params) { + super(...params); - /** - * Creates a new select menu option builder from JSON data - * @param {JSONEncodable|APISelectMenuOption} other The other data - * @returns {SelectMenuOptionBuilder} - */ - static from(other) { - if (isJSONEncodable(other)) { - return new this(other.toJSON()); + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuOptionBuilder class is deprecated, use StringSelectMenuOptionBuilder instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; } - return new this(other); } } module.exports = SelectMenuOptionBuilder; - -/** - * @external BuildersSelectMenuOption - * @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuOptionBuilder} - */ diff --git a/packages/discord.js/src/structures/StringSelectMenuBuilder.js b/packages/discord.js/src/structures/StringSelectMenuBuilder.js new file mode 100644 index 000000000000..3dd645e3cb53 --- /dev/null +++ b/packages/discord.js/src/structures/StringSelectMenuBuilder.js @@ -0,0 +1,78 @@ +'use strict'; + +const { SelectMenuBuilder: BuildersSelectMenu, isJSONEncodable, normalizeArray } = require('@discordjs/builders'); +const { toSnakeCase } = require('../util/Transformers'); +const { resolvePartialEmoji } = require('../util/Util'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersSelectMenu} + */ +class StringSelectMenuBuilder extends BuildersSelectMenu { + constructor({ options, ...data } = {}) { + super( + toSnakeCase({ + ...data, + options: options?.map(({ emoji, ...option }) => ({ + ...option, + emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, + })), + }), + ); + } + + /** + * Normalizes a select menu option emoji + * @param {SelectMenuOptionData|JSONEncodable} selectMenuOption The option to normalize + * @returns {SelectMenuOptionBuilder|APISelectMenuOption} + * @private + */ + static normalizeEmoji(selectMenuOption) { + if (isJSONEncodable(selectMenuOption)) { + return selectMenuOption; + } + + const { emoji, ...option } = selectMenuOption; + return { + ...option, + emoji: typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, + }; + } + + /** + * Adds options to this select menu + * @param {RestOrArray} options The options to add to this select menu + * @returns {StringSelectMenuBuilder} + */ + addOptions(...options) { + return super.addOptions(normalizeArray(options).map(option => StringSelectMenuBuilder.normalizeEmoji(option))); + } + + /** + * Sets the options on this select menu + * @param {RestOrArray} options The options to set on this select menu + * @returns {StringSelectMenuBuilder} + */ + setOptions(...options) { + return super.setOptions(normalizeArray(options).map(option => StringSelectMenuBuilder.normalizeEmoji(option))); + } + + /** + * Creates a new select menu builder from json data + * @param {JSONEncodable | APISelectMenuComponent} other The other data + * @returns {StringSelectMenuBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = StringSelectMenuBuilder; + +/** + * @external BuildersSelectMenu + * @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuBuilder} + */ diff --git a/packages/discord.js/src/structures/StringSelectMenuComponent.js b/packages/discord.js/src/structures/StringSelectMenuComponent.js new file mode 100644 index 000000000000..e008ae5f2b08 --- /dev/null +++ b/packages/discord.js/src/structures/StringSelectMenuComponent.js @@ -0,0 +1,20 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a string select menu component + * @extends {BaseSelectMenuComponent} + */ +class StringSelectMenuComponent extends BaseSelectMenuComponent { + /** + * The options in this select menu + * @type {APISelectMenuOption[]} + * @readonly + */ + get options() { + return this.data.options; + } +} + +module.exports = StringSelectMenuComponent; diff --git a/packages/discord.js/src/structures/StringSelectMenuInteraction.js b/packages/discord.js/src/structures/StringSelectMenuInteraction.js new file mode 100644 index 000000000000..1db8c28f6711 --- /dev/null +++ b/packages/discord.js/src/structures/StringSelectMenuInteraction.js @@ -0,0 +1,21 @@ +'use strict'; + +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a {@link ComponentType.StringSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class StringSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + + /** + * The values selected + * @type {string[]} + */ + this.values = data.data.values ?? []; + } +} + +module.exports = StringSelectMenuInteraction; diff --git a/packages/discord.js/src/structures/StringSelectMenuOptionBuilder.js b/packages/discord.js/src/structures/StringSelectMenuOptionBuilder.js new file mode 100644 index 000000000000..f5fa6d9e6884 --- /dev/null +++ b/packages/discord.js/src/structures/StringSelectMenuOptionBuilder.js @@ -0,0 +1,51 @@ +'use strict'; + +const { SelectMenuOptionBuilder: BuildersSelectMenuOption, isJSONEncodable } = require('@discordjs/builders'); +const { toSnakeCase } = require('../util/Transformers'); +const { resolvePartialEmoji } = require('../util/Util'); + +/** + * Represents a select menu option builder. + * @extends {BuildersSelectMenuOption} + */ +class StringSelectMenuOptionBuilder extends BuildersSelectMenuOption { + constructor({ emoji, ...data } = {}) { + super( + toSnakeCase({ + ...data, + emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, + }), + ); + } + + /** + * Sets the emoji to display on this option + * @param {ComponentEmojiResolvable} emoji The emoji to display on this option + * @returns {StringSelectMenuOptionBuilder} + */ + setEmoji(emoji) { + if (typeof emoji === 'string') { + return super.setEmoji(resolvePartialEmoji(emoji)); + } + return super.setEmoji(emoji); + } + + /** + * Creates a new select menu option builder from JSON data + * @param {JSONEncodable|APISelectMenuOption} other The other data + * @returns {StringSelectMenuOptionBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = StringSelectMenuOptionBuilder; + +/** + * @external BuildersSelectMenuOption + * @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuOptionBuilder} + */ diff --git a/packages/discord.js/src/structures/UserSelectMenuBuilder.js b/packages/discord.js/src/structures/UserSelectMenuBuilder.js new file mode 100644 index 000000000000..39db60fff326 --- /dev/null +++ b/packages/discord.js/src/structures/UserSelectMenuBuilder.js @@ -0,0 +1,33 @@ +'use strict'; + +const { UserSelectMenuBuilder: BuildersUserSelectMenu, isJSONEncodable } = require('@discordjs/builders'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersUserSelectMenu} + */ +class UserSelectMenuBuilder extends BuildersUserSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from json data + * @param {JSONEncodable | APISelectMenuComponent} other The other data + * @returns {UserSelectMenuBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = UserSelectMenuBuilder; + +/** + * @external BuildersUserSelectMenu + * @see {@link https://discord.js.org/#/docs/builders/main/class/UserSelectMenuBuilder} + */ diff --git a/packages/discord.js/src/structures/UserSelectMenuComponent.js b/packages/discord.js/src/structures/UserSelectMenuComponent.js new file mode 100644 index 000000000000..0acacdfab2e5 --- /dev/null +++ b/packages/discord.js/src/structures/UserSelectMenuComponent.js @@ -0,0 +1,11 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a user select menu component + * @extends {BaseSelectMenuComponent} + */ +class UserSelectMenuComponent extends BaseSelectMenuComponent {} + +module.exports = UserSelectMenuComponent; diff --git a/packages/discord.js/src/structures/UserSelectMenuInteraction.js b/packages/discord.js/src/structures/UserSelectMenuInteraction.js new file mode 100644 index 000000000000..d2af4176a5e7 --- /dev/null +++ b/packages/discord.js/src/structures/UserSelectMenuInteraction.js @@ -0,0 +1,49 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); +const Events = require('../util/Events'); + +/** + * Represents a {@link ComponentType.UserSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class UserSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + + /** + * Collection of the selected users + * @type {Collection} + */ + this.users = new Collection(); + + /** + * Collection of the selected members + * @type {Collection} + */ + this.members = new Collection(); + + for (const user of Object.values(data.data.resolved.users)) { + this.users.set(user.id, this.client.users._add(user)); + } + + if (data.data.resolved.members) { + for (const [id, member] of Object.entries(data.data.resolved.members)) { + const user = data.data.resolved.users[id]; + if (!user) { + this.client.emit( + Events.Debug, + `[UserSelectMenuInteraction] Received a member without a user, skipping ${id}`, + ); + + continue; + } + + this.members.set(id, this.guild?.members._add({ user, ...member }) ?? { user, ...member }); + } + } + } +} + +module.exports = UserSelectMenuInteraction; diff --git a/packages/discord.js/src/util/Components.js b/packages/discord.js/src/util/Components.js index 02411f42b11c..42bee7f2afa0 100644 --- a/packages/discord.js/src/util/Components.js +++ b/packages/discord.js/src/util/Components.js @@ -82,10 +82,18 @@ function createComponent(data) { return new ActionRow(data); case ComponentType.Button: return new ButtonComponent(data); - case ComponentType.SelectMenu: - return new SelectMenuComponent(data); + case ComponentType.StringSelect: + return new StringSelectMenuComponent(data); case ComponentType.TextInput: return new TextInputComponent(data); + case ComponentType.UserSelect: + return new UserSelectMenuComponent(data); + case ComponentType.RoleSelect: + return new RoleSelectMenuComponent(data); + case ComponentType.MentionableSelect: + return new MentionableSelectMenuComponent(data); + case ComponentType.ChannelSelect: + return new ChannelSelectMenuComponent(data); default: return new Component(data); } @@ -106,10 +114,18 @@ function createComponentBuilder(data) { return new ActionRowBuilder(data); case ComponentType.Button: return new ButtonBuilder(data); - case ComponentType.SelectMenu: - return new SelectMenuBuilder(data); + case ComponentType.StringSelect: + return new StringSelectMenuBuilder(data); case ComponentType.TextInput: return new TextInputBuilder(data); + case ComponentType.UserSelect: + return new UserSelectMenuBuilder(data); + case ComponentType.RoleSelect: + return new RoleSelectMenuBuilder(data); + case ComponentType.MentionableSelect: + return new MentionableSelectMenuBuilder(data); + case ComponentType.ChannelSelect: + return new ChannelSelectMenuBuilder(data); default: return new ComponentBuilder(data); } @@ -121,11 +137,19 @@ const ActionRow = require('../structures/ActionRow'); const ActionRowBuilder = require('../structures/ActionRowBuilder'); const ButtonBuilder = require('../structures/ButtonBuilder'); const ButtonComponent = require('../structures/ButtonComponent'); +const ChannelSelectMenuBuilder = require('../structures/ChannelSelectMenuBuilder'); +const ChannelSelectMenuComponent = require('../structures/ChannelSelectMenuComponent'); const Component = require('../structures/Component'); -const SelectMenuBuilder = require('../structures/SelectMenuBuilder'); -const SelectMenuComponent = require('../structures/SelectMenuComponent'); +const MentionableSelectMenuBuilder = require('../structures/MentionableSelectMenuBuilder'); +const MentionableSelectMenuComponent = require('../structures/MentionableSelectMenuComponent'); +const RoleSelectMenuBuilder = require('../structures/RoleSelectMenuBuilder'); +const RoleSelectMenuComponent = require('../structures/RoleSelectMenuComponent'); +const StringSelectMenuBuilder = require('../structures/StringSelectMenuBuilder'); +const StringSelectMenuComponent = require('../structures/StringSelectMenuComponent'); const TextInputBuilder = require('../structures/TextInputBuilder'); const TextInputComponent = require('../structures/TextInputComponent'); +const UserSelectMenuBuilder = require('../structures/UserSelectMenuBuilder'); +const UserSelectMenuComponent = require('../structures/UserSelectMenuComponent'); /** * @external JSONEncodable diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index 61a5d191eac7..a2d98c1788af 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -1,6 +1,6 @@ 'use strict'; -const { ChannelType, MessageType } = require('discord-api-types/v10'); +const { ChannelType, MessageType, ComponentType } = require('discord-api-types/v10'); /** * The name of an item to be swept in Sweepers @@ -113,6 +113,23 @@ exports.ThreadChannelTypes = [ChannelType.AnnouncementThread, ChannelType.Public */ exports.VoiceBasedChannelTypes = [ChannelType.GuildVoice, ChannelType.GuildStageVoice]; +/** + * The types of select menus. The available types are: + * * {@link ComponentType.StringSelect} + * * {@link ComponentType.UserSelect} + * * {@link ComponentType.RoleSelect} + * * {@link ComponentType.MentionableSelect} + * * {@link ComponentType.ChannelSelect} + * @typedef {ComponentType[]} SelectMenuTypes + */ +exports.SelectMenuTypes = [ + ComponentType.StringSelect, + ComponentType.UserSelect, + ComponentType.RoleSelect, + ComponentType.MentionableSelect, + ComponentType.ChannelSelect, +]; + /** * @typedef {Object} Constants Constants that can be used in an enum or object-like way. * @property {SweeperKey[]} SweeperKeys The possible names of items that can be swept in sweepers @@ -120,4 +137,5 @@ exports.VoiceBasedChannelTypes = [ChannelType.GuildVoice, ChannelType.GuildStage * @property {TextBasedChannelTypes} TextBasedChannelTypes The types of channels that are text-based * @property {ThreadChannelTypes} ThreadChannelTypes The types of channels that are threads * @property {VoiceBasedChannelTypes} VoiceBasedChannelTypes The types of channels that are voice-based + * @property {SelectMenuTypes} SelectMenuTypes The types of components that are select menus. */ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 5edb087bf6b7..36e3e6a9acfb 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -14,14 +14,17 @@ import { italic, quote, roleMention, - SelectMenuBuilder as BuilderSelectMenuComponent, + ChannelSelectMenuBuilder as BuilderChannelSelectMenuComponent, + MentionableSelectMenuBuilder as BuilderMentionableSelectMenuComponent, + RoleSelectMenuBuilder as BuilderRoleSelectMenuComponent, + StringSelectMenuBuilder as BuilderStringSelectMenuComponent, + UserSelectMenuBuilder as BuilderUserSelectMenuComponent, TextInputBuilder as BuilderTextInputComponent, SelectMenuOptionBuilder as BuildersSelectMenuOption, spoiler, strikethrough, time, TimestampStyles, - TimestampStylesString, underscore, userMention, ModalActionRowComponentBuilder, @@ -35,7 +38,6 @@ import { Collection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; import { APIActionRowComponent, - APIApplicationCommand, APIApplicationCommandInteractionData, APIApplicationCommandOption, APIAuditLogChange, @@ -126,6 +128,17 @@ import { TextChannelType, ChannelFlags, SortOrderType, + APIMessageStringSelectInteractionData, + APIMessageUserSelectInteractionData, + APIStringSelectComponent, + APIUserSelectComponent, + APIRoleSelectComponent, + APIMentionableSelectComponent, + APIChannelSelectComponent, + APIGuildMember, + APIMessageRoleSelectInteractionData, + APIMessageMentionableSelectInteractionData, + APIMessageChannelSelectInteractionData, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -158,13 +171,11 @@ import { RawInviteData, RawInviteGuildData, RawInviteStageInstance, - RawAttachmentData, RawMessageButtonInteractionData, RawMessageComponentInteractionData, RawMessageData, RawMessagePayloadData, RawMessageReactionData, - RawMessageSelectMenuInteractionData, RawOAuth2GuildData, RawPartialGroupDMChannelData, RawPartialMessageData, @@ -242,7 +253,11 @@ export interface BaseComponentData { export type MessageActionRowComponentData = | JSONEncodable | ButtonComponentData - | SelectMenuComponentData; + | StringSelectMenuComponentData + | UserSelectMenuComponentData + | RoleSelectMenuComponentData + | MentionableSelectMenuComponentData + | ChannelSelectMenuComponentData; export type ModalActionRowComponentData = JSONEncodable | TextInputComponentData; @@ -269,7 +284,13 @@ export class ActionRowBuilder extends Component< @@ -604,8 +625,8 @@ export class ButtonBuilder extends BuilderButtonComponent { public override setEmoji(emoji: ComponentEmojiResolvable): this; } -export class SelectMenuBuilder extends BuilderSelectMenuComponent { - public constructor(data?: Partial); +export class StringSelectMenuBuilder extends BuilderStringSelectMenuComponent { + public constructor(data?: Partial); private static normalizeEmoji( selectMenuOption: JSONEncodable | SelectMenuComponentOptionData, ): (APISelectMenuOption | SelectMenuOptionBuilder)[]; @@ -615,7 +636,34 @@ export class SelectMenuBuilder extends BuilderSelectMenuComponent { public override setOptions( ...options: RestOrArray ): this; - public static from(other: JSONEncodable | APISelectMenuComponent): SelectMenuBuilder; + public static from(other: JSONEncodable | APISelectMenuComponent): StringSelectMenuBuilder; +} + +export { + /** @deprecated Use {@link StringSelectMenuBuilder} instead */ + StringSelectMenuBuilder as SelectMenuBuilder, +}; + +export class UserSelectMenuBuilder extends BuilderUserSelectMenuComponent { + public constructor(data?: Partial); + public static from(other: JSONEncodable | APISelectMenuComponent): UserSelectMenuBuilder; +} + +export class RoleSelectMenuBuilder extends BuilderRoleSelectMenuComponent { + public constructor(data?: Partial); + public static from(other: JSONEncodable | APISelectMenuComponent): RoleSelectMenuBuilder; +} + +export class MentionableSelectMenuBuilder extends BuilderMentionableSelectMenuComponent { + public constructor(data?: Partial); + public static from( + other: JSONEncodable | APISelectMenuComponent, + ): MentionableSelectMenuBuilder; +} + +export class ChannelSelectMenuBuilder extends BuilderChannelSelectMenuComponent { + public constructor(data?: Partial); + public static from(other: JSONEncodable | APISelectMenuComponent): ChannelSelectMenuBuilder; } export class SelectMenuOptionBuilder extends BuildersSelectMenuOption { @@ -639,16 +687,34 @@ export class TextInputComponent extends Component { public get value(): string; } -export class SelectMenuComponent extends Component { - private constructor(data: APISelectMenuComponent); +export class BaseSelectMenuComponent extends Component { + protected constructor(data: Data); public get placeholder(): string | null; public get maxValues(): number | null; public get minValues(): number | null; public get customId(): string; public get disabled(): boolean | null; +} + +export class StringSelectMenuComponent extends BaseSelectMenuComponent { public get options(): APISelectMenuOption[]; } +export { + /** @deprecated Use {@link StringSelectMenuComponent} instead */ + StringSelectMenuComponent as SelectMenuComponent, +}; + +export class UserSelectMenuComponent extends BaseSelectMenuComponent {} + +export class RoleSelectMenuComponent extends BaseSelectMenuComponent {} + +export class MentionableSelectMenuComponent extends BaseSelectMenuComponent {} + +export class ChannelSelectMenuComponent extends BaseSelectMenuComponent { + public getChannelTypes(): ChannelType[] | null; +} + export interface EmbedData { title?: string; type?: EmbedType; @@ -1501,7 +1567,7 @@ export type Interaction = | ChatInputCommandInteraction | MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction - | SelectMenuInteraction + | AnySelectMenuInteraction | ButtonInteraction | AutocompleteInteraction | ModalSubmitInteraction; @@ -1550,7 +1616,14 @@ export class BaseInteraction extends Base public isMessageContextMenuCommand(): this is MessageContextMenuCommandInteraction; public isModalSubmit(): this is ModalSubmitInteraction; public isUserContextMenuCommand(): this is UserContextMenuCommandInteraction; - public isSelectMenu(): this is SelectMenuInteraction; + /** @deprecated Use {@link BaseInteraction#isStringSelectMenu} instead */ + public isSelectMenu(): this is StringSelectMenuInteraction; + public isAnySelectMenu(): this is AnySelectMenuInteraction; + public isStringSelectMenu(): this is StringSelectMenuInteraction; + public isUserSelectMenu(): this is UserSelectMenuInteraction; + public isRoleSelectMenu(): this is RoleSelectMenuInteraction; + public isMentionableSelectMenu(): this is MentionableSelectMenuInteraction; + public isChannelSelectMenu(): this is ChannelSelectMenuInteraction; public isRepliable(): this is RepliableInteraction; } @@ -1673,7 +1746,11 @@ export type AwaitMessageCollectorOptionsParams { Button: ButtonInteraction; - SelectMenu: SelectMenuInteraction; + StringSelectMenu: StringSelectMenuInteraction; + UserSelectMenu: UserSelectMenuInteraction; + RoleSelectMenu: RoleSelectMenuInteraction; + MentionableSelectMenu: MentionableSelectMenuInteraction; + ChannelSelectMenu: ChannelSelectMenuInteraction; ActionRow: MessageComponentInteraction; } @@ -1681,7 +1758,11 @@ export type WrapBooleanCache = If; export interface MappedInteractionTypes { [ComponentType.Button]: ButtonInteraction>; - [ComponentType.SelectMenu]: SelectMenuInteraction>; + [ComponentType.StringSelect]: StringSelectMenuInteraction>; + [ComponentType.UserSelect]: UserSelectMenuInteraction>; + [ComponentType.RoleSelect]: RoleSelectMenuInteraction>; + [ComponentType.MentionableSelect]: MentionableSelectMenuInteraction>; + [ComponentType.ChannelSelect]: ChannelSelectMenuInteraction>; } export class Message extends Base { @@ -2254,22 +2335,116 @@ export class Role extends Base { public toString(): RoleMention; } -export class SelectMenuInteraction extends MessageComponentInteraction { - public constructor(client: Client, data: RawMessageSelectMenuInteractionData); +export class StringSelectMenuInteraction< + Cached extends CacheType = CacheType, +> extends MessageComponentInteraction { + public constructor(client: Client, data: APIMessageStringSelectInteractionData); public get component(): CacheTypeReducer< Cached, - SelectMenuComponent, - APISelectMenuComponent, - SelectMenuComponent | APISelectMenuComponent, - SelectMenuComponent | APISelectMenuComponent + StringSelectMenuComponent, + APIStringSelectComponent, + StringSelectMenuComponent | APIStringSelectComponent, + StringSelectMenuComponent | APIStringSelectComponent >; - public componentType: ComponentType.SelectMenu; + public componentType: ComponentType.StringSelect; public values: string[]; - public inGuild(): this is SelectMenuInteraction<'raw' | 'cached'>; - public inCachedGuild(): this is SelectMenuInteraction<'cached'>; - public inRawGuild(): this is SelectMenuInteraction<'raw'>; + public inGuild(): this is StringSelectMenuInteraction<'raw' | 'cached'>; + public inCachedGuild(): this is StringSelectMenuInteraction<'cached'>; + public inRawGuild(): this is StringSelectMenuInteraction<'raw'>; } +export { + /** @deprecated Use {@link StringSelectMenuInteraction} instead */ + StringSelectMenuInteraction as SelectMenuInteraction, +}; + +export class UserSelectMenuInteraction< + Cached extends CacheType = CacheType, +> extends MessageComponentInteraction { + public constructor(client: Client, data: APIMessageUserSelectInteractionData); + public get component(): CacheTypeReducer< + Cached, + UserSelectMenuComponent, + APIUserSelectComponent, + UserSelectMenuComponent | APIUserSelectComponent, + UserSelectMenuComponent | APIUserSelectComponent + >; + public componentType: ComponentType.UserSelect; + public users: Collection; + public members: Collection>; + public inGuild(): this is UserSelectMenuInteraction<'raw' | 'cached'>; + public inCachedGuild(): this is UserSelectMenuInteraction<'cached'>; + public inRawGuild(): this is UserSelectMenuInteraction<'raw'>; +} + +export class RoleSelectMenuInteraction< + Cached extends CacheType = CacheType, +> extends MessageComponentInteraction { + public constructor(client: Client, data: APIMessageRoleSelectInteractionData); + public get component(): CacheTypeReducer< + Cached, + RoleSelectMenuComponent, + APIRoleSelectComponent, + RoleSelectMenuComponent | APIRoleSelectComponent, + RoleSelectMenuComponent | APIRoleSelectComponent + >; + public componentType: ComponentType.RoleSelect; + public roles: Collection>; + public inGuild(): this is RoleSelectMenuInteraction<'raw' | 'cached'>; + public inCachedGuild(): this is RoleSelectMenuInteraction<'cached'>; + public inRawGuild(): this is RoleSelectMenuInteraction<'raw'>; +} + +export class MentionableSelectMenuInteraction< + Cached extends CacheType = CacheType, +> extends MessageComponentInteraction { + public constructor(client: Client, data: APIMessageMentionableSelectInteractionData); + public get component(): CacheTypeReducer< + Cached, + MentionableSelectMenuComponent, + APIMentionableSelectComponent, + MentionableSelectMenuComponent | APIMentionableSelectComponent, + MentionableSelectMenuComponent | APIMentionableSelectComponent + >; + public componentType: ComponentType.MentionableSelect; + public users: Collection; + public members: Collection>; + public roles: Collection>; + public inGuild(): this is MentionableSelectMenuInteraction<'raw' | 'cached'>; + public inCachedGuild(): this is MentionableSelectMenuInteraction<'cached'>; + public inRawGuild(): this is MentionableSelectMenuInteraction<'raw'>; +} + +export class ChannelSelectMenuInteraction< + Cached extends CacheType = CacheType, +> extends MessageComponentInteraction { + public constructor(client: Client, data: APIMessageChannelSelectInteractionData); + public get component(): CacheTypeReducer< + Cached, + ChannelSelectMenuComponent, + APIChannelSelectComponent, + ChannelSelectMenuComponent | APIChannelSelectComponent, + ChannelSelectMenuComponent | APIChannelSelectComponent + >; + public componentType: ComponentType.ChannelSelect; + public channels: Collection>; + public inGuild(): this is ChannelSelectMenuInteraction<'raw' | 'cached'>; + public inCachedGuild(): this is ChannelSelectMenuInteraction<'cached'>; + public inRawGuild(): this is ChannelSelectMenuInteraction<'raw'>; +} + +// 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 = + | StringSelectMenuInteraction + | UserSelectMenuInteraction + | RoleSelectMenuInteraction + | MentionableSelectMenuInteraction + | ChannelSelectMenuInteraction; + +export type SelectMenuType = APISelectMenuComponent['type']; + export interface ShardEventTypes { death: [process: ChildProcess | Worker]; disconnect: []; @@ -2770,14 +2945,22 @@ export function parseWebhookURL(url: string): WebhookClientDataIdWithToken | nul export interface MappedComponentBuilderTypes { [ComponentType.Button]: ButtonBuilder; - [ComponentType.SelectMenu]: SelectMenuBuilder; + [ComponentType.StringSelect]: StringSelectMenuBuilder; + [ComponentType.UserSelect]: UserSelectMenuBuilder; + [ComponentType.RoleSelect]: RoleSelectMenuBuilder; + [ComponentType.MentionableSelect]: MentionableSelectMenuBuilder; + [ComponentType.ChannelSelect]: ChannelSelectMenuBuilder; [ComponentType.ActionRow]: ActionRowBuilder; [ComponentType.TextInput]: TextInputBuilder; } export interface MappedComponentTypes { [ComponentType.Button]: ButtonComponent; - [ComponentType.SelectMenu]: SelectMenuComponent; + [ComponentType.StringSelect]: StringSelectMenuComponent; + [ComponentType.UserSelect]: UserSelectMenuComponent; + [ComponentType.RoleSelect]: RoleSelectMenuComponent; + [ComponentType.MentionableSelect]: MentionableSelectMenuComponent; + [ComponentType.ChannelSelect]: ChannelSelectMenuComponent; [ComponentType.ActionRow]: ActionRowComponent; [ComponentType.TextInput]: TextInputComponent; } @@ -3116,6 +3299,7 @@ export const Constants: { TextBasedChannelTypes: TextBasedChannelTypes[]; ThreadChannelTypes: ThreadChannelType[]; VoiceBasedChannelTypes: VoiceBasedChannelTypes[]; + SelectMenuTypes: SelectMenuType[]; }; export const version: string; @@ -5144,7 +5328,11 @@ export interface IntegrationAccount { export type IntegrationType = 'twitch' | 'youtube' | 'discord'; export type CollectedInteraction = - | SelectMenuInteraction + | StringSelectMenuInteraction + | UserSelectMenuInteraction + | RoleSelectMenuInteraction + | MentionableSelectMenuInteraction + | ChannelSelectMenuInteraction | ButtonInteraction | ModalSubmitInteraction; @@ -5216,7 +5404,13 @@ export interface MakeErrorOptions { stack: string; } -export type ActionRowComponentOptions = ButtonComponentData | SelectMenuComponentData; +export type ActionRowComponentOptions = + | ButtonComponentData + | StringSelectMenuComponentData + | UserSelectMenuComponentData + | RoleSelectMenuComponentData + | MentionableSelectMenuComponentData + | ChannelSelectMenuComponentData; export type MessageActionRowComponentResolvable = MessageActionRowComponent | ActionRowComponentOptions; @@ -5253,7 +5447,11 @@ export type MessageComponent = | Component | ActionRowBuilder | ButtonComponent - | SelectMenuComponent; + | StringSelectMenuComponent + | UserSelectMenuComponent + | RoleSelectMenuComponent + | MentionableSelectMenuComponent + | ChannelSelectMenuComponent; export type CollectedMessageInteraction = Exclude< CollectedInteraction, @@ -5351,16 +5549,36 @@ export interface MessageReference { export type MessageResolvable = Message | Snowflake; -export interface SelectMenuComponentData extends BaseComponentData { - type: ComponentType.SelectMenu; +export interface BaseSelectMenuComponentData extends BaseComponentData { customId: string; disabled?: boolean; maxValues?: number; minValues?: number; - options?: SelectMenuComponentOptionData[]; placeholder?: string; } +export interface StringSelectMenuComponentData extends BaseSelectMenuComponentData { + type: ComponentType.StringSelect; + options?: SelectMenuComponentOptionData[]; +} + +export interface UserSelectMenuComponentData extends BaseSelectMenuComponentData { + type: ComponentType.UserSelect; +} + +export interface RoleSelectMenuComponentData extends BaseSelectMenuComponentData { + type: ComponentType.RoleSelect; +} + +export interface MentionableSelectMenuComponentData extends BaseSelectMenuComponentData { + type: ComponentType.MentionableSelect; +} + +export interface ChannelSelectMenuComponentData extends BaseSelectMenuComponentData { + type: ComponentType.ChannelSelect; + channelTypes?: ChannelType[]; +} + export interface MessageSelectOption { default: boolean; description: string | null; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 48022353dab0..c78de7c6715d 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -24,6 +24,7 @@ import { APIEmbed, ApplicationCommandType, APIMessage, + APIStringSelectComponent, } from 'discord-api-types/v10'; import { ApplicationCommand, @@ -72,7 +73,6 @@ import { ReactionCollector, Role, RoleManager, - SelectMenuInteraction, Serialized, ShardClientUtil, ShardingManager, @@ -113,7 +113,7 @@ import { ButtonBuilder, EmbedBuilder, MessageActionRowComponent, - SelectMenuBuilder, + StringSelectMenuBuilder, TextInputBuilder, TextInputComponent, Embed, @@ -141,6 +141,13 @@ import { ChannelFlagsBitField, GuildForumThreadManager, GuildTextThreadManager, + AnySelectMenuInteraction, + StringSelectMenuInteraction, + StringSelectMenuComponent, + UserSelectMenuInteraction, + RoleSelectMenuInteraction, + ChannelSelectMenuInteraction, + MentionableSelectMenuInteraction, } from '.'; import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; @@ -361,14 +368,14 @@ client.on('messageCreate', async message => { expectAssignable>(buttonCollector); // Verify that select menus interaction are inferred. - const selectMenuCollector = message.createMessageComponentCollector({ componentType: ComponentType.SelectMenu }); - expectAssignable>( - message.awaitMessageComponent({ componentType: ComponentType.SelectMenu }), + const selectMenuCollector = message.createMessageComponentCollector({ componentType: ComponentType.StringSelect }); + expectAssignable>( + message.awaitMessageComponent({ componentType: ComponentType.StringSelect }), ); - expectAssignable>( - channel.awaitMessageComponent({ componentType: ComponentType.SelectMenu }), + expectAssignable>( + channel.awaitMessageComponent({ componentType: ComponentType.StringSelect }), ); - expectAssignable>(selectMenuCollector); + expectAssignable>(selectMenuCollector); // Verify that message component interactions are default collected types. const defaultCollector = message.createMessageComponentCollector(); @@ -405,9 +412,9 @@ client.on('messageCreate', async message => { }); message.createMessageComponentCollector({ - componentType: ComponentType.SelectMenu, + componentType: ComponentType.StringSelect, filter: i => { - expectType(i); + expectType(i); return true; }, }); @@ -428,9 +435,9 @@ client.on('messageCreate', async message => { }); message.awaitMessageComponent({ - componentType: ComponentType.SelectMenu, + componentType: ComponentType.StringSelect, filter: i => { - expectType(i); + expectType(i); return true; }, }); @@ -464,9 +471,9 @@ client.on('messageCreate', async message => { }); channel.awaitMessageComponent({ - componentType: ComponentType.SelectMenu, + componentType: ComponentType.StringSelect, filter: i => { - expectType>(i); + expectType>(i); return true; }, }); @@ -489,9 +496,9 @@ client.on('messageCreate', async message => { const selectsRow: ActionRowData = { type: ComponentType.ActionRow, components: [ - new SelectMenuBuilder(), + new StringSelectMenuBuilder(), { - type: ComponentType.SelectMenu, + type: ComponentType.StringSelect, label: 'select menu', options: [{ label: 'test', value: 'test' }], customId: 'test', @@ -1122,8 +1129,8 @@ client.on('guildCreate', async g => { new ButtonBuilder(), { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'string', customId: 'foo' }, { type: ComponentType.Button, style: ButtonStyle.Link, label: 'test', url: 'test' }, - { type: ComponentType.SelectMenu, customId: 'foo' }, - new SelectMenuBuilder(), + { type: ComponentType.StringSelect, customId: 'foo' }, + new StringSelectMenuBuilder(), // @ts-expect-error { type: ComponentType.TextInput, style: TextInputStyle.Paragraph, customId: 'foo', label: 'test' }, // @ts-expect-error @@ -1136,7 +1143,7 @@ client.on('guildCreate', async g => { components: [ { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'string', customId: 'foo' }, { type: ComponentType.Button, style: ButtonStyle.Link, label: 'test', url: 'test' }, - { type: ComponentType.SelectMenu, customId: 'foo' }, + { type: ComponentType.StringSelect, customId: 'foo' }, ], }); @@ -1508,7 +1515,7 @@ if (interaction.inGuild()) { client.on('interactionCreate', async interaction => { if (interaction.type === InteractionType.MessageComponent) { - expectType(interaction); + expectType(interaction); expectType(interaction.component); expectType(interaction.message); if (interaction.inCachedGuild()) { @@ -1640,25 +1647,28 @@ client.on('interactionCreate', async interaction => { } } - if (interaction.type === InteractionType.MessageComponent && interaction.componentType === ComponentType.SelectMenu) { - expectType(interaction); - expectType(interaction.component); + if ( + interaction.type === InteractionType.MessageComponent && + interaction.componentType === ComponentType.StringSelect + ) { + expectType(interaction); + expectType(interaction.component); expectType(interaction.message); if (interaction.inCachedGuild()) { - expectAssignable(interaction); + expectAssignable(interaction); expectType(interaction.component); expectType>(interaction.message); expectType(interaction.guild); expectType>>(interaction.reply({ fetchReply: true })); } else if (interaction.inRawGuild()) { - expectAssignable(interaction); - expectType(interaction.component); + expectAssignable(interaction); + expectType(interaction.component); expectType>(interaction.message); expectType(interaction.guild); expectType>>(interaction.reply({ fetchReply: true })); } else if (interaction.inGuild()) { - expectAssignable(interaction); - expectType(interaction.component); + expectAssignable(interaction); + expectType(interaction.component); expectType(interaction.message); expectType(interaction.guild); expectType>(interaction.reply({ fetchReply: true })); @@ -1882,7 +1892,7 @@ const button = new ButtonBuilder({ customId: 'test', }); -const selectMenu = new SelectMenuBuilder({ +const selectMenu = new StringSelectMenuBuilder({ maxValues: 10, minValues: 2, customId: 'test', @@ -1892,7 +1902,7 @@ new ActionRowBuilder({ components: [selectMenu.toJSON(), button.toJSON()], }); -new SelectMenuBuilder({ +new StringSelectMenuBuilder({ customId: 'foo', }); @@ -1951,10 +1961,10 @@ chatInputInteraction.showModal({ }); declare const selectMenuData: APISelectMenuComponent; -SelectMenuBuilder.from(selectMenuData); +StringSelectMenuBuilder.from(selectMenuData); declare const selectMenuComp: SelectMenuComponent; -SelectMenuBuilder.from(selectMenuComp); +StringSelectMenuBuilder.from(selectMenuComp); declare const buttonData: APIButtonComponent; ButtonBuilder.from(buttonData); @@ -2025,3 +2035,22 @@ expectType>(categoryChannel.flags); expectType>(threadChannel.flags); expectType(partialGroupDMChannel.flags); + +// Select menu type narrowing +if (interaction.isAnySelectMenu()) { + expectType(interaction); +} + +declare const anySelectMenu: AnySelectMenuInteraction; + +if (anySelectMenu.isStringSelectMenu()) { + expectType(anySelectMenu); +} else if (anySelectMenu.isUserSelectMenu()) { + expectType(anySelectMenu); +} else if (anySelectMenu.isRoleSelectMenu()) { + expectType(anySelectMenu); +} else if (anySelectMenu.isChannelSelectMenu()) { + expectType(anySelectMenu); +} else if (anySelectMenu.isMentionableSelectMenu()) { + expectType(anySelectMenu); +} diff --git a/packages/rest/package.json b/packages/rest/package.json index 21d5d334c5cf..cb40dd28a283 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -56,7 +56,7 @@ "@discordjs/util": "workspace:^", "@sapphire/async-queue": "^1.5.0", "@sapphire/snowflake": "^3.2.2", - "discord-api-types": "^0.37.14", + "discord-api-types": "^0.37.15", "file-type": "^18.0.0", "tslib": "^2.4.0", "undici": "^5.11.0" diff --git a/packages/voice/package.json b/packages/voice/package.json index 21675f83be5f..6eacc40ac6ce 100644 --- a/packages/voice/package.json +++ b/packages/voice/package.json @@ -53,7 +53,7 @@ "homepage": "https://discord.js.org", "dependencies": { "@types/ws": "^8.5.3", - "discord-api-types": "^0.37.14", + "discord-api-types": "^0.37.15", "prism-media": "^1.3.4", "tslib": "^2.4.0", "ws": "^8.9.0" diff --git a/packages/ws/package.json b/packages/ws/package.json index ed96837a8b7f..f768c6430f60 100644 --- a/packages/ws/package.json +++ b/packages/ws/package.json @@ -58,7 +58,7 @@ "@sapphire/async-queue": "^1.5.0", "@types/ws": "^8.5.3", "@vladfrangu/async_event_emitter": "^2.1.2", - "discord-api-types": "^0.37.14", + "discord-api-types": "^0.37.15", "tslib": "^2.4.0", "ws": "^8.9.0" }, diff --git a/yarn.lock b/yarn.lock index 4ad919155112..972837c67d03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2063,7 +2063,7 @@ __metadata: "@types/node": 16.11.68 "@vitest/coverage-c8": ^0.24.3 cross-env: ^7.0.3 - discord-api-types: ^0.37.14 + discord-api-types: ^0.37.15 esbuild-plugin-version-injector: ^1.0.0 eslint: ^8.25.0 eslint-config-neon: ^0.1.39 @@ -2250,7 +2250,7 @@ __metadata: "@types/node": 16.11.68 "@vitest/coverage-c8": ^0.24.3 cross-env: ^7.0.3 - discord-api-types: ^0.37.14 + discord-api-types: ^0.37.15 esbuild-plugin-version-injector: ^1.0.0 eslint: ^8.25.0 eslint-config-neon: ^0.1.39 @@ -2354,7 +2354,7 @@ __metadata: "@types/node": 16.11.68 "@types/ws": ^8.5.3 cross-env: ^7.0.3 - discord-api-types: ^0.37.14 + discord-api-types: ^0.37.15 esbuild-plugin-version-injector: ^1.0.0 eslint: ^8.25.0 eslint-config-neon: ^0.1.39 @@ -2444,7 +2444,7 @@ __metadata: "@vitest/coverage-c8": ^0.24.3 "@vladfrangu/async_event_emitter": ^2.1.2 cross-env: ^7.0.3 - discord-api-types: ^0.37.14 + discord-api-types: ^0.37.15 esbuild-plugin-version-injector: ^1.0.0 eslint: ^8.25.0 eslint-config-neon: ^0.1.39 @@ -8256,10 +8256,10 @@ __metadata: languageName: node linkType: hard -"discord-api-types@npm:^0.37.14": - version: 0.37.14 - resolution: "discord-api-types@npm:0.37.14" - checksum: 8f45f202e66acfd7b25624c8f4d225b363d9d8991d766959bcf246761548b99e21c12d9f7eafe00903913af66058595e5e56329dfb219eab8bb75a84f6413983 +"discord-api-types@npm:^0.37.15": + version: 0.37.15 + resolution: "discord-api-types@npm:0.37.15" + checksum: c54d2feeb8074509bdda430fb8ec0f6ff315512e7327d47399e0e7a78bbd0a6f0f0dcfc4b5e39825eb6141a13f33efa942711af89c9a5936a721cfc1e1d69d19 languageName: node linkType: hard @@ -8276,7 +8276,7 @@ __metadata: "@sapphire/snowflake": ^3.2.2 "@types/node": 16.11.68 "@types/ws": ^8.5.3 - discord-api-types: ^0.37.14 + discord-api-types: ^0.37.15 dtslint: ^4.2.1 eslint: ^8.25.0 eslint-formatter-pretty: ^4.1.0