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;