diff --git a/src/client/actions/InteractionCreate.js b/src/client/actions/InteractionCreate.js deleted file mode 100644 index 158b11f..00000000 --- a/src/client/actions/InteractionCreate.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -const process = require('node:process'); -const Action = require('./Action'); -const AutocompleteInteraction = require('../../structures/AutocompleteInteraction'); -const ButtonInteraction = require('../../structures/ButtonInteraction'); -const CommandInteraction = require('../../structures/CommandInteraction'); -const MessageContextMenuInteraction = require('../../structures/MessageContextMenuInteraction'); -const ModalSubmitInteraction = require('../../structures/ModalSubmitInteraction'); -const { - ChannelSelectInteraction, - MentionableSelectInteraction, - RoleSelectInteraction, - SelectMenuInteraction, - UserSelectInteraction, -} = require('../../structures/SelectMenuInteraction'); -const UserContextMenuInteraction = require('../../structures/UserContextMenuInteraction'); -const { Events, InteractionTypes, MessageComponentTypes, ApplicationCommandTypes } = require('../../util/Constants'); - -let deprecationEmitted = false; - -class InteractionCreateAction extends Action { - handle(data) { - const client = this.client; - - // Resolve and cache partial channels for Interaction#channel getter - const channel = this.getChannel(data); - // Do not emit this for interactions that cache messages that are non-text-based. - let InteractionType; - switch (data.type) { - case InteractionTypes.APPLICATION_COMMAND: - switch (data.data.type) { - case ApplicationCommandTypes.CHAT_INPUT: - InteractionType = CommandInteraction; - break; - case ApplicationCommandTypes.USER: - InteractionType = UserContextMenuInteraction; - break; - case ApplicationCommandTypes.MESSAGE: - InteractionType = MessageContextMenuInteraction; - break; - default: - client.emit( - Events.DEBUG, - `[INTERACTION] Received application command interaction with unknown type: ${data.data.type}`, - ); - return; - } - break; - case InteractionTypes.MESSAGE_COMPONENT: - if (channel && !channel.isText()) return; - switch (data.data.component_type) { - case MessageComponentTypes.BUTTON: - InteractionType = ButtonInteraction; - break; - case MessageComponentTypes.STRING_SELECT: - InteractionType = SelectMenuInteraction; - break; - case MessageComponentTypes.CHANNEL_SELECT: - InteractionType = ChannelSelectInteraction; - break; - case MessageComponentTypes.MENTIONABLE_SELECT: - InteractionType = MentionableSelectInteraction; - break; - case MessageComponentTypes.ROLE_SELECT: - InteractionType = RoleSelectInteraction; - break; - case MessageComponentTypes.USER_SELECT: - InteractionType = UserSelectInteraction; - break; - default: - client.emit( - Events.DEBUG, - `[INTERACTION] Received component interaction with unknown type: ${data.data.component_type}`, - ); - return; - } - break; - case InteractionTypes.APPLICATION_COMMAND_AUTOCOMPLETE: - InteractionType = AutocompleteInteraction; - break; - case InteractionTypes.MODAL_SUBMIT: - InteractionType = ModalSubmitInteraction; - break; - default: - client.emit( - Events.DEBUG, - `[INTERACTION] Received [BOT] / Send (Selfbot) interactionID ${data.id} with unknown type: ${data.type}`, - ); - return; - } - - const interaction = new InteractionType(client, data); - - /** - * Emitted when an interaction is created. - * @event Client#interactionCreate - * @param {InteractionResponseBody | Interaction} interaction The interaction which was created. - */ - client.emit(Events.INTERACTION_CREATE, interaction); - - /** - * Emitted when an interaction is created. - * @event Client#interaction - * @param {Interaction} interaction The interaction which was created - * @deprecated Use {@link Client#event:interactionCreate} instead - */ - if (client.emit('interaction', interaction) && !deprecationEmitted) { - deprecationEmitted = true; - process.emitWarning('The interaction event is deprecated. Use interactionCreate instead', 'DeprecationWarning'); - } - } -} - -module.exports = InteractionCreateAction; diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js deleted file mode 100644 index 0007962..00000000 --- a/src/client/websocket/handlers/INTERACTION_CREATE.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -const { Events } = require('../../../util/Constants'); - -/** - * @typedef {Object} InteractionResponseBody - * @property {Snowflake} id Interaction ID - * @property {Snowflake} nonce nonce in POST /interactions - */ - -module.exports = (client, { d: data }) => { - if (client.user.bot) { - client.actions.InteractionCreate.handle(data); - } else { - client.emit(Events.INTERACTION_CREATE, data); - } -}; diff --git a/src/client/websocket/handlers/INTERACTION_FAILURE.js b/src/client/websocket/handlers/INTERACTION_FAILURE.js deleted file mode 100644 index ebd6701..00000000 --- a/src/client/websocket/handlers/INTERACTION_FAILURE.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -const { Events } = require('../../../util/Constants'); - -module.exports = (client, { d: data }) => { - /** - * Emitted whenever client user send interaction and error - * @event Client#interactionFailure - * @param {InteractionResponseBody} data data - */ - client.emit(Events.INTERACTION_FAILURE, data); - client.emit('interactionResponse', { - status: false, - metadata: client._interactionCache.get(data.nonce), - error: 'No response from bot', - }); - // Delete cache - client._interactionCache.delete(data.nonce); -}; diff --git a/src/client/websocket/handlers/INTERACTION_MODAL_CREATE.js b/src/client/websocket/handlers/INTERACTION_MODAL_CREATE.js index b94363e..584af85 100644 --- a/src/client/websocket/handlers/INTERACTION_MODAL_CREATE.js +++ b/src/client/websocket/handlers/INTERACTION_MODAL_CREATE.js @@ -1,6 +1,7 @@ 'use strict'; const Modal = require('../../../structures/Modal'); const { Events } = require('../../../util/Constants'); + module.exports = (client, { d: data }) => { /** * Emitted whenever client user receive interaction.showModal() diff --git a/src/client/websocket/handlers/INTERACTION_SUCCESS.js b/src/client/websocket/handlers/INTERACTION_SUCCESS.js deleted file mode 100644 index ff6fe44..00000000 --- a/src/client/websocket/handlers/INTERACTION_SUCCESS.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; -const { Events } = require('../../../util/Constants'); - -module.exports = (client, { d: data }) => { - /** - * Emitted whenever client user send interaction and success - * @event Client#interactionSuccess - * @param {InteractionResponseBody} data data - */ - client.emit(Events.INTERACTION_SUCCESS, data); - // Get channel data - const cache = client._interactionCache.get(data.nonce); - if (!cache) return; - 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_, - error: 'Success', - }); - // Delete cache - // client._interactionCache.delete(data.nonce); -}; diff --git a/src/client/websocket/handlers/RELATIONSHIP_ADD.js b/src/client/websocket/handlers/RELATIONSHIP_ADD.js index d2007de..422ba72 100644 --- a/src/client/websocket/handlers/RELATIONSHIP_ADD.js +++ b/src/client/websocket/handlers/RELATIONSHIP_ADD.js @@ -1,17 +1,19 @@ 'use strict'; -const { Events, RelationshipTypes } = require('../../../util/Constants'); +const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { if (data.user) { client.users._add(data.user); } client.relationships.cache.set(data.id, data.type); + client.relationships.friendNicknames.set(data.id, data.nickname); + client.relationships.sinceCache.set(data.id, new Date(data.since || 0)); /** - * Emitted whenever a relationship is updated. + * Emitted when a relationship is created, relevant to the current user. * @event Client#relationshipAdd - * @param {Snowflake} user The userID that was updated - * @param {RelationshipTypes} type The new relationship type + * @param {Snowflake} user Target userId + * @param {boolean} shouldNotify Whether the client should notify the user of this relationship's creation */ - client.emit(Events.RELATIONSHIP_ADD, data.id, RelationshipTypes[data.type]); + client.emit(Events.RELATIONSHIP_ADD, data.id, Boolean(data.should_notify)); }; diff --git a/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js b/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js index cb613c5..584128d 100644 --- a/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js +++ b/src/client/websocket/handlers/RELATIONSHIP_REMOVE.js @@ -1,15 +1,17 @@ 'use strict'; -const { Events, RelationshipTypes } = require('../../../util/Constants'); +const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { client.relationships.cache.delete(data.id); - client.user.friendNicknames.delete(data.id); + client.relationships.friendNicknames.delete(data.id); + client.relationships.sinceCache.delete(data.id); /** - * Emitted whenever a relationship is delete. + * Emitted when a relationship is removed, relevant to the current user. * @event Client#relationshipRemove * @param {Snowflake} user The userID that was updated * @param {RelationshipTypes} type The type of the old relationship + * @param {string | null} nickname The nickname of the user in this relationship (1-32 characters) */ - client.emit(Events.RELATIONSHIP_REMOVE, data.id, RelationshipTypes[data.type]); + client.emit(Events.RELATIONSHIP_REMOVE, data.id, data.type, data.nickname); }; diff --git a/src/client/websocket/handlers/RELATIONSHIP_UPDATE.js b/src/client/websocket/handlers/RELATIONSHIP_UPDATE.js index 6fa196d..fd8d84c 100644 --- a/src/client/websocket/handlers/RELATIONSHIP_UPDATE.js +++ b/src/client/websocket/handlers/RELATIONSHIP_UPDATE.js @@ -1,18 +1,41 @@ 'use strict'; -const { Events, RelationshipTypes } = require('../../../util/Constants'); +const { Events } = require('../../../util/Constants'); module.exports = (client, { d: data }) => { - client.relationships.cache.set(data.id, data.type); /** - * Emitted whenever a relationship is updated. + * @typedef {Object} RelationshipUpdateObject + * @property {RelationshipTypes} type The type of relationship + * @property {Date} since When the user requested a relationship + * @property {string | null} nickname The nickname of the user in this relationship (1-32 characters) + */ + /** + * Emitted when a relationship is updated, relevant to the current user (e.g. friend nickname changed). + * This is not sent when the type of a relationship changes; see {@link Client#relationshipAdd} and {@link Client#relationshipRemove} for that. * @event Client#relationshipUpdate * @param {Snowflake} user The userID that was updated - * @param {RelationshipTypes} type The new relationship type - * @param {Object} data The raw data + * @param {RelationshipUpdateObject} oldData Old data + * @param {RelationshipUpdateObject} newData New data */ - if ('nickname' in data) { - client.user.friendNicknames.set(data.id, data.nickname); - } - client.emit(Events.RELATIONSHIP_UPDATE, data.id, RelationshipTypes[data.type], data); + const oldType = client.relationships.cache.get(data.id); + const oldSince = client.relationships.sinceCache.get(data.id); + const oldNickname = client.relationships.friendNicknames.get(data.id); + // Update + if (data.type) client.relationships.cache.set(data.id, data.type); + if (data.nickname) client.relationships.friendNicknames.set(data.id, data.nickname); + if (data.since) client.relationships.sinceCache.set(data.id, new Date(data.since || 0)); + client.emit( + Events.RELATIONSHIP_UPDATE, + data.id, + { + type: oldType, + nickname: oldNickname, + since: oldSince, + }, + { + type: data.type, + nickname: data.nickname, + since: new Date(data.since || 0), + }, + ); }; diff --git a/src/managers/RelationshipManager.js b/src/managers/RelationshipManager.js index 5c33159..d57d195 100644 --- a/src/managers/RelationshipManager.js +++ b/src/managers/RelationshipManager.js @@ -1,7 +1,7 @@ 'use strict'; -const Buffer = require('node:buffer').Buffer; const { Collection } = require('@discordjs/collection'); +const BaseManager = require('./BaseManager'); const { GuildMember } = require('../structures/GuildMember'); const { Message } = require('../structures/Message'); const ThreadMember = require('../structures/ThreadMember'); @@ -11,19 +11,22 @@ const { RelationshipTypes } = require('../util/Constants'); /** * Manages API methods for Relationships and stores their cache. */ -class RelationshipManager { +class RelationshipManager extends BaseManager { constructor(client, users) { - /** - * The client that instantiated this manager. - * @type {Client} - */ - this.client = client; + super(client); /** * A collection of users this manager is caching. (Type: Number) * @type {Collection} - * @readonly */ this.cache = new Collection(); + /** + * @type {Collection} + */ + this.friendNicknames = new Collection(); + /** + * @type {Collection} + */ + this.sinceCache = new Collection(); this._setup(users); } @@ -35,7 +38,7 @@ class RelationshipManager { get friendCache() { const users = this.cache .filter(value => value === RelationshipTypes.FRIEND) - .map((value, key) => [key, this.client.users.cache.get(key)]); + .map((_, key) => [key, this.client.users.cache.get(key)]); return new Collection(users); } @@ -47,7 +50,7 @@ class RelationshipManager { get blockedCache() { const users = this.cache .filter(value => value === RelationshipTypes.BLOCKED) - .map((value, key) => [key, this.client.users.cache.get(key)]); + .map((_, key) => [key, this.client.users.cache.get(key)]); return new Collection(users); } @@ -59,7 +62,7 @@ class RelationshipManager { get incomingCache() { const users = this.cache .filter(value => value === RelationshipTypes.PENDING_INCOMING) - .map((value, key) => [key, this.client.users.cache.get(key)]); + .map((_, key) => [key, this.client.users.cache.get(key)]); return new Collection(users); } @@ -71,16 +74,29 @@ class RelationshipManager { get outgoingCache() { const users = this.cache .filter(value => value === RelationshipTypes.PENDING_OUTGOING) - .map((value, key) => [key, this.client.users.cache.get(key)]); + .map((_, key) => [key, this.client.users.cache.get(key)]); return new Collection(users); } /** - * Return array of cache - * @returns {Array<{id: Snowflake, type: RelationshipTypes}>} + * @typedef {Object} RelationshipJSONData + * @property {Snowflake} id The ID of the target user + * @property {RelationshipTypes} type The type of relationship + * @property {string | null} nickname The nickname of the user in this relationship (1-32 characters) + * @property {string} since When the user requested a relationship (ISO8601 timestamp) */ - toArray() { - return this.cache.map((value, key) => ({ id: key, type: RelationshipTypes[value] })); + + /** + * Return array of cache + * @returns {RelationshipJSONData[]} + */ + toJSON() { + return this.cache.map((value, key) => ({ + id: key, + type: RelationshipTypes[value], + nickname: this.friendNicknames.get(key), + since: this.sinceCache.get(key).toISOString(), + })); } /** @@ -91,8 +107,9 @@ class RelationshipManager { _setup(users) { if (!Array.isArray(users)) return; for (const relationShip of users) { - this.client.user.friendNicknames.set(relationShip.id, relationShip.nickname); + this.friendNicknames.set(relationShip.id, relationShip.nickname); this.cache.set(relationShip.id, relationShip.type); + this.sinceCache.set(relationShip.id, new Date(relationShip.since || 0)); } } @@ -133,66 +150,55 @@ class RelationshipManager { } /** - * Deletes a friend relationship with a client user. + * Deletes a friend / blocked relationship with a client user or cancels a friend request. * @param {UserResolvable} user Target * @returns {Promise} */ - deleteFriend(user) { + async deleteRelationship(user) { const id = this.resolveId(user); - // Check if already friends - if (this.cache.get(id) !== RelationshipTypes.FRIEND) return false; - return this.__cancel(id); + if ( + ![RelationshipTypes.FRIEND, RelationshipTypes.BLOCKED, RelationshipTypes.PENDING_OUTGOING].includes( + this.cache.get(id), + ) + ) { + return Promise.resolve(false); + } + await this.client.api.users['@me'].relationships[id].delete({ + DiscordContext: { location: 'Friends' }, + }); + return true; } /** - * Deletes a blocked relationship with a client user. - * @param {UserResolvable} user Target - * @returns {Promise} + * @typedef {Object} FriendRequestOptions + * @property {UserResolvable} [user] Target + * @property {string} [username] Discord username + * @property {number | null} [discriminator] Discord discriminator */ - deleteBlocked(user) { - const id = this.resolveId(user); - // Check if already blocked - if (this.cache.get(id) !== RelationshipTypes.BLOCKED) return false; - return this.__cancel(id); - } /** * Sends a friend request. - * @param {string} username Username of the user to send the request to - * @param {?number} discriminator Discriminator of the user to send the request to + * @param {FriendRequestOptions} options Target * @returns {Promise} */ - async sendFriendRequest(username, discriminator) { - await this.client.api.users('@me').relationships.post({ - data: { - username, - discriminator: discriminator == 0 ? null : parseInt(discriminator), - }, - headers: { - 'X-Context-Properties': Buffer.from(JSON.stringify({ location: 'Add Friend' }), 'utf8').toString('base64'), - }, - }); - return true; - } - - /** - * Cancels a friend request. - * @param {UserResolvable} user the user you want to delete - * @returns {Promise} - */ - cancelFriendRequest(user) { - const id = this.resolveId(user); - if (this.cache.get(id) !== RelationshipTypes.PENDING_OUTGOING) return false; - return this.__cancel(id); - } - - async __cancel(id) { - await this.client.api.users['@me'].relationships[id].delete({ - headers: { - 'X-Context-Properties': Buffer.from(JSON.stringify({ location: 'Friends' }), 'utf8').toString('base64'), - }, - }); - return true; + async sendFriendRequest(options) { + if (options?.user) { + const id = this.resolveId(options.user); + await this.client.api.users['@me'].relationships[id].put({ + data: {}, + DiscordContext: { location: 'ContextMenu' }, + }); + return true; + } else { + await this.client.api.users['@me'].relationships.post({ + data: { + username: options.username, + discriminator: options.discriminator, + }, + DiscordContext: { location: 'Add Friend' }, + }); + return true; + } } /** @@ -203,16 +209,14 @@ class RelationshipManager { async addFriend(user) { const id = this.resolveId(user); // Check if already friends - if (this.cache.get(id) === RelationshipTypes.FRIEND) return false; + if (this.cache.get(id) === RelationshipTypes.FRIEND) return Promise.resolve(false); // Check if outgoing request - if (this.cache.get(id) === RelationshipTypes.PENDING_OUTGOING) return false; + if (this.cache.get(id) === RelationshipTypes.PENDING_OUTGOING) return Promise.resolve(false); await this.client.api.users['@me'].relationships[id].put({ data: { type: RelationshipTypes.FRIEND, }, - headers: { - 'X-Context-Properties': Buffer.from(JSON.stringify({ location: 'Friends' }), 'utf8').toString('base64'), - }, + DiscordContext: { location: 'Friends' }, }); return true; } @@ -223,14 +227,19 @@ class RelationshipManager { * @param {?string} nickname New nickname * @returns {Promise} */ - async setNickname(user, nickname) { + async setNickname(user, nickname = null) { const id = this.resolveId(user); - if (this.cache.get(id) !== RelationshipTypes.FRIEND) return false; + if (this.cache.get(id) !== RelationshipTypes.FRIEND) return Promise.resolve(false); await this.client.api.users['@me'].relationships[id].patch({ data: { nickname: typeof nickname === 'string' ? nickname : null, }, }); + if (nickname) { + this.friendNicknames.set(id, nickname); + } else { + this.friendNicknames.delete(id); + } return true; } @@ -242,14 +251,12 @@ class RelationshipManager { async addBlocked(user) { const id = this.resolveId(user); // Check - if (this.cache.get(id) === RelationshipTypes.BLOCKED) return false; + if (this.cache.get(id) === RelationshipTypes.BLOCKED) return Promise.resolve(false); await this.client.api.users['@me'].relationships[id].put({ data: { type: RelationshipTypes.BLOCKED, }, - headers: { - 'X-Context-Properties': Buffer.from(JSON.stringify({ location: 'ContextMenu' }), 'utf8').toString('base64'), - }, + DiscordContext: { location: 'ContextMenu' }, }); return true; } diff --git a/src/structures/InteractionResponse.js b/src/structures/InteractionResponse.js deleted file mode 100644 index e86f786..00000000 --- a/src/structures/InteractionResponse.js +++ /dev/null @@ -1,114 +0,0 @@ -'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 - * @returns {Modal} - */ - awaitModal(time) { - if (!time || typeof time !== 'number' || time < 0) throw new Error('INVALID_TIME'); - 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).unref(); - this.client.incrementMaxListeners(); - this.client.on(Events.INTERACTION_MODAL_CREATE, handler); - }); - } -} - -module.exports = InteractionResponse; diff --git a/src/structures/Modal.js b/src/structures/Modal.js index 6e4aad1..697195e 100644 --- a/src/structures/Modal.js +++ b/src/structures/Modal.js @@ -2,24 +2,15 @@ const { setTimeout } = require('node:timers'); const BaseMessageComponent = require('./BaseMessageComponent'); -const User = require('./User'); +const { InteractionTypes, Events } = require('../util/Constants'); const SnowflakeUtil = require('../util/SnowflakeUtil'); -const Util = require('../util/Util'); /** * Represents a modal (form) to be shown in response to an interaction */ class Modal { /** - * @typedef {Object} ModalOptions - * @property {string} [customId] A unique string to be sent in the interaction when clicked - * @property {string} [title] The title to be displayed on this modal - * @property {Array<(MessageActionRow|MessageActionRowOptions)>} [components] - * Action rows containing interactive components for the modal (text input components) - */ - - /** - * @param {Modal|ModalOptions} data Modal to clone or raw data + * @param {Object} data Modal to clone or raw data * @param {Client} client The client constructing this Modal, if provided */ constructor(data = {}, client = null) { @@ -33,106 +24,69 @@ class Modal { * A unique string to be sent in the interaction when submitted * @type {?string} */ - this.customId = data.custom_id ?? data.customId ?? null; + this.customId = data.custom_id; /** * The title to be displayed on this modal * @type {?string} */ - this.title = data.title ?? null; + this.title = data.title; /** * Timestamp (Discord epoch) of when this modal was created - * @type {?Snowflake} + * @type {Snowflake} */ - this.nonce = data.nonce ?? null; + this.nonce = data.nonce; /** * ID slash / button / menu when modal is displayed - * @type {?Snowflake} + * @type {Snowflake} */ - this.id = data.id ?? null; + this.id = data.id; /** * Application sending the modal - * @type {?Object} + * @type {Snowflake} */ - this.application = data.application - ? { - ...data.application, - bot: data.application.bot ? new User(client, data.application.bot, data.application) : null, - } - : null; + this.applicationId = data.application.id; - this.client = client; + /** + * The id of the channel the message was sent in + * @type {Snowflake} + */ + this.channelId = data.channel_id; + + Object.defineProperty(this, 'client', { + value: client, + writable: false, + }); } /** - * Get Interaction Response - * @type {?InteractionResponse} + * The id of the guild the message was sent in, if any + * @type {?Snowflake} * @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; + get guildId() { + return this.client.channels.cache.get(this.channelId)?.guildId || null; } /** - * Adds components to the modal. - * @param {...MessageActionRowResolvable[]} components The components to add - * @returns {Modal} + * The channel that the message was sent in + * @type {TextBasedChannels} + * @readonly */ - addComponents(...components) { - this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); - return this; + get channel() { + return this.client.channels.resolve(this.channelId); } /** - * Sets the components of the modal. - * @param {...MessageActionRowResolvable[]} components The components to set - * @returns {Modal} + * The guild the message was sent in (if in a guild channel) + * @type {?Guild} + * @readonly */ - setComponents(...components) { - this.spliceComponents(0, this.components.length, components); - return this; - } - - /** - * Sets the custom id for this modal - * @param {string} customId A unique string to be sent in the interaction when submitted - * @returns {Modal} - */ - setCustomId(customId) { - this.customId = Util.verifyString(customId, RangeError, 'MODAL_CUSTOM_ID'); - return this; - } - - /** - * Removes, replaces, and inserts components in the modal. - * @param {number} index The index to start at - * @param {number} deleteCount The number of components to remove - * @param {...MessageActionRowResolvable[]} [components] The replacing components - * @returns {Modal} - */ - spliceComponents(index, deleteCount, ...components) { - this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c))); - return this; - } - - /** - * Sets the title of this modal - * @param {string} title The title to be displayed on this modal - * @returns {Modal} - */ - setTitle(title) { - this.title = Util.verifyString(title, RangeError, 'MODAL_TITLE'); - return this; + get guild() { + return this.client.guilds.resolve(this.guildId) ?? this.channel?.guild ?? null; } toJSON() { @@ -144,136 +98,77 @@ class Modal { }; } - /** - * @typedef {Object} TextInputComponentReplyData - * @property {string} [customId] TextInputComponent custom id - * @property {string} [value] TextInputComponent value - */ - - /** - * @typedef {Object} ModalReplyData - * @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 * client.on('interactionModalCreate', modal => { - * // 1. - * modal.reply({ - * data: [ - * { - * customId: 'code', - * value: '1+1' - * }, { - * customId: 'message', - * value: 'hello' - * } - * ], - * channel: 'id', // optional - * guild: 'id', // optional - * }) - * // or 2. - * modal.components[0].components[0].setValue('1+1'); - * modal.components[1].components[0].setValue('hello'); - * modal.reply(); + * // Modal > ActionRow > TextInput + * modal.components[0].components[0].setValue('1+1'); + * modal.components[1].components[0].setValue('hello'); + * modal.reply(); * }) */ - async reply(data = {}) { - if (!this.application) throw new Error('Modal cannot reply (Missing Application)'); - 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; - if (!channel) throw new Error('Modal cannot reply (Missing data)'); - // Add data to components - // this.components = [ MessageActionRow.components = [ TextInputComponent ] ] - // 5 MessageActionRow / Modal, 1 TextInputComponent / 1 MessageActionRow - if (Array.isArray(data?.data) && data?.data?.length > 0) { - for (let i = 0; i < this.components.length; i++) { - const value = data.data.find(d => d.customId == this.components[i].components[0].customId); - if (this.components[i].components[0].required == true && !value) { - throw new Error( - 'MODAL_REQUIRED_FIELD_MISSING\n' + - `Required fieldId ${this.components[i].components[0].customId} missing value`, - ); - } - if (value) { - if (value?.value?.includes('\n') && this.components[i].components[0].style == 'SHORT') { - throw new Error( - 'MODAL_REPLY_DATA_INVALID\n' + - `value must be a single line, got multiple lines [Custom ID: ${value.customId}]`, - ); - } - this.components[i].components[0].setValue(value.value); - } - } - } + reply() { + if (!this.applicationId || !this.client || !this.channelId) throw new Error('Modal cannot reply'); // Get Object const dataFinal = this.toJSON(); dataFinal.components = dataFinal.components .map(c => { - delete c.components[0].max_length; - delete c.components[0].min_length; - delete c.components[0].required; - delete c.components[0].placeholder; - delete c.components[0].label; - delete c.components[0].style; + c.components[0] = { + type: c.components[0].type, + value: c.components[0].value, + custom_id: c.components[0].custom_id, + }; return c; }) .filter(c => c.components[0].value && c.components[0].value !== ''); delete dataFinal.title; const nonce = SnowflakeUtil.generate(); const postData = { - type: 5, // Modal - application_id: this.application.id, - guild_id: guild || null, - channel_id: channel, + type: InteractionTypes.MODAL_SUBMIT, // Modal + application_id: this.applicationId, + guild_id: this.guildId, + channel_id: this.channelId, data: dataFinal, nonce, - session_id: this.client.sessionId, + session_id: this.client.ws.shards.first()?.sessionId, }; - await this.client.api.interactions.post({ + this.client.api.interactions.post({ data: postData, }); - this.client._interactionCache.set(nonce, { - channelId: channel, - guildId: guild, - metadata: postData, - }); return new Promise((resolve, reject) => { + const timeoutMs = 15_000; + // Waiting for MsgCreate / ModalCreate const handler = data => { - timeout.refresh(); - if (data.metadata?.nonce !== nonce) return; + if (data.nonce !== nonce) return; clearTimeout(timeout); - this.client.removeListener('interactionResponse', handler); + this.client.removeListener(Events.MESSAGE_CREATE, handler); + this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler); this.client.decrementMaxListeners(); - if (data.status) { - resolve(data.metadata); - } else { - reject( - new Error('INTERACTION_ERROR', { - cause: data, - }), - ); - } + resolve(data); }; const timeout = setTimeout(() => { - this.client.removeListener('interactionResponse', handler); + this.client.removeListener(Events.MESSAGE_CREATE, handler); + this.client.removeListener(Events.INTERACTION_MODAL_CREATE, handler); this.client.decrementMaxListeners(); - reject( - new Error('INTERACTION_TIMEOUT', { - cause: postData, - }), - ); - }, this.client.options.interactionTimeout).unref(); + reject(new Error('INTERACTION_FAILED')); + }, timeoutMs).unref(); this.client.incrementMaxListeners(); - this.client.on('interactionResponse', handler); + this.client.on(Events.MESSAGE_CREATE, handler); + this.client.on(Events.INTERACTION_MODAL_CREATE, handler); }); } + + // TypeScript + /** + * Check data + * @type {boolean} + * @readonly + */ + get isMessage() { + return false; + } } module.exports = Modal;