diff --git a/src/client/Client.js b/src/client/Client.js index 383a190..84c5be8 100644 --- a/src/client/Client.js +++ b/src/client/Client.js @@ -204,6 +204,8 @@ class Client extends BaseClient { this.token = null; } + this._interactionCache = new Collection(); + /** * User that the client is logged in as * @type {?ClientUser} @@ -340,7 +342,7 @@ class Client extends BaseClient { this.emit(Events.DEBUG, `Added Fingerprint: ${res.data.fingerprint}`); }) .catch(err => { - this.emit(Events.DEBUG, `Finding Cookie and Fingerprint failed: ${err.message}`); + this.emit(Events.DEBUG, `Update Cookie and Fingerprint failed: ${err.message}`); }); } diff --git a/src/client/websocket/handlers/INTERACTION_FAILURE.js b/src/client/websocket/handlers/INTERACTION_FAILURE.js index 516b93f..27540b2 100644 --- a/src/client/websocket/handlers/INTERACTION_FAILURE.js +++ b/src/client/websocket/handlers/INTERACTION_FAILURE.js @@ -12,4 +12,6 @@ module.exports = (client, { d: data }) => { status: false, metadata: data, }); + // Delete cache + client._interactionCache.delete(data.nonce); }; diff --git a/src/client/websocket/handlers/INTERACTION_SUCCESS.js b/src/client/websocket/handlers/INTERACTION_SUCCESS.js index c1595a7..4faf7f4 100644 --- a/src/client/websocket/handlers/INTERACTION_SUCCESS.js +++ b/src/client/websocket/handlers/INTERACTION_SUCCESS.js @@ -8,8 +8,21 @@ module.exports = (client, { d: data }) => { * @param {InteractionResponseBody} data data */ client.emit(Events.INTERACTION_SUCCESS, data); + // Get channel data + const cache = client._interactionCache.get(data.nonce); + const channel = cache.guildId + ? client.guilds.cache.get(cache.guildId)?.channels.cache.get(cache.channelId) + : client.channels.cache.get(cache.channelId); + // Set data + const interaction = { + ...cache, + ...data, + }; + const data_ = channel.interactions._add(interaction); client.emit('interactionResponse', { status: true, - metadata: data, + metadata: data_, }); + // Delete cache + // client._interactionCache.delete(data.nonce); }; diff --git a/src/managers/InteractionManager.js b/src/managers/InteractionManager.js new file mode 100644 index 00000000..98e11b9 --- /dev/null +++ b/src/managers/InteractionManager.js @@ -0,0 +1,39 @@ +'use strict'; + +const CachedManager = require('./CachedManager'); +const InteractionResponse = require('../structures/InteractionResponse'); + +/** + * Manages API methods for InteractionResponse and holds their cache. + * @extends {CachedManager} + */ +class InteractionManager extends CachedManager { + constructor(channel, iterable) { + super(channel.client, InteractionResponse, iterable); + + /** + * The channel that the messages belong to + * @type {TextBasedChannels} + */ + this.channel = channel; + } + + /** + * The cache of InteractionResponse + * @type {Collection} + * @name InteractionManager#cache + */ + + _add(data, cache) { + data = { + ...data, + channelId: this.channel.id, + guildId: this.channel.guild?.id, + }; + if (!data.id) return; + // eslint-disable-next-line consistent-return + return super._add(data, cache); + } +} + +module.exports = InteractionManager; diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js index c16bdf9..1257523 100644 --- a/src/structures/ApplicationCommand.js +++ b/src/structures/ApplicationCommand.js @@ -597,7 +597,7 @@ class ApplicationCommand extends Base { * @param {Message} message Discord Message * @param {Array} subCommandArray SubCommand Array * @param {Array} options The options to Slash Command - * @returns {Promise} + * @returns {Promise} */ // eslint-disable-next-line consistent-return async sendSlashCommand(message, subCommandArray = [], options = []) { @@ -861,6 +861,11 @@ class ApplicationCommand extends Base { body: data, files: attachmentsBuffer, }); + this.client._interactionCache.set(nonce, { + channelId: message.channelId, + guildId: message.guildId, + metadata: data, + }); return new Promise((resolve, reject) => { const handler = data => { timeout.refresh(); @@ -911,7 +916,7 @@ class ApplicationCommand extends Base { /** * Message Context Menu * @param {Message} message Discord Message - * @returns {Promise} + * @returns {Promise} */ async sendContextMenu(message) { if (!(message instanceof Message())) { @@ -941,6 +946,11 @@ class ApplicationCommand extends Base { await this.client.api.interactions.post({ body: data, }); + this.client._interactionCache.set(nonce, { + channelId: message.channelId, + guildId: message.guildId, + metadata: data, + }); return new Promise((resolve, reject) => { const handler = data => { timeout.refresh(); diff --git a/src/structures/BaseGuildTextChannel.js b/src/structures/BaseGuildTextChannel.js index 033d8cf..582e65b 100644 --- a/src/structures/BaseGuildTextChannel.js +++ b/src/structures/BaseGuildTextChannel.js @@ -3,6 +3,7 @@ const GuildChannel = require('./GuildChannel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const GuildTextThreadManager = require('../managers/GuildTextThreadManager'); +const InteractionManager = require('../managers/InteractionManager'); const MessageManager = require('../managers/MessageManager'); /** @@ -20,6 +21,12 @@ class BaseGuildTextChannel extends GuildChannel { */ this.messages = new MessageManager(this); + /** + * A manager of the interactions sent to this channel + * @type {InteractionManager} + */ + this.interactions = new InteractionManager(this); + /** * A manager of the threads belonging to this channel * @type {GuildTextThreadManager} diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 8081fe8..ca1444a 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -4,6 +4,7 @@ const { Collection } = require('@discordjs/collection'); const { joinVoiceChannel, entersState, VoiceConnectionStatus } = require('@discordjs/voice'); const { Channel } = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const InteractionManager = require('../managers/InteractionManager'); const MessageManager = require('../managers/MessageManager'); const { Status, Opcodes } = require('../util/Constants'); @@ -24,6 +25,12 @@ class DMChannel extends Channel { * @type {MessageManager} */ this.messages = new MessageManager(this); + + /** + * A manager of the interactions sent to this channel + * @type {InteractionManager} + */ + this.interactions = new InteractionManager(this); } _patch(data) { diff --git a/src/structures/InteractionResponse.js b/src/structures/InteractionResponse.js new file mode 100644 index 00000000..2a8ba11 --- /dev/null +++ b/src/structures/InteractionResponse.js @@ -0,0 +1,113 @@ +'use strict'; + +const { setTimeout } = require('node:timers'); +const Base = require('./Base'); +const { Events } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * Represents a interaction on Discord. + * @extends {Base} + */ +class InteractionResponse extends Base { + constructor(client, data) { + super(client); + /** + * The id of the channel the interaction was sent in + * @type {Snowflake} + */ + this.channelId = data.channelId; + + /** + * The id of the guild the interaction was sent in, if any + * @type {?Snowflake} + */ + this.guildId = data.guildId ?? this.channel?.guild?.id ?? null; + + /** + * The interaction data was sent in + * @type {Object} + */ + this.sendData = data.metadata; + this._patch(data); + } + + _patch(data) { + if ('id' in data) { + /** + * The interaction response's ID + * @type {Snowflake} + */ + this.id = data.id; + } + if ('nonce' in data) { + /** + * The interaction response's nonce + * @type {Snowflake} + */ + this.nonce = data.nonce; + } + } + /** + * The timestamp the interaction response was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.timestampFrom(this.id); + } + + /** + * The time the interaction response was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The channel that the interaction was sent in + * @type {TextBasedChannels} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * The guild the inteaaction was sent in (if in a guild channel) + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId) ?? this.channel?.guild ?? null; + } + + /** + * Get Modal send from interaction + * @param {?number} time Time to wait for modal (Default: 120000) + * @returns {Modal} + */ + awaitModal(time = 120_000) { + return new Promise((resolve, reject) => { + const handler = modal => { + timeout.refresh(); + if (modal.nonce != this.nonce || modal.id != this.id) return; + clearTimeout(timeout); + this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler); + this.client.decrementMaxListeners(); + resolve(modal); + }; + const timeout = setTimeout(() => { + this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler); + this.client.decrementMaxListeners(); + reject(new Error('MODAL_TIMEOUT')); + }, time || 120_000).unref(); + this.client.incrementMaxListeners(); + this.client.on(Events.INTERACTION_MODAL_CREATE, handler); + }); + } +} + +module.exports = InteractionResponse; diff --git a/src/structures/Message.js b/src/structures/Message.js index 125bdb7..e65535d 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -1043,7 +1043,7 @@ class Message extends Base { /** * Click specific button * @param {MessageButton|string} button Button ID - * @returns {Promise} + * @returns {Promise} */ clickButton(button) { let buttonID; @@ -1066,7 +1066,7 @@ class Message extends Base { * Select specific menu or First Menu * @param {string|Array} menuID Select Menu specific id or auto select first Menu * @param {Array} options Menu Options - * @returns {Promise} + * @returns {Promise} */ async selectMenu(menuID, options = []) { if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS'); @@ -1100,7 +1100,7 @@ class Message extends Base { * Send context Menu v2 * @param {Snowflake} botId Bot id * @param {string} commandName Command name in Context Menu - * @returns {Promise} + * @returns {Promise} */ async contextMenu(botId, commandName) { if (!botId) throw new Error('Bot ID is required'); diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js index 044bae3..7fa9ff5 100644 --- a/src/structures/MessageButton.js +++ b/src/structures/MessageButton.js @@ -168,27 +168,33 @@ class MessageButton extends BaseMessageComponent { /** * Click the button * @param {Message} message Discord Message - * @returns {Promise} + * @returns {Promise} */ async click(message) { const nonce = SnowflakeUtil.generate(); if (!(message instanceof Message())) throw new Error('[UNKNOWN_MESSAGE] Please pass a valid Message'); if (!this.customId || this.style == 5 || this.disabled) return false; // Button URL, Disabled - await message.client.api.interactions.post({ + const data = { + type: 3, // ? + nonce, + guild_id: message.guild?.id ?? null, // In DMs + channel_id: message.channel.id, + message_id: message.id, + application_id: message.applicationId ?? message.author.id, + session_id: message.client.session_id, + message_flags: message.flags.bitfield, data: { - type: 3, // ? - nonce, - guild_id: message.guild?.id ?? null, // In DMs - channel_id: message.channel.id, - message_id: message.id, - application_id: message.applicationId ?? message.author.id, - session_id: message.client.session_id, - message_flags: message.flags.bitfield, - data: { - component_type: 2, // Button - custom_id: this.customId, - }, + component_type: 2, // Button + custom_id: this.customId, }, + }; + await message.client.api.interactions.post({ + data, + }); + message.client._interactionCache.set(nonce, { + channelId: message.channelId, + guildId: message.guildId, + metadata: data, }); return new Promise((resolve, reject) => { const handler = data => { diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js index 084fb3d..08de0bb 100644 --- a/src/structures/MessageSelectMenu.js +++ b/src/structures/MessageSelectMenu.js @@ -215,7 +215,7 @@ class MessageSelectMenu extends BaseMessageComponent { * Mesage select menu * @param {Message} message The message this select menu is for * @param {Array} values The values of the select menu - * @returns {Promise} + * @returns {Promise} */ async select(message, values = []) { // Github copilot is the best :)) @@ -242,23 +242,29 @@ class MessageSelectMenu extends BaseMessageComponent { ); } const nonce = SnowflakeUtil.generate(); - await message.client.api.interactions.post({ + const data = { + type: 3, // ? + guild_id: message.guild?.id ?? null, // In DMs + channel_id: message.channel.id, + message_id: message.id, + application_id: message.applicationId ?? message.author.id, + session_id: message.client.session_id, + message_flags: message.flags.bitfield, data: { - type: 3, // ? - guild_id: message.guild?.id ?? null, // In DMs - channel_id: message.channel.id, - message_id: message.id, - application_id: message.applicationId ?? message.author.id, - session_id: message.client.session_id, - message_flags: message.flags.bitfield, - data: { - component_type: 3, // Select Menu - custom_id: this.customId, - type: 3, // Select Menu - values, - }, - nonce, + component_type: 3, // Select Menu + custom_id: this.customId, + type: 3, // Select Menu + values, }, + nonce, + }; + await message.client.api.interactions.post({ + data, + }); + message.client._interactionCache.set(nonce, { + channelId: message.channelId, + guildId: message.guildId, + metadata: data, }); return new Promise((resolve, reject) => { const handler = data => { diff --git a/src/structures/Modal.js b/src/structures/Modal.js index b58be82..3185ea9 100644 --- a/src/structures/Modal.js +++ b/src/structures/Modal.js @@ -59,13 +59,29 @@ class Modal { this.application = data.application ? { ...data.application, - bot: data.application.bot ? new User(client, data.application.bot) : null, + bot: data.application.bot ? new User(client, data.application.bot, data.application) : null, } : null; this.client = client; } + /** + * Get Interaction Response + * @type {?InteractionResponse} + * @readonly + */ + get sendFromInteraction() { + if (this.id && this.nonce && this.client) { + const cache = this.client._interactionCache.get(this.nonce); + const channel = cache.guildId + ? this.client.guilds.cache.get(cache.guildId)?.channels.cache.get(cache.channelId) + : this.client.channels.cache.get(cache.channelId); + return channel.interactions.cache.get(this.id); + } + return null; + } + /** * Adds components to the modal. * @param {...MessageActionRowResolvable[]} components The components to add @@ -135,24 +151,27 @@ class Modal { /** * @typedef {Object} ModalReplyData - * @property {GuildResolvable} [guild] Guild to send the modal to - * @property {TextChannelResolvable} [channel] User to send the modal to + * @property {?GuildResolvable} [guild] Guild to send the modal to + * @property {?TextChannelResolvable} [channel] User to send the modal to * @property {TextInputComponentReplyData[]} [data] Reply data */ /** * Reply to this modal with data. (Event only) * @param {ModalReplyData} data Data to send with the modal - * @returns {Promise} + * @returns {Promise} * @example - * // With Event * client.on('interactionModalCreate', modal => { - * modal.reply('guildId', 'channelId', { - * customId: 'code', - * value: '1+1' - * }, { - * customId: 'message', - * value: 'hello' + * modal.reply({ + * data: [ + * { + * customId: 'code', + * value: '1+1' + * }, { + * customId: 'message', + * value: 'hello' + * } + * ] * }) * }) */ @@ -160,8 +179,9 @@ class Modal { if (typeof data !== 'object') throw new TypeError('ModalReplyData must be an object'); if (!Array.isArray(data.data)) throw new TypeError('ModalReplyData.data must be an array'); if (!this.application) throw new Error('Modal cannot reply (Missing Application)'); - const guild = this.client.guilds.resolveId(data.guild); - const channel = this.client.channels.resolveId(data.channel); + const data_cache = this.sendFromInteraction; + const guild = this.client.guilds.resolveId(data.guild) || data_cache.guildId || null; + const channel = this.client.channels.resolveId(data.channel) || data_cache.channelId; // Add data to components // this.components = [ MessageActionRow.components = [ TextInputComponent ] ] // 5 MessageActionRow / Modal, 1 TextInputComponent / 1 MessageActionRow diff --git a/src/structures/ThreadChannel.js b/src/structures/ThreadChannel.js index 60b4553..28b74de 100644 --- a/src/structures/ThreadChannel.js +++ b/src/structures/ThreadChannel.js @@ -3,6 +3,7 @@ const { Channel } = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { RangeError } = require('../errors'); +const InteractionManager = require('../managers/InteractionManager'); const MessageManager = require('../managers/MessageManager'); const ThreadMemberManager = require('../managers/ThreadMemberManager'); const Permissions = require('../util/Permissions'); @@ -35,6 +36,12 @@ class ThreadChannel extends Channel { */ this.messages = new MessageManager(this); + /** + * A manager of the interactions sent to this channel + * @type {InteractionManager} + */ + this.interactions = new InteractionManager(this); + /** * A manager of the members that are part of this thread * @type {ThreadMemberManager} diff --git a/src/structures/User.js b/src/structures/User.js index 7e7e715..6c8943a 100644 --- a/src/structures/User.js +++ b/src/structures/User.js @@ -16,7 +16,7 @@ const UserFlags = require('../util/UserFlags'); * @extends {Base} */ class User extends Base { - constructor(client, data) { + constructor(client, data, application) { super(client); /** * The user's id @@ -78,7 +78,7 @@ class User extends Base { * @type {?ClientApplication} * @readonly */ - this.application = null; + this.application = application ? new ClientApplication(this.client, application, this) : null; this._partial = true; this._patch(data); } @@ -100,7 +100,7 @@ class User extends Base { * @type {?boolean} */ this.bot = Boolean(data.bot); - if (this.bot === true) { + if (this.bot === true && !this.application) { this.application = new ClientApplication(this.client, { id: this.id }, this); this.botInGuildsCount = null; } diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index c09fc81..6505cdd 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -3,6 +3,7 @@ const process = require('node:process'); const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const InteractionManager = require('../managers/InteractionManager'); const MessageManager = require('../managers/MessageManager'); const { VideoQualityModes } = require('../util/Constants'); const Permissions = require('../util/Permissions'); @@ -24,6 +25,12 @@ class VoiceChannel extends BaseGuildVoiceChannel { */ this.messages = new MessageManager(this); + /** + * A manager of the interactions sent to this channel + * @type {InteractionManager} + */ + this.interactions = new InteractionManager(this); + /** * If the guild considers this channel NSFW * @type {boolean} diff --git a/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js index 36f7257..aeef235 100644 --- a/src/structures/interfaces/TextBasedChannel.js +++ b/src/structures/interfaces/TextBasedChannel.js @@ -1,6 +1,7 @@ 'use strict'; /* eslint-disable import/order */ +const InteractionManager = require('../../managers/InteractionManager'); const MessageCollector = require('../MessageCollector'); const MessagePayload = require('../MessagePayload'); const SnowflakeUtil = require('../../util/SnowflakeUtil'); @@ -31,6 +32,12 @@ class TextBasedChannel { */ this.messages = new MessageManager(this); + /** + * A manager of the interactions sent to this channel + * @type {InteractionManager} + */ + this.interactions = new InteractionManager(this); + /** * The channel's last message id, if one was sent * @type {?Snowflake} @@ -405,7 +412,7 @@ class TextBasedChannel { * @param {UserResolvable} bot Bot user * @param {string} commandString Command name (and sub / group formats) * @param {...?string|string[]} args Command arguments - * @returns {Promise} + * @returns {Promise} * @example * // Send Slash to this channel * // Demo: diff --git a/typings/index.d.ts b/typings/index.d.ts index 5efe606..662560f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -509,8 +509,8 @@ export class ApplicationCommand extends Base { message: Message, subCommandArray?: string[], options?: string[], - ): Promise; - public static sendContextMenu(message: Message): Promise; + ): Promise; + public static sendContextMenu(message: Message): Promise; } export type ApplicationResolvable = Application | Activity | Snowflake; @@ -1934,10 +1934,10 @@ export class Message extends Base { // Added public markUnread(): Promise; public markRead(): Promise; - public clickButton(button: MessageButton | string): Promise; - public selectMenu(menuID: string, options: string[]): Promise; - public selectMenu(options: string[]): Promise; - public contextMenu(botID: Snowflake, commandName: string): Promise; + public clickButton(button: MessageButton | string): Promise; + public selectMenu(menuID: string, options: string[]): Promise; + public selectMenu(options: string[]): Promise; + public contextMenu(botID: Snowflake, commandName: string): Promise; } export class MessageActionRow< @@ -2001,7 +2001,7 @@ export class MessageButton extends BaseMessageComponent { public setStyle(style: MessageButtonStyleResolvable): this; public setURL(url: string): this; public toJSON(): APIButtonComponent; - public click(message: Message): Promise; + public click(message: Message): Promise; private static resolveStyle(style: MessageButtonStyleResolvable): MessageButtonStyle; } @@ -2241,7 +2241,7 @@ export class MessageSelectMenu extends BaseMessageComponent { ...options: MessageSelectOptionData[] | MessageSelectOptionData[][] ): this; public toJSON(): APISelectMenuComponent; - public select(message: Message, values: string[]): Promise; + public select(message: Message, values: string[]): Promise; } // Todo @@ -2253,6 +2253,7 @@ export class Modal { public application: object | null; public client: Client | null; public nonce: Snowflake | null; + public readonly sendFromInteraction: InteractionResponse | null; public addComponents( ...components: ( | MessageActionRow @@ -2276,12 +2277,12 @@ export class Modal { ): this; public setTitle(title: string): this; public toJSON(): RawModalSubmitInteractionData; - public reply(data: ModalReplyData): Promise; + public reply(data: ModalReplyData): Promise; } export interface ModalReplyData { - guild: GuildResolvable; - channel: TextChannelResolvable; + guild?: GuildResolvable; + channel?: TextChannelResolvable; data: TextInputComponentReplyData[]; } @@ -3901,6 +3902,26 @@ export class MessageManager extends CachedManager; } +export class InteractionManager extends CachedManager { + private constructor(channel: TextBasedChannel, iterable?: Iterable); + public channel: TextBasedChannel; + public cache: Collection; +} + +export class InteractionResponse extends Base { + private constructor(client: Client, data: Object); + public readonly channel: GuildTextBasedChannel | TextBasedChannel; + public channelId: Snowflake; + public readonly createdAt: Date; + public createdTimestamp: number; + public guildId: Snowflake | null; + public readonly guild: Snowflake | null; + public id: Snowflake; + public nonce: Snowflake; + public sendData: Object; + public awaitModal(time?: number): Modal; +} + export interface MessageSearchOptions { author: Snowflake[]; content: string; @@ -4069,6 +4090,7 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { lastPinTimestamp: number | null; readonly lastPinAt: Date | null; messages: MessageManager; + interactions: InteractionManager; awaitMessageComponent( options?: AwaitMessageCollectorOptionsParams, ): Promise; @@ -4086,7 +4108,7 @@ export interface TextBasedChannelFields extends PartialTextBasedChannelFields { setNSFW(nsfw?: boolean, reason?: string): Promise; fetchWebhooks(): Promise>; sendTyping(): Promise; - sendSlash(bot: UserResolvable, commandName: string, ...args: any): Promise; + sendSlash(bot: UserResolvable, commandName: string, ...args: any): Promise; } export function PartialWebhookMixin(Base?: Constructable): Constructable; @@ -6616,7 +6638,7 @@ export type AnyChannel = | VoiceChannel | ForumChannel; -export type TextBasedChannel = Exclude, ForumChannel>; +export type TextBasedChannel = Exclude, ForumChannel>; export type TextBasedChannelTypes = TextBasedChannel['type'];