feat: new select menus (#8793)

* 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 <julian@aternos.org>

* fix: properly handle resolved members

* fix: collectors

* chore: suggested changes

Co-authored-by: Almeida <almeidx@pm.me>

* 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 <kyradiscord@gmail.com>

Co-authored-by: Julian Vennen <julian@aternos.org>
Co-authored-by: Almeida <almeidx@pm.me>
Co-authored-by: Aura Román <kyradiscord@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
DD
2022-11-01 19:36:05 +02:00
committed by GitHub
parent 8b400ca975
commit 5152abf728
50 changed files with 1528 additions and 469 deletions

View File

@@ -9,8 +9,8 @@ import {
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
createComponentBuilder, createComponentBuilder,
SelectMenuBuilder, StringSelectMenuBuilder,
SelectMenuOptionBuilder, StringSelectMenuOptionBuilder,
} from '../../src/index.js'; } from '../../src/index.js';
const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = { const rowWithButtonData: APIActionRowComponent<APIMessageActionRowComponent> = {
@@ -29,7 +29,7 @@ const rowWithSelectMenuData: APIActionRowComponent<APIMessageActionRowComponent>
type: ComponentType.ActionRow, type: ComponentType.ActionRow,
components: [ components: [
{ {
type: ComponentType.SelectMenu, type: ComponentType.StringSelect,
custom_id: '1234', custom_id: '1234',
options: [ options: [
{ {
@@ -73,7 +73,7 @@ describe('Action Row Components', () => {
url: 'https://google.com', url: 'https://google.com',
}, },
{ {
type: ComponentType.SelectMenu, type: ComponentType.StringSelect,
placeholder: 'test', placeholder: 'test',
custom_id: 'test', custom_id: 'test',
options: [ options: [
@@ -108,7 +108,7 @@ describe('Action Row Components', () => {
type: ComponentType.ActionRow, type: ComponentType.ActionRow,
components: [ components: [
{ {
type: ComponentType.SelectMenu, type: ComponentType.StringSelect,
custom_id: '1234', custom_id: '1234',
options: [ options: [
{ {
@@ -134,17 +134,17 @@ describe('Action Row Components', () => {
test('GIVEN valid builder options THEN valid JSON output is given 2', () => { test('GIVEN valid builder options THEN valid JSON output is given 2', () => {
const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123'); const button = new ButtonBuilder().setLabel('test').setStyle(ButtonStyle.Primary).setCustomId('123');
const selectMenu = new SelectMenuBuilder() const selectMenu = new StringSelectMenuBuilder()
.setCustomId('1234') .setCustomId('1234')
.setMaxValues(10) .setMaxValues(10)
.setMinValues(12) .setMinValues(12)
.setOptions( .setOptions(
new SelectMenuOptionBuilder().setLabel('one').setValue('one'), new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new SelectMenuOptionBuilder().setLabel('two').setValue('two'), new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
) )
.setOptions([ .setOptions([
new SelectMenuOptionBuilder().setLabel('one').setValue('one'), new StringSelectMenuOptionBuilder().setLabel('one').setValue('one'),
new SelectMenuOptionBuilder().setLabel('two').setValue('two'), new StringSelectMenuOptionBuilder().setLabel('two').setValue('two'),
]); ]);
expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData); expect(new ActionRowBuilder().addComponents(button).toJSON()).toEqual(rowWithButtonData);

View File

@@ -13,12 +13,12 @@ import {
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
createComponentBuilder, createComponentBuilder,
SelectMenuBuilder, StringSelectMenuBuilder,
TextInputBuilder, TextInputBuilder,
} from '../../src/index.js'; } from '../../src/index.js';
describe('createComponentBuilder', () => { describe('createComponentBuilder', () => {
test.each([ButtonBuilder, SelectMenuBuilder, TextInputBuilder])( test.each([ButtonBuilder, StringSelectMenuBuilder, TextInputBuilder])(
'passing an instance of %j should return itself', 'passing an instance of %j should return itself',
(Builder) => { (Builder) => {
const builder = new Builder(); const builder = new Builder();
@@ -45,14 +45,14 @@ describe('createComponentBuilder', () => {
expect(createComponentBuilder(button)).toBeInstanceOf(ButtonBuilder); 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 = { const selectMenu: APISelectMenuComponent = {
custom_id: 'abc', custom_id: 'abc',
options: [], 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', () => { test('GIVEN a text input component THEN returns a TextInputBuilder', () => {

View File

@@ -1,9 +1,9 @@
import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10'; import { ComponentType, type APISelectMenuComponent, type APISelectMenuOption } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest'; 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 selectMenu = () => new StringSelectMenuBuilder();
const selectMenuOption = () => new SelectMenuOptionBuilder(); const selectMenuOption = () => new StringSelectMenuOptionBuilder();
const longStr = 'a'.repeat(256); const longStr = 'a'.repeat(256);
@@ -165,16 +165,16 @@ describe('Select Menu Components', () => {
test('GIVEN valid JSON input THEN valid JSON history is correct', () => { test('GIVEN valid JSON input THEN valid JSON history is correct', () => {
expect( expect(
new SelectMenuBuilder(selectMenuDataWithoutOptions) new StringSelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions(new SelectMenuOptionBuilder(selectMenuOptionData)) .addOptions(new StringSelectMenuOptionBuilder(selectMenuOptionData))
.toJSON(), .toJSON(),
).toEqual(selectMenuData); ).toEqual(selectMenuData);
expect( expect(
new SelectMenuBuilder(selectMenuDataWithoutOptions) new StringSelectMenuBuilder(selectMenuDataWithoutOptions)
.addOptions([new SelectMenuOptionBuilder(selectMenuOptionData)]) .addOptions([new StringSelectMenuOptionBuilder(selectMenuOptionData)])
.toJSON(), .toJSON(),
).toEqual(selectMenuData); ).toEqual(selectMenuData);
expect(new SelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); expect(new StringSelectMenuOptionBuilder(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData);
}); });
}); });
}); });

View File

@@ -56,7 +56,7 @@
"dependencies": { "dependencies": {
"@discordjs/util": "workspace:^", "@discordjs/util": "workspace:^",
"@sapphire/shapeshift": "^3.7.0", "@sapphire/shapeshift": "^3.7.0",
"discord-api-types": "^0.37.14", "discord-api-types": "^0.37.15",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.1", "ts-mixer": "^6.0.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"

View File

@@ -11,14 +11,24 @@ import { normalizeArray, type RestOrArray } from '../util/normalizeArray.js';
import { ComponentBuilder } from './Component.js'; import { ComponentBuilder } from './Component.js';
import { createComponentBuilder } from './Components.js'; import { createComponentBuilder } from './Components.js';
import type { ButtonBuilder } from './button/Button.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'; import type { TextInputBuilder } from './textInput/TextInput.js';
export type MessageComponentBuilder = export type MessageComponentBuilder =
| ActionRowBuilder<MessageActionRowComponentBuilder> | ActionRowBuilder<MessageActionRowComponentBuilder>
| MessageActionRowComponentBuilder; | MessageActionRowComponentBuilder;
export type ModalComponentBuilder = ActionRowBuilder<ModalActionRowComponentBuilder> | ModalActionRowComponentBuilder; export type ModalComponentBuilder = ActionRowBuilder<ModalActionRowComponentBuilder> | ModalActionRowComponentBuilder;
export type MessageActionRowComponentBuilder = ButtonBuilder | SelectMenuBuilder; export type MessageActionRowComponentBuilder =
| ButtonBuilder
| ChannelSelectMenuBuilder
| MentionableSelectMenuBuilder
| RoleSelectMenuBuilder
| StringSelectMenuBuilder
| UserSelectMenuBuilder;
export type ModalActionRowComponentBuilder = TextInputBuilder; export type ModalActionRowComponentBuilder = TextInputBuilder;
export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder; export type AnyComponentBuilder = MessageActionRowComponentBuilder | ModalActionRowComponentBuilder;

View File

@@ -1,7 +1,7 @@
import { s } from '@sapphire/shapeshift'; 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 { isValidationEnabled } from '../util/validation.js';
import { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption.js'; import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';
export const customIdValidator = s.string export const customIdValidator = s.string
.lengthGreaterThanOrEqual(1) .lengthGreaterThanOrEqual(1)
@@ -46,7 +46,7 @@ export const jsonOptionValidator = s
}) })
.setValidationEnabled(isValidationEnabled); .setValidationEnabled(isValidationEnabled);
export const optionValidator = s.instance(SelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled); export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
export const optionsValidator = optionValidator.array export const optionsValidator = optionValidator.array
.lengthGreaterThanOrEqual(0) .lengthGreaterThanOrEqual(0)
@@ -56,7 +56,7 @@ export const optionsLengthValidator = s.number.int
.lessThanOrEqual(25) .lessThanOrEqual(25)
.setValidationEnabled(isValidationEnabled); .setValidationEnabled(isValidationEnabled);
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) { export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId); customIdValidator.parse(customId);
optionsValidator.parse(options); optionsValidator.parse(options);
} }
@@ -68,6 +68,8 @@ export function validateRequiredSelectMenuOptionParameters(label?: string, value
labelValueDescriptionValidator.parse(value); labelValueDescriptionValidator.parse(value);
} }
export const channelTypesValidator = s.nativeEnum(ChannelType).array.setValidationEnabled(isValidationEnabled);
export const urlValidator = s.string export const urlValidator = s.string
.url({ .url({
allowedProtocols: ['http:', 'https:', 'discord:'], allowedProtocols: ['http:', 'https:', 'discord:'],

View File

@@ -7,14 +7,22 @@ import {
} from './ActionRow.js'; } from './ActionRow.js';
import { ComponentBuilder } from './Component.js'; import { ComponentBuilder } from './Component.js';
import { ButtonBuilder } from './button/Button.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'; import { TextInputBuilder } from './textInput/TextInput.js';
export interface MappedComponentTypes { export interface MappedComponentTypes {
[ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>; [ComponentType.ActionRow]: ActionRowBuilder<AnyComponentBuilder>;
[ComponentType.Button]: ButtonBuilder; [ComponentType.Button]: ButtonBuilder;
[ComponentType.SelectMenu]: SelectMenuBuilder; [ComponentType.StringSelect]: StringSelectMenuBuilder;
[ComponentType.TextInput]: TextInputBuilder; [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); return new ActionRowBuilder(data);
case ComponentType.Button: case ComponentType.Button:
return new ButtonBuilder(data); return new ButtonBuilder(data);
case ComponentType.SelectMenu: case ComponentType.StringSelect:
return new SelectMenuBuilder(data); return new StringSelectMenuBuilder(data);
case ComponentType.TextInput: case ComponentType.TextInput:
return new TextInputBuilder(data); 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: default:
// @ts-expect-error: This case can still occur if we get a newer unsupported component type // @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}`); throw new Error(`Cannot properly serialize component type: ${data.type}`);

View File

@@ -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<SelectMenuType> {
/**
* 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;
}
}

View File

@@ -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<APIChannelSelectComponent> {
/**
* 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<APIChannelSelectComponent>) {
super({ ...data, type: ComponentType.ChannelSelect });
}
public addChannelTypes(...types: RestOrArray<ChannelType>) {
// 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<ChannelType>) {
// 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;
}
}

View File

@@ -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<APIMentionableSelectComponent> {
/**
* 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<APIMentionableSelectComponent>) {
super({ ...data, type: ComponentType.MentionableSelect });
}
}

View File

@@ -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<APIRoleSelectComponent> {
/**
* 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<APIRoleSelectComponent>) {
super({ ...data, type: ComponentType.RoleSelect });
}
}

View File

@@ -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<APISelectMenuComponent> {
/**
* 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<APISelectMenuComponent>) {
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<APISelectMenuOption | SelectMenuOptionBuilder>) {
// 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<APISelectMenuOption | SelectMenuOptionBuilder>) {
// 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;
}
}

View File

@@ -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<APIStringSelectComponent> {
/**
* 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<APIStringSelectComponent>) {
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<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
// 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<APISelectMenuOption | StringSelectMenuOptionBuilder>) {
// 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;
}
}

View File

@@ -8,15 +8,15 @@ import {
} from '../Assertions.js'; } 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<APISelectMenuOption> { export class StringSelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOption> {
/** /**
* 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 * @example
* Creating a select menu option from an API data object * Creating a string select menu option from an API data object
* ```ts * ```ts
* const selectMenuOption = new SelectMenuOptionBuilder({ * const selectMenuOption = new SelectMenuOptionBuilder({
* label: 'catchy label', * label: 'catchy label',
@@ -24,7 +24,7 @@ export class SelectMenuOptionBuilder implements JSONEncodable<APISelectMenuOptio
* }); * });
* ``` * ```
* @example * @example
* Creating a select menu option using setters and API data * Creating a string select menu option using setters and API data
* ```ts * ```ts
* const selectMenuOption = new SelectMenuOptionBuilder({ * const selectMenuOption = new SelectMenuOptionBuilder({
* default: true, * default: true,

View File

@@ -0,0 +1,31 @@
import type { APIUserSelectComponent } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';
export class UserSelectMenuBuilder extends BaseSelectMenuBuilder<APIUserSelectComponent> {
/**
* 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<APIUserSelectComponent>) {
super({ ...data, type: ComponentType.UserSelect });
}
}

View File

@@ -11,8 +11,27 @@ export * from './components/textInput/TextInput.js';
export * as TextInputAssertions from './components/textInput/Assertions.js'; export * as TextInputAssertions from './components/textInput/Assertions.js';
export * from './interactions/modals/Modal.js'; export * from './interactions/modals/Modal.js';
export * as ModalAssertions from './interactions/modals/Assertions.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 * as SlashCommandAssertions from './interactions/slashCommands/Assertions.js';
export * from './interactions/slashCommands/SlashCommandBuilder.js'; export * from './interactions/slashCommands/SlashCommandBuilder.js';

View File

@@ -55,7 +55,7 @@
"@discordjs/util": "workspace:^", "@discordjs/util": "workspace:^",
"@sapphire/snowflake": "^3.2.2", "@sapphire/snowflake": "^3.2.2",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"discord-api-types": "^0.37.14", "discord-api-types": "^0.37.15",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"lodash.snakecase": "^4.1.1", "lodash.snakecase": "^4.1.1",
"tslib": "^2.4.0", "tslib": "^2.4.0",

View File

@@ -4,11 +4,15 @@ const { InteractionType, ComponentType, ApplicationCommandType } = require('disc
const Action = require('./Action'); const Action = require('./Action');
const AutocompleteInteraction = require('../../structures/AutocompleteInteraction'); const AutocompleteInteraction = require('../../structures/AutocompleteInteraction');
const ButtonInteraction = require('../../structures/ButtonInteraction'); const ButtonInteraction = require('../../structures/ButtonInteraction');
const ChannelSelectMenuInteraction = require('../../structures/ChannelSelectMenuInteraction');
const ChatInputCommandInteraction = require('../../structures/ChatInputCommandInteraction'); const ChatInputCommandInteraction = require('../../structures/ChatInputCommandInteraction');
const MentionableSelectMenuInteraction = require('../../structures/MentionableSelectMenuInteraction');
const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction'); const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction');
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction'); 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 UserContextMenuCommandInteraction = require('../../structures/UserContextMenuCommandInteraction');
const UserSelectMenuInteraction = require('../../structures/UserSelectMenuInteraction');
const Events = require('../../util/Events'); const Events = require('../../util/Events');
class InteractionCreateAction extends Action { class InteractionCreateAction extends Action {
@@ -49,8 +53,20 @@ class InteractionCreateAction extends Action {
case ComponentType.Button: case ComponentType.Button:
InteractionClass = ButtonInteraction; InteractionClass = ButtonInteraction;
break; break;
case ComponentType.SelectMenu: case ComponentType.StringSelect:
InteractionClass = SelectMenuInteraction; 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; break;
default: default:
client.emit( client.emit(

View File

@@ -154,9 +154,27 @@ exports.ReactionEmoji = require('./structures/ReactionEmoji');
exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets; exports.RichPresenceAssets = require('./structures/Presence').RichPresenceAssets;
exports.Role = require('./structures/Role').Role; exports.Role = require('./structures/Role').Role;
exports.SelectMenuBuilder = require('./structures/SelectMenuBuilder'); 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.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.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.SelectMenuOptionBuilder = require('./structures/SelectMenuOptionBuilder');
exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder');
exports.StageChannel = require('./structures/StageChannel'); exports.StageChannel = require('./structures/StageChannel');
exports.StageInstance = require('./structures/StageInstance').StageInstance; exports.StageInstance = require('./structures/StageInstance').StageInstance;
exports.Sticker = require('./structures/Sticker').Sticker; exports.Sticker = require('./structures/Sticker').Sticker;

View File

@@ -1,8 +1,10 @@
'use strict'; 'use strict';
const { deprecate } = require('node:util');
const { DiscordSnowflake } = require('@sapphire/snowflake'); const { DiscordSnowflake } = require('@sapphire/snowflake');
const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v10'); const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v10');
const Base = require('./Base'); const Base = require('./Base');
const { SelectMenuTypes } = require('../util/Constants');
const PermissionsBitField = require('../util/PermissionsBitField'); const PermissionsBitField = require('../util/PermissionsBitField');
/** /**
@@ -268,12 +270,63 @@ class BaseInteraction extends Base {
return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.Button; 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} * @returns {boolean}
*
* @deprecated Use {@link Interaction#isStringSelectMenu} instead
*/ */
isSelectMenu() { 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; module.exports = BaseInteraction;

View File

@@ -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;

View File

@@ -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> | 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}
*/

View File

@@ -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;

View File

@@ -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<Snowflake, Channel|APIChannel>}
*/
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;

View File

@@ -147,10 +147,17 @@ class InteractionCollector extends Collector {
* @event InteractionCollector#collect * @event InteractionCollector#collect
* @param {BaseInteraction} interaction The interaction that was collected * @param {BaseInteraction} interaction The interaction that was collected
*/ */
if (this.interactionType && interaction.type !== this.interactionType) return null; if (this.interactionType && interaction.type !== this.interactionType) return null;
if (this.componentType && interaction.componentType !== this.componentType) return null; if (this.componentType && interaction.componentType !== this.componentType) return null;
if (this.messageId && interaction.message?.id !== this.messageId) 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.channelId && interaction.channelId !== this.channelId) return null;
if (this.guildId && interaction.guildId !== this.guildId) return null; if (this.guildId && interaction.guildId !== this.guildId) return null;

View File

@@ -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> | 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}
*/

View File

@@ -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;

View File

@@ -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<Snowflake, User>}
*/
this.users = new Collection();
/**
* Collection of the selected users
* @type {Collection<Snowflake, GuildMember|APIGuildMember>}
*/
this.members = new Collection();
/**
* Collection of the selected roles
* @type {Collection<Snowflake, Role|APIRole>}
*/
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;

View File

@@ -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> | 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}
*/

View File

@@ -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;

View File

@@ -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<Snowflake, Role|APIRole>}
*/
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;

View File

@@ -1,78 +1,25 @@
'use strict'; 'use strict';
const { SelectMenuBuilder: BuildersSelectMenu, isJSONEncodable, normalizeArray } = require('@discordjs/builders'); const process = require('node:process');
const { toSnakeCase } = require('../util/Transformers'); const StringSelectMenuBuilder = require('./StringSelectMenuBuilder');
const { resolvePartialEmoji } = require('../util/Util');
let deprecationEmitted = false;
/** /**
* Class used to build select menu components to be sent through the API * @deprecated Use {@link StringSelectMenuBuilder} instead.
* @extends {BuildersSelectMenu}
*/ */
class SelectMenuBuilder extends BuildersSelectMenu { class SelectMenuBuilder extends StringSelectMenuBuilder {
constructor({ options, ...data } = {}) { constructor(...params) {
super( super(...params);
toSnakeCase({
...data,
options: options?.map(({ emoji, ...option }) => ({
...option,
emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji,
})),
}),
);
}
/** if (!deprecationEmitted) {
* Normalizes a select menu option emoji process.emitWarning(
* @param {SelectMenuOptionData|JSONEncodable<APISelectMenuOption>} selectMenuOption The option to normalize 'The SelectMenuBuilder class is deprecated, use StringSelectMenuBuilder instead.',
* @returns {Array<SelectMenuOptionBuilder|APISelectMenuOption>} 'DeprecationWarning',
* @private );
*/ deprecationEmitted = true;
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<APISelectMenuOption>} 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<APISelectMenuOption>} 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> | APISelectMenuComponent} other The other data
* @returns {SelectMenuBuilder}
*/
static from(other) {
if (isJSONEncodable(other)) {
return new this(other.toJSON());
}
return new this(other);
} }
} }
module.exports = SelectMenuBuilder; module.exports = SelectMenuBuilder;
/**
* @external BuildersSelectMenu
* @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuBuilder}
*/

View File

@@ -1,64 +1,24 @@
'use strict'; 'use strict';
const Component = require('./Component'); const process = require('node:process');
const StringSelectMenuComponent = require('./StringSelectMenuComponent');
let deprecationEmitted = false;
/** /**
* Represents a select menu component * @deprecated Use {@link StringSelectMenuComponent} instead.
* @extends {Component}
*/ */
class SelectMenuComponent extends Component { class SelectMenuComponent extends StringSelectMenuComponent {
/** constructor(...params) {
* The placeholder for this select menu super(...params);
* @type {?string}
* @readonly
*/
get placeholder() {
return this.data.placeholder ?? null;
}
/** if (!deprecationEmitted) {
* The maximum amount of options that can be selected process.emitWarning(
* @type {?number} 'The SelectMenuComponent class is deprecated, use StringSelectMenuComponent instead.',
* @readonly 'DeprecationWarning',
*/ );
get maxValues() { deprecationEmitted = true;
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;
} }
} }

View File

@@ -1,20 +1,24 @@
'use strict'; 'use strict';
const MessageComponentInteraction = require('./MessageComponentInteraction'); const process = require('node:process');
const StringSelectMenuInteraction = require('./StringSelectMenuInteraction');
let deprecationEmitted = false;
/** /**
* Represents a select menu interaction. * @deprecated Use {@link StringSelectMenuInteraction} instead.
* @extends {MessageComponentInteraction}
*/ */
class SelectMenuInteraction extends MessageComponentInteraction { class SelectMenuInteraction extends StringSelectMenuInteraction {
constructor(client, data) { constructor(...params) {
super(client, data); super(...params);
/** if (!deprecationEmitted) {
* The values selected, if the component which was interacted with was a select menu process.emitWarning(
* @type {string[]} 'The SelectMenuInteraction class is deprecated, use StringSelectMenuInteraction instead.',
*/ 'DeprecationWarning',
this.values = data.data.values ?? []; );
deprecationEmitted = true;
}
} }
} }

View File

@@ -1,50 +1,25 @@
'use strict'; 'use strict';
const { SelectMenuOptionBuilder: BuildersSelectMenuOption, isJSONEncodable } = require('@discordjs/builders'); const process = require('node:process');
const { toSnakeCase } = require('../util/Transformers'); const StringSelectMenuOptionBuilder = require('./StringSelectMenuOptionBuilder');
const { resolvePartialEmoji } = require('../util/Util');
let deprecationEmitted = false;
/** /**
* Represents a select menu option builder. * @deprecated Use {@link StringSelectMenuOptionBuilder} instead.
* @extends {BuildersSelectMenuOption}
*/ */
class SelectMenuOptionBuilder extends BuildersSelectMenuOption { class SelectMenuOptionBuilder extends StringSelectMenuOptionBuilder {
constructor({ emoji, ...data } = {}) { constructor(...params) {
super( super(...params);
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);
}
/** if (!deprecationEmitted) {
* Creates a new select menu option builder from JSON data process.emitWarning(
* @param {JSONEncodable<APISelectMenuOption>|APISelectMenuOption} other The other data 'The SelectMenuOptionBuilder class is deprecated, use StringSelectMenuOptionBuilder instead.',
* @returns {SelectMenuOptionBuilder} 'DeprecationWarning',
*/ );
static from(other) { deprecationEmitted = true;
if (isJSONEncodable(other)) {
return new this(other.toJSON());
} }
return new this(other);
} }
} }
module.exports = SelectMenuOptionBuilder; module.exports = SelectMenuOptionBuilder;
/**
* @external BuildersSelectMenuOption
* @see {@link https://discord.js.org/#/docs/builders/main/class/SelectMenuOptionBuilder}
*/

View File

@@ -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<APISelectMenuOption>} 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<APISelectMenuOption>} 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<APISelectMenuOption>} 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> | 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}
*/

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>|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}
*/

View File

@@ -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> | 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}
*/

View File

@@ -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;

View File

@@ -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<Snowflake, User>}
*/
this.users = new Collection();
/**
* Collection of the selected members
* @type {Collection<Snowflake, GuildMember|APIGuildMember>}
*/
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;

View File

@@ -82,10 +82,18 @@ function createComponent(data) {
return new ActionRow(data); return new ActionRow(data);
case ComponentType.Button: case ComponentType.Button:
return new ButtonComponent(data); return new ButtonComponent(data);
case ComponentType.SelectMenu: case ComponentType.StringSelect:
return new SelectMenuComponent(data); return new StringSelectMenuComponent(data);
case ComponentType.TextInput: case ComponentType.TextInput:
return new TextInputComponent(data); 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: default:
return new Component(data); return new Component(data);
} }
@@ -106,10 +114,18 @@ function createComponentBuilder(data) {
return new ActionRowBuilder(data); return new ActionRowBuilder(data);
case ComponentType.Button: case ComponentType.Button:
return new ButtonBuilder(data); return new ButtonBuilder(data);
case ComponentType.SelectMenu: case ComponentType.StringSelect:
return new SelectMenuBuilder(data); return new StringSelectMenuBuilder(data);
case ComponentType.TextInput: case ComponentType.TextInput:
return new TextInputBuilder(data); 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: default:
return new ComponentBuilder(data); return new ComponentBuilder(data);
} }
@@ -121,11 +137,19 @@ const ActionRow = require('../structures/ActionRow');
const ActionRowBuilder = require('../structures/ActionRowBuilder'); const ActionRowBuilder = require('../structures/ActionRowBuilder');
const ButtonBuilder = require('../structures/ButtonBuilder'); const ButtonBuilder = require('../structures/ButtonBuilder');
const ButtonComponent = require('../structures/ButtonComponent'); const ButtonComponent = require('../structures/ButtonComponent');
const ChannelSelectMenuBuilder = require('../structures/ChannelSelectMenuBuilder');
const ChannelSelectMenuComponent = require('../structures/ChannelSelectMenuComponent');
const Component = require('../structures/Component'); const Component = require('../structures/Component');
const SelectMenuBuilder = require('../structures/SelectMenuBuilder'); const MentionableSelectMenuBuilder = require('../structures/MentionableSelectMenuBuilder');
const SelectMenuComponent = require('../structures/SelectMenuComponent'); 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 TextInputBuilder = require('../structures/TextInputBuilder');
const TextInputComponent = require('../structures/TextInputComponent'); const TextInputComponent = require('../structures/TextInputComponent');
const UserSelectMenuBuilder = require('../structures/UserSelectMenuBuilder');
const UserSelectMenuComponent = require('../structures/UserSelectMenuComponent');
/** /**
* @external JSONEncodable * @external JSONEncodable

View File

@@ -1,6 +1,6 @@
'use strict'; '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 * 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]; 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. * @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 * @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 {TextBasedChannelTypes} TextBasedChannelTypes The types of channels that are text-based
* @property {ThreadChannelTypes} ThreadChannelTypes The types of channels that are threads * @property {ThreadChannelTypes} ThreadChannelTypes The types of channels that are threads
* @property {VoiceBasedChannelTypes} VoiceBasedChannelTypes The types of channels that are voice-based * @property {VoiceBasedChannelTypes} VoiceBasedChannelTypes The types of channels that are voice-based
* @property {SelectMenuTypes} SelectMenuTypes The types of components that are select menus.
*/ */

View File

@@ -14,14 +14,17 @@ import {
italic, italic,
quote, quote,
roleMention, roleMention,
SelectMenuBuilder as BuilderSelectMenuComponent, ChannelSelectMenuBuilder as BuilderChannelSelectMenuComponent,
MentionableSelectMenuBuilder as BuilderMentionableSelectMenuComponent,
RoleSelectMenuBuilder as BuilderRoleSelectMenuComponent,
StringSelectMenuBuilder as BuilderStringSelectMenuComponent,
UserSelectMenuBuilder as BuilderUserSelectMenuComponent,
TextInputBuilder as BuilderTextInputComponent, TextInputBuilder as BuilderTextInputComponent,
SelectMenuOptionBuilder as BuildersSelectMenuOption, SelectMenuOptionBuilder as BuildersSelectMenuOption,
spoiler, spoiler,
strikethrough, strikethrough,
time, time,
TimestampStyles, TimestampStyles,
TimestampStylesString,
underscore, underscore,
userMention, userMention,
ModalActionRowComponentBuilder, ModalActionRowComponentBuilder,
@@ -35,7 +38,6 @@ import { Collection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
import { import {
APIActionRowComponent, APIActionRowComponent,
APIApplicationCommand,
APIApplicationCommandInteractionData, APIApplicationCommandInteractionData,
APIApplicationCommandOption, APIApplicationCommandOption,
APIAuditLogChange, APIAuditLogChange,
@@ -126,6 +128,17 @@ import {
TextChannelType, TextChannelType,
ChannelFlags, ChannelFlags,
SortOrderType, SortOrderType,
APIMessageStringSelectInteractionData,
APIMessageUserSelectInteractionData,
APIStringSelectComponent,
APIUserSelectComponent,
APIRoleSelectComponent,
APIMentionableSelectComponent,
APIChannelSelectComponent,
APIGuildMember,
APIMessageRoleSelectInteractionData,
APIMessageMentionableSelectInteractionData,
APIMessageChannelSelectInteractionData,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import { ChildProcess } from 'node:child_process'; import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
@@ -158,13 +171,11 @@ import {
RawInviteData, RawInviteData,
RawInviteGuildData, RawInviteGuildData,
RawInviteStageInstance, RawInviteStageInstance,
RawAttachmentData,
RawMessageButtonInteractionData, RawMessageButtonInteractionData,
RawMessageComponentInteractionData, RawMessageComponentInteractionData,
RawMessageData, RawMessageData,
RawMessagePayloadData, RawMessagePayloadData,
RawMessageReactionData, RawMessageReactionData,
RawMessageSelectMenuInteractionData,
RawOAuth2GuildData, RawOAuth2GuildData,
RawPartialGroupDMChannelData, RawPartialGroupDMChannelData,
RawPartialMessageData, RawPartialMessageData,
@@ -242,7 +253,11 @@ export interface BaseComponentData {
export type MessageActionRowComponentData = export type MessageActionRowComponentData =
| JSONEncodable<APIMessageActionRowComponent> | JSONEncodable<APIMessageActionRowComponent>
| ButtonComponentData | ButtonComponentData
| SelectMenuComponentData; | StringSelectMenuComponentData
| UserSelectMenuComponentData
| RoleSelectMenuComponentData
| MentionableSelectMenuComponentData
| ChannelSelectMenuComponentData;
export type ModalActionRowComponentData = JSONEncodable<APIModalActionRowComponent> | TextInputComponentData; export type ModalActionRowComponentData = JSONEncodable<APIModalActionRowComponent> | TextInputComponentData;
@@ -269,7 +284,13 @@ export class ActionRowBuilder<T extends AnyComponentBuilder = AnyComponentBuilde
): ActionRowBuilder; ): ActionRowBuilder;
} }
export type MessageActionRowComponent = ButtonComponent | SelectMenuComponent; export type MessageActionRowComponent =
| ButtonComponent
| StringSelectMenuComponent
| UserSelectMenuComponent
| RoleSelectMenuComponent
| MentionableSelectMenuComponent
| ChannelSelectMenuComponent;
export type ModalActionRowComponent = TextInputComponent; export type ModalActionRowComponent = TextInputComponent;
export class ActionRow<T extends MessageActionRowComponent | ModalActionRowComponent> extends Component< export class ActionRow<T extends MessageActionRowComponent | ModalActionRowComponent> extends Component<
@@ -604,8 +625,8 @@ export class ButtonBuilder extends BuilderButtonComponent {
public override setEmoji(emoji: ComponentEmojiResolvable): this; public override setEmoji(emoji: ComponentEmojiResolvable): this;
} }
export class SelectMenuBuilder extends BuilderSelectMenuComponent { export class StringSelectMenuBuilder extends BuilderStringSelectMenuComponent {
public constructor(data?: Partial<SelectMenuComponentData | APISelectMenuComponent>); public constructor(data?: Partial<StringSelectMenuComponentData | APIStringSelectComponent>);
private static normalizeEmoji( private static normalizeEmoji(
selectMenuOption: JSONEncodable<APISelectMenuOption> | SelectMenuComponentOptionData, selectMenuOption: JSONEncodable<APISelectMenuOption> | SelectMenuComponentOptionData,
): (APISelectMenuOption | SelectMenuOptionBuilder)[]; ): (APISelectMenuOption | SelectMenuOptionBuilder)[];
@@ -615,7 +636,34 @@ export class SelectMenuBuilder extends BuilderSelectMenuComponent {
public override setOptions( public override setOptions(
...options: RestOrArray<BuildersSelectMenuOption | SelectMenuComponentOptionData | APISelectMenuOption> ...options: RestOrArray<BuildersSelectMenuOption | SelectMenuComponentOptionData | APISelectMenuOption>
): this; ): this;
public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): SelectMenuBuilder; public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): StringSelectMenuBuilder;
}
export {
/** @deprecated Use {@link StringSelectMenuBuilder} instead */
StringSelectMenuBuilder as SelectMenuBuilder,
};
export class UserSelectMenuBuilder extends BuilderUserSelectMenuComponent {
public constructor(data?: Partial<UserSelectMenuComponentData | APIUserSelectComponent>);
public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): UserSelectMenuBuilder;
}
export class RoleSelectMenuBuilder extends BuilderRoleSelectMenuComponent {
public constructor(data?: Partial<RoleSelectMenuComponentData | APIRoleSelectComponent>);
public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): RoleSelectMenuBuilder;
}
export class MentionableSelectMenuBuilder extends BuilderMentionableSelectMenuComponent {
public constructor(data?: Partial<MentionableSelectMenuComponentData | APIMentionableSelectComponent>);
public static from(
other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent,
): MentionableSelectMenuBuilder;
}
export class ChannelSelectMenuBuilder extends BuilderChannelSelectMenuComponent {
public constructor(data?: Partial<ChannelSelectMenuComponentData | APIChannelSelectComponent>);
public static from(other: JSONEncodable<APISelectMenuComponent> | APISelectMenuComponent): ChannelSelectMenuBuilder;
} }
export class SelectMenuOptionBuilder extends BuildersSelectMenuOption { export class SelectMenuOptionBuilder extends BuildersSelectMenuOption {
@@ -639,16 +687,34 @@ export class TextInputComponent extends Component<APITextInputComponent> {
public get value(): string; public get value(): string;
} }
export class SelectMenuComponent extends Component<APISelectMenuComponent> { export class BaseSelectMenuComponent<Data extends APISelectMenuComponent> extends Component<Data> {
private constructor(data: APISelectMenuComponent); protected constructor(data: Data);
public get placeholder(): string | null; public get placeholder(): string | null;
public get maxValues(): number | null; public get maxValues(): number | null;
public get minValues(): number | null; public get minValues(): number | null;
public get customId(): string; public get customId(): string;
public get disabled(): boolean | null; public get disabled(): boolean | null;
}
export class StringSelectMenuComponent extends BaseSelectMenuComponent<APIStringSelectComponent> {
public get options(): APISelectMenuOption[]; public get options(): APISelectMenuOption[];
} }
export {
/** @deprecated Use {@link StringSelectMenuComponent} instead */
StringSelectMenuComponent as SelectMenuComponent,
};
export class UserSelectMenuComponent extends BaseSelectMenuComponent<APIUserSelectComponent> {}
export class RoleSelectMenuComponent extends BaseSelectMenuComponent<APIRoleSelectComponent> {}
export class MentionableSelectMenuComponent extends BaseSelectMenuComponent<APIMentionableSelectComponent> {}
export class ChannelSelectMenuComponent extends BaseSelectMenuComponent<APIChannelSelectComponent> {
public getChannelTypes(): ChannelType[] | null;
}
export interface EmbedData { export interface EmbedData {
title?: string; title?: string;
type?: EmbedType; type?: EmbedType;
@@ -1501,7 +1567,7 @@ export type Interaction<Cached extends CacheType = CacheType> =
| ChatInputCommandInteraction<Cached> | ChatInputCommandInteraction<Cached>
| MessageContextMenuCommandInteraction<Cached> | MessageContextMenuCommandInteraction<Cached>
| UserContextMenuCommandInteraction<Cached> | UserContextMenuCommandInteraction<Cached>
| SelectMenuInteraction<Cached> | AnySelectMenuInteraction<Cached>
| ButtonInteraction<Cached> | ButtonInteraction<Cached>
| AutocompleteInteraction<Cached> | AutocompleteInteraction<Cached>
| ModalSubmitInteraction<Cached>; | ModalSubmitInteraction<Cached>;
@@ -1550,7 +1616,14 @@ export class BaseInteraction<Cached extends CacheType = CacheType> extends Base
public isMessageContextMenuCommand(): this is MessageContextMenuCommandInteraction<Cached>; public isMessageContextMenuCommand(): this is MessageContextMenuCommandInteraction<Cached>;
public isModalSubmit(): this is ModalSubmitInteraction<Cached>; public isModalSubmit(): this is ModalSubmitInteraction<Cached>;
public isUserContextMenuCommand(): this is UserContextMenuCommandInteraction<Cached>; public isUserContextMenuCommand(): this is UserContextMenuCommandInteraction<Cached>;
public isSelectMenu(): this is SelectMenuInteraction<Cached>; /** @deprecated Use {@link BaseInteraction#isStringSelectMenu} instead */
public isSelectMenu(): this is StringSelectMenuInteraction<Cached>;
public isAnySelectMenu(): this is AnySelectMenuInteraction<Cached>;
public isStringSelectMenu(): this is StringSelectMenuInteraction<Cached>;
public isUserSelectMenu(): this is UserSelectMenuInteraction<Cached>;
public isRoleSelectMenu(): this is RoleSelectMenuInteraction<Cached>;
public isMentionableSelectMenu(): this is MentionableSelectMenuInteraction<Cached>;
public isChannelSelectMenu(): this is ChannelSelectMenuInteraction<Cached>;
public isRepliable(): this is RepliableInteraction<Cached>; public isRepliable(): this is RepliableInteraction<Cached>;
} }
@@ -1673,7 +1746,11 @@ export type AwaitMessageCollectorOptionsParams<T extends MessageComponentType, C
export interface StringMappedInteractionTypes<Cached extends CacheType = CacheType> { export interface StringMappedInteractionTypes<Cached extends CacheType = CacheType> {
Button: ButtonInteraction<Cached>; Button: ButtonInteraction<Cached>;
SelectMenu: SelectMenuInteraction<Cached>; StringSelectMenu: StringSelectMenuInteraction<Cached>;
UserSelectMenu: UserSelectMenuInteraction<Cached>;
RoleSelectMenu: RoleSelectMenuInteraction<Cached>;
MentionableSelectMenu: MentionableSelectMenuInteraction<Cached>;
ChannelSelectMenu: ChannelSelectMenuInteraction<Cached>;
ActionRow: MessageComponentInteraction<Cached>; ActionRow: MessageComponentInteraction<Cached>;
} }
@@ -1681,7 +1758,11 @@ export type WrapBooleanCache<T extends boolean> = If<T, 'cached', CacheType>;
export interface MappedInteractionTypes<Cached extends boolean = boolean> { export interface MappedInteractionTypes<Cached extends boolean = boolean> {
[ComponentType.Button]: ButtonInteraction<WrapBooleanCache<Cached>>; [ComponentType.Button]: ButtonInteraction<WrapBooleanCache<Cached>>;
[ComponentType.SelectMenu]: SelectMenuInteraction<WrapBooleanCache<Cached>>; [ComponentType.StringSelect]: StringSelectMenuInteraction<WrapBooleanCache<Cached>>;
[ComponentType.UserSelect]: UserSelectMenuInteraction<WrapBooleanCache<Cached>>;
[ComponentType.RoleSelect]: RoleSelectMenuInteraction<WrapBooleanCache<Cached>>;
[ComponentType.MentionableSelect]: MentionableSelectMenuInteraction<WrapBooleanCache<Cached>>;
[ComponentType.ChannelSelect]: ChannelSelectMenuInteraction<WrapBooleanCache<Cached>>;
} }
export class Message<InGuild extends boolean = boolean> extends Base { export class Message<InGuild extends boolean = boolean> extends Base {
@@ -2254,22 +2335,116 @@ export class Role extends Base {
public toString(): RoleMention; public toString(): RoleMention;
} }
export class SelectMenuInteraction<Cached extends CacheType = CacheType> extends MessageComponentInteraction<Cached> { export class StringSelectMenuInteraction<
public constructor(client: Client<true>, data: RawMessageSelectMenuInteractionData); Cached extends CacheType = CacheType,
> extends MessageComponentInteraction<Cached> {
public constructor(client: Client<true>, data: APIMessageStringSelectInteractionData);
public get component(): CacheTypeReducer< public get component(): CacheTypeReducer<
Cached, Cached,
SelectMenuComponent, StringSelectMenuComponent,
APISelectMenuComponent, APIStringSelectComponent,
SelectMenuComponent | APISelectMenuComponent, StringSelectMenuComponent | APIStringSelectComponent,
SelectMenuComponent | APISelectMenuComponent StringSelectMenuComponent | APIStringSelectComponent
>; >;
public componentType: ComponentType.SelectMenu; public componentType: ComponentType.StringSelect;
public values: string[]; public values: string[];
public inGuild(): this is SelectMenuInteraction<'raw' | 'cached'>; public inGuild(): this is StringSelectMenuInteraction<'raw' | 'cached'>;
public inCachedGuild(): this is SelectMenuInteraction<'cached'>; public inCachedGuild(): this is StringSelectMenuInteraction<'cached'>;
public inRawGuild(): this is SelectMenuInteraction<'raw'>; public inRawGuild(): this is StringSelectMenuInteraction<'raw'>;
} }
export {
/** @deprecated Use {@link StringSelectMenuInteraction} instead */
StringSelectMenuInteraction as SelectMenuInteraction,
};
export class UserSelectMenuInteraction<
Cached extends CacheType = CacheType,
> extends MessageComponentInteraction<Cached> {
public constructor(client: Client<true>, data: APIMessageUserSelectInteractionData);
public get component(): CacheTypeReducer<
Cached,
UserSelectMenuComponent,
APIUserSelectComponent,
UserSelectMenuComponent | APIUserSelectComponent,
UserSelectMenuComponent | APIUserSelectComponent
>;
public componentType: ComponentType.UserSelect;
public users: Collection<Snowflake, User>;
public members: Collection<Snowflake, CacheTypeReducer<Cached, GuildMember, APIGuildMember>>;
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<Cached> {
public constructor(client: Client<true>, data: APIMessageRoleSelectInteractionData);
public get component(): CacheTypeReducer<
Cached,
RoleSelectMenuComponent,
APIRoleSelectComponent,
RoleSelectMenuComponent | APIRoleSelectComponent,
RoleSelectMenuComponent | APIRoleSelectComponent
>;
public componentType: ComponentType.RoleSelect;
public roles: Collection<Snowflake, CacheTypeReducer<Cached, Role, APIRole>>;
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<Cached> {
public constructor(client: Client<true>, data: APIMessageMentionableSelectInteractionData);
public get component(): CacheTypeReducer<
Cached,
MentionableSelectMenuComponent,
APIMentionableSelectComponent,
MentionableSelectMenuComponent | APIMentionableSelectComponent,
MentionableSelectMenuComponent | APIMentionableSelectComponent
>;
public componentType: ComponentType.MentionableSelect;
public users: Collection<Snowflake, User>;
public members: Collection<Snowflake, CacheTypeReducer<Cached, GuildMember, APIGuildMember>>;
public roles: Collection<Snowflake, CacheTypeReducer<Cached, Role, APIRole>>;
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<Cached> {
public constructor(client: Client<true>, data: APIMessageChannelSelectInteractionData);
public get component(): CacheTypeReducer<
Cached,
ChannelSelectMenuComponent,
APIChannelSelectComponent,
ChannelSelectMenuComponent | APIChannelSelectComponent,
ChannelSelectMenuComponent | APIChannelSelectComponent
>;
public componentType: ComponentType.ChannelSelect;
public channels: Collection<Snowflake, CacheTypeReducer<Cached, Channel, APIChannel>>;
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<Cached extends CacheType = CacheType> =
| StringSelectMenuInteraction<Cached>
| UserSelectMenuInteraction<Cached>
| RoleSelectMenuInteraction<Cached>
| MentionableSelectMenuInteraction<Cached>
| ChannelSelectMenuInteraction<Cached>;
export type SelectMenuType = APISelectMenuComponent['type'];
export interface ShardEventTypes { export interface ShardEventTypes {
death: [process: ChildProcess | Worker]; death: [process: ChildProcess | Worker];
disconnect: []; disconnect: [];
@@ -2770,14 +2945,22 @@ export function parseWebhookURL(url: string): WebhookClientDataIdWithToken | nul
export interface MappedComponentBuilderTypes { export interface MappedComponentBuilderTypes {
[ComponentType.Button]: ButtonBuilder; [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.ActionRow]: ActionRowBuilder;
[ComponentType.TextInput]: TextInputBuilder; [ComponentType.TextInput]: TextInputBuilder;
} }
export interface MappedComponentTypes { export interface MappedComponentTypes {
[ComponentType.Button]: ButtonComponent; [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.ActionRow]: ActionRowComponent;
[ComponentType.TextInput]: TextInputComponent; [ComponentType.TextInput]: TextInputComponent;
} }
@@ -3116,6 +3299,7 @@ export const Constants: {
TextBasedChannelTypes: TextBasedChannelTypes[]; TextBasedChannelTypes: TextBasedChannelTypes[];
ThreadChannelTypes: ThreadChannelType[]; ThreadChannelTypes: ThreadChannelType[];
VoiceBasedChannelTypes: VoiceBasedChannelTypes[]; VoiceBasedChannelTypes: VoiceBasedChannelTypes[];
SelectMenuTypes: SelectMenuType[];
}; };
export const version: string; export const version: string;
@@ -5144,7 +5328,11 @@ export interface IntegrationAccount {
export type IntegrationType = 'twitch' | 'youtube' | 'discord'; export type IntegrationType = 'twitch' | 'youtube' | 'discord';
export type CollectedInteraction<Cached extends CacheType = CacheType> = export type CollectedInteraction<Cached extends CacheType = CacheType> =
| SelectMenuInteraction<Cached> | StringSelectMenuInteraction<Cached>
| UserSelectMenuInteraction<Cached>
| RoleSelectMenuInteraction<Cached>
| MentionableSelectMenuInteraction<Cached>
| ChannelSelectMenuInteraction<Cached>
| ButtonInteraction<Cached> | ButtonInteraction<Cached>
| ModalSubmitInteraction<Cached>; | ModalSubmitInteraction<Cached>;
@@ -5216,7 +5404,13 @@ export interface MakeErrorOptions {
stack: string; stack: string;
} }
export type ActionRowComponentOptions = ButtonComponentData | SelectMenuComponentData; export type ActionRowComponentOptions =
| ButtonComponentData
| StringSelectMenuComponentData
| UserSelectMenuComponentData
| RoleSelectMenuComponentData
| MentionableSelectMenuComponentData
| ChannelSelectMenuComponentData;
export type MessageActionRowComponentResolvable = MessageActionRowComponent | ActionRowComponentOptions; export type MessageActionRowComponentResolvable = MessageActionRowComponent | ActionRowComponentOptions;
@@ -5253,7 +5447,11 @@ export type MessageComponent =
| Component | Component
| ActionRowBuilder<MessageActionRowComponentBuilder | ModalActionRowComponentBuilder> | ActionRowBuilder<MessageActionRowComponentBuilder | ModalActionRowComponentBuilder>
| ButtonComponent | ButtonComponent
| SelectMenuComponent; | StringSelectMenuComponent
| UserSelectMenuComponent
| RoleSelectMenuComponent
| MentionableSelectMenuComponent
| ChannelSelectMenuComponent;
export type CollectedMessageInteraction<Cached extends CacheType = CacheType> = Exclude< export type CollectedMessageInteraction<Cached extends CacheType = CacheType> = Exclude<
CollectedInteraction<Cached>, CollectedInteraction<Cached>,
@@ -5351,16 +5549,36 @@ export interface MessageReference {
export type MessageResolvable = Message | Snowflake; export type MessageResolvable = Message | Snowflake;
export interface SelectMenuComponentData extends BaseComponentData { export interface BaseSelectMenuComponentData extends BaseComponentData {
type: ComponentType.SelectMenu;
customId: string; customId: string;
disabled?: boolean; disabled?: boolean;
maxValues?: number; maxValues?: number;
minValues?: number; minValues?: number;
options?: SelectMenuComponentOptionData[];
placeholder?: string; 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 { export interface MessageSelectOption {
default: boolean; default: boolean;
description: string | null; description: string | null;

View File

@@ -24,6 +24,7 @@ import {
APIEmbed, APIEmbed,
ApplicationCommandType, ApplicationCommandType,
APIMessage, APIMessage,
APIStringSelectComponent,
} from 'discord-api-types/v10'; } from 'discord-api-types/v10';
import { import {
ApplicationCommand, ApplicationCommand,
@@ -72,7 +73,6 @@ import {
ReactionCollector, ReactionCollector,
Role, Role,
RoleManager, RoleManager,
SelectMenuInteraction,
Serialized, Serialized,
ShardClientUtil, ShardClientUtil,
ShardingManager, ShardingManager,
@@ -113,7 +113,7 @@ import {
ButtonBuilder, ButtonBuilder,
EmbedBuilder, EmbedBuilder,
MessageActionRowComponent, MessageActionRowComponent,
SelectMenuBuilder, StringSelectMenuBuilder,
TextInputBuilder, TextInputBuilder,
TextInputComponent, TextInputComponent,
Embed, Embed,
@@ -141,6 +141,13 @@ import {
ChannelFlagsBitField, ChannelFlagsBitField,
GuildForumThreadManager, GuildForumThreadManager,
GuildTextThreadManager, GuildTextThreadManager,
AnySelectMenuInteraction,
StringSelectMenuInteraction,
StringSelectMenuComponent,
UserSelectMenuInteraction,
RoleSelectMenuInteraction,
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
} from '.'; } from '.';
import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd';
import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders'; import type { ContextMenuCommandBuilder, SlashCommandBuilder } from '@discordjs/builders';
@@ -361,14 +368,14 @@ client.on('messageCreate', async message => {
expectAssignable<InteractionCollector<ButtonInteraction>>(buttonCollector); expectAssignable<InteractionCollector<ButtonInteraction>>(buttonCollector);
// Verify that select menus interaction are inferred. // Verify that select menus interaction are inferred.
const selectMenuCollector = message.createMessageComponentCollector({ componentType: ComponentType.SelectMenu }); const selectMenuCollector = message.createMessageComponentCollector({ componentType: ComponentType.StringSelect });
expectAssignable<Promise<SelectMenuInteraction>>( expectAssignable<Promise<StringSelectMenuInteraction>>(
message.awaitMessageComponent({ componentType: ComponentType.SelectMenu }), message.awaitMessageComponent({ componentType: ComponentType.StringSelect }),
); );
expectAssignable<Promise<SelectMenuInteraction>>( expectAssignable<Promise<StringSelectMenuInteraction>>(
channel.awaitMessageComponent({ componentType: ComponentType.SelectMenu }), channel.awaitMessageComponent({ componentType: ComponentType.StringSelect }),
); );
expectAssignable<InteractionCollector<SelectMenuInteraction>>(selectMenuCollector); expectAssignable<InteractionCollector<StringSelectMenuInteraction>>(selectMenuCollector);
// Verify that message component interactions are default collected types. // Verify that message component interactions are default collected types.
const defaultCollector = message.createMessageComponentCollector(); const defaultCollector = message.createMessageComponentCollector();
@@ -405,9 +412,9 @@ client.on('messageCreate', async message => {
}); });
message.createMessageComponentCollector({ message.createMessageComponentCollector({
componentType: ComponentType.SelectMenu, componentType: ComponentType.StringSelect,
filter: i => { filter: i => {
expectType<SelectMenuInteraction>(i); expectType<StringSelectMenuInteraction>(i);
return true; return true;
}, },
}); });
@@ -428,9 +435,9 @@ client.on('messageCreate', async message => {
}); });
message.awaitMessageComponent({ message.awaitMessageComponent({
componentType: ComponentType.SelectMenu, componentType: ComponentType.StringSelect,
filter: i => { filter: i => {
expectType<SelectMenuInteraction>(i); expectType<StringSelectMenuInteraction>(i);
return true; return true;
}, },
}); });
@@ -464,9 +471,9 @@ client.on('messageCreate', async message => {
}); });
channel.awaitMessageComponent({ channel.awaitMessageComponent({
componentType: ComponentType.SelectMenu, componentType: ComponentType.StringSelect,
filter: i => { filter: i => {
expectType<SelectMenuInteraction<'cached'>>(i); expectType<StringSelectMenuInteraction<'cached'>>(i);
return true; return true;
}, },
}); });
@@ -489,9 +496,9 @@ client.on('messageCreate', async message => {
const selectsRow: ActionRowData<MessageActionRowComponentData> = { const selectsRow: ActionRowData<MessageActionRowComponentData> = {
type: ComponentType.ActionRow, type: ComponentType.ActionRow,
components: [ components: [
new SelectMenuBuilder(), new StringSelectMenuBuilder(),
{ {
type: ComponentType.SelectMenu, type: ComponentType.StringSelect,
label: 'select menu', label: 'select menu',
options: [{ label: 'test', value: 'test' }], options: [{ label: 'test', value: 'test' }],
customId: 'test', customId: 'test',
@@ -1122,8 +1129,8 @@ client.on('guildCreate', async g => {
new ButtonBuilder(), new ButtonBuilder(),
{ type: ComponentType.Button, style: ButtonStyle.Primary, label: 'string', customId: 'foo' }, { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'string', customId: 'foo' },
{ type: ComponentType.Button, style: ButtonStyle.Link, label: 'test', url: 'test' }, { type: ComponentType.Button, style: ButtonStyle.Link, label: 'test', url: 'test' },
{ type: ComponentType.SelectMenu, customId: 'foo' }, { type: ComponentType.StringSelect, customId: 'foo' },
new SelectMenuBuilder(), new StringSelectMenuBuilder(),
// @ts-expect-error // @ts-expect-error
{ type: ComponentType.TextInput, style: TextInputStyle.Paragraph, customId: 'foo', label: 'test' }, { type: ComponentType.TextInput, style: TextInputStyle.Paragraph, customId: 'foo', label: 'test' },
// @ts-expect-error // @ts-expect-error
@@ -1136,7 +1143,7 @@ client.on('guildCreate', async g => {
components: [ components: [
{ type: ComponentType.Button, style: ButtonStyle.Primary, label: 'string', customId: 'foo' }, { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'string', customId: 'foo' },
{ type: ComponentType.Button, style: ButtonStyle.Link, label: 'test', url: 'test' }, { 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 => { client.on('interactionCreate', async interaction => {
if (interaction.type === InteractionType.MessageComponent) { if (interaction.type === InteractionType.MessageComponent) {
expectType<SelectMenuInteraction | ButtonInteraction>(interaction); expectType<AnySelectMenuInteraction | ButtonInteraction>(interaction);
expectType<MessageActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component); expectType<MessageActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<Message>(interaction.message); expectType<Message>(interaction.message);
if (interaction.inCachedGuild()) { if (interaction.inCachedGuild()) {
@@ -1640,25 +1647,28 @@ client.on('interactionCreate', async interaction => {
} }
} }
if (interaction.type === InteractionType.MessageComponent && interaction.componentType === ComponentType.SelectMenu) { if (
expectType<SelectMenuInteraction>(interaction); interaction.type === InteractionType.MessageComponent &&
expectType<SelectMenuComponent | APISelectMenuComponent>(interaction.component); interaction.componentType === ComponentType.StringSelect
) {
expectType<StringSelectMenuInteraction>(interaction);
expectType<StringSelectMenuComponent | APIStringSelectComponent>(interaction.component);
expectType<Message>(interaction.message); expectType<Message>(interaction.message);
if (interaction.inCachedGuild()) { if (interaction.inCachedGuild()) {
expectAssignable<SelectMenuInteraction>(interaction); expectAssignable<StringSelectMenuInteraction>(interaction);
expectType<SelectMenuComponent>(interaction.component); expectType<SelectMenuComponent>(interaction.component);
expectType<Message<true>>(interaction.message); expectType<Message<true>>(interaction.message);
expectType<Guild>(interaction.guild); expectType<Guild>(interaction.guild);
expectType<Promise<Message<true>>>(interaction.reply({ fetchReply: true })); expectType<Promise<Message<true>>>(interaction.reply({ fetchReply: true }));
} else if (interaction.inRawGuild()) { } else if (interaction.inRawGuild()) {
expectAssignable<SelectMenuInteraction>(interaction); expectAssignable<StringSelectMenuInteraction>(interaction);
expectType<APISelectMenuComponent>(interaction.component); expectType<APIStringSelectComponent>(interaction.component);
expectType<Message<false>>(interaction.message); expectType<Message<false>>(interaction.message);
expectType<null>(interaction.guild); expectType<null>(interaction.guild);
expectType<Promise<Message<false>>>(interaction.reply({ fetchReply: true })); expectType<Promise<Message<false>>>(interaction.reply({ fetchReply: true }));
} else if (interaction.inGuild()) { } else if (interaction.inGuild()) {
expectAssignable<SelectMenuInteraction>(interaction); expectAssignable<StringSelectMenuInteraction>(interaction);
expectType<SelectMenuComponent | APISelectMenuComponent>(interaction.component); expectType<SelectMenuComponent | APIStringSelectComponent>(interaction.component);
expectType<Message>(interaction.message); expectType<Message>(interaction.message);
expectType<Guild | null>(interaction.guild); expectType<Guild | null>(interaction.guild);
expectType<Promise<Message>>(interaction.reply({ fetchReply: true })); expectType<Promise<Message>>(interaction.reply({ fetchReply: true }));
@@ -1882,7 +1892,7 @@ const button = new ButtonBuilder({
customId: 'test', customId: 'test',
}); });
const selectMenu = new SelectMenuBuilder({ const selectMenu = new StringSelectMenuBuilder({
maxValues: 10, maxValues: 10,
minValues: 2, minValues: 2,
customId: 'test', customId: 'test',
@@ -1892,7 +1902,7 @@ new ActionRowBuilder({
components: [selectMenu.toJSON(), button.toJSON()], components: [selectMenu.toJSON(), button.toJSON()],
}); });
new SelectMenuBuilder({ new StringSelectMenuBuilder({
customId: 'foo', customId: 'foo',
}); });
@@ -1951,10 +1961,10 @@ chatInputInteraction.showModal({
}); });
declare const selectMenuData: APISelectMenuComponent; declare const selectMenuData: APISelectMenuComponent;
SelectMenuBuilder.from(selectMenuData); StringSelectMenuBuilder.from(selectMenuData);
declare const selectMenuComp: SelectMenuComponent; declare const selectMenuComp: SelectMenuComponent;
SelectMenuBuilder.from(selectMenuComp); StringSelectMenuBuilder.from(selectMenuComp);
declare const buttonData: APIButtonComponent; declare const buttonData: APIButtonComponent;
ButtonBuilder.from(buttonData); ButtonBuilder.from(buttonData);
@@ -2025,3 +2035,22 @@ expectType<Readonly<ChannelFlagsBitField>>(categoryChannel.flags);
expectType<Readonly<ChannelFlagsBitField>>(threadChannel.flags); expectType<Readonly<ChannelFlagsBitField>>(threadChannel.flags);
expectType<null>(partialGroupDMChannel.flags); expectType<null>(partialGroupDMChannel.flags);
// Select menu type narrowing
if (interaction.isAnySelectMenu()) {
expectType<AnySelectMenuInteraction>(interaction);
}
declare const anySelectMenu: AnySelectMenuInteraction;
if (anySelectMenu.isStringSelectMenu()) {
expectType<StringSelectMenuInteraction>(anySelectMenu);
} else if (anySelectMenu.isUserSelectMenu()) {
expectType<UserSelectMenuInteraction>(anySelectMenu);
} else if (anySelectMenu.isRoleSelectMenu()) {
expectType<RoleSelectMenuInteraction>(anySelectMenu);
} else if (anySelectMenu.isChannelSelectMenu()) {
expectType<ChannelSelectMenuInteraction>(anySelectMenu);
} else if (anySelectMenu.isMentionableSelectMenu()) {
expectType<MentionableSelectMenuInteraction>(anySelectMenu);
}

View File

@@ -56,7 +56,7 @@
"@discordjs/util": "workspace:^", "@discordjs/util": "workspace:^",
"@sapphire/async-queue": "^1.5.0", "@sapphire/async-queue": "^1.5.0",
"@sapphire/snowflake": "^3.2.2", "@sapphire/snowflake": "^3.2.2",
"discord-api-types": "^0.37.14", "discord-api-types": "^0.37.15",
"file-type": "^18.0.0", "file-type": "^18.0.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"undici": "^5.11.0" "undici": "^5.11.0"

View File

@@ -53,7 +53,7 @@
"homepage": "https://discord.js.org", "homepage": "https://discord.js.org",
"dependencies": { "dependencies": {
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"discord-api-types": "^0.37.14", "discord-api-types": "^0.37.15",
"prism-media": "^1.3.4", "prism-media": "^1.3.4",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"ws": "^8.9.0" "ws": "^8.9.0"

View File

@@ -58,7 +58,7 @@
"@sapphire/async-queue": "^1.5.0", "@sapphire/async-queue": "^1.5.0",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"@vladfrangu/async_event_emitter": "^2.1.2", "@vladfrangu/async_event_emitter": "^2.1.2",
"discord-api-types": "^0.37.14", "discord-api-types": "^0.37.15",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"ws": "^8.9.0" "ws": "^8.9.0"
}, },

View File

@@ -2063,7 +2063,7 @@ __metadata:
"@types/node": 16.11.68 "@types/node": 16.11.68
"@vitest/coverage-c8": ^0.24.3 "@vitest/coverage-c8": ^0.24.3
cross-env: ^7.0.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 esbuild-plugin-version-injector: ^1.0.0
eslint: ^8.25.0 eslint: ^8.25.0
eslint-config-neon: ^0.1.39 eslint-config-neon: ^0.1.39
@@ -2250,7 +2250,7 @@ __metadata:
"@types/node": 16.11.68 "@types/node": 16.11.68
"@vitest/coverage-c8": ^0.24.3 "@vitest/coverage-c8": ^0.24.3
cross-env: ^7.0.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 esbuild-plugin-version-injector: ^1.0.0
eslint: ^8.25.0 eslint: ^8.25.0
eslint-config-neon: ^0.1.39 eslint-config-neon: ^0.1.39
@@ -2354,7 +2354,7 @@ __metadata:
"@types/node": 16.11.68 "@types/node": 16.11.68
"@types/ws": ^8.5.3 "@types/ws": ^8.5.3
cross-env: ^7.0.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 esbuild-plugin-version-injector: ^1.0.0
eslint: ^8.25.0 eslint: ^8.25.0
eslint-config-neon: ^0.1.39 eslint-config-neon: ^0.1.39
@@ -2444,7 +2444,7 @@ __metadata:
"@vitest/coverage-c8": ^0.24.3 "@vitest/coverage-c8": ^0.24.3
"@vladfrangu/async_event_emitter": ^2.1.2 "@vladfrangu/async_event_emitter": ^2.1.2
cross-env: ^7.0.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 esbuild-plugin-version-injector: ^1.0.0
eslint: ^8.25.0 eslint: ^8.25.0
eslint-config-neon: ^0.1.39 eslint-config-neon: ^0.1.39
@@ -8256,10 +8256,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"discord-api-types@npm:^0.37.14": "discord-api-types@npm:^0.37.15":
version: 0.37.14 version: 0.37.15
resolution: "discord-api-types@npm:0.37.14" resolution: "discord-api-types@npm:0.37.15"
checksum: 8f45f202e66acfd7b25624c8f4d225b363d9d8991d766959bcf246761548b99e21c12d9f7eafe00903913af66058595e5e56329dfb219eab8bb75a84f6413983 checksum: c54d2feeb8074509bdda430fb8ec0f6ff315512e7327d47399e0e7a78bbd0a6f0f0dcfc4b5e39825eb6141a13f33efa942711af89c9a5936a721cfc1e1d69d19
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8276,7 +8276,7 @@ __metadata:
"@sapphire/snowflake": ^3.2.2 "@sapphire/snowflake": ^3.2.2
"@types/node": 16.11.68 "@types/node": 16.11.68
"@types/ws": ^8.5.3 "@types/ws": ^8.5.3
discord-api-types: ^0.37.14 discord-api-types: ^0.37.15
dtslint: ^4.2.1 dtslint: ^4.2.1
eslint: ^8.25.0 eslint: ^8.25.0
eslint-formatter-pretty: ^4.1.0 eslint-formatter-pretty: ^4.1.0