feat: Add Modals and Text Inputs (#7023)

Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Ryan Munro <monbrey@gmail.com>
Co-authored-by: Vitor <milagre.vitor@gmail.com>
This commit is contained in:
Suneet Tipirneni
2022-03-04 02:53:41 -05:00
committed by GitHub
parent 53defb82e3
commit ed92015634
31 changed files with 1075 additions and 115 deletions

View File

@@ -52,7 +52,7 @@
"@discordjs/rest": "workspace:^",
"@sapphire/snowflake": "^3.1.0",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.27.0",
"discord-api-types": "^0.27.3",
"lodash.snakecase": "^4.1.1",
"undici": "^4.14.1",
"ws": "^8.5.0"

View File

@@ -6,6 +6,7 @@ const AutocompleteInteraction = require('../../structures/AutocompleteInteractio
const ButtonInteraction = require('../../structures/ButtonInteraction');
const ChatInputCommandInteraction = require('../../structures/ChatInputCommandInteraction');
const MessageContextMenuCommandInteraction = require('../../structures/MessageContextMenuCommandInteraction');
const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction');
const SelectMenuInteraction = require('../../structures/SelectMenuInteraction');
const UserContextMenuCommandInteraction = require('../../structures/UserContextMenuCommandInteraction');
const Events = require('../../util/Events');
@@ -57,6 +58,9 @@ class InteractionCreateAction extends Action {
case InteractionType.ApplicationCommandAutocomplete:
InteractionClass = AutocompleteInteraction;
break;
case InteractionType.ModalSubmit:
InteractionClass = ModalSubmitInteraction;
break;
default:
client.emit(Events.Debug, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
return;

View File

@@ -141,6 +141,10 @@ const Messages = {
COMMAND_INTERACTION_OPTION_NO_SUB_COMMAND_GROUP: 'No subcommand group specified for interaction.',
AUTOCOMPLETE_INTERACTION_OPTION_NO_FOCUSED_OPTION: 'No focused option for autocomplete interaction.',
MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND: customId => `Required field with custom id "${customId}" not found.`,
MODAL_SUBMIT_INTERACTION_FIELD_TYPE: (customId, type, expected) =>
`Field with custom id "${customId}" is of type: ${type}; expected ${expected}.`,
INVITE_MISSING_SCOPES: 'At least one valid scope must be provided for the invite',
NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,

View File

@@ -124,6 +124,9 @@ exports.MessageContextMenuCommandInteraction = require('./structures/MessageCont
exports.MessageMentions = require('./structures/MessageMentions');
exports.MessagePayload = require('./structures/MessagePayload');
exports.MessageReaction = require('./structures/MessageReaction');
exports.Modal = require('./structures/Modal');
exports.ModalSubmitInteraction = require('./structures/ModalSubmitInteraction');
exports.ModalSubmitFieldsResolver = require('./structures/ModalSubmitFieldsResolver');
exports.NewsChannel = require('./structures/NewsChannel');
exports.OAuth2Guild = require('./structures/OAuth2Guild');
exports.PartialGroupDMChannel = require('./structures/PartialGroupDMChannel');
@@ -143,6 +146,7 @@ exports.StoreChannel = require('./structures/StoreChannel');
exports.Team = require('./structures/Team');
exports.TeamMember = require('./structures/TeamMember');
exports.TextChannel = require('./structures/TextChannel');
exports.TextInputComponent = require('./structures/TextInputComponent');
exports.ThreadChannel = require('./structures/ThreadChannel');
exports.ThreadMember = require('./structures/ThreadMember');
exports.Typing = require('./structures/Typing');
@@ -193,6 +197,7 @@ exports.RESTJSONErrorCodes = require('discord-api-types/v9').RESTJSONErrorCodes;
exports.StageInstancePrivacyLevel = require('discord-api-types/v9').StageInstancePrivacyLevel;
exports.StickerType = require('discord-api-types/v9').StickerType;
exports.StickerFormatType = require('discord-api-types/v9').StickerFormatType;
exports.TextInputStyle = require('discord-api-types/v9').TextInputStyle;
exports.UserFlags = require('discord-api-types/v9').UserFlags;
exports.WebhookType = require('discord-api-types/v9').WebhookType;
exports.UnsafeButtonComponent = require('@discordjs/builders').UnsafeButtonComponent;

View File

@@ -203,6 +203,7 @@ class CommandInteraction extends Interaction {
editReply() {}
deleteReply() {}
followUp() {}
showModal() {}
}
InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']);

View File

@@ -191,6 +191,14 @@ class Interaction extends Base {
return this.isContextMenuCommand() && this.commandType === ApplicationCommandType.Message;
}
/**
* Indicates whether this interaction is a {@link ModalSubmitInteraction}
* @returns {boolean}
*/
isModalSubmit() {
return this.type === InteractionType.ModalSubmit;
}
/**
* Indicates whether this interaction is an {@link AutocompleteInteraction}
* @returns {boolean}

View File

@@ -90,6 +90,7 @@ class MessageComponentInteraction extends Interaction {
followUp() {}
deferUpdate() {}
update() {}
showModal() {}
}
InteractionResponses.applyToClass(MessageComponentInteraction);

View File

@@ -0,0 +1,12 @@
'use strict';
const { Modal: BuildersModal } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class Modal extends BuildersModal {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = Modal;

View File

@@ -0,0 +1,54 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { ComponentType } = require('discord-api-types/v9');
const { TypeError } = require('../errors');
/**
* Represents the serialized fields from a modal submit interaction
*/
class ModalSubmitFieldsResolver {
constructor(components) {
/**
* The components within the modal
* @type {Array<ActionRow<ModalFieldData>>} The components in the modal
*/
this.components = components;
/**
* The extracted fields from the modal
* @type {Collection<string, ModalFieldData>} The fields in the modal
*/
this.fields = components.reduce((accumulator, next) => {
next.components.forEach(c => accumulator.set(c.customId, c));
return accumulator;
}, new Collection());
}
/**
* Gets a field given a custom id from a component
* @param {string} customId The custom id of the component
* @returns {ModalFieldData}
*/
getField(customId) {
const field = this.fields.get(customId);
if (!field) throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_NOT_FOUND', customId);
return field;
}
/**
* Gets the value of a text input component given a custom id
* @param {string} customId The custom id of the text input component
* @returns {string}
*/
getTextInputValue(customId) {
const field = this.getField(customId);
const expectedType = ComponentType.TextInput;
if (field.type !== expectedType) {
throw new TypeError('MODAL_SUBMIT_INTERACTION_FIELD_TYPE', customId, field.type, expectedType);
}
return field.value;
}
}
module.exports = ModalSubmitFieldsResolver;

View File

@@ -0,0 +1,93 @@
'use strict';
const { createComponent } = require('@discordjs/builders');
const Interaction = require('./Interaction');
const InteractionWebhook = require('./InteractionWebhook');
const ModalSubmitFieldsResolver = require('./ModalSubmitFieldsResolver');
const InteractionResponses = require('./interfaces/InteractionResponses');
/**
* @typedef {Object} ModalFieldData
* @property {string} value The value of the field
* @property {ComponentType} type The component type of the field
* @property {string} customId The custom id of the field
*/
/**
* Represents a modal interaction
* @implements {InteractionResponses}
*/
class ModalSubmitInteraction extends Interaction {
constructor(client, data) {
super(client, data);
/**
* The custom id of the modal.
* @type {string}
*/
this.customId = data.data.custom_id;
if ('message' in data) {
/**
* The message associated with this interaction
* @type {?(Message|APIMessage)}
*/
this.message = this.channel?.messages._add(data.message) ?? data.message;
} else {
this.message = null;
}
/**
* The components within the modal
* @type {ActionRow[]}
*/
this.components = data.data.components?.map(c => createComponent(c)) ?? [];
/**
* The fields within the modal
* @type {ModalSubmitFieldsResolver}
*/
this.fields = new ModalSubmitFieldsResolver(this.components);
/**
* An associated interaction webhook, can be used to further interact with this interaction
* @type {InteractionWebhook}
*/
this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token);
}
/**
* Transforms component data to discord.js-compatible data
* @param {*} rawComponent The data to transform
* @returns {ModalFieldData[]}
*/
static transformComponent(rawComponent) {
return {
value: rawComponent.value,
type: rawComponent.type,
customId: rawComponent.custom_id,
};
}
/**
* Whether this is from a {@link MessageComponentInteraction}.
* @returns {boolean}
*/
isFromMessage() {
return Boolean(this.message);
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
deferReply() {}
reply() {}
fetchReply() {}
editReply() {}
deleteReply() {}
followUp() {}
deferUpdate() {}
update() {}
}
InteractionResponses.applyToClass(ModalSubmitInteraction, 'showModal');
module.exports = ModalSubmitInteraction;

View File

@@ -0,0 +1,12 @@
'use strict';
const { TextInputComponent: BuildersTextInputComponent } = require('@discordjs/builders');
const Transformers = require('../util/Transformers');
class TextInputComponent extends BuildersTextInputComponent {
constructor(data) {
super(Transformers.toSnakeCase(data));
}
}
module.exports = TextInputComponent;

View File

@@ -1,9 +1,18 @@
'use strict';
const { isJSONEncodable } = require('@discordjs/builders');
const { InteractionResponseType, MessageFlags, Routes } = require('discord-api-types/v9');
const { Error } = require('../../errors');
const Transformers = require('../../util/Transformers');
const MessagePayload = require('../MessagePayload');
/**
* @typedef {Object} ModalData
* @property {string} title The title of the modal
* @property {string} customId The custom id of the modal
* @property {ActionRowData[]} components The components within this modal
*/
/**
* Interface for classes that support shared interaction response types.
* @interface
@@ -225,6 +234,21 @@ class InteractionResponses {
return options.fetchReply ? this.fetchReply() : undefined;
}
/**
* Shows a modal component
* @param {APIModal|ModalData|Modal} modal The modal to show
*/
async showModal(modal) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.Modal,
data: isJSONEncodable(modal) ? modal.toJSON() : Transformers.toSnakeCase(modal),
},
});
this.replied = true;
}
static applyToClass(structure, ignore = []) {
const props = [
'deferReply',
@@ -235,6 +259,7 @@ class InteractionResponses {
'followUp',
'deferUpdate',
'update',
'showModal',
];
for (const prop of props) {

View File

@@ -1,44 +1,46 @@
'use strict';
// This file contains the typedefs for camel-cased json data
/**
* @typedef {Object} BaseComponentData
* @property {ComponentType} type
* @property {ComponentType} type The type of component
*/
/**
* @typedef {BaseComponentData} ActionRowData
* @property {ComponentData[]} components
* @property {ComponentData[]} components The components in this action row
*/
/**
* @typedef {BaseComponentData} ButtonComponentData
* @property {ButtonStyle} style
* @property {?boolean} disabled
* @property {string} label
* @property {?APIComponentEmoji} emoji
* @property {?string} customId
* @property {?string} url
* @property {ButtonStyle} style The style of the button
* @property {?boolean} disabled Whether this button is disabled
* @property {string} label The label of this button
* @property {?APIComponentEmoji} emoji The emoji on this button
* @property {?string} customId The custom id of the button
* @property {?string} url The URL of the button
*/
/**
* @typedef {object} SelectMenuComponentOptionData
* @property {string} label
* @property {string} value
* @property {?string} description
* @property {?APIComponentEmoji} emoji
* @property {?boolean} default
* @property {string} label The label of the option
* @property {string} value The value of the option
* @property {?string} description The description of the option
* @property {?APIComponentEmoji} emoji The emoji on the option
* @property {?boolean} default Whether this option is selected by default
*/
/**
* @typedef {BaseComponentData} SelectMenuComponentData
* @property {string} customId
* @property {?boolean} disabled
* @property {?number} maxValues
* @property {?number} minValues
* @property {?SelectMenuComponentOptionData[]} options
* @property {?string} placeholder
* @property {string} customId The custom id of the select menu
* @property {?boolean} disabled Whether the select menu is disabled or not
* @property {?number} maxValues The maximum amount of options that can be selected
* @property {?number} minValues The minimum amount of options that can be selected
* @property {?SelectMenuComponentOptionData[]} options The options in this select menu
* @property {?string} placeholder The placeholder of the select menu
*/
/**
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData} ComponentData
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData} MessageComponentData
/
/**
* @typedef {ActionRowData|ButtonComponentData|SelectMenuComponentData|TextInputComponentData} ComponentData
*/

View File

@@ -2,47 +2,47 @@
/**
* @typedef {Object} EmbedData
* @property {?string} title
* @property {?EmbedType} type
* @property {?string} description
* @property {?string} url
* @property {?string} timestamp
* @property {?number} color
* @property {?EmbedFooterData} footer
* @property {?EmbedImageData} image
* @property {?EmbedImageData} thumbnail
* @property {?EmbedProviderData} provider
* @property {?EmbedAuthorData} author
* @property {?EmbedFieldData[]} fields
* @property {?string} title The title of the embed
* @property {?EmbedType} type The type of the embed
* @property {?string} description The description of the embed
* @property {?string} url The URL of the embed
* @property {?string} timestamp The timestamp on the embed
* @property {?number} color The color of the embed
* @property {?EmbedFooterData} footer The footer of the embed
* @property {?EmbedImageData} image The image of the embed
* @property {?EmbedImageData} thumbnail The thumbnail of the embed
* @property {?EmbedProviderData} provider The provider of the embed
* @property {?EmbedAuthorData} author The author in the embed
* @property {?EmbedFieldData[]} fields The fields in this embed
*/
/**
* @typedef {Object} EmbedFooterData
* @property {string} text
* @property {?string} iconURL
* @property {string} text The text of the footer
* @property {?string} iconURL The URL of the icon
*/
/**
* @typedef {Object} EmbedImageData
* @property {?string} url
* @property {?string} url The URL of the image
*/
/**
* @typedef {Object} EmbedProviderData
* @property {?string} name
* @property {?string} url
* @property {?string} name The name of the provider
* @property {?string} url The URL of the provider
*/
/**
* @typedef {Object} EmbedAuthorData
* @property {string} name
* @property {?string} url
* @property {?string} iconURL
* @property {string} name The name of the author
* @property {?string} url The URL of the author
* @property {?string} iconURL The icon URL of the author
*/
/**
* @typedef {Object} EmbedFieldData
* @property {string} name
* @property {string} value
* @property {?boolean} inline
* @property {string} name The name of the field
* @property {string} value The value of the field
* @property {?boolean} inline Whether to inline this field
*/

View File

@@ -1,6 +1,6 @@
import {
ActionRow as BuilderActionRow,
ActionRowComponent,
MessageActionRowComponent,
blockQuote,
bold,
ButtonComponent as BuilderButtonComponent,
@@ -14,9 +14,11 @@ import {
inlineCode,
italic,
memberNicknameMention,
Modal as BuilderModal,
quote,
roleMention,
SelectMenuComponent as BuilderSelectMenuComponent,
TextInputComponent as BuilderTextInputComponent,
spoiler,
strikethrough,
time,
@@ -24,6 +26,7 @@ import {
TimestampStylesString,
underscore,
userMention,
ModalActionRowComponent,
} from '@discordjs/builders';
import { Collection } from '@discordjs/collection';
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest';
@@ -95,6 +98,13 @@ import {
APIMessageComponentEmoji,
EmbedType,
APIActionRowComponentTypes,
APIModalInteractionResponseCallbackData,
APIModalSubmitInteraction,
APIMessageActionRowComponent,
TextInputStyle,
APITextInputComponent,
APIModalActionRowComponent,
APIModalComponent,
} from 'discord-api-types/v9';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
@@ -198,17 +208,23 @@ export interface BaseComponentData {
type?: ComponentType;
}
export type ActionRowComponentData = ButtonComponentData | SelectMenuComponentData;
export type MessageActionRowComponentData = ButtonComponentData | SelectMenuComponentData;
export type ModalActionRowComponentData = TextInputComponentData;
export interface ActionRowData extends BaseComponentData {
components: ActionRowComponentData[];
export interface ActionRowData<T extends MessageActionRowComponentData | ModalActionRowComponentData>
extends BaseComponentData {
components: T[];
}
export class ActionRow<T extends ActionRowComponent = ActionRowComponent> extends BuilderActionRow<T> {
export class ActionRow<
T extends MessageActionRowComponent | ModalActionRowComponent = MessageActionRowComponent,
> extends BuilderActionRow<T> {
constructor(
data?:
| ActionRowData
| (Omit<APIActionRowComponent<APIMessageComponent>, 'type'> & { type?: ComponentType.ActionRow }),
| ActionRowData<MessageActionRowComponentData | ModalActionRowComponentData>
| (Omit<APIActionRowComponent<APIMessageActionRowComponent | APIModalActionRowComponent>, 'type'> & {
type?: ComponentType.ActionRow;
}),
);
}
@@ -336,6 +352,7 @@ export interface InteractionResponseFields<Cached extends CacheType = CacheType>
deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
fetchReply(): Promise<GuildCacheMessage<Cached>>;
followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
showModal(modal: Modal): Promise<void>;
}
export abstract class CommandInteraction<Cached extends CacheType = CacheType> extends Interaction<Cached> {
@@ -374,6 +391,7 @@ export abstract class CommandInteraction<Cached extends CacheType = CacheType> e
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public showModal(modal: Modal): Promise<void>;
private transformOption(
option: APIApplicationCommandOption,
resolved: APIApplicationCommandInteractionData['resolved'],
@@ -490,6 +508,14 @@ export class SelectMenuComponent extends BuilderSelectMenuComponent {
);
}
export class TextInputComponent extends BuilderTextInputComponent {
public constructor(data?: TextInputComponentData | APITextInputComponent);
}
export class Modal extends BuilderModal {
public constructor(data?: ModalData | APIModalActionRowComponent);
}
export interface EmbedData {
title?: string;
type?: EmbedType;
@@ -1355,6 +1381,7 @@ export class Interaction<Cached extends CacheType = CacheType> extends Base {
public isMessageComponent(): this is MessageComponentInteraction<Cached>;
public isSelectMenu(): this is SelectMenuInteraction<Cached>;
public isRepliable(): this is this & InteractionResponseFields<Cached>;
public isModalSubmit(): this is ModalSubmitInteraction<Cached>;
}
export class InteractionCollector<T extends Interaction> extends Collector<Snowflake, T> {
@@ -1447,17 +1474,19 @@ export class LimitedCollection<K, V> extends Collection<K, V> {
public keepOverLimit: ((value: V, key: K, collection: this) => boolean) | null;
}
export type MessageCollectorOptionsParams<T extends ComponentType, Cached extends boolean = boolean> =
export type MessageComponentType = Exclude<ComponentType, ComponentType.TextInput>;
export type MessageCollectorOptionsParams<T extends MessageComponentType, Cached extends boolean = boolean> =
| {
componentType?: T;
} & MessageComponentCollectorOptions<MappedInteractionTypes<Cached>[T]>;
export type MessageChannelCollectorOptionsParams<T extends ComponentType, Cached extends boolean = boolean> =
export type MessageChannelCollectorOptionsParams<T extends MessageComponentType, Cached extends boolean = boolean> =
| {
componentType?: T;
} & MessageChannelComponentCollectorOptions<MappedInteractionTypes<Cached>[T]>;
export type AwaitMessageCollectorOptionsParams<T extends ComponentType, Cached extends boolean = boolean> =
export type AwaitMessageCollectorOptionsParams<T extends MessageComponentType, Cached extends boolean = boolean> =
| { componentType?: T } & Pick<
InteractionCollectorOptions<MappedInteractionTypes<Cached>[T]>,
keyof AwaitMessageComponentOptions<any>
@@ -1490,7 +1519,7 @@ export class Message<Cached extends boolean = boolean> extends Base {
public get channel(): If<Cached, GuildTextBasedChannel, TextBasedChannel>;
public channelId: Snowflake;
public get cleanContent(): string;
public components: ActionRow<ActionRowComponent>[];
public components: ActionRow<MessageActionRowComponent>[];
public content: string;
public get createdAt(): Date;
public createdTimestamp: number;
@@ -1522,12 +1551,12 @@ export class Message<Cached extends boolean = boolean> extends Base {
public webhookId: Snowflake | null;
public flags: Readonly<MessageFlagsBitField>;
public reference: MessageReference | null;
public awaitMessageComponent<T extends ComponentType = ComponentType.ActionRow>(
public awaitMessageComponent<T extends MessageComponentType = ComponentType.ActionRow>(
options?: AwaitMessageCollectorOptionsParams<T, Cached>,
): Promise<MappedInteractionTypes<Cached>[T]>;
public awaitReactions(options?: AwaitReactionsOptions): Promise<Collection<Snowflake | string, MessageReaction>>;
public createReactionCollector(options?: ReactionCollectorOptions): ReactionCollector;
public createMessageComponentCollector<T extends ComponentType = ComponentType.ActionRow>(
public createMessageComponentCollector<T extends MessageComponentType = ComponentType.ActionRow>(
options?: MessageCollectorOptionsParams<T, Cached>,
): InteractionCollector<MappedInteractionTypes<Cached>[T]>;
public delete(): Promise<Message>;
@@ -1541,7 +1570,7 @@ export class Message<Cached extends boolean = boolean> extends Base {
public react(emoji: EmojiIdentifierResolvable): Promise<MessageReaction>;
public removeAttachments(): Promise<Message>;
public reply(options: string | MessagePayload | ReplyMessageOptions): Promise<Message>;
public resolveComponent(customId: string): ActionRowComponent | null;
public resolveComponent(customId: string): MessageActionRowComponent | null;
public startThread(options: StartThreadOptions): Promise<ThreadChannel>;
public suppressEmbeds(suppress?: boolean): Promise<Message>;
public toJSON(): unknown;
@@ -1590,10 +1619,10 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> e
protected constructor(client: Client, data: RawMessageComponentInteractionData);
public get component(): CacheTypeReducer<
Cached,
ActionRowComponent,
Exclude<APIMessageComponent, APIActionRowComponent<APIMessageComponent>>,
ActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIMessageComponent>>,
ActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIMessageComponent>>
MessageActionRowComponent,
Exclude<APIMessageComponent, APIActionRowComponent<APIMessageActionRowComponent>>,
MessageActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIMessageActionRowComponent>>,
MessageActionRowComponent | Exclude<APIMessageComponent, APIActionRowComponent<APIMessageActionRowComponent>>
>;
public componentType: Exclude<ComponentType, ComponentType.ActionRow>;
public customId: string;
@@ -1618,6 +1647,7 @@ export class MessageComponentInteraction<Cached extends CacheType = CacheType> e
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public update(options: string | MessagePayload | InteractionUpdateOptions): Promise<void>;
public showModal(modal: Modal): Promise<void>;
}
export class MessageContextMenuCommandInteraction<
@@ -1706,6 +1736,57 @@ export class MessageReaction {
public toJSON(): unknown;
}
export interface ModalFieldData {
value: string;
type: ComponentType;
customId: string;
}
export class ModalSubmitFieldsResolver {
constructor(components: ModalFieldData[][]);
public fields: Collection<string, ModalFieldData>;
public getField(customId: string): ModalFieldData;
public getTextInputValue(customId: string): string;
}
export interface ModalMessageModalSubmitInteraction<Cached extends CacheType = CacheType>
extends ModalSubmitInteraction<Cached> {
message: GuildCacheMessage<Cached> | null;
update(options: InteractionUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
update(options: string | MessagePayload | InteractionUpdateOptions): Promise<void>;
deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
deferUpdate(options?: InteractionDeferUpdateOptions): Promise<void>;
inGuild(): this is ModalMessageModalSubmitInteraction<'raw' | 'cached'>;
inCachedGuild(): this is ModalMessageModalSubmitInteraction<'cached'>;
inRawGuild(): this is ModalMessageModalSubmitInteraction<'raw'>;
}
export interface ModalSubmitActionRow {
type: ComponentType.ActionRow;
components: ModalFieldData[];
}
export class ModalSubmitInteraction<Cached extends CacheType = CacheType> extends Interaction<Cached> {
private constructor(client: Client, data: APIModalSubmitInteraction);
public readonly customId: string;
// TODO: fix this type when #7517 is implemented
public readonly components: ModalSubmitActionRow[];
public readonly fields: ModalSubmitFieldsResolver;
public readonly webhook: InteractionWebhook;
public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public reply(options: string | MessagePayload | InteractionReplyOptions): Promise<void>;
public deleteReply(): Promise<void>;
public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise<GuildCacheMessage<Cached>>;
public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise<GuildCacheMessage<Cached>>;
public deferReply(options?: InteractionDeferReplyOptions): Promise<void>;
public fetchReply(): Promise<GuildCacheMessage<Cached>>;
public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise<GuildCacheMessage<Cached>>;
public inGuild(): this is ModalSubmitInteraction<'raw' | 'cached'>;
public inCachedGuild(): this is ModalSubmitInteraction<'cached'>;
public inRawGuild(): this is ModalSubmitInteraction<'raw'>;
public isFromMessage(): this is ModalMessageModalSubmitInteraction<Cached>;
}
export class NewsChannel extends BaseGuildTextChannel {
public threads: ThreadManager<AllowedThreadTypeForNewsChannel>;
public type: ChannelType.GuildNews;
@@ -2381,7 +2462,10 @@ export class Formatters extends null {
public static userMention: typeof userMention;
}
export type ComponentData = ActionRowComponentData | ButtonComponentData | SelectMenuComponentData;
export type ComponentData =
| MessageActionRowComponentData
| ModalActionRowComponentData
| ActionRowData<MessageActionRowComponentData | ModalActionRowComponentData>;
export class VoiceChannel extends BaseGuildVoiceChannel {
public get speakable(): boolean;
@@ -3132,8 +3216,8 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields {
lastMessageId: Snowflake | null;
get lastMessage(): Message | null;
lastPinTimestamp: number | null;
get lastPinAt(): Date | null;
awaitMessageComponent<T extends ComponentType = ComponentType.ActionRow>(
readonly lastPinAt: Date | null;
awaitMessageComponent<T extends MessageComponentType = ComponentType.ActionRow>(
options?: AwaitMessageCollectorOptionsParams<T, true>,
): Promise<MappedInteractionTypes[T]>;
awaitMessages(options?: AwaitMessagesOptions): Promise<Collection<Snowflake, Message>>;
@@ -3141,7 +3225,7 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields {
messages: Collection<Snowflake, Message> | readonly MessageResolvable[] | number,
filterOld?: boolean,
): Promise<Collection<Snowflake, Message>>;
createMessageComponentCollector<T extends ComponentType = ComponentType.ActionRow>(
createMessageComponentCollector<T extends MessageComponentType = ComponentType.ActionRow>(
options?: MessageChannelCollectorOptionsParams<T, true>,
): InteractionCollector<MappedInteractionTypes[T]>;
createMessageCollector(options?: MessageCollectorOptions): MessageCollector;
@@ -4518,7 +4602,7 @@ export type ActionRowComponentOptions =
| (Required<BaseComponentData> & ButtonComponentData)
| (Required<BaseComponentData> & SelectMenuComponentData);
export type MessageActionRowComponentResolvable = ActionRowComponent | ActionRowComponentOptions;
export type MessageActionRowComponentResolvable = MessageActionRowComponent | ActionRowComponentOptions;
export interface MessageActivity {
partyId: string;
@@ -4548,7 +4632,7 @@ export interface MessageCollectorOptions extends CollectorOptions<[Message]> {
maxProcessed?: number;
}
export type MessageComponent = Component | ActionRow<ActionRowComponent> | ButtonComponent | SelectMenuComponent;
export type MessageComponent = Component | ActionRow<MessageActionRowComponent> | ButtonComponent | SelectMenuComponent;
export type MessageComponentCollectorOptions<T extends MessageComponentInteraction> = Omit<
InteractionCollectorOptions<T>,
@@ -4567,7 +4651,11 @@ export interface MessageEditOptions {
files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[];
flags?: BitFieldResolvable<MessageFlagsString, number>;
allowedMentions?: MessageMentionOptions;
components?: (ActionRow<ActionRowComponent> | (Required<BaseComponentData> & ActionRowData))[];
components?: (
| ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData>)
| APIActionRowComponent<APIMessageActionRowComponent>
)[];
}
export interface MessageEvent {
@@ -4605,9 +4693,9 @@ export interface MessageOptions {
content?: string | null;
embeds?: (Embed | APIEmbed)[];
components?: (
| ActionRow<ActionRowComponent>
| (Required<BaseComponentData> & ActionRowData)
| APIActionRowComponent<APIActionRowComponentTypes>
| ActionRow<MessageActionRowComponent>
| (Required<BaseComponentData> & ActionRowData<MessageActionRowComponentData>)
| APIActionRowComponent<APIMessageActionRowComponent>
)[];
allowedMentions?: MessageMentionOptions;
files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[];
@@ -4658,6 +4746,23 @@ export interface SelectMenuComponentOptionData {
value: string;
}
export interface TextInputComponentData extends BaseComponentData {
customId: string;
style: TextInputStyle;
label: string;
minLength?: number;
maxLength?: number;
required?: boolean;
value?: string;
placeholder?: string;
}
export interface ModalData {
customId: string;
title: string;
components: ActionRowData<ModalActionRowComponentData>[];
}
export type MessageTarget =
| Interaction
| InteractionWebhook
@@ -5156,6 +5261,7 @@ export {
StageInstancePrivacyLevel,
StickerType,
StickerFormatType,
TextInputStyle,
GuildSystemChannelFlags,
ThreadMemberFlags,
UserFlags,
@@ -5166,7 +5272,8 @@ export {
UnsafeSelectMenuComponent,
SelectMenuOption,
UnsafeSelectMenuOption,
ActionRowComponent,
MessageActionRowComponent,
UnsafeEmbed,
ModalActionRowComponent,
} from '@discordjs/builders';
export { DiscordAPIError, HTTPError, RateLimitError } from '@discordjs/rest';

View File

@@ -96,7 +96,7 @@ import {
ActionRow,
ButtonComponent,
SelectMenuComponent,
ActionRowComponent,
MessageActionRowComponent,
InteractionResponseFields,
ThreadChannelType,
Events,
@@ -104,6 +104,7 @@ import {
Status,
CategoryChannelChildManager,
ActionRowData,
MessageActionRowComponentData,
} from '.';
import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd';
import { Embed } from '@discordjs/builders';
@@ -723,11 +724,14 @@ client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
void new ActionRow<ActionRowComponent>();
void new ActionRow<MessageActionRowComponent>();
const button = new ButtonComponent();
const actionRow = new ActionRow<ActionRowComponent>({ type: ComponentType.ActionRow, components: [button.toJSON()] });
const actionRow = new ActionRow<MessageActionRowComponent>({
type: ComponentType.ActionRow,
components: [button.toJSON()],
});
await interaction.reply({ content: 'Hi!', components: [actionRow] });
@@ -1092,11 +1096,11 @@ client.on('interactionCreate', async interaction => {
if (interaction.isMessageComponent()) {
expectType<MessageComponentInteraction>(interaction);
expectType<ActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<MessageActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
if (interaction.inCachedGuild()) {
expectAssignable<MessageComponentInteraction>(interaction);
expectType<ActionRowComponent>(interaction.component);
expectType<MessageActionRowComponent>(interaction.component);
expectType<Message<true>>(interaction.message);
expectType<Guild>(interaction.guild);
expectAssignable<Promise<Message>>(interaction.reply({ fetchReply: true }));
@@ -1108,7 +1112,7 @@ client.on('interactionCreate', async interaction => {
expectType<Promise<APIMessage>>(interaction.reply({ fetchReply: true }));
} else if (interaction.inGuild()) {
expectAssignable<MessageComponentInteraction>(interaction);
expectType<ActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<MessageActionRowComponent | APIButtonComponent | APISelectMenuComponent>(interaction.component);
expectType<Message | APIMessage>(interaction.message);
expectType<Guild | null>(interaction.guild);
expectType<Promise<APIMessage | Message>>(interaction.reply({ fetchReply: true }));
@@ -1336,7 +1340,7 @@ new ButtonComponent({
style: ButtonStyle.Danger,
});
expectNotAssignable<ActionRowData>({
expectNotAssignable<ActionRowData<MessageActionRowComponentData>>({
type: ComponentType.ActionRow,
components: [
{