diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js
index cdedc8aa45c0..aad3b506f36b 100644
--- a/packages/discord.js/src/index.js
+++ b/packages/discord.js/src/index.js
@@ -142,6 +142,9 @@ exports.GuildScheduledEvent = require('./structures/GuildScheduledEvent').GuildS
exports.GuildTemplate = require('./structures/GuildTemplate');
exports.Integration = require('./structures/Integration');
exports.IntegrationApplication = require('./structures/IntegrationApplication');
+exports.InteractionCallback = require('./structures/InteractionCallback');
+exports.InteractionCallbackResource = require('./structures/InteractionCallbackResource');
+exports.InteractionCallbackResponse = require('./structures/InteractionCallbackResponse');
exports.BaseInteraction = require('./structures/BaseInteraction');
exports.InteractionCollector = require('./structures/InteractionCollector');
exports.InteractionResponse = require('./structures/InteractionResponse');
diff --git a/packages/discord.js/src/structures/InteractionCallback.js b/packages/discord.js/src/structures/InteractionCallback.js
new file mode 100644
index 000000000000..e2d2cf9b3c5b
--- /dev/null
+++ b/packages/discord.js/src/structures/InteractionCallback.js
@@ -0,0 +1,92 @@
+'use strict';
+
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+
+/**
+ * Represents an interaction callback response from Discord
+ */
+class InteractionCallback {
+ constructor(client, data) {
+ /**
+ * The client that instantiated this.
+ * @name InteractionCallback#client
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * The id of the original interaction response
+ * @type {Snowflake}
+ */
+ this.id = data.id;
+
+ /**
+ * The type of the original interaction
+ * @type {InteractionType}
+ */
+ this.type = data.type;
+
+ /**
+ * The instance id of the Activity if one was launched or joined
+ * @type {?string}
+ */
+ this.activityInstanceId = data.activity_instance_id ?? null;
+
+ /**
+ * The id of the message that was created by the interaction
+ * @type {?Snowflake}
+ */
+ this.responseMessageId = data.response_message_id ?? null;
+
+ /**
+ * Whether the message is in a loading state
+ * @type {?boolean}
+ */
+ this.responseMessageLoading = data.response_message_loading ?? null;
+
+ /**
+ * Whether the response message was ephemeral
+ * @type {?boolean}
+ */
+ this.responseMessageEphemeral = data.response_message_ephemeral ?? null;
+ }
+
+ /**
+ * The timestamp the original interaction was created at
+ * @type {number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return DiscordSnowflake.timestampFrom(this.id);
+ }
+
+ /**
+ * The time the original interaction was created at
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * The channel the original interaction was sent in
+ * @type {?TextBasedChannels}
+ * @readonly
+ */
+ get channel() {
+ return this.client.channels.cache.get(this.channelId) ?? null;
+ }
+
+ /**
+ * The guild the original interaction was sent in
+ * @type {?Guild}
+ * @readonly
+ */
+ get guild() {
+ return this.client.guilds.cache.get(this.guildId) ?? null;
+ }
+}
+
+module.exports = InteractionCallback;
diff --git a/packages/discord.js/src/structures/InteractionCallbackResource.js b/packages/discord.js/src/structures/InteractionCallbackResource.js
new file mode 100644
index 000000000000..ffb088d8100e
--- /dev/null
+++ b/packages/discord.js/src/structures/InteractionCallbackResource.js
@@ -0,0 +1,52 @@
+'use strict';
+
+const { lazy } = require('@discordjs/util');
+
+const getMessage = lazy(() => require('./Message').Message);
+
+/**
+ * Represents the resource that was created by the interaction response.
+ */
+class InteractionCallbackResource {
+ constructor(client, data) {
+ /**
+ * The client that instantiated this
+ * @name InteractionCallbackResource#client
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * The interaction callback type
+ * @type {InteractionResponseType}
+ */
+ this.type = data.type;
+
+ /**
+ * The Activity launched by an interaction
+ * @typedef {Object} ActivityInstance
+ * @property {string} id The instance id of the Activity
+ */
+
+ /**
+ * Represents the Activity launched by this interaction
+ * @type {?ActivityInstance}
+ */
+ this.activityInstance = data.activity_instance ?? null;
+
+ if ('message' in data) {
+ /**
+ * The message created by the interaction
+ * @type {?Message}
+ */
+ this.message =
+ this.client.channels.cache.get(data.message.channel_id)?.messages._add(data.message) ??
+ new (getMessage())(client, data.message);
+ } else {
+ this.message = null;
+ }
+ }
+}
+
+module.exports = InteractionCallbackResource;
diff --git a/packages/discord.js/src/structures/InteractionCallbackResponse.js b/packages/discord.js/src/structures/InteractionCallbackResponse.js
new file mode 100644
index 000000000000..c114648398ae
--- /dev/null
+++ b/packages/discord.js/src/structures/InteractionCallbackResponse.js
@@ -0,0 +1,33 @@
+'use strict';
+
+const InteractionCallback = require('./InteractionCallback');
+const InteractionCallbackResource = require('./InteractionCallbackResource');
+
+/**
+ * Represents an interaction's response
+ */
+class InteractionCallbackResponse {
+ constructor(client, data) {
+ /**
+ * The client that instantiated this
+ * @name InteractionCallbackResponse#client
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * The interaction object associated with the interaction callback response
+ * @type {InteractionCallback}
+ */
+ this.interaction = new InteractionCallback(client, data.interaction);
+
+ /**
+ * The resource that was created by the interaction response
+ * @type {?InteractionCallbackResource}
+ */
+ this.resource = data.resource ? new InteractionCallbackResource(client, data.resource) : null;
+ }
+}
+
+module.exports = InteractionCallbackResponse;
diff --git a/packages/discord.js/src/structures/interfaces/InteractionResponses.js b/packages/discord.js/src/structures/interfaces/InteractionResponses.js
index e448e19fd17e..47167097ac0e 100644
--- a/packages/discord.js/src/structures/interfaces/InteractionResponses.js
+++ b/packages/discord.js/src/structures/interfaces/InteractionResponses.js
@@ -1,8 +1,10 @@
'use strict';
+const { makeURLSearchParams } = require('@discordjs/rest');
const { isJSONEncodable } = require('@discordjs/util');
const { InteractionResponseType, MessageFlags, Routes, InteractionType } = require('discord-api-types/v10');
const { DiscordjsError, ErrorCodes } = require('../../errors');
+const InteractionCallbackResponse = require('../InteractionCallbackResponse');
const InteractionCollector = require('../InteractionCollector');
const InteractionResponse = require('../InteractionResponse');
const MessagePayload = require('../MessagePayload');
@@ -19,9 +21,15 @@ const MessagePayload = require('../MessagePayload');
* @interface
*/
class InteractionResponses {
+ /**
+ * Shared options for responses to a {@link BaseInteraction}.
+ * @typedef {Object} SharedInteractionResponseOptions
+ * @property {boolean} [withResponse] Whether to include an {@link InteractionCallbackResponse} as the response
+ */
+
/**
* Options for deferring the reply to an {@link BaseInteraction}.
- * @typedef {Object} InteractionDeferReplyOptions
+ * @typedef {SharedInteractionResponseOptions} InteractionDeferReplyOptions
* @property {MessageFlagsResolvable} [flags] Flags for the reply.
* Only `MessageFlags.Ephemeral` can be set.
* @property {boolean} [fetchReply] Whether to fetch the reply
@@ -29,13 +37,13 @@ class InteractionResponses {
/**
* Options for deferring and updating the reply to a {@link MessageComponentInteraction}.
- * @typedef {Object} InteractionDeferUpdateOptions
+ * @typedef {SharedInteractionResponseOptions} InteractionDeferUpdateOptions
* @property {boolean} [fetchReply] Whether to fetch the reply
*/
/**
* Options for a reply to a {@link BaseInteraction}.
- * @typedef {BaseMessageOptionsWithPoll} InteractionReplyOptions
+ * @typedef {BaseMessageOptionsWithPoll|SharedInteractionResponseOptions} InteractionReplyOptions
* @property {boolean} [tts=false] Whether the message should be spoken aloud
* @property {boolean} [fetchReply] Whether to fetch the reply
* @property {MessageFlagsResolvable} [flags] Which flags to set for the message.
@@ -45,14 +53,19 @@ class InteractionResponses {
/**
* Options for updating the message received from a {@link MessageComponentInteraction}.
- * @typedef {MessageEditOptions} InteractionUpdateOptions
+ * @typedef {MessageEditOptions|SharedInteractionResponseOptions} InteractionUpdateOptions
* @property {boolean} [fetchReply] Whether to fetch the reply
*/
+ /**
+ * Options for showing a modal in response to a {@link BaseInteraction}
+ * @typedef {SharedInteractionResponseOptions} ShowModalOptions
+ */
+
/**
* Defers the reply to this interaction.
* @param {InteractionDeferReplyOptions} [options] Options for deferring the reply to this interaction
- * @returns {Promise}
+ * @returns {Promise}
* @example
* // Defer the reply to this interaction
* interaction.deferReply()
@@ -67,7 +80,7 @@ class InteractionResponses {
async deferReply(options = {}) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
- await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
+ const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
@@ -75,10 +88,16 @@ class InteractionResponses {
},
},
auth: false,
+ query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.deferred = true;
this.ephemeral = Boolean(options.flags & MessageFlags.Ephemeral);
+
+ if (options.withResponse) {
+ return new InteractionCallbackResponse(this.client, response);
+ }
+
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this);
}
@@ -86,7 +105,7 @@ class InteractionResponses {
* Creates a reply to this interaction.
* Use the `fetchReply` option to get the bot's reply message.
* @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply
- * @returns {Promise}
+ * @returns {Promise}
* @example
* // Reply to the interaction and fetch the response
* interaction.reply({ content: 'Pong!', fetchReply: true })
@@ -109,17 +128,23 @@ class InteractionResponses {
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
- await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
+ const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.ChannelMessageWithSource,
data,
},
files,
auth: false,
+ query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.ephemeral = Boolean(options.flags & MessageFlags.Ephemeral);
this.replied = true;
+
+ if (options.withResponse) {
+ return new InteractionCallbackResponse(this.client, response);
+ }
+
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this);
}
@@ -192,7 +217,7 @@ class InteractionResponses {
/**
* Defers an update to the message to which the component was attached.
* @param {InteractionDeferUpdateOptions} [options] Options for deferring the update to this interaction
- * @returns {Promise}
+ * @returns {Promise}
* @example
* // Defer updating and reset the component's loading state
* interaction.deferUpdate()
@@ -201,21 +226,26 @@ class InteractionResponses {
*/
async deferUpdate(options = {}) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
- await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
+ const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.DeferredMessageUpdate,
},
auth: false,
+ query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.deferred = true;
+ if (options.withResponse) {
+ return new InteractionCallbackResponse(this.client, response);
+ }
+
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message?.interaction?.id);
}
/**
* Updates the original message of the component on which the interaction was received on.
* @param {string|MessagePayload|InteractionUpdateOptions} options The options for the updated message
- * @returns {Promise}
+ * @returns {Promise}
* @example
* // Remove the components from the message
* interaction.update({
@@ -234,34 +264,43 @@ class InteractionResponses {
const { body: data, files } = await messagePayload.resolveBody().resolveFiles();
- await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
+ const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.UpdateMessage,
data,
},
files,
auth: false,
+ query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.replied = true;
+ if (options.withResponse) {
+ return new InteractionCallbackResponse(this.client, response);
+ }
+
return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message.interaction?.id);
}
/**
* Shows a modal component
* @param {ModalBuilder|ModalComponentData|APIModalInteractionResponseCallbackData} modal The modal to show
- * @returns {Promise}
+ * @param {ShowModalOptions} options The options for sending this interaction response
+ * @returns {Promise}
*/
- async showModal(modal) {
+ async showModal(modal, options = {}) {
if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied);
- await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
+ const response = await this.client.rest.post(Routes.interactionCallback(this.id, this.token), {
body: {
type: InteractionResponseType.Modal,
data: isJSONEncodable(modal) ? modal.toJSON() : this.client.options.jsonTransformer(modal),
},
auth: false,
+ query: makeURLSearchParams({ with_response: options.withResponse ?? false }),
});
this.replied = true;
+
+ return options.withResponse ? new InteractionCallbackResponse(this.client, response) : undefined;
}
/**
diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts
index afb73b20a91b..936859c48053 100644
--- a/packages/discord.js/typings/index.d.ts
+++ b/packages/discord.js/typings/index.d.ts
@@ -166,6 +166,10 @@ import {
GuildScheduledEventRecurrenceRuleFrequency,
GatewaySendPayload,
GatewayDispatchPayload,
+ RESTPostAPIInteractionCallbackWithResponseResult,
+ RESTAPIInteractionCallbackObject,
+ RESTAPIInteractionCallbackResourceObject,
+ InteractionResponseType,
} from 'discord-api-types/v10';
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
@@ -258,6 +262,10 @@ export class Activity {
public toString(): string;
}
+export interface ActivityInstance {
+ id: string;
+}
+
export type ActivityFlagsString = keyof typeof ActivityFlags;
export interface BaseComponentData {
@@ -558,6 +566,9 @@ export abstract class CommandInteraction e
public inGuild(): this is CommandInteraction<'raw' | 'cached'>;
public inCachedGuild(): this is CommandInteraction<'cached'>;
public inRawGuild(): this is CommandInteraction<'raw'>;
+ public deferReply(
+ options: InteractionDeferReplyOptions & { withResponse: true },
+ ): Promise;
public deferReply(
options: InteractionDeferReplyOptions & { fetchReply: true },
): Promise>>;
@@ -1973,6 +1984,37 @@ export class BaseInteraction extends Base
public isRepliable(): this is RepliableInteraction;
}
+export class InteractionCallback {
+ public constructor(client: Client, data: RESTAPIInteractionCallbackObject);
+ public activityInstanceId: string | null;
+ public readonly client: Client;
+ public get channel(): TextBasedChannel | null;
+ public channelId: Snowflake | null;
+ public get createdAt(): Date;
+ public get createdTimestamp(): number;
+ public get guild(): Guild | null;
+ public guildId: Snowflake | null;
+ public id: Snowflake;
+ public responseMessageEphemeral: boolean | null;
+ public responseMessageId: Snowflake | null;
+ public responseMessageLoading: boolean | null;
+ public type: InteractionType;
+}
+
+export class InteractionCallbackResponse {
+ private constructor(client: Client, data: RESTPostAPIInteractionCallbackWithResponseResult);
+ public readonly client: Client;
+ public interaction: InteractionCallback;
+ public resource: InteractionCallbackResource | null;
+}
+
+export class InteractionCallbackResource {
+ private constructor(client: Client, data: RESTAPIInteractionCallbackResourceObject);
+ public activityInstance: ActivityInstance | null;
+ public message: Message | null;
+ public type: InteractionResponseType;
+}
+
export class InteractionCollector extends Collector<
Snowflake,
Interaction,
@@ -6073,7 +6115,7 @@ export interface InteractionCollectorOptions<
interactionResponse?: InteractionResponse>;
}
-export interface InteractionDeferReplyOptions {
+export interface InteractionDeferReplyOptions extends SharedInteractionResponseOptions {
flags?: BitFieldResolvable<
Extract,
MessageFlags.Ephemeral | MessageFlags.SuppressEmbeds | MessageFlags.SuppressNotifications
@@ -6085,7 +6127,7 @@ export interface InteractionDeferUpdateOptions {
fetchReply?: boolean;
}
-export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll {
+export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll, SharedInteractionResponseOptions {
tts?: boolean;
fetchReply?: boolean;
flags?: BitFieldResolvable<
@@ -6094,7 +6136,7 @@ export interface InteractionReplyOptions extends BaseMessageOptionsWithPoll {
>;
}
-export interface InteractionUpdateOptions extends MessageEditOptions {
+export interface InteractionUpdateOptions extends MessageEditOptions, SharedInteractionResponseOptions {
fetchReply?: boolean;
}
@@ -6606,6 +6648,13 @@ export interface ShardingManagerOptions {
execArgv?: readonly string[];
}
+export interface SharedInteractionResponseOptions {
+ withResponse?: boolean;
+}
+
+// tslint:disable-next-line no-empty-interface
+export interface ShowModalOptions extends SharedInteractionResponseOptions {}
+
export { Snowflake };
export type StageInstanceResolvable = StageInstance | Snowflake;