feat(MessageComponents): clickybois (MessageButton, MessageActionRow, associated Collectors) (#5674)

Co-authored-by: Vicente <33096355+Vicente015@users.noreply.github.com>
Co-authored-by: Shubham Parihar <shubhamparihar391@gmail.com>
Co-authored-by: SpaceEEC <spaceeec@yahoo.com>
Co-authored-by: BannerBomb <BannerBomb55@gmail.com>
Co-authored-by: Arechi <22101241+Arechii@users.noreply.github.com>
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com>
Co-authored-by: Antonio Román <kyradiscord@gmail.com>
This commit is contained in:
monbrey
2021-06-05 01:49:46 +10:00
committed by GitHub
parent df9b67894a
commit cbd7f2b9aa
19 changed files with 1190 additions and 158 deletions

View File

@@ -4,20 +4,31 @@ const { Events, InteractionTypes } = require('../../../util/Constants');
let Structures;
module.exports = (client, { d: data }) => {
if (data.type === InteractionTypes.APPLICATION_COMMAND) {
if (!Structures) Structures = require('../../../util/Structures');
const CommandInteraction = Structures.get('CommandInteraction');
let interaction;
switch (data.type) {
case InteractionTypes.APPLICATION_COMMAND: {
if (!Structures) Structures = require('../../../util/Structures');
const CommandInteraction = Structures.get('CommandInteraction');
const interaction = new CommandInteraction(client, data);
interaction = new CommandInteraction(client, data);
break;
}
case InteractionTypes.MESSAGE_COMPONENT: {
if (!Structures) Structures = require('../../../util/Structures');
const MessageComponentInteraction = Structures.get('MessageComponentInteraction');
/**
* Emitted when an interaction is created.
* @event Client#interaction
* @param {Interaction} interaction The interaction which was created
*/
client.emit(Events.INTERACTION_CREATE, interaction);
return;
interaction = new MessageComponentInteraction(client, data);
break;
}
default:
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
return;
}
client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`);
/**
* Emitted when an interaction is created.
* @event Client#interaction
* @param {Interaction} interaction The interaction which was created
*/
client.emit(Events.INTERACTION_CREATE, interaction);
};

View File

@@ -44,6 +44,10 @@ const Messages = {
EMBED_DESCRIPTION: 'MessageEmbed description must be a string.',
EMBED_AUTHOR_NAME: 'MessageEmbed author name must be a string.',
BUTTON_LABEL: 'MessageButton label must be a string',
BUTTON_URL: 'MessageButton url must be a string',
BUTTON_CUSTOM_ID: 'MessageButton customID must be a string',
FILE_NOT_FOUND: file => `File could not be found: ${file}`,
USER_NO_DMCHANNEL: 'No DM Channel exists!',

View File

@@ -68,6 +68,7 @@ module.exports = {
BaseGuild: require('./structures/BaseGuild'),
BaseGuildEmoji: require('./structures/BaseGuildEmoji'),
BaseGuildVoiceChannel: require('./structures/BaseGuildVoiceChannel'),
BaseMessageComponent: require('./structures/BaseMessageComponent'),
CategoryChannel: require('./structures/CategoryChannel'),
Channel: require('./structures/Channel'),
ClientApplication: require('./structures/ClientApplication'),
@@ -92,8 +93,12 @@ module.exports = {
Interaction: require('./structures/Interaction'),
Invite: require('./structures/Invite'),
Message: require('./structures/Message'),
MessageActionRow: require('./structures/MessageActionRow'),
MessageAttachment: require('./structures/MessageAttachment'),
MessageButton: require('./structures/MessageButton'),
MessageCollector: require('./structures/MessageCollector'),
MessageComponentInteraction: require('./structures/MessageComponentInteraction'),
MessageComponentInteractionCollector: require('./structures/MessageComponentInteractionCollector'),
MessageEmbed: require('./structures/MessageEmbed'),
MessageMentions: require('./structures/MessageMentions'),
MessageReaction: require('./structures/MessageReaction'),

View File

@@ -1,5 +1,6 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const MessageAttachment = require('./MessageAttachment');
const MessageEmbed = require('./MessageEmbed');
const { RangeError } = require('../errors');
@@ -151,6 +152,8 @@ class APIMessage {
}
const embeds = embedLikes.map(e => new MessageEmbed(e).toJSON());
const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON());
let username;
let avatarURL;
if (isWebhook) {
@@ -196,6 +199,7 @@ class APIMessage {
nonce,
embed: !isWebhookLike ? (this.options.embed === null ? null : embeds[0]) : undefined,
embeds: isWebhookLike ? embeds : undefined,
components,
username,
avatar_url: avatarURL,
allowed_mentions:

View File

@@ -0,0 +1,94 @@
'use strict';
const { TypeError } = require('../errors');
const { MessageComponentTypes, Events } = require('../util/Constants');
/**
* Represents an interactive component of a Message. It should not be necessary to construct this directly.
* See {@link MessageComponent}
*/
class BaseMessageComponent {
/**
* Options for a BaseMessageComponent
* @typedef {Object} BaseMessageComponentOptions
* @property {MessageComponentTypeResolvable} type The type of this component
*/
/**
* Data that can be resolved into options for a MessageComponent. This can be:
* * MessageActionRowOptions
* * MessageButtonOptions
* @typedef {MessageActionRowOptions|MessageButtonOptions} MessageComponentOptions
*/
/**
* Components that can be sent in a message
* @typedef {MessageActionRow|MessageButton} MessageComponent
*/
/**
* Data that can be resolved to a MessageComponentType. This can be:
* * {@link MessageComponentType}
* * string
* * number
* @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable
*/
/**
* @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component
*/
constructor(data) {
/**
* The type of this component
* @type {?MessageComponentType}
*/
this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null;
}
/**
* Constructs a MessageComponent based on the type of the incoming data
* @param {MessageComponentOptions} data Data for a MessageComponent
* @param {Client|WebhookClient} [client] Client constructing this component
* @param {boolean} [skipValidation=false] Whether or not to validate the component type
* @returns {?MessageComponent}
* @private
*/
static create(data, client, skipValidation = false) {
let component;
let type = data.type;
if (typeof type === 'string') type = MessageComponentTypes[type];
switch (type) {
case MessageComponentTypes.ACTION_ROW: {
const MessageActionRow = require('./MessageActionRow');
component = new MessageActionRow(data);
break;
}
case MessageComponentTypes.BUTTON: {
const MessageButton = require('./MessageButton');
component = new MessageButton(data);
break;
}
default:
if (client) {
client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
} else if (!skipValidation) {
throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType');
}
}
return component;
}
/**
* Resolves the type of a MessageComponent
* @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType}
* @private
*/
static resolveType(type) {
return typeof type === 'string' ? type : MessageComponentTypes[type];
}
}
module.exports = BaseMessageComponent;

View File

@@ -1,16 +1,15 @@
'use strict';
const APIMessage = require('./APIMessage');
const Interaction = require('./Interaction');
const InteractionResponses = require('./interfaces/InteractionResponses');
const WebhookClient = require('../client/WebhookClient');
const { Error } = require('../errors');
const Collection = require('../util/Collection');
const { ApplicationCommandOptionTypes, InteractionResponseTypes } = require('../util/Constants');
const MessageFlags = require('../util/MessageFlags');
const { ApplicationCommandOptionTypes } = require('../util/Constants');
/**
* Represents a command interaction.
* @extends {Interaction}
* @implements {InteractionResponses}
*/
class CommandInteraction extends Interaction {
constructor(client, data) {
@@ -69,126 +68,6 @@ class CommandInteraction extends Interaction {
return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null;
}
/**
* Options for deferring the reply to a {@link CommandInteraction}.
* @typedef {Object} InteractionDeferOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
*/
/**
* Defers the reply to this interaction.
* @param {InteractionDeferOptions} [options] Options for deferring the reply to this interaction
* @returns {Promise<void>}
* @example
* // Defer the reply to this interaction
* interaction.defer()
* .then(console.log)
* .catch(console.error)
* @example
* // Defer to send an ephemeral reply later
* interaction.defer({ ephemeral: true })
* .then(console.log)
* .catch(console.error);
*/
async defer({ ephemeral } = {}) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined,
},
},
});
this.deferred = true;
}
/**
* Options for a reply to an interaction.
* @typedef {BaseMessageOptions} InteractionReplyOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
* @property {MessageEmbed[]|Object[]} [embeds] An array of embeds for the message
*/
/**
* Creates a reply to this interaction.
* @param {string|APIMessage|MessageAdditions} content The content for the reply
* @param {InteractionReplyOptions} [options] Additional options for the reply
* @returns {Promise<void>}
* @example
* // Reply to the interaction with an embed
* const embed = new MessageEmbed().setDescription('Pong!');
*
* interaction.reply(embed)
* .then(console.log)
* .catch(console.error);
* @example
* // Create an ephemeral reply
* interaction.reply('Pong!', { ephemeral: true })
* .then(console.log)
* .catch(console.error);
*/
async reply(content, options) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options);
const { data, files } = await apiMessage.resolveData().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE,
data,
},
files,
});
this.replied = true;
}
/**
* Fetches the initial reply to this interaction.
* @see Webhook#fetchMessage
* @returns {Promise<Message|Object>}
* @example
* // Fetch the reply to this interaction
* interaction.fetchReply()
* .then(reply => console.log(`Replied with ${reply.content}`))
* .catch(console.error);
*/
async fetchReply() {
const raw = await this.webhook.fetchMessage('@original');
return this.channel?.messages.add(raw) ?? raw;
}
/**
* Edits the initial reply to this interaction.
* @see Webhook#editMessage
* @param {string|APIMessage|MessageAdditions} content The new content for the message
* @param {WebhookEditMessageOptions} [options] The options to provide
* @returns {Promise<Message|Object>}
* @example
* // Edit the reply to this interaction
* interaction.editReply('New content')
* .then(console.log)
* .catch(console.error);
*/
async editReply(content, options) {
const raw = await this.webhook.editMessage('@original', content, options);
return this.channel?.messages.add(raw) ?? raw;
}
/**
* Deletes the initial reply to this interaction.
* @see Webhook#deleteMessage
* @returns {Promise<void>}
* @example
* // Delete the reply to this interaction
* interaction.deleteReply()
* .then(console.log)
* .catch(console.error);
*/
async deleteReply() {
await this.webhook.deleteMessage('@original');
}
/**
* Represents an option of a received command interaction.
* @typedef {Object} CommandInteractionOption
@@ -203,24 +82,6 @@ class CommandInteraction extends Interaction {
* @property {Role|Object} [role] The resolved role
*/
/**
* Send a follow-up message to this interaction.
* @param {string|APIMessage|MessageAdditions} content The content for the reply
* @param {InteractionReplyOptions} [options] Additional options for the reply
* @returns {Promise<Message|Object>}
*/
async followUp(content, options) {
const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options);
const { data, files } = await apiMessage.resolveData().resolveFiles();
const raw = await this.client.api.webhooks(this.applicationID, this.token).post({
data,
files,
});
return this.channel?.messages.add(raw) ?? raw;
}
/**
* Transforms an option received from the API.
* @param {Object} option The received option
@@ -267,6 +128,17 @@ class CommandInteraction extends Interaction {
}
return optionsCollection;
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
defer() {}
reply() {}
fetchReply() {}
editReply() {}
deleteReply() {}
followUp() {}
}
InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']);
module.exports = CommandInteraction;

View File

@@ -91,6 +91,8 @@ class DMChannel extends Channel {
get typingCount() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentInteractionCollector() {}
awaitMessageComponentInteractions() {}
// Doesn't work on DM channels; bulkDelete() {}
}

View File

@@ -112,6 +112,14 @@ class Interaction extends Base {
isCommand() {
return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND;
}
/**
* Indicates whether this interaction is a component interaction.
* @returns {boolean}
*/
isMessageComponent() {
return InteractionTypes[this.type] === InteractionTypes.MESSAGE_COMPONENT;
}
}
module.exports = Interaction;

View File

@@ -2,8 +2,10 @@
const APIMessage = require('./APIMessage');
const Base = require('./Base');
const BaseMessageComponent = require('./BaseMessageComponent');
const ClientApplication = require('./ClientApplication');
const MessageAttachment = require('./MessageAttachment');
const MessageComponentInteractionCollector = require('./MessageComponentInteractionCollector');
const Embed = require('./MessageEmbed');
const Mentions = require('./MessageMentions');
const ReactionCollector = require('./ReactionCollector');
@@ -123,6 +125,12 @@ class Message extends Base {
*/
this.embeds = (data.embeds || []).map(e => new Embed(e, true));
/**
* A list of MessageActionRows in the message
* @type {MessageActionRow[]}
*/
this.components = (data.components ?? []).map(c => BaseMessageComponent.create(c, this.client));
/**
* A collection of attachments in the message - e.g. Pictures - mapped by their ID
* @type {Collection<Snowflake, MessageAttachment>}
@@ -282,6 +290,8 @@ class Message extends Base {
if ('tts' in data) this.tts = data.tts;
if ('embeds' in data) this.embeds = data.embeds.map(e => new Embed(e, true));
else this.embeds = this.embeds.slice();
if ('components' in data) this.components = data.components.map(c => BaseMessageComponent.create(c, this.client));
else this.components = this.components.slice();
if ('attachments' in data) {
this.attachments = new Collection();
@@ -407,6 +417,51 @@ class Message extends Base {
});
}
/**
* Creates a message component interaction collector.
* @param {CollectorFilter} filter The filter to apply
* @param {MessageComponentInteractionCollectorOptions} [options={}] Options to send to the collector
* @returns {MessageComponentInteractionCollector}
* @example
* // Create a message component interaction collector
* const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID';
* const collector = message.createMessageComponentInteractionCollector(filter, { time: 15000 });
* collector.on('collect', i => console.log(`Collected ${i.customID}`));
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
*/
createMessageComponentInteractionCollector(filter, options = {}) {
return new MessageComponentInteractionCollector(this, filter, options);
}
/**
* An object containing the same properties as CollectorOptions, but a few more:
* @typedef {MessageComponentInteractionCollectorOptions} AwaitMessageComponentInteractionsOptions
* @property {string[]} [errors] Stop/end reasons that cause the promise to reject
*/
/**
* Similar to createMessageComponentInteractionCollector but in promise form.
* Resolves with a collection of interactions that pass the specified filter.
* @param {CollectorFilter} filter The filter function to use
* @param {AwaitMessageComponentInteractionsOptions} [options={}] Optional options to pass to the internal collector
* @returns {Promise<Collection<string, MessageComponentInteraction>>}
* @example
* // Create a message component interaction collector
* const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID';
* message.awaitMessageComponentInteractions(filter, { time: 15000 })
* .then(collected => console.log(`Collected ${collected.size} interactions`))
* .catch(console.error);
*/
awaitMessageComponentInteractions(filter, options = {}) {
return new Promise((resolve, reject) => {
const collector = this.createMessageComponentInteractionCollector(filter, options);
collector.once('end', (interactions, reason) => {
if (options.errors && options.errors.includes(reason)) reject(interactions);
else resolve(interactions);
});
});
}
/**
* Whether the message is editable by the client user
* @type {boolean}

View File

@@ -0,0 +1,87 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const { MessageComponentTypes } = require('../util/Constants');
/**
* Represents an ActionRow containing message components.
* @extends {BaseMessageComponent}
*/
class MessageActionRow extends BaseMessageComponent {
/**
* Components that can be placed in a MessageActionRow
* * MessageButton
* @typedef {MessageButton} MessageActionRowComponent
*/
/**
* Options for components that can be placed in a MessageActionRow
* * MessageButtonOptions
* @typedef {MessageButtonOptions} MessageActionRowComponentOptions
*/
/**
* Data that can be resolved into a components that can be placed in a MessageActionRow
* * MessageActionRowComponent
* * MessageActionRowComponentOptions
* @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable
*/
/**
* @typedef {BaseMessageComponentOptions} MessageActionRowOptions
* @property {MessageActionRowComponentResolvable[]} [components]
* The components to place in this ActionRow
*/
/**
* @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data
*/
constructor(data = {}) {
super({ type: 'ACTION_ROW' });
/**
* The components in this MessageActionRow
* @type {MessageActionRowComponent[]}
*/
this.components = (data.components ?? []).map(c => BaseMessageComponent.create(c, null, true));
}
/**
* Adds components to the row.
* @param {...MessageActionRowComponentResolvable[]} components The components to add
* @returns {MessageActionRow}
*/
addComponents(...components) {
this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c, null, true)));
return this;
}
/**
* Removes, replaces, and inserts components in the action row.
* @param {number} index The index to start at
* @param {number} deleteCount The number of components to remove
* @param {...MessageActionRowComponentResolvable[]} [components] The replacing components
* @returns {MessageSelectMenu}
*/
spliceComponents(index, deleteCount, ...components) {
this.components.splice(
index,
deleteCount,
...components.flat(Infinity).map(c => BaseMessageComponent.create(c, null, true)),
);
return this;
}
/**
* Transforms the action row to a plain object.
* @returns {Object} The raw data of this action row
*/
toJSON() {
return {
components: this.components.map(c => c.toJSON()),
type: MessageComponentTypes[this.type],
};
}
}
module.exports = MessageActionRow;

View File

@@ -0,0 +1,166 @@
'use strict';
const BaseMessageComponent = require('./BaseMessageComponent');
const { RangeError } = require('../errors');
const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants');
const Util = require('../util/Util');
/**
* Represents a Button message component.
* @extends {BaseMessageComponent}
*/
class MessageButton extends BaseMessageComponent {
/**
* @typedef {BaseMessageComponentOptions} MessageButtonOptions
* @property {string} [label] The text to be displayed on this button
* @property {string} [customID] A unique string to be sent in the interaction when clicked
* @property {MessageButtonStyleResolvable} [style] The style of this button
* @property {Emoji} [emoji] The emoji to be displayed to the left of the text
* @property {string} [url] Optional URL for link-style buttons
* @property {boolean} [disabled=false] Disables the button to prevent interactions
*/
/**
* @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data
*/
constructor(data = {}) {
super({ type: 'BUTTON' });
this.setup(data);
}
setup(data) {
/**
* The text to be displayed on this button
* @type {?string}
*/
this.label = data.label ?? null;
/**
* A unique string to be sent in the interaction when clicked
* @type {?string}
*/
this.customID = data.custom_id ?? data.customID ?? null;
/**
* The style of this button
* @type {?MessageButtonStyle}
*/
this.style = data.style ? MessageButton.resolveStyle(data.style) : null;
/**
* Emoji for this button
* @type {?Emoji|string}
*/
this.emoji = data.emoji ?? null;
/**
* The URL this button links to, if it is a Link style button
* @type {?string}
*/
this.url = data.url ?? null;
/**
* Whether this button is currently disabled
* @type {?boolean}
*/
this.disabled = data.disabled ?? false;
}
/**
* Sets the custom ID of this button
* @param {string} customID A unique string to be sent in the interaction when clicked
* @returns {MessageButton}
*/
setCustomID(customID) {
this.customID = Util.verifyString(customID, RangeError, 'BUTTON_CUSTOM_ID');
return this;
}
/**
* Sets the interactive status of the button
* @param {boolean} disabled Whether this button should be disabled
* @returns {MessageButton}
*/
setDisabled(disabled) {
this.disabled = disabled;
return this;
}
/**
* Set the emoji of this button
* @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button
* @returns {MessageButton}
*/
setEmoji(emoji) {
if (/^\d{17,19}$/.test(emoji)) this.emoji = { id: emoji };
else this.emoji = Util.parseEmoji(`${emoji}`);
return this;
}
/**
* Sets the label of this button
* @param {string} label The text to be displayed on this button
* @returns {MessageButton}
*/
setLabel(label) {
this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL');
return this;
}
/**
* Sets the style of this button
* @param {MessageButtonStyleResolvable} style The style of this button
* @returns {MessageButton}
*/
setStyle(style) {
this.style = MessageButton.resolveStyle(style);
return this;
}
/**
* Sets the URL of this button. MessageButton#style should be LINK
* @param {string} url The URL of this button
* @returns {MessageButton}
*/
setURL(url) {
this.url = Util.verifyString(url, RangeError, 'BUTTON_URL');
return this;
}
/**
* Transforms the button to a plain object.
* @returns {Object} The raw data of this button
*/
toJSON() {
return {
custom_id: this.customID,
disabled: this.disabled,
emoji: this.emoji,
label: this.label,
style: MessageButtonStyles[this.style],
type: MessageComponentTypes[this.type],
url: this.url,
};
}
/**
* Data that can be resolved to a MessageButtonStyle. This can be
* * {@link MessageButtonStyle}
* * string
* * number
* @typedef {string|number|MessageButtonStyle} MessageButtonStyleResolvable
*/
/**
* Resolves the style of a MessageButton
* @param {MessageButtonStyleResolvable} style The style to resolve
* @returns {MessageButtonStyle}
* @private
*/
static resolveStyle(style) {
return typeof style === 'string' ? style : MessageButtonStyles[style];
}
}
module.exports = MessageButton;

View File

@@ -0,0 +1,78 @@
'use strict';
const Interaction = require('./Interaction');
const InteractionResponses = require('./interfaces/InteractionResponses');
const WebhookClient = require('../client/WebhookClient');
const { MessageComponentTypes } = require('../util/Constants');
/**
* Represents a message component interaction.
* @extends {Interaction}
* @implements {InteractionResponses}
*/
class MessageComponentInteraction extends Interaction {
constructor(client, data) {
super(client, data);
/**
* The message to which the component was attached
* @type {?Message|Object}
*/
this.message = data.message ? this.channel?.messages.add(data.message) ?? data.message : null;
/**
* The custom ID of the component which was clicked
* @type {string}
*/
this.customID = data.data.custom_id;
/**
* The type of component that was interacted with
* @type {string}
*/
this.componentType = MessageComponentInteraction.resolveType(data.data.component_type);
/**
* Whether the reply to this interaction has been deferred
* @type {boolean}
*/
this.deferred = false;
/**
* Whether this interaction has already been replied to
* @type {boolean}
*/
this.replied = false;
/**
* An associated webhook client, can be used to create deferred replies
* @type {WebhookClient}
*/
this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options);
}
/**
* Resolves the type of a MessageComponent
* @param {MessageComponentTypeResolvable} type The type to resolve
* @returns {MessageComponentType}
* @private
*/
static resolveType(type) {
return typeof type === 'string' ? type : MessageComponentTypes[type];
}
// These are here only for documentation purposes - they are implemented by InteractionResponses
/* eslint-disable no-empty-function */
defer() {}
reply() {}
fetchReply() {}
editReply() {}
deleteReply() {}
followUp() {}
deferUpdate() {}
update() {}
}
InteractionResponses.applyToClass(MessageComponentInteraction);
module.exports = MessageComponentInteraction;

View File

@@ -0,0 +1,178 @@
'use strict';
const Collector = require('./interfaces/Collector');
const Collection = require('../util/Collection');
const { Events } = require('../util/Constants');
/**
* @typedef {CollectorOptions} MessageComponentInteractionCollectorOptions
* @property {number} max The maximum total amount of interactions to collect
* @property {number} maxComponents The maximum number of components to collect
* @property {number} maxUsers The maximum number of users to interact
*/
/**
* Collects interaction on message components.
* Will automatically stop if the message (`'messageDelete'`),
* channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted.
* @extends {Collector}
*/
class MessageComponentInteractionCollector extends Collector {
/**
* @param {Message|TextChannel|DMChannel|NewsChannel} source
* The source from which to collect message component interactions
* @param {CollectorFilter} filter The filter to apply to this collector
* @param {MessageComponentInteractionCollectorOptions} [options={}] The options to apply to this collector
*/
constructor(source, filter, options = {}) {
super(source.client, filter, options);
/**
* The message from which to collect message component interactions, if provided
* @type {?Message}
*/
this.message = source instanceof require('./Message') ? source : null;
/**
* The source channel from which to collect message component interactions
* @type {TextChannel|DMChannel|NewsChannel}
*/
this.channel = this.message ? this.message.channel : source;
/**
* The users which have interacted to buttons on this collector
* @type {Collection}
*/
this.users = new Collection();
/**
* The total number of interactions collected
* @type {number}
*/
this.total = 0;
this.empty = this.empty.bind(this);
this._handleChannelDeletion = this._handleChannelDeletion.bind(this);
this._handleGuildDeletion = this._handleGuildDeletion.bind(this);
this._handleMessageDeletion = this._handleMessageDeletion.bind(this);
this.client.incrementMaxListeners();
this.client.on(Events.INTERACTION_CREATE, this.handleCollect);
if (this.message) this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion);
this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion);
this.once('end', () => {
this.client.removeListener(Events.INTERACTION_CREATE, this.handleCollect);
if (this.message) this.client.removeListener(Events.MESSAGE_DELETE, this._handleMessageDeletion);
this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion);
this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion);
this.client.decrementMaxListeners();
});
this.on('collect', interaction => {
this.total++;
this.users.set(interaction.user.id, interaction.user);
});
}
/**
* Handles an incoming interaction for possible collection.
* @param {Interaction} interaction The interaction to possibly collect
* @returns {?Snowflake|string}
* @private
*/
collect(interaction) {
/**
* Emitted whenever a interaction is collected.
* @event MessageComponentInteractionCollector#collect
* @param {Interaction} interaction The interaction that was collected
*/
if (!interaction.isMessageComponent()) return null;
if (this.message) {
return interaction.message.id === this.message.id ? interaction.id : null;
}
return interaction.channel.id === this.channel.id ? interaction.id : null;
}
/**
* Handles an interaction for possible disposal.
* @param {Interaction} interaction The interaction that could be disposed of
* @returns {?Snowflake}
*/
dispose(interaction) {
/**
* Emitted whenever an interaction is disposed of.
* @event MessageComponentInteractionCollector#dispose
* @param {Interaction} interaction The interaction that was disposed of
*/
if (!interaction.isMessageComponent()) return null;
if (this.message) {
return interaction.message.id === this.message.id ? interaction.id : null;
}
return interaction.channel.id === this.channel.id ? interaction.id : null;
}
/**
* Empties this message component collector.
*/
empty() {
this.total = 0;
this.collected.clear();
this.users.clear();
this.checkEnd();
}
get endReason() {
if (this.options.max && this.total >= this.options.max) return 'limit';
if (this.options.maxComponents && this.collected.size >= this.options.maxComponents) return 'componentLimit';
if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit';
return null;
}
/**
* Handles checking if the message has been deleted, and if so, stops the collector with the reason 'messageDelete'.
* @private
* @param {Message} message The message that was deleted
* @returns {void}
*/
_handleMessageDeletion(message) {
if (message.id === this.message?.id) {
this.stop('messageDelete');
}
}
/**
* Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'.
* @private
* @param {GuildChannel} channel The channel that was deleted
* @returns {void}
*/
_handleChannelDeletion(channel) {
if (channel.id === this.channel.id) {
this.stop('channelDelete');
}
}
/**
* Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'.
* @private
* @param {Guild} guild The guild that was deleted
* @returns {void}
*/
_handleGuildDeletion(guild) {
if (guild.id === this.channel.guild?.id) {
this.stop('guildDelete');
}
}
}
module.exports = MessageComponentInteractionCollector;

View File

@@ -157,6 +157,8 @@ class TextChannel extends GuildChannel {
get typingCount() {}
createMessageCollector() {}
awaitMessages() {}
createMessageComponentInteractionCollector() {}
awaitMessageComponentInteractions() {}
bulkDelete() {}
}

View File

@@ -0,0 +1,208 @@
'use strict';
const { InteractionResponseTypes } = require('../../util/Constants');
const MessageFlags = require('../../util/MessageFlags');
const APIMessage = require('../APIMessage');
/**
* Interface for classes that support shared interaction response types.
* @interface
*/
class InteractionResponses {
/**
* Options for deferring the reply to a {@link CommandInteraction}.
* @typedef {InteractionDeferOptions}
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
*/
/**
* Options for a reply to an interaction.
* @typedef {BaseMessageOptions} InteractionReplyOptions
* @property {boolean} [ephemeral] Whether the reply should be ephemeral
* @property {MessageEmbed[]|Object[]} [embeds] An array of embeds for the message
*/
/**
* Defers the reply to this interaction.
* @param {InteractionDeferOptions} [options] Options for deferring the reply to this interaction
* @returns {Promise<void>}
* @example
* // Defer the reply to this interaction
* interaction.defer()
* .then(console.log)
* .catch(console.error)
* @example
* // Defer to send an ephemeral reply later
* interaction.defer({ ephemeral: true })
* .then(console.log)
* .catch(console.error);
*/
async defer({ ephemeral } = {}) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined,
},
},
});
this.deferred = true;
}
/**
* Creates a reply to this interaction.
* @param {string|APIMessage|MessageAdditions} content The content for the reply
* @param {InteractionReplyOptions} [options] Additional options for the reply
* @returns {Promise<void>}
* @example
* // Reply to the interaction with an embed
* const embed = new MessageEmbed().setDescription('Pong!');
*
* interaction.reply(embed)
* .then(console.log)
* .catch(console.error);
* @example
* // Create an ephemeral reply
* interaction.reply('Pong!', { ephemeral: true })
* .then(console.log)
* .catch(console.error);
*/
async reply(content, options) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options);
const { data, files } = await apiMessage.resolveData().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE,
data,
},
files,
});
this.replied = true;
}
/**
* Fetches the initial reply to this interaction.
* @see Webhook#fetchMessage
* @returns {Promise<Message|Object>}
* @example
* // Fetch the reply to this interaction
* interaction.fetchReply()
* .then(reply => console.log(`Replied with ${reply.content}`))
* .catch(console.error);
*/
async fetchReply() {
const raw = await this.webhook.fetchMessage('@original');
return this.channel?.messages.add(raw) ?? raw;
}
/**
* Edits the initial reply to this interaction.
* @see Webhook#editMessage
* @param {string|APIMessage|MessageAdditions} content The new content for the message
* @param {WebhookEditMessageOptions} [options] The options to provide
* @returns {Promise<Message|Object>}
* @example
* // Edit the reply to this interaction
* interaction.editReply('New content')
* .then(console.log)
* .catch(console.error);
*/
async editReply(content, options) {
const raw = await this.webhook.editMessage('@original', content, options);
return this.channel?.messages.add(raw) ?? raw;
}
/**
* Deletes the initial reply to this interaction.
* @see Webhook#deleteMessage
* @returns {Promise<void>}
* @example
* // Delete the reply to this interaction
* interaction.deleteReply()
* .then(console.log)
* .catch(console.error);
*/
async deleteReply() {
await this.webhook.deleteMessage('@original');
}
/**
* Send a follow-up message to this interaction.
* @param {string|APIMessage|MessageAdditions} content The content for the reply
* @param {InteractionReplyOptions} [options] Additional options for the reply
* @returns {Promise<Message|Object>}
*/
async followUp(content, options) {
const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options);
const { data, files } = await apiMessage.resolveData().resolveFiles();
const raw = await this.client.api.webhooks(this.applicationID, this.token).post({
data,
files,
});
return this.channel?.messages.add(raw) ?? raw;
}
/**
* Defers an update to the message to which the button was attached
* @returns {Promise<void>}
* @example
* // Defer to update the button to a loading state
* interaction.deferUpdate()
* .then(console.log)
* .catch(console.error);
*/
async deferUpdate() {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.DEFERRED_MESSAGE_UPDATE,
},
});
this.deferred = true;
}
/**
* Updates the original message whose button was pressed
* @param {string|APIMessage|MessageAdditions} content The content for the reply
* @param {WebhookEditMessageOptions} [options] Additional options for the reply
* @returns {Promise<void>}
* @example
* // Remove the buttons from the message
* interaction.update("A button was clicked", { components: [] })
* .then(console.log)
* .catch(console.error);
*/
async update(content, options) {
if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED');
const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options);
const { data, files } = await apiMessage.resolveData().resolveFiles();
await this.client.api.interactions(this.id, this.token).callback.post({
data: {
type: InteractionResponseTypes.UPDATE_MESSAGE,
data,
},
files,
});
this.replied = true;
}
static applyToClass(structure, ignore = []) {
const props = ['defer', 'reply', 'fetchReply', 'editReply', 'deleteReply', 'followUp', 'deferUpdate', 'update'];
for (const prop of props) {
if (ignore.includes(prop)) continue;
Object.defineProperty(
structure.prototype,
prop,
Object.getOwnPropertyDescriptor(InteractionResponses.prototype, prop),
);
}
}
}
module.exports = InteractionResponses;

View File

@@ -6,6 +6,7 @@ const APIMessage = require('../APIMessage');
const SnowflakeUtil = require('../../util/SnowflakeUtil');
const Collection = require('../../util/Collection');
const { RangeError, TypeError } = require('../../errors');
const MessageComponentInteractionCollector = require('../MessageComponentInteractionCollector');
/**
* Interface for classes that have text-channel-like features.
@@ -315,6 +316,45 @@ class TextBasedChannel {
});
}
/**
* Creates a button interaction collector.
* @param {CollectorFilter} filter The filter to apply
* @param {MessageComponentInteractionCollectorOptions} [options={}] Options to send to the collector
* @returns {MessageComponentInteractionCollector}
* @example
* // Create a button interaction collector
* const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID';
* const collector = channel.createMessageComponentInteractionCollector(filter, { time: 15000 });
* collector.on('collect', i => console.log(`Collected ${i.customID}`));
* collector.on('end', collected => console.log(`Collected ${collected.size} items`));
*/
createMessageComponentInteractionCollector(filter, options = {}) {
return new MessageComponentInteractionCollector(this, filter, options);
}
/**
* Similar to createMessageComponentInteractionCollector but in promise form.
* Resolves with a collection of interactions that pass the specified filter.
* @param {CollectorFilter} filter The filter function to use
* @param {AwaitMessageComponentInteractionsOptions} [options={}] Optional options to pass to the internal collector
* @returns {Promise<Collection<string, MessageComponentInteraction>>}
* @example
* // Create a button interaction collector
* const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID';
* channel.awaitMessageComponentInteractions(filter, { time: 15000 })
* .then(collected => console.log(`Collected ${collected.size} interactions`))
* .catch(console.error);
*/
awaitMessageComponentInteractions(filter, options = {}) {
return new Promise((resolve, reject) => {
const collector = this.createMessageComponentInteractionCollector(filter, options);
collector.once('end', (interactions, reason) => {
if (options.errors && options.errors.includes(reason)) reject(interactions);
else resolve(interactions);
});
});
}
/**
* Bulk deletes given messages that are newer than two weeks.
* @param {Collection<Snowflake, Message>|MessageResolvable[]|number} messages
@@ -379,6 +419,8 @@ class TextBasedChannel {
'typingCount',
'createMessageCollector',
'awaitMessages',
'createMessageComponentInteractionCollector',
'awaitMessageComponentInteractions',
);
}
for (const prop of props) {

View File

@@ -799,15 +799,18 @@ exports.ApplicationCommandPermissionTypes = createEnum([null, 'ROLE', 'USER']);
* The type of an {@link Interaction} object:
* * PING
* * APPLICATION_COMMAND
* * MESSAGE_COMPONENT
* @typedef {string} InteractionType
*/
exports.InteractionTypes = createEnum([null, 'PING', 'APPLICATION_COMMAND']);
exports.InteractionTypes = createEnum([null, 'PING', 'APPLICATION_COMMAND', 'MESSAGE_COMPONENT']);
/**
* The type of an interaction response:
* * PONG
* * CHANNEL_MESSAGE_WITH_SOURCE
* * DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
* * DEFERRED_MESSAGE_UPDATE
* * UPDATE_MESSAGE
* @typedef {string} InteractionResponseType
*/
exports.InteractionResponseTypes = createEnum([
@@ -817,8 +820,29 @@ exports.InteractionResponseTypes = createEnum([
null,
'CHANNEL_MESSAGE_WITH_SOURCE',
'DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE',
'DEFERRED_MESSAGE_UPDATE',
'UPDATE_MESSAGE',
]);
/**
* The type of a message component
* ACTION_ROW
* BUTTON
* @typedef {string} MessageComponentType
*/
exports.MessageComponentTypes = createEnum([null, 'ACTION_ROW', 'BUTTON']);
/**
* The style of a message button
* PRIMARY
* SECONDARY
* SUCCESS
* DANGER
* LINK
* @typedef {string} MessageButtonStyle
*/
exports.MessageButtonStyles = createEnum([null, 'PRIMARY', 'SECONDARY', 'SUCCESS', 'DANGER', 'LINK']);
/**
* NSFW level of a Guild
* * DEFAULT

View File

@@ -20,6 +20,7 @@
* * **`Role`**
* * **`User`**
* * **`CommandInteraction`**
* * **`MessageComponentInteraction`**
* @typedef {string} ExtendableStructure
*/
@@ -111,6 +112,7 @@ const structures = {
Role: require('../structures/Role'),
User: require('../structures/User'),
CommandInteraction: require('../structures/CommandInteraction'),
MessageComponentInteraction: require('../structures/MessageComponentInteraction'),
};
module.exports = Structures;

196
typings/index.d.ts vendored
View File

@@ -25,11 +25,14 @@ declare enum InteractionResponseTypes {
PONG = 1,
CHANNEL_MESSAGE_WITH_SOURCE = 4,
DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5,
DEFERRED_MESSAGE_UPDATE = 6,
UPDATE_MESSAGE = 7,
}
declare enum InteractionTypes {
PING = 1,
APPLICATION_COMMAND = 2,
MESSAGE_COMPONENT = 3,
}
declare enum InviteTargetType {
@@ -37,6 +40,19 @@ declare enum InviteTargetType {
EMBEDDED_APPLICATION = 2,
}
declare enum MessageButtonStyles {
PRIMARY = 1,
SECONDARY = 2,
SUCCESS = 3,
DANGER = 4,
LINK = 5,
}
declare enum MessageComponentTypes {
ACTION_ROW = 1,
BUTTON = 2,
}
declare enum NSFWLevels {
DEFAULT = 0,
EXPLICIT = 1,
@@ -61,15 +77,16 @@ declare module 'discord.js' {
import BaseCollection from '@discordjs/collection';
import { ChildProcess } from 'child_process';
import {
ApplicationCommandOptionType as ApplicationCommandOptionTypes,
ApplicationCommandPermissionType as ApplicationCommandPermissionTypes,
APIInteractionDataResolvedChannel as RawInteractionDataResolvedChannel,
APIInteractionDataResolvedGuildMember as RawInteractionDataResolvedGuildMember,
APIInteractionGuildMember as RawInteractionGuildMember,
APIMessage as RawMessage,
APIOverwrite as RawOverwrite,
APIPartialEmoji as RawEmoji,
APIRole as RawRole,
Snowflake as APISnowflake,
ApplicationCommandOptionType as ApplicationCommandOptionTypes,
ApplicationCommandPermissionType as ApplicationCommandPermissionTypes,
} from 'discord-api-types/v8';
import { EventEmitter } from 'events';
import { PathLike } from 'fs';
@@ -269,6 +286,13 @@ declare module 'discord.js' {
public setRTCRegion(region: string | null): Promise<this>;
}
export class BaseMessageComponent {
constructor(data?: BaseMessageComponent | BaseMessageComponentOptions);
public type: MessageComponentType | null;
private static create(data: MessageComponentOptions): MessageComponent;
private static resolveType(type: MessageComponentTypeResolvable): MessageComponentType;
}
class BroadcastDispatcher extends VolumeMixin(StreamDispatcher) {
public broadcast: VoiceBroadcast;
}
@@ -689,6 +713,8 @@ declare module 'discord.js' {
ApplicationCommandPermissionTypes: typeof ApplicationCommandPermissionTypes;
InteractionTypes: typeof InteractionTypes;
InteractionResponseTypes: typeof InteractionResponseTypes;
MessageComponentTypes: typeof MessageComponentTypes;
MessageButtonStyles: typeof MessageButtonStyles;
NSFWLevels: typeof NSFWLevels;
};
@@ -1110,6 +1136,7 @@ declare module 'discord.js' {
public user: User;
public version: number;
public isCommand(): this is CommandInteraction;
public isMessageComponent(): this is MessageComponentInteraction;
}
export class Invite extends Base {
@@ -1149,6 +1176,7 @@ declare module 'discord.js' {
public author: User;
public channel: TextChannel | DMChannel | NewsChannel;
public readonly cleanContent: string;
public components: MessageActionRow[];
public content: string;
public readonly createdAt: Date;
public createdTimestamp: number;
@@ -1177,6 +1205,10 @@ declare module 'discord.js' {
public webhookID: Snowflake | null;
public flags: Readonly<MessageFlags>;
public reference: MessageReference | null;
public awaitMessageComponentInteractions(
filter: CollectorFilter<[MessageComponentInteraction]>,
options?: AwaitMessageComponentInteractionsOptions,
): Promise<Collection<Snowflake, MessageComponentInteraction>>;
public awaitReactions(
filter: CollectorFilter<[MessageReaction, User]>,
options?: AwaitReactionsOptions,
@@ -1185,6 +1217,10 @@ declare module 'discord.js' {
filter: CollectorFilter<[MessageReaction, User]>,
options?: ReactionCollectorOptions,
): ReactionCollector;
public createMessageComponentInteractionCollector(
filter: CollectorFilter<[MessageComponentInteraction]>,
options?: AwaitMessageComponentInteractionsOptions,
): MessageComponentInteractionCollector;
public delete(): Promise<Message>;
public edit(
content: string | null | MessageEditOptions | MessageEmbed | APIMessage | MessageAttachment | MessageAttachment[],
@@ -1221,6 +1257,21 @@ declare module 'discord.js' {
public unpin(): Promise<Message>;
}
export class MessageActionRow extends BaseMessageComponent {
constructor(data?: MessageActionRow | MessageActionRowOptions);
public type: 'ACTION_ROW';
public components: MessageActionRowComponent[];
public addComponents(
...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][]
): this;
public spliceComponents(
index: number,
deleteCount: number,
...components: MessageActionRowComponentResolvable[] | MessageActionRowComponentResolvable[][]
): this;
public toJSON(): unknown;
}
export class MessageAttachment {
constructor(attachment: BufferResolvable | Stream, name?: string, data?: unknown);
@@ -1239,6 +1290,25 @@ declare module 'discord.js' {
public toJSON(): unknown;
}
export class MessageButton extends BaseMessageComponent {
constructor(data?: MessageButton | MessageButtonOptions);
public customID: string | null;
public disabled: boolean;
public emoji: string | RawEmoji | null;
public label: string | null;
public style: MessageButtonStyle | null;
public type: 'BUTTON';
public url: string | null;
public setCustomID(customID: string): this;
public setDisabled(disabled: boolean): this;
public setEmoji(emoji: EmojiIdentifierResolvable): this;
public setLabel(label: string): this;
public setStyle(style: MessageButtonStyleResolvable): this;
public setURL(url: string): this;
public toJSON(): unknown;
private static resolveStyle(style: MessageButtonStyleResolvable): MessageButtonStyle;
}
export class MessageCollector extends Collector<Snowflake, Message> {
constructor(
channel: TextChannel | DMChannel,
@@ -1257,6 +1327,68 @@ declare module 'discord.js' {
public dispose(message: Message): Snowflake;
}
export class MessageComponentInteraction extends Interaction {
public customID: string;
public deferred: boolean;
public message: Message | RawMessage;
public replied: boolean;
public webhook: WebhookClient;
public defer(ephemeral?: boolean): Promise<void>;
public deferUpdate(): Promise<void>;
public deleteReply(): Promise<void>;
public editReply(
content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[],
): Promise<Message | RawMessage>;
public editReply(content: string, options?: WebhookEditMessageOptions): Promise<Message | RawMessage>;
public fetchReply(): Promise<Message | RawMessage>;
public followUp(
content: string | APIMessage | InteractionReplyOptions | MessageAdditions,
): Promise<Message | RawMessage>;
public followUp(content: string, options?: InteractionReplyOptions): Promise<Message | RawMessage>;
public reply(content: string | APIMessage | InteractionReplyOptions | MessageAdditions): Promise<void>;
public reply(content: string, options?: InteractionReplyOptions): Promise<void>;
public update(
content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[],
): Promise<Message | RawMessage>;
public update(content: string, options?: WebhookEditMessageOptions): Promise<Message | RawMessage>;
public static resolveType(type: MessageComponentTypeResolvable): MessageComponentType;
}
export class MessageComponentInteractionCollector extends Collector<Snowflake, Interaction> {
constructor(
source: Message | TextChannel | NewsChannel | DMChannel,
filter: CollectorFilter<[MessageComponentInteraction]>,
options?: MessageComponentInteractionCollectorOptions,
);
private _handleMessageDeletion(message: Message): void;
private _handleChannelDeletion(channel: GuildChannel): void;
private _handleGuildDeletion(guild: Guild): void;
public channel: TextChannel | NewsChannel | DMChannel;
public empty(): void;
public readonly endReason: string | null;
public message: Message | null;
public options: MessageComponentInteractionCollectorOptions;
public total: number;
public users: Collection<Snowflake, User>;
public collect(interaction: Interaction): Snowflake;
public dispose(interaction: Interaction): Snowflake;
public on(event: 'collect' | 'dispose', listener: (interaction: Interaction) => Awaited<void>): this;
public on(
event: 'end',
listener: (collected: Collection<Snowflake, Interaction>, reason: string) => Awaited<void>,
): this;
public on(event: string, listener: (...args: any[]) => Awaited<void>): this;
public once(event: 'collect' | 'dispose', listener: (interaction: Interaction) => Awaited<void>): this;
public once(
event: 'end',
listener: (collected: Collection<Snowflake, Interaction>, reason: string) => Awaited<void>,
): this;
public once(event: string, listener: (...args: any[]) => Awaited<void>): this;
}
export class MessageEmbed {
constructor(data?: MessageEmbed | MessageEmbedOptions);
public author: MessageEmbedAuthor | null;
@@ -2338,6 +2470,10 @@ declare module 'discord.js' {
readonly lastPinAt: Date | null;
typing: boolean;
typingCount: number;
awaitMessageComponentInteractions(
filter: CollectorFilter<[MessageComponentInteraction]>,
options?: AwaitMessageComponentInteractionsOptions,
): Promise<Collection<Snowflake, MessageComponentInteraction>>;
awaitMessages(
filter: CollectorFilter<[Message]>,
options?: AwaitMessagesOptions,
@@ -2346,6 +2482,10 @@ declare module 'discord.js' {
messages: Collection<Snowflake, Message> | readonly MessageResolvable[] | number,
filterOld?: boolean,
): Promise<Collection<Snowflake, Message>>;
createMessageComponentInteractionCollector(
filter: CollectorFilter<[MessageComponentInteraction]>,
options?: MessageComponentInteractionCollectorOptions,
): MessageComponentInteractionCollector;
createMessageCollector(filter: CollectorFilter<[Message]>, options?: MessageCollectorOptions): MessageCollector;
startTyping(count?: number): Promise<void>;
stopTyping(force?: boolean): void;
@@ -2552,6 +2692,10 @@ declare module 'discord.js' {
new?: any;
}
interface AwaitMessageComponentInteractionsOptions extends MessageComponentInteractionCollectorOptions {
errors?: string[];
}
interface AwaitMessagesOptions extends MessageCollectorOptions {
errors?: string[];
}
@@ -2569,6 +2713,10 @@ declare module 'discord.js' {
type Base64String = string;
interface BaseMessageComponentOptions {
type?: MessageComponentType | MessageComponentTypes;
}
type BitFieldResolvable<T extends string, N extends number | bigint> =
| RecursiveReadonlyArray<T | N | Readonly<BitField<T, N>>>
| T
@@ -3201,16 +3349,53 @@ declare module 'discord.js' {
type MessageAdditions = MessageEmbed | MessageAttachment | (MessageEmbed | MessageAttachment)[];
type MessageActionRowComponent = MessageButton;
type MessageActionRowComponentOptions = MessageButtonOptions;
type MessageActionRowComponentResolvable = MessageActionRowComponent | MessageActionRowComponentOptions;
interface MessageActionRowOptions extends BaseMessageComponentOptions {
components?: MessageActionRowComponentResolvable[];
}
interface MessageActivity {
partyID: string;
type: number;
}
interface MessageButtonOptions extends BaseMessageComponentOptions {
customID?: string;
disabled?: boolean;
emoji?: RawEmoji;
label?: string;
style: MessageButtonStyleResolvable;
url?: string;
}
type MessageButtonStyle = keyof typeof MessageButtonStyles;
type MessageButtonStyleResolvable = MessageButtonStyle | MessageButtonStyles;
interface MessageCollectorOptions extends CollectorOptions {
max?: number;
maxProcessed?: number;
}
type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton;
interface MessageComponentInteractionCollectorOptions extends CollectorOptions {
max?: number;
maxComponents?: number;
maxUsers?: number;
}
type MessageComponentOptions = BaseMessageComponentOptions | MessageActionRowOptions | MessageButtonOptions;
type MessageComponentType = keyof typeof MessageComponentTypes;
type MessageComponentTypeResolvable = MessageComponentType | MessageComponentTypes;
interface MessageEditOptions {
attachments?: MessageAttachment[];
content?: string | null;
@@ -3219,6 +3404,7 @@ declare module 'discord.js' {
files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[];
flags?: BitFieldResolvable<MessageFlagsString, number>;
allowedMentions?: MessageMentionOptions;
components?: MessageActionRow[] | MessageActionRowOptions[];
}
interface MessageEmbedAuthor {
@@ -3311,6 +3497,7 @@ declare module 'discord.js' {
nonce?: string | number;
content?: string;
embed?: MessageEmbed | MessageEmbedOptions;
components?: MessageActionRow[] | MessageActionRowOptions[];
allowedMentions?: MessageMentionOptions;
files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[];
code?: string | boolean;
@@ -3715,7 +3902,10 @@ declare module 'discord.js' {
reason?: string;
}
type WebhookEditMessageOptions = Pick<WebhookMessageOptions, 'content' | 'embeds' | 'files' | 'allowedMentions'>;
type WebhookEditMessageOptions = Pick<
WebhookMessageOptions,
'content' | 'embeds' | 'files' | 'allowedMentions' | 'components'
>;
interface WebhookMessageOptions extends Omit<MessageOptions, 'embed' | 'reply'> {
username?: string;