From a58556adc02b2b9239c8f277a4387c743c9d6f04 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sun, 17 Apr 2022 04:59:36 -0400 Subject: [PATCH] feat: allow `createMessageComponentCollector` without using `fetchReply` (#7623) * feat: allow creation of message component collectors without fetchReply * chore: attempt at requested changes * fix: collector bug * refactor: use better names * feat: add update() support * feat: add defer support * refactor: InteractionReply -> InteractionResponse * fix: remove log * chore: make requested changes * Update packages/discord.js/src/structures/InteractionResponse.js Co-authored-by: Vlad Frangu * chore: make requested changes Co-authored-by: Vlad Frangu --- packages/discord.js/src/index.js | 1 + .../src/structures/InteractionCollector.js | 22 ++++++- .../src/structures/InteractionResponse.js | 61 +++++++++++++++++++ .../interfaces/InteractionResponses.js | 15 ++--- packages/discord.js/typings/index.d.ts | 39 +++++++++--- 5 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 packages/discord.js/src/structures/InteractionResponse.js diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index e28a0e599..e29de8c4b 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -117,6 +117,7 @@ exports.Integration = require('./structures/Integration'); exports.IntegrationApplication = require('./structures/IntegrationApplication'); exports.Interaction = require('./structures/Interaction'); exports.InteractionCollector = require('./structures/InteractionCollector'); +exports.InteractionResponse = require('./structures/InteractionResponse'); exports.InteractionWebhook = require('./structures/InteractionWebhook'); exports.Invite = require('./structures/Invite'); exports.InviteStageInstance = require('./structures/InviteStageInstance'); diff --git a/packages/discord.js/src/structures/InteractionCollector.js b/packages/discord.js/src/structures/InteractionCollector.js index 400b0c2f6..cfbb9170d 100644 --- a/packages/discord.js/src/structures/InteractionCollector.js +++ b/packages/discord.js/src/structures/InteractionCollector.js @@ -14,6 +14,8 @@ const Events = require('../util/Events'); * @property {number} [maxComponents] The maximum number of components to collect * @property {number} [maxUsers] The maximum number of users to interact * @property {Message|APIMessage} [message] The message to listen to interactions from + * @property {InteractionResponse} interactionResponse The interaction response to listen + * to message component interactions from */ /** @@ -40,18 +42,28 @@ class InteractionCollector extends Collector { */ this.messageId = options.message?.id ?? null; + /** + * The message interaction id from which to collect interactions, if provided + * @type {?Snowflake} + */ + this.messageInteractionId = options.interactionResponse?.id ?? null; + /** * The channel from which to collect interactions, if provided * @type {?Snowflake} */ this.channelId = - options.message?.channelId ?? options.message?.channel_id ?? this.client.channels.resolveId(options.channel); + options.interactionResponse?.interaction.channelId ?? + options.message?.channelId ?? + options.message?.channel_id ?? + this.client.channels.resolveId(options.channel); /** * The guild from which to collect interactions, if provided * @type {?Snowflake} */ this.guildId = + options.interactionResponse?.interaction.guildId ?? options.message?.guildId ?? options.message?.guild_id ?? this.client.guilds.resolveId(options.channel?.guild) ?? @@ -87,7 +99,7 @@ class InteractionCollector extends Collector { if (messages.has(this.messageId)) this.stop('messageDelete'); }; - if (this.messageId) { + if (this.messageId || this.messageInteractionId) { this._handleMessageDeletion = this._handleMessageDeletion.bind(this); this.client.on(Events.MessageDelete, this._handleMessageDeletion); this.client.on(Events.MessageBulkDelete, bulkDeleteListener); @@ -138,6 +150,7 @@ class InteractionCollector extends Collector { if (this.interactionType && interaction.type !== this.interactionType) return null; if (this.componentType && interaction.componentType !== this.componentType) return null; if (this.messageId && interaction.message?.id !== this.messageId) return null; + if (this.messageInteractionId && interaction.message?.interaction?.id !== this.messageInteractionId) return null; if (this.channelId && interaction.channelId !== this.channelId) return null; if (this.guildId && interaction.guildId !== this.guildId) return null; @@ -158,6 +171,7 @@ class InteractionCollector extends Collector { if (this.type && interaction.type !== this.type) return null; if (this.componentType && interaction.componentType !== this.componentType) return null; if (this.messageId && interaction.message?.id !== this.messageId) return null; + if (this.messageInteractionId && interaction.message?.interaction?.id !== this.messageInteractionId) return null; if (this.channelId && interaction.channelId !== this.channelId) return null; if (this.guildId && interaction.guildId !== this.guildId) return null; @@ -196,6 +210,10 @@ class InteractionCollector extends Collector { if (message.id === this.messageId) { this.stop('messageDelete'); } + + if (message.interaction.id === this.messageInteractionId) { + this.stop('messageDelete'); + } } /** diff --git a/packages/discord.js/src/structures/InteractionResponse.js b/packages/discord.js/src/structures/InteractionResponse.js new file mode 100644 index 000000000..e1cae7f63 --- /dev/null +++ b/packages/discord.js/src/structures/InteractionResponse.js @@ -0,0 +1,61 @@ +'use strict'; + +const { InteractionType } = require('discord-api-types/v10'); + +/** + * Represents an interaction's response + */ +class InteractionResponse { + /** + * @param {Interaction} interaction The interaction associated with this response + * @param {Snowflake?} id The interaction id associated with the original response + * @private + */ + constructor(interaction, id) { + /** + * The interaction associated with the interaction response + * @type {Interaction} + */ + this.interaction = interaction; + /** + * The id of the original interaction response + * @type {Snowflake} + */ + this.id = id ?? interaction.id; + this.client = interaction.client; + } + + /** + * Collects a single component interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector + * @returns {Promise} + */ + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; + return new Promise((resolve, reject) => { + const collector = this.createMessageComponentCollector(_options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new Error('INTERACTION_COLLECTOR_ERROR', reason)); + }); + }); + } + + /** + * Creates a message component interaction collector + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} + */ + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionResponse: this, + interactionType: InteractionType.MessageComponent, + }); + } +} + +const InteractionCollector = require('./InteractionCollector'); +module.exports = InteractionResponse; diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js index f23d7bfe7..3a05b8811 100644 --- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js +++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js @@ -4,6 +4,7 @@ const { isJSONEncodable } = require('@discordjs/builders'); const { InteractionResponseType, MessageFlags, Routes, InteractionType } = require('discord-api-types/v10'); const { Error } = require('../../errors'); const InteractionCollector = require('../InteractionCollector'); +const InteractionResponse = require('../InteractionResponse'); const MessagePayload = require('../MessagePayload'); /** @@ -49,7 +50,7 @@ class InteractionResponses { /** * Defers the reply to this interaction. * @param {InteractionDeferReplyOptions} [options] Options for deferring the reply to this interaction - * @returns {Promise} + * @returns {Promise} * @example * // Defer the reply to this interaction * interaction.deferReply() @@ -75,14 +76,14 @@ class InteractionResponses { }); this.deferred = true; - return options.fetchReply ? this.fetchReply() : undefined; + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this); } /** * Creates a reply to this interaction. * Use the `fetchReply` option to get the bot's reply message. * @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply - * @returns {Promise} + * @returns {Promise} * @example * // Reply to the interaction and fetch the response * interaction.reply({ content: 'Pong!', fetchReply: true }) @@ -116,7 +117,7 @@ class InteractionResponses { }); this.replied = true; - return options.fetchReply ? this.fetchReply() : undefined; + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this); } /** @@ -179,7 +180,7 @@ class InteractionResponses { /** * Defers an update to the message to which the component was attached. * @param {InteractionDeferUpdateOptions} [options] Options for deferring the update to this interaction - * @returns {Promise} + * @returns {Promise} * @example * // Defer updating and reset the component's loading state * interaction.deferUpdate() @@ -196,7 +197,7 @@ class InteractionResponses { }); this.deferred = true; - return options.fetchReply ? this.fetchReply() : undefined; + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message.interaction.id); } /** @@ -231,7 +232,7 @@ class InteractionResponses { }); this.replied = true; - return options.fetchReply ? this.fetchReply() : undefined; + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message.interaction.id); } /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index bd567379d..d7f8f19e7 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -397,6 +397,8 @@ export interface InteractionResponseFields awaitModalSubmit(options: AwaitModalSubmitOptions): Promise>; } +export type BooleanCache = T extends ['cached'] ? true : false; + export abstract class CommandInteraction extends Interaction { public get command(): ApplicationCommand | ApplicationCommand<{ guild: GuildResolvable }> | null; public options: Omit< @@ -426,13 +428,15 @@ export abstract class CommandInteraction e public inCachedGuild(): this is CommandInteraction<'cached'>; public inRawGuild(): this is CommandInteraction<'raw'>; public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise>; - public deferReply(options?: InteractionDeferReplyOptions): Promise; + public deferReply(options?: InteractionDeferReplyOptions): Promise>>; public deleteReply(): Promise; public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise>; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; - public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; + public reply( + options: string | MessagePayload | InteractionReplyOptions, + ): Promise>>; public showModal( modal: JSONEncodable | ModalData | APIModalInteractionResponseCallbackData, ): Promise; @@ -445,6 +449,19 @@ export abstract class CommandInteraction e ): CommandInteractionResolvedData; } +export class InteractionResponse { + private constructor(interaction: Interaction, id?: Snowflake); + public interaction: Interaction>; + public client: Client; + public id: Snowflake; + public awaitMessageComponent( + options?: AwaitMessageCollectorOptionsParams, + ): Promise[T]>; + public createMessageComponentCollector( + options?: MessageCollectorOptionsParams, + ): InteractionCollector[T]>; +} + export abstract class BaseGuild extends Base { protected constructor(client: Client, data: RawBaseGuildData); public get createdAt(): Date; @@ -1522,6 +1539,7 @@ export class InteractionCollector extends Collector e public inCachedGuild(): this is MessageComponentInteraction<'cached'>; public inRawGuild(): this is MessageComponentInteraction<'raw'>; public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise>; - public deferReply(options?: InteractionDeferReplyOptions): Promise; + public deferReply(options?: InteractionDeferReplyOptions): Promise>>; public deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise>; - public deferUpdate(options?: InteractionDeferUpdateOptions): Promise; + public deferUpdate(options?: InteractionDeferUpdateOptions): Promise; public deleteReply(): Promise; public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise>; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; - public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; + public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; public update(options: InteractionUpdateOptions & { fetchReply: true }): Promise>; - public update(options: string | MessagePayload | InteractionUpdateOptions): Promise; + public update(options: string | MessagePayload | InteractionUpdateOptions): Promise; public showModal( modal: JSONEncodable | ModalData | APIModalInteractionResponseCallbackData, ): Promise; @@ -1882,9 +1900,9 @@ export interface ModalMessageModalSubmitInteraction { message: GuildCacheMessage; update(options: InteractionUpdateOptions & { fetchReply: true }): Promise>; - update(options: string | MessagePayload | InteractionUpdateOptions): Promise; + update(options: string | MessagePayload | InteractionUpdateOptions): Promise; deferUpdate(options: InteractionDeferUpdateOptions & { fetchReply: true }): Promise>; - deferUpdate(options?: InteractionDeferUpdateOptions): Promise; + deferUpdate(options?: InteractionDeferUpdateOptions): Promise; inGuild(): this is ModalMessageModalSubmitInteraction<'raw' | 'cached'>; inCachedGuild(): this is ModalMessageModalSubmitInteraction<'cached'>; inRawGuild(): this is ModalMessageModalSubmitInteraction<'raw'>; @@ -1901,11 +1919,11 @@ export class ModalSubmitInteraction extend public replied: boolean; public readonly webhook: InteractionWebhook; public reply(options: InteractionReplyOptions & { fetchReply: true }): Promise>; - public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; + public reply(options: string | MessagePayload | InteractionReplyOptions): Promise; public deleteReply(): Promise; public editReply(options: string | MessagePayload | WebhookEditMessageOptions): Promise>; public deferReply(options: InteractionDeferReplyOptions & { fetchReply: true }): Promise>; - public deferReply(options?: InteractionDeferReplyOptions): Promise; + public deferReply(options?: InteractionDeferReplyOptions): Promise; public fetchReply(): Promise>; public followUp(options: string | MessagePayload | InteractionReplyOptions): Promise>; public inGuild(): this is ModalSubmitInteraction<'raw' | 'cached'>; @@ -4661,6 +4679,7 @@ export interface InteractionCollectorOptions; + interactionResponse?: InteractionResponse; } export interface InteractionDeferReplyOptions {