structures_Interaction.js
// Require the classes for the interaction
const Member = require("./Member.js");
const User = require("./User.js");
const Guild = require("./Guild.js");
const InteractionOptions = require("./InteractionOptions.js");
const Message = require("./Message.js");
const Application = require("../application/base.js");
// Require all needed types
const InteractionResponseFlags = require("./InteractionResponseFlags");
const InteractionType = require("./InteractionType");
const InteractionResponseType = require("./InteractionResponseType");
const ModalComponents = require("./ModalComponents");
// Require the package version
const version = require("../../package.json").version;
// Rest Handler
const Rest = require("./Rest");
/**
* Create a formatted Interaction Object
*
* @example
* const Interaction = new Interaction(request, client, response);
*
*/
class Interaction {
constructor(req, c, res) {
/**
* the client that is bound to the interaction
* @type {Application}
*/
this.client = c;
/**
* interaction data payload
* @type {object}
*/
this.data = req.body.data;
/**
* interaction command name
* @type {string|null}
*/
this.commandName = req?.body?.data?.name ?? null;
// Only using options when it's needed.
if (req.body.type === InteractionType.APPLICATION_COMMAND || req.body.type === InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE) {
/**
* Return the options of the interaction
* @type {InteractionOptions}
* @return {InteractionOptions}
* @example
* const subCommandOption = interaction.options.getSubCommand(); // returns the subcommand option
* const subCommandGroupOption = interaction.options.getSubCommandGroup(); // returns the subcommand group option
* const stringOption = interaction.options.getString("optionName"); // returns the string option
* const integerOption = interaction.options.getInteger("optionName"); // returns the integer option
* const booleanOption = interaction.options.getBoolean("optionName"); // returns the boolean option
* const userOption = interaction.options.getUser("optionName"); // returns the user option
* const memberOption = interaction.options.getMember("optionName"); // returns the member option
* const channelOption = interaction.options.getChannel("optionName"); // returns the channel option
* const roleOption = interaction.options.getRole("optionName"); // returns the role option
* const numberOption = interaction.options.getNumber("optionName"); // returns the number option
* const mentionableOption = interaction.options.getMentionable("optionName"); // returns the mentionable option
*/
this.options = new InteractionOptions(req?.body?.data?.options ?? null);
}
// Only using components when it's needed.
if (req.body.type === InteractionType.MODAL_SUBMIT) {
/**
* Return the components data of the interaction (for modals)
* @type {Interaction}
* @return {ModalComponents}
* @example
* const fieldTest = interaction.components.getDataById("fieldTest"); // Returns the object of field "fieldTest"!
* const fieldTestValue = interaction.components.getDataById("fieldTest").value; // Returns the value of field "fieldTest"!
* const fieldTestValueTwo = interaction.components.getValueById("fieldTest"); // Returns also the value of field "fieldTest"!
*/
this.components = new ModalComponents(req?.body?.data?.components ?? []);
}
/**
* select menu values if select menu interaction
* @type {array}
*/
this.values = req?.body?.data?.values ?? [];
/**
* For monetized apps, any entitlements for the invoking user, representing access to premium SKUs
* @type {array}
*/
this.entitlements = req?.body?.entitlements ?? [];
/**
* interaction custom id
* @type {string|null}
*/
this.customId = req?.body?.data?.custom_id ?? null;
/**
* continuation token for responding to the interaction
* @type {string}
*/
this.token = req.body.token;
/**
* ID of the application this interaction is for
* @type {string}
*/
this.applicationId = req.body.application_id;
/**
* ID of the interaction
* @type {string}
*/
this.id = req.body.id;
/**
* type of interaction
* @type {number}
*/
this.type = req.body.type;
/**
* the guild data of the interaction
* @type {object}
*/
this.guild = new Guild(req.body);
/**
* channel that the interaction was sent from
* @type {number}
*/
this.channelId = req?.body?.channel_id ?? null;
/**
* the member data of the interaction
* @type {Member}
*/
this.member = new Member(req?.body?.member ?? null);
/**
* bitwise set of permissions the app or bot has within the channel the interaction was sent from
* @type {number}
*/
this.appPermissions = req?.body?.app_permissions ?? null;
/**
* the user data of the interaction
* @type {User}
*/
this.user = new User(req?.body?.user ?? req?.body?.member?.user ?? null);
/**
* selected language of the invoking user
* @type {number}
*/
this.locale = req?.body?.locale ?? null;
/**
* the message data of the interaction
* @type {Message}
*/
this.message = new Message(req?.body?.message ?? null);
/**
* private res property
* @private
*/
this._res = res;
/**
* Add data to the cache
*/
if (this.isInGuild()) {
if (this.client.cacheMembers) {
this.client._cache.setMember(this.guild.id, this.member);
}
if (this.client.cacheGuilds) {
this.client._cache.setGuild(this.guild.id, this.guild);
}
}
if (this.client.cacheUsers) {
this.client._cache.setUser(this.user.id, this.user);
}
}
/**
* Get all entitlements for the current application
* @returns {array}
*/
getEntitlements() {
return this.entitlements.filter((entitlement) => entitlement.application_id === this.client.applicationId);
}
/**
* Check if the guild has a premium subscription
* @returns {boolean}
*/
guildHavePremium() {
return this.getEntitlements().filter((entitlement) => entitlement.guild_id === this.guild.id) >= 0;
}
/**
* Check if the user has a premium subscription
* @returns {boolean}
*/
userHavePremium() {
return this.getEntitlements().filter((entitlement) => entitlement.user_id === this.user.id) >= 0;
}
/**
* Reply to an Interaction
* @param options The message payload (embeds, components, content, files, ephemeral)
* @param {string} options.content The content of the message
* @param {array} options.embeds The embeds of the message
* @param {array} options.components The components of the message
* @param {array} options.files The files of the message
* @param {boolean} options.ephemeral If the message should be ephemeral
* @example
* interaction.reply({ content: "Hello World" });
*/
reply(options) {
if (
!options.hasOwnProperty('content') &&
!options.hasOwnProperty('embeds') &&
!options.hasOwnProperty('components') &&
!options.hasOwnProperty('files')
) throw new Error("[Interactions.js => <Interaction>.reply] You need to provide a MessagePayload (Content or Embeds or Components or files)");
this.client.emit('debug', "[DEBUG] Sending a reply to " + this.id);
let payload = {};
if (options.hasOwnProperty('content')) {
payload.content = options.content;
}
if (options.hasOwnProperty('embeds')) {
payload.embeds = options.embeds;
}
if (options.hasOwnProperty('components')) {
payload.components = options.components;
}
if (options.hasOwnProperty('files')) {
payload.files = options.files;
}
if (options.hasOwnProperty('ephemeral')) {
payload.flags = InteractionResponseFlags.EPHEMERAL;
}
this._res.header('User-Agent', `DiscordBot (https://github.com/fb-sean/interactions.js, v${version})`);
return this._res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: payload,
});
}
/**
* Reply to an Interaction with a premium message
* @example
* interaction.replyPremium();
*/
replyPremium() {
this.client.emit('debug', "[DEBUG] Sending a premium reply to " + this.id);
this._res.header('User-Agent', `DiscordBot (https://github.com/fb-sean/interactions.js, v${version})`);
return this._res.send({
type: 10,
data: {},
});
}
/**
* For components, ACK an interaction and edit the original message later; the user does not see a loading state
* @example
* interaction.deferUpdate();
*/
deferUpdate() {
this.client.emit('debug', "[DEBUG] Sending a defer update to " + this.id);
this._res.header('User-Agent', `DiscordBot (https://github.com/fb-sean/interactions.js, v${version})`);
return this._res.send({
type: 6,
data: {},
});
}
/**
* ACK an interaction and edit a response later, the user sees a loading state
* @param {boolean} ephemeral if the message should be ephemeral
* @example
* interaction.deferReply(true); // true or false to make it ephemeral
*/
deferReply(ephemeral = false) {
this.client.emit('debug', "[DEBUG] Sending a defer to " + this.id);
this._res.header('User-Agent', `DiscordBot (https://github.com/fb-sean/interactions.js, v${version})`);
return this._res.send({
type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
data: {
flags: ephemeral ? InteractionResponseFlags.EPHEMERAL : null,
},
});
}
/**
* Edit the Reply
* @param options The message payload (embeds, components, content, files, ephemeral)
* @param {string} options.content The content of the message
* @param {array} options.embeds The embeds of the message
* @param {array} options.components The components of the message
* @param {array} options.files The files of the message
* @example
* const response = await interaction.editReply({ content: "Hello World" });
* console.log(response);
*/
editReply(options = {}) {
if (
!options.hasOwnProperty('content') &&
!options.hasOwnProperty('embeds') &&
!options.hasOwnProperty('components') &&
!options.hasOwnProperty('files')
) throw new Error("[Interactions.js => <Interaction>.editReply] You need to provide a MessagePayload (Content or Embeds or Components or files)");
this.client.emit('debug', "[DEBUG] Sending a edit");
let payload = {};
if (options.hasOwnProperty('content')) {
payload.content = options.content;
}
if (options.hasOwnProperty('embeds')) {
payload.embeds = options.embeds;
}
if (options.hasOwnProperty('components')) {
payload.components = options.components;
}
if (options.hasOwnProperty('files')) {
payload.files = options.files;
}
const rest = Rest.getRest();
return rest.patch(
`/webhooks/${this.client.applicationId}/${this.token}/messages/@original?wait=true`,
{
body: payload,
files: payload.files ?? undefined,
}
);
}
/**
* Send a simple follow-up message
* @param options The message payload (embeds, components, content, files, ephemeral)
* @param {string} options.content The content of the message
* @param {array} options.embeds The embeds of the message
* @param {array} options.components The components of the message
* @param {array} options.files The files of the message
* @param {boolean} options.ephemeral If the message should be ephemeral
* @example
* const response = await interaction.followUp({ content: "Hello World" });
* console.log(response);
*/
followUp(options) {
if (
!options.hasOwnProperty('content') &&
!options.hasOwnProperty('embeds') &&
!options.hasOwnProperty('components') &&
!options.hasOwnProperty('files')
) throw new Error("[Interactions.js => <Interaction>.followUp] You need to provide a MessagePayload (Content or Embeds or Components or files)");
this.client.emit('debug', "[DEBUG] Sending a follow up");
let payload = {};
if (options.hasOwnProperty('content')) {
payload.content = options.content;
}
if (options.hasOwnProperty('embeds')) {
payload.embeds = options.embeds;
}
if (options.hasOwnProperty('components')) {
payload.components = options.components;
}
if (options.hasOwnProperty('files')) {
payload.files = options.files;
}
if (options.hasOwnProperty('ephemeral')) {
payload.flags = InteractionResponseFlags.EPHEMERAL;
}
const rest = Rest.getRest();
return rest.post(
`/webhooks/${this.client.applicationId}/${this.token}?wait=true`,
{
body: payload,
files: payload.files ?? undefined,
}
);
}
/**
* Update an Interaction
* @param options The message payload (embeds, components, content, files)
* @example
* interaction.update({ content: "Hello World" });
*/
update({embeds = [], components = [], content = null, files = []}) {
if (embeds?.length <= 0 && components?.length <= 0 && files?.length <= 0 && !content) throw new Error("[Interactions.js => <Interaction>.update] You need to provide a MessagePayload (Content or Embeds or Components or files)");
this.client.emit('debug', "[DEBUG] Sending a interaction update to " + this.id);
this._res.header('User-Agent', `DiscordBot (https://github.com/fb-sean/interactions.js, v${version})`);
return this._res.send({
type: InteractionResponseType.UPDATE_MESSAGE,
data: {
content: content,
embeds: embeds,
components: components,
files: files,
},
});
}
/**
* Response to an interaction with a modal
* @param {object | modal} modal
* @example
* const { Modal, TextInput, TextInputStyles } = require('interactions.js');
*
* const modal = new Modal()
* .setCustomId("test")
* .setTitle("Test Modal")
* .addComponent(
* new ActionRow()
* .addComponent(
* new TextInput()
* .setCustomId("test")
* .setPlaceholder("test")
* .setStyle(TextInputStyles.Short)
* .setLabel("test")
* )
* );
*
* return interaction.showModal(modal);
*/
showModal(modal) {
if (this.isModal()) {
throw new Error("[Interactions.js => <Interaction>.showModal] You can't reply with a modal to a modal");
}
if (!modal) {
throw new Error("[Interactions.js => <Interaction>.showModal] You need to provide a modal");
}
this.client.emit('debug', "[DEBUG] Sending a modal to " + this.id);
const data = modal?.data ? modal.toJSON() : modal;
if (!data.custom_id) {
throw new Error("[Interactions.js => <Interaction>.showModal] You need to provide a customId");
}
if (!data.title) {
throw new Error("[Interactions.js => <Interaction>.showModal] You need to provide a title");
}
if (data.components > 5) {
throw new Error("[Interactions.js => <Interaction>.showModal] You can't provide more than 5 components");
}
if (data.components?.length < 1) {
throw new Error("[Interactions.js => <Interaction>.showModal] You need to provide at least 1 component");
}
if (data.components?.length > 0) {
for (const component of data.components) {
if (component.components?.length > 5) {
throw new Error("[Interactions.js => <Interaction>.showModal] You can't provide more than 5 components in a row");
}
};
}
this._res.header('User-Agent', `DiscordBot (https://github.com/fb-sean/interactions.js, v${version})`);
return this._res.send({
type: InteractionResponseType.MODAL,
data,
});
}
/**
* Response to an autocomplete interaction
* @param {object[]} choices the choices including (name, name_localizations?, value)
*/
sendAutoComplete(choices = []) {
for (const choice of choices) {
if (!choice.name) {
throw new Error("[Interactions.js => <Interaction>.sendAutoComplete] You need to provide a name for each choice");
}
if (!choice.value) {
throw new Error("[Interactions.js => <Interaction>.sendAutoComplete] You need to provide a value for each choice");
}
if (choice.name.length > 100) {
throw new Error("[Interactions.js => <Interaction>.sendAutoComplete] You can't provide a name longer than 100 characters");
}
if (choice.value.length > 100) {
throw new Error("[Interactions.js => <Interaction>.sendAutoComplete] You can't provide a value longer than 100 characters");
}
}
this._res.header('User-Agent', `DiscordBot (https://github.com/fb-sean/interactions.js, v${version})`);
return this._res.send({
type: InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
data: {
choices: choices,
},
});
}
/**
* Check if the interaction is a modal submit
* @type {boolean}
* @return {boolean}
* @readonly
*/
isModal() {
return this.type === InteractionType.MODAL_SUBMIT;
}
/**
* Check if the interaction is a message component
* @type {boolean}
* @return {boolean}
* @readonly
*/
isComponent() {
return this.type === InteractionType.MESSAGE_COMPONENT;
}
/**
* Check if the interaction is an auto complete
* @type {boolean}
* @return {boolean}
* @readonly
*/
isAutoComplete() {
return this.type === InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE;
}
/**
* Check if the interaction is an application command
* @type {boolean}
* @return {boolean}
* @readonly
*/
isCommand() {
return this.type === InteractionType.APPLICATION_COMMAND;
}
/**
* Check if the interaction is in a guild
* @return {boolean}
*/
isInGuild() {
return this.guild !== null;
}
}
module.exports = Interaction;