From 374c779f7f8bbaa9bf06fa2b5b16f60da5095b5c Mon Sep 17 00:00:00 2001 From: monbrey Date: Sat, 3 Jul 2021 22:35:39 +1000 Subject: [PATCH] feat(InteractionCollector): reworked to be more generic (#5999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antonio Román Co-authored-by: SpaceEEC --- src/index.js | 2 +- ...onCollector.js => InteractionCollector.js} | 120 +++++++++++------- src/structures/Message.js | 32 +++-- src/structures/interfaces/TextBasedChannel.js | 26 ++-- typings/index.d.ts | 116 ++++++++--------- 5 files changed, 172 insertions(+), 124 deletions(-) rename src/structures/{MessageComponentInteractionCollector.js => InteractionCollector.js} (51%) diff --git a/src/index.js b/src/index.js index 43c46a4f5..02aa7da1f 100644 --- a/src/index.js +++ b/src/index.js @@ -86,6 +86,7 @@ module.exports = { Integration: require('./structures/Integration'), IntegrationApplication: require('./structures/IntegrationApplication'), Interaction: require('./structures/Interaction'), + InteractionCollector: require('./structures/InteractionCollector'), InteractionWebhook: require('./structures/InteractionWebhook'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), @@ -94,7 +95,6 @@ module.exports = { MessageButton: require('./structures/MessageButton'), MessageCollector: require('./structures/MessageCollector'), MessageComponentInteraction: require('./structures/MessageComponentInteraction'), - MessageComponentInteractionCollector: require('./structures/MessageComponentInteractionCollector'), MessageEmbed: require('./structures/MessageEmbed'), MessageMentions: require('./structures/MessageMentions'), MessagePayload: require('./structures/MessagePayload'), diff --git a/src/structures/MessageComponentInteractionCollector.js b/src/structures/InteractionCollector.js similarity index 51% rename from src/structures/MessageComponentInteractionCollector.js rename to src/structures/InteractionCollector.js index 2f31d3763..8ccefda68 100644 --- a/src/structures/MessageComponentInteractionCollector.js +++ b/src/structures/InteractionCollector.js @@ -3,40 +3,69 @@ const Collector = require('./interfaces/Collector'); const Collection = require('../util/Collection'); const { Events } = require('../util/Constants'); +const { InteractionTypes, MessageComponentTypes } = 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 + * @typedef {CollectorOptions} InteractionCollectorOptions + * @property {TextChannel|DMChannel|NewsChannel} [channel] The channel to listen to interactions from + * @property {MessageComponentType} [componentType] The type of component to listen for + * @property {Guild} [guild] The guild to listen to interactions from + * @property {InteractionType} [interactionType] The type of interaction to listen for + * @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 + * @property {Message} [message] The message to listen to interactions from */ /** - * Collects interaction on message components. + * Collects interactions. * Will automatically stop if the message (`'messageDelete'`), * channel (`'channelDelete'`), or guild (`'guildDelete'`) are deleted. * @extends {Collector} */ -class MessageComponentInteractionCollector extends Collector { +class InteractionCollector extends Collector { /** - * @param {Message|TextChannel|DMChannel|NewsChannel} source - * The source from which to collect message component interactions - * @param {MessageComponentInteractionCollectorOptions} [options={}] The options to apply to this collector + * @param {Client} client The client on which to collect message component interactions + * @param {InteractionCollectorOptions} [options={}] The options to apply to this collector */ - constructor(source, options = {}) { - super(source.client, options); + constructor(client, options = {}) { + super(client, options); /** - * The message from which to collect message component interactions, if provided + * The message from which to collect interactions, if provided * @type {?Message} */ - this.message = source instanceof require('./Message') ? source : null; + this.message = options.message ?? null; /** - * The source channel from which to collect message component interactions - * @type {TextChannel|DMChannel|NewsChannel} + * The channel from which to collect interactions, if provided + * @type {?TextChannel|DMChannel|NewsChannel} */ - this.channel = this.message?.channel ?? source; + this.channel = this.message?.channel ?? options.channel ?? null; + + /** + * The guild from which to collect interactions, if provided + * @type {?Guild} + */ + this.guild = this.channel?.guild ?? options.guild ?? null; + + /** + * The the type of interaction to collect + * @type {?InteractionType} + */ + this.interactionType = + typeof options.interactionType === 'number' + ? InteractionTypes[options.interactionType] + : options.interactionType ?? null; + + /** + * The the type of component to collect + * @type {?MessageComponentType} + */ + this.componentType = + typeof options.componentType === 'number' + ? MessageComponentTypes[options.componentType] + : options.componentType ?? null; /** * The users which have interacted to components on this collector @@ -51,23 +80,28 @@ class MessageComponentInteractionCollector extends Collector { 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(); + + if (this.message) { + this._handleMessageDeletion = this._handleMessageDeletion.bind(this); + this.client.on(Events.MESSAGE_DELETE, this._handleMessageDeletion); + } + + if (this.channel) { + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this.client.on(Events.CHANNEL_DELETE, this._handleChannelDeletion); + } + + if (this.guild) { + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + this.client.on(Events.GUILD_DELETE, this._handleGuildDeletion); + } + 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.MESSAGE_DELETE, this._handleMessageDeletion); this.client.removeListener(Events.CHANNEL_DELETE, this._handleChannelDeletion); this.client.removeListener(Events.GUILD_DELETE, this._handleGuildDeletion); this.client.decrementMaxListeners(); @@ -91,13 +125,13 @@ class MessageComponentInteractionCollector extends Collector { * @event MessageComponentInteractionCollector#collect * @param {Interaction} interaction The interaction that was collected */ - if (!interaction.isMessageComponent()) return null; + if (this.interactionType && interaction.type !== this.interactionType) return null; + if (this.componentType && interaction.componentType !== this.componentType) return null; + if (this.message && interaction.message?.id !== this.message.id) return null; + if (this.channel && interaction.channelID !== this.channel.id) return null; + if (this.guild && interaction.guildID !== this.guild.id) 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; + return interaction.id; } /** @@ -111,13 +145,13 @@ class MessageComponentInteractionCollector extends Collector { * @event MessageComponentInteractionCollector#dispose * @param {Interaction} interaction The interaction that was disposed of */ - if (!interaction.isMessageComponent()) return null; + if (this.type && interaction.type !== this.type) return null; + if (this.componentType && interaction.componentType !== this.componentType) return null; + if (this.message && interaction.message?.id !== this.message.id) return null; + if (this.channel && interaction.channelID !== this.channel.id) return null; + if (this.guild && interaction.guildID !== this.guild.id) 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; + return interaction.id; } /** @@ -161,7 +195,7 @@ class MessageComponentInteractionCollector extends Collector { * @returns {void} */ _handleChannelDeletion(channel) { - if (channel.id === this.channel.id) { + if (channel.id === this.channel?.id) { this.stop('channelDelete'); } } @@ -173,10 +207,10 @@ class MessageComponentInteractionCollector extends Collector { * @returns {void} */ _handleGuildDeletion(guild) { - if (guild.id === this.channel.guild?.id) { + if (guild.id === this.guild?.id) { this.stop('guildDelete'); } } } -module.exports = MessageComponentInteractionCollector; +module.exports = InteractionCollector; diff --git a/src/structures/Message.js b/src/structures/Message.js index 9cbb86915..06df14914 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -3,8 +3,8 @@ const Base = require('./Base'); const BaseMessageComponent = require('./BaseMessageComponent'); const ClientApplication = require('./ClientApplication'); +const InteractionCollector = require('./InteractionCollector'); const MessageAttachment = require('./MessageAttachment'); -const MessageComponentInteractionCollector = require('./MessageComponentInteractionCollector'); const Embed = require('./MessageEmbed'); const Mentions = require('./MessageMentions'); const MessagePayload = require('./MessagePayload'); @@ -439,10 +439,18 @@ class Message extends Base { }); } + /** + * @typedef {CollectorOptions} MessageComponentCollectorOptions + * @property {MessageComponentType} [componentType] The type of component to listen for + * @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 + */ + /** * Creates a message component interaction collector. - * @param {MessageComponentInteractionCollectorOptions} [options={}] Options to send to the collector - * @returns {MessageComponentInteractionCollector} + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} * @example * // Create a message component interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; @@ -450,21 +458,26 @@ class Message extends Base { * collector.on('collect', i => console.log(`Collected ${i.customID}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ - createMessageComponentInteractionCollector(options = {}) { - return new MessageComponentInteractionCollector(this, options); + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionType: InteractionTypes.MESSAGE_COMPONENT, + message: this, + }); } /** * An object containing the same properties as CollectorOptions, but a few more: - * @typedef {Object} AwaitMessageComponentInteractionOptions + * @typedef {Object} AwaitMessageComponentOptions * @property {CollectorFilter} [filter] The filter applied to this collector * @property {number} [time] Time to wait for an interaction before rejecting + * @property {MessageComponentType} [componentType] The type of component interaction to collect */ /** * Collects a single component interaction that passes the filter. * The Promise will reject if the time expires. - * @param {AwaitMessageComponentInteractionOptions} [options={}] Options to pass to the internal collector + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector * @returns {Promise} * @example * // Collect a message component interaction @@ -473,9 +486,10 @@ class Message extends Base { * .then(interaction => console.log(`${interaction.customID} was clicked!`)) * .catch(console.error); */ - awaitMessageComponentInteraction(options = {}) { + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; return new Promise((resolve, reject) => { - const collector = this.createMessageComponentInteractionCollector({ ...options, max: 1 }); + const collector = this.createMessageComponentCollector(_options); collector.once('end', (interactions, reason) => { const interaction = interactions.first(); if (interaction) resolve(interaction); diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index bc68c2457..d36f483f3 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -5,8 +5,9 @@ const MessageCollector = require('../MessageCollector'); const MessagePayload = require('../MessagePayload'); const SnowflakeUtil = require('../../util/SnowflakeUtil'); const Collection = require('../../util/Collection'); +const { InteractionTypes } = require('../../util/Constants'); const { RangeError, TypeError, Error } = require('../../errors'); -const MessageComponentInteractionCollector = require('../MessageComponentInteractionCollector'); +const InteractionCollector = require('../InteractionCollector'); /** * Interface for classes that have text-channel-like features. @@ -304,8 +305,8 @@ class TextBasedChannel { /** * Creates a button interaction collector. - * @param {MessageComponentInteractionCollectorOptions} [options={}] Options to send to the collector - * @returns {MessageComponentInteractionCollector} + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} * @example * // Create a button interaction collector * const filter = (interaction) => interaction.customID === 'button' && interaction.user.id === 'someID'; @@ -313,14 +314,18 @@ class TextBasedChannel { * collector.on('collect', i => console.log(`Collected ${i.customID}`)); * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); */ - createMessageComponentInteractionCollector(options = {}) { - return new MessageComponentInteractionCollector(this, options); + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionType: InteractionTypes.MESSAGE_COMPONENT, + channel: this, + }); } /** * Collects a single component interaction that passes the filter. * The Promise will reject if the time expires. - * @param {AwaitMessageComponentInteractionOptions} [options={}] Options to pass to the internal collector + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector * @returns {Promise} * @example * // Collect a message component interaction @@ -329,9 +334,10 @@ class TextBasedChannel { * .then(interaction => console.log(`${interaction.customID} was clicked!`)) * .catch(console.error); */ - awaitMessageComponentInteraction(options = {}) { + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; return new Promise((resolve, reject) => { - const collector = this.createMessageComponentInteractionCollector({ ...options, max: 1 }); + const collector = this.createMessageComponentCollector(_options); collector.once('end', (interactions, reason) => { const interaction = interactions.first(); if (interaction) resolve(interaction); @@ -404,8 +410,8 @@ class TextBasedChannel { 'typingCount', 'createMessageCollector', 'awaitMessages', - 'createMessageComponentInteractionCollector', - 'awaitMessageComponentInteraction', + 'createMessageComponentCollector', + 'awaitMessageComponent', ); } for (const prop of props) { diff --git a/typings/index.d.ts b/typings/index.d.ts index 3ec2bbcf5..28f9a871c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1224,6 +1224,34 @@ declare module 'discord.js' { public isSelectMenu(): this is SelectMenuInteraction; } + export class InteractionCollector extends Collector { + constructor(client: Client, options?: InteractionCollectorOptions); + private _handleMessageDeletion(message: Message): void; + private _handleChannelDeletion(channel: GuildChannel): void; + private _handleGuildDeletion(guild: Guild): void; + + public channel: TextChannel | NewsChannel | DMChannel | null; + public componentType: MessageComponentType | null; + public readonly endReason: string | null; + public guild: Guild | null; + public interactionType: InteractionType | null; + public message: Message | null; + public options: InteractionCollectorOptions; + public total: number; + public users: Collection; + + public collect(interaction: Interaction): Snowflake; + public empty(): void; + public dispose(interaction: Interaction): Snowflake; + public on(event: 'collect' | 'dispose', listener: (interaction: T) => Awaited): this; + public on(event: 'end', listener: (collected: Collection, reason: string) => Awaited): this; + public on(event: string, listener: (...args: any[]) => Awaited): this; + + public once(event: 'collect' | 'dispose', listener: (interaction: T) => Awaited): this; + public once(event: 'end', listener: (collected: Collection, reason: string) => Awaited): this; + public once(event: string, listener: (...args: any[]) => Awaited): this; + } + export class InteractionWebhook extends PartialWebhookMixin() { constructor(client: Client, id: Snowflake, token: string); public token: string; @@ -1316,14 +1344,14 @@ declare module 'discord.js' { public webhookID: Snowflake | null; public flags: Readonly; public reference: MessageReference | null; - public awaitMessageComponentInteraction( - options?: AwaitMessageComponentInteractionOptions, - ): Promise; + public awaitMessageComponent( + options?: AwaitMessageComponentOptions, + ): Promise; public awaitReactions(options?: AwaitReactionsOptions): Promise>; public createReactionCollector(options?: ReactionCollectorOptions): ReactionCollector; - public createMessageComponentInteractionCollector( - options?: MessageComponentInteractionCollectorOptions, - ): MessageComponentInteractionCollector; + public createMessageComponentCollector( + options?: InteractionCollectorOptions, + ): InteractionCollector; public delete(): Promise; public edit(content: string | MessageEditOptions | MessagePayload): Promise; public equals(message: Message, rawData: unknown): boolean; @@ -1438,46 +1466,6 @@ declare module 'discord.js' { public static resolveType(type: MessageComponentTypeResolvable): MessageComponentType; } - export class MessageComponentInteractionCollector extends Collector { - constructor( - source: Message | TextChannel | DMChannel | ThreadChannel, - options?: MessageComponentInteractionCollectorOptions, - ); - private _handleMessageDeletion(message: Message): void; - private _handleChannelDeletion(channel: GuildChannel): void; - private _handleGuildDeletion(guild: Guild): void; - - public channel: TextChannel | DMChannel | ThreadChannel; - public empty(): void; - public readonly endReason: string | null; - public message: Message | null; - public options: MessageComponentInteractionCollectorOptions; - public total: number; - public users: Collection; - - public collect(interaction: Interaction): Snowflake | null; - public dispose(interaction: Interaction): Snowflake | null; - public on( - event: 'collect' | 'dispose', - listener: (interaction: MessageComponentInteraction) => Awaited, - ): this; - public on( - event: 'end', - listener: (collected: Collection, reason: string) => Awaited, - ): this; - public on(event: string, listener: (...args: any[]) => Awaited): this; - - public once( - event: 'collect' | 'dispose', - listener: (interaction: MessageComponentInteraction) => Awaited, - ): this; - public once( - event: 'end', - listener: (collected: Collection, reason: string) => Awaited, - ): this; - public once(event: string, listener: (...args: any[]) => Awaited): this; - } - export class MessageEmbed { constructor(data?: MessageEmbed | MessageEmbedOptions); public author: MessageEmbedAuthor | null; @@ -2734,17 +2722,17 @@ declare module 'discord.js' { readonly lastPinAt: Date | null; typing: boolean; typingCount: number; - awaitMessageComponentInteraction( - options?: AwaitMessageComponentInteractionOptions, - ): Promise; + awaitMessageComponent( + options?: AwaitMessageComponentOptions, + ): Promise; awaitMessages(options?: AwaitMessagesOptions): Promise>; bulkDelete( messages: Collection | readonly MessageResolvable[] | number, filterOld?: boolean, ): Promise>; - createMessageComponentInteractionCollector( - options?: MessageComponentInteractionCollectorOptions, - ): MessageComponentInteractionCollector; + createMessageComponentCollector( + options?: InteractionCollectorOptions, + ): InteractionCollector; createMessageCollector(options?: MessageCollectorOptions): MessageCollector; startTyping(count?: number): Promise; stopTyping(force?: boolean): void; @@ -2987,10 +2975,8 @@ declare module 'discord.js' { new?: any; } - interface AwaitMessageComponentInteractionOptions { - filter?: CollectorFilter<[MessageComponentInteraction]>; - time?: number; - } + interface AwaitMessageComponentOptions + extends Omit, 'max' | 'maxComponents' | 'maxUsers'> {} interface AwaitMessagesOptions extends MessageCollectorOptions { errors?: string[]; @@ -3674,6 +3660,17 @@ declare module 'discord.js' { name: string; } + interface InteractionCollectorOptions extends CollectorOptions<[T]> { + channel?: TextChannel | DMChannel | NewsChannel | ThreadChannel; + componentType?: MessageComponentType | MessageComponentTypes; + guild?: Guild; + interactionType?: InteractionType | InteractionTypes; + max?: number; + maxComponents?: number; + maxUsers?: number; + message?: Message; + } + interface InteractionDeferOptions { ephemeral?: boolean; fetchReply?: boolean; @@ -3789,11 +3786,8 @@ declare module 'discord.js' { type MessageComponent = BaseMessageComponent | MessageActionRow | MessageButton | MessageSelectMenu; - interface MessageComponentInteractionCollectorOptions extends CollectorOptions<[MessageComponentInteraction]> { - max?: number; - maxComponents?: number; - maxUsers?: number; - } + interface MessageComponentCollectorOptions + extends Omit, 'channel' | 'message' | 'guild' | 'interactionType'> {} type MessageComponentOptions = | BaseMessageComponentOptions