update
This commit is contained in:
@@ -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;
|
@@ -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<InteractionResponse>}
|
||||
* @returns {Promise<Message|Modal>}
|
||||
* @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;
|
||||
|
Reference in New Issue
Block a user