Honie
Honie
DIAdiscord.js - Imagine an app
Created by Honie on 11/25/2023 in #djs-questions
Collector buttons work but just once
Hello! I'm working on a leaderboard system that lets you swap through pages by clicking buttons as a menu of sorts. It seems to work fine enough the first time you click a button (with the slight annoyance that the three loading dots disappear way too early). By the second time something goes off, and the bot crashes with a DiscordAPIError[10062]: Unknown interaction error. Anyone got an idea of why that might be?
async createLeaderboardCollector(rankingData, interaction, channel, client, karmaData, page, pageLength) {

const filter = (i) => i.user.id === interaction.user.id;
const time = 1000 * 60 * 5;

const collector = channel.createMessageComponentCollector({ filter, time });

collector.on('collect', async (newInt) => {

if (!newInt) { return; }
await newInt.deferUpdate(); // Crashes on this line!

if (newInt.customId !== 'prev_leader' && newInt.customId !== 'next_leader') return;

if (newInt.customId === 'prev_leader' && page > 0) {
page -= 1;
} else if (newInt.customId === 'next_leader' && page < pageLength) {
page += 1;
}

await this.createLeaderboardEmbed(rankingData, newInt, channel, client, karmaData, page);
});
}

async createLeaderboardEmbed(rankingData, interaction, channel, client, karmaData, page) {

// Irrelevant stuff ommited
// Embed creation, database querying, etc

const row = await this.createButtonRow(page, pageLength);

await interaction.editReply({ embeds: [embed], components: [row]});
await this.createLeaderboardCollector(rankingData, interaction, channel, client, karmaData, page, pageLength);
}
async createLeaderboardCollector(rankingData, interaction, channel, client, karmaData, page, pageLength) {

const filter = (i) => i.user.id === interaction.user.id;
const time = 1000 * 60 * 5;

const collector = channel.createMessageComponentCollector({ filter, time });

collector.on('collect', async (newInt) => {

if (!newInt) { return; }
await newInt.deferUpdate(); // Crashes on this line!

if (newInt.customId !== 'prev_leader' && newInt.customId !== 'next_leader') return;

if (newInt.customId === 'prev_leader' && page > 0) {
page -= 1;
} else if (newInt.customId === 'next_leader' && page < pageLength) {
page += 1;
}

await this.createLeaderboardEmbed(rankingData, newInt, channel, client, karmaData, page);
});
}

async createLeaderboardEmbed(rankingData, interaction, channel, client, karmaData, page) {

// Irrelevant stuff ommited
// Embed creation, database querying, etc

const row = await this.createButtonRow(page, pageLength);

await interaction.editReply({ embeds: [embed], components: [row]});
await this.createLeaderboardCollector(rankingData, interaction, channel, client, karmaData, page, pageLength);
}
16 replies
DIAdiscord.js - Imagine an app
Created by Honie on 7/28/2023 in #djs-questions
Creating a modal after a button click shows an "Unknown Interaction" error
I've created a system which dynamically creates a sequence of embeds, components and select fields for a Setup command. My hope is that I can also add into it an option for creating a Modal, so when I need a text input from the user I can show it in a more elegant way than just "type a message in chat". Whenever I send a modal using this system (clicking a button can generate a Modal element), the modal shows up on Discord, but the bot crashes with an DiscordAPIError[10062]: Unknown interaction error. Researching, I came across this:
Showing a modal must be the first response to an interaction. You cannot defer() or deferUpdate() then show a modal later.
Does this mean that this kind of action is not possible, and that modals are exclusive to being used immediately after a Slash Command is executed, or is there a way to implement this? Error code:
/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640
throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
^

DiscordAPIError[10062]: Unknown interaction
at handleErrors (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640:13)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async BurstHandler.runRequest (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:736:23)
at async REST.request (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:1387:22)
at async ButtonInteraction.deferUpdate (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:200:5) {
requestBody: { files: undefined, json: { type: 6 } },
rawError: { message: 'Unknown interaction', code: 10062 },
code: 10062,
status: 404,
method: 'POST',
url: 'https://discord.com/api/v10/interactions/---------/-------------------/callback'
}
/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640
throw new DiscordAPIError(data, "code" in data ? data.code : data.error, status, method, url, requestData);
^

DiscordAPIError[10062]: Unknown interaction
at handleErrors (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:640:13)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async BurstHandler.runRequest (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:736:23)
at async REST.request (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/@discordjs/rest/dist/index.js:1387:22)
at async ButtonInteraction.deferUpdate (/mnt/c/Users/Honie/Documents/Retool JS/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js:200:5) {
requestBody: { files: undefined, json: { type: 6 } },
rawError: { message: 'Unknown interaction', code: 10062 },
code: 10062,
status: 404,
method: 'POST',
url: 'https://discord.com/api/v10/interactions/---------/-------------------/callback'
}
Code:
/*
createCollector is used when receiving a button interaction.
createModal is a response to the new interaction.
*/
async createCollector (currentSetup, msgInt, channel, member, client) {
// Create a collector
if (!currentSetup.components) return;

const collector = msgInt.channel?.createMessageComponentCollector({
max: 1,
time: 1000 * 60,
})

// Add a listener to the collector
collector?.on('collect', async (i) => {
// Remove previous info
i.deferUpdate();
msgInt.editReply({ components: [] });
collector.stop();

// Buttons
for (let component of currentSetup.components) {
if (component.id === i.customId) {
// Modals
if (component.modal) {
const modal = await this.createModal(component.modal, i);
return;
}
}
}
})

async createModal(modal, interaction) {
// Generate modal
const modalBuilder = new ModalBuilder()
.setCustomId(modal.id)
.setTitle(modal.title);

// Generate inputs
for (let input of modal.inputs) {
let modalElement = new TextInputBuilder()
.setCustomId(input.id)
.setLabel(input.label);

modal.longForm ?
modalElement.setStyle(TextInputStyle.Paragraph) :
modalElement.setStyle(TextInputStyle.Short);

if (input.placeholder) modalElement.setPlaceholder(input.placeholder);
if (input.required) modalElement.setRequired(true);

const actionRow = new ActionRowBuilder().addComponents(modalElement);
modalBuilder.addComponents(actionRow);
}

// Send modal
await interaction.showModal(modalBuilder);
}
/*
createCollector is used when receiving a button interaction.
createModal is a response to the new interaction.
*/
async createCollector (currentSetup, msgInt, channel, member, client) {
// Create a collector
if (!currentSetup.components) return;

const collector = msgInt.channel?.createMessageComponentCollector({
max: 1,
time: 1000 * 60,
})

// Add a listener to the collector
collector?.on('collect', async (i) => {
// Remove previous info
i.deferUpdate();
msgInt.editReply({ components: [] });
collector.stop();

// Buttons
for (let component of currentSetup.components) {
if (component.id === i.customId) {
// Modals
if (component.modal) {
const modal = await this.createModal(component.modal, i);
return;
}
}
}
})

async createModal(modal, interaction) {
// Generate modal
const modalBuilder = new ModalBuilder()
.setCustomId(modal.id)
.setTitle(modal.title);

// Generate inputs
for (let input of modal.inputs) {
let modalElement = new TextInputBuilder()
.setCustomId(input.id)
.setLabel(input.label);

modal.longForm ?
modalElement.setStyle(TextInputStyle.Paragraph) :
modalElement.setStyle(TextInputStyle.Short);

if (input.placeholder) modalElement.setPlaceholder(input.placeholder);
if (input.required) modalElement.setRequired(true);

const actionRow = new ActionRowBuilder().addComponents(modalElement);
modalBuilder.addComponents(actionRow);
}

// Send modal
await interaction.showModal(modalBuilder);
}
11 replies
DIAdiscord.js - Imagine an app
Created by Honie on 8/21/2022 in #djs-questions
Sending an error to the channel the error was caught on
I have a simple error handling script that checks for uncaughtExceptions, and if one pops up sends an error log both to the bot's console and to a dedicated error channel for internal use as an embed. If possible, I'd also like to send this embed to the user - for example, if a Slash Command fails, generate an error message and send it to the same channel the Slash Command was sent on. Is it possible to somehow get the sent interaction (and with it, the channel) that caused the error? I know I could encase my commands in a try-catch loop, but if possible, I'd rather not do that for every command I have.
export default (client: Client, instance: WOKCommands) => {
function generateGuruMeditation (errorType: string) {
// Just a string I can ask for to reference the error back,
// without asking the end user to take a screenshot.
return errorType + "-" + Math.floor(Date.now() / 1000);
}

function sendErrorToConsole (reason: any, parameter: any, guruMeditation: string) {
console.log("❗" + reason + " (" + parameter + ")\n🧘: " + guruMeditation);
}

function sendErrorToChannel (reason: any, parameter: any, guruMeditation: string) {
// NOTE: This is an internal bot error channel.
// What I'd need is to also send this embed to the channel that made the error happen, if possible.
if (!process.env.ERROR_CHANNEL) return;
const channel = client.channels.cache.get(process.env.ERROR_CHANNEL);
if (!channel) return;

(channel as TextChannel).send({embeds: [createErrorEmbed(reason, parameter, guruMeditation)]});
}

function createErrorEmbed(reason: any, parameter: any, guruMeditation: string) {
return new MessageEmbed()
.setColor("RED")
.setTitle("⚠️ Error")
.setDescription("Looks like something went wrong!\nFeel free to try again - and if that doesn't help, ask in our [support server](" + process.env.SUPPORT_SERVER + ").\n\n" + reason + "\n\n" + parameter)
.setTimestamp()
.setFooter({text: ":person_in_lotus_position:: " + guruMeditation});
}

process.on("uncaughtException", (reason, parameter) => {
const guru = generateGuruMeditation("UNCEXC");
sendErrorToConsole(reason, parameter, guru);
sendErrorToChannel(reason, parameter, guru);
});

}
export default (client: Client, instance: WOKCommands) => {
function generateGuruMeditation (errorType: string) {
// Just a string I can ask for to reference the error back,
// without asking the end user to take a screenshot.
return errorType + "-" + Math.floor(Date.now() / 1000);
}

function sendErrorToConsole (reason: any, parameter: any, guruMeditation: string) {
console.log("❗" + reason + " (" + parameter + ")\n🧘: " + guruMeditation);
}

function sendErrorToChannel (reason: any, parameter: any, guruMeditation: string) {
// NOTE: This is an internal bot error channel.
// What I'd need is to also send this embed to the channel that made the error happen, if possible.
if (!process.env.ERROR_CHANNEL) return;
const channel = client.channels.cache.get(process.env.ERROR_CHANNEL);
if (!channel) return;

(channel as TextChannel).send({embeds: [createErrorEmbed(reason, parameter, guruMeditation)]});
}

function createErrorEmbed(reason: any, parameter: any, guruMeditation: string) {
return new MessageEmbed()
.setColor("RED")
.setTitle("⚠️ Error")
.setDescription("Looks like something went wrong!\nFeel free to try again - and if that doesn't help, ask in our [support server](" + process.env.SUPPORT_SERVER + ").\n\n" + reason + "\n\n" + parameter)
.setTimestamp()
.setFooter({text: ":person_in_lotus_position:: " + guruMeditation});
}

process.on("uncaughtException", (reason, parameter) => {
const guru = generateGuruMeditation("UNCEXC");
sendErrorToConsole(reason, parameter, guru);
sendErrorToChannel(reason, parameter, guru);
});

}
6 replies
DIAdiscord.js - Imagine an app
Created by Honie on 8/17/2022 in #djs-questions
Creating a MessageComponentCollector in a function separate to the base command
To clean up a bit of code, I'm trying to send a couple collectors to a function in a different class. As is, my code creates an embed with a set of buttons, then attempts to run the channel.createMessageComponentCollector function. This works as intended in the base command, but doesn't work at all on the function I am delegating this to. The stacktrace:
classes/setup.ts:40:29 - error TS2339: Property 'createMessageComponentCollector' does not exist on type 'Channel'.

40 const collector = channel.createMessageComponentCollector({
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

at createTSError (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:859:12)
at reportTSError (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:863:19)
at getOutput (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1077:36)
at Object.compile (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1433:41)
at Module.m._compile (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1617:30)
at Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
at Object.require.extensions.<computed> [as .ts] (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1621:12)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Module.require (node:internal/modules/cjs/loader:1005:19) {
diagnosticCodes: [ 2339 ]
}
[nodemon] app crashed - waiting for file changes before starting...
classes/setup.ts:40:29 - error TS2339: Property 'createMessageComponentCollector' does not exist on type 'Channel'.

40 const collector = channel.createMessageComponentCollector({
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

at createTSError (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:859:12)
at reportTSError (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:863:19)
at getOutput (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1077:36)
at Object.compile (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1433:41)
at Module.m._compile (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1617:30)
at Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
at Object.require.extensions.<computed> [as .ts] (/mnt/c/Users/Erik/Documents/Retool/node_modules/ts-node/src/index.ts:1621:12)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Module.require (node:internal/modules/cjs/loader:1005:19) {
diagnosticCodes: [ 2339 ]
}
[nodemon] app crashed - waiting for file changes before starting...
Command code (Note, using WOKCommands for this part, though it should be irrelevant to this problem.):
export default {
category: 'Configuration',
description: 'Creates all necessary roles, emoji, and channels for the bot to function.',

slash: 'both',
testOnly: true, // This only works for test servers!

callback: async ({ interaction: msgInt, channel }) => {
Setup.createEmbed('setupType', msgInt, channel);
},
} as ICommand
export default {
category: 'Configuration',
description: 'Creates all necessary roles, emoji, and channels for the bot to function.',

slash: 'both',
testOnly: true, // This only works for test servers!

callback: async ({ interaction: msgInt, channel }) => {
Setup.createEmbed('setupType', msgInt, channel);
},
} as ICommand
Function code:
export default class Setup {

static async createEmbed (id: string, msgInt: CommandInteraction, channel: Channel) {
if (!setupSteps) return;

// Find the setup assigned with the sent ID
const currentSetup = setupSteps.find(x => x.id === id);
if (!currentSetup) return;

// Setup default colour
currentSetup.embed.color = 0xff00a2;

// Create action components
let actionComponents = []
for (let component of currentSetup.components) {
actionComponents.push(new MessageButton()
.setCustomId(component.id)
.setLabel(component.label)
.setStyle(component.style as MessageButtonStyleResolvable)
.setDisabled(component.disabled)
);
}

// TO-DO: Support multiple rows
const row = new MessageActionRow().addComponents(actionComponents)

// Send the embed
let sentReply = msgInt.reply({
embeds: [currentSetup.embed],
components: [row]
})

// Create a collector
const collector = channel.createMessageComponentCollector({
max: 1,
time: 1000 * 15,
})

// TO-DO: Add a listener to the collector
/*
collector.on('collect', (i: ButtonInteraction) => {
// blah blah
})
*/
return sentReply;
}
}
export default class Setup {

static async createEmbed (id: string, msgInt: CommandInteraction, channel: Channel) {
if (!setupSteps) return;

// Find the setup assigned with the sent ID
const currentSetup = setupSteps.find(x => x.id === id);
if (!currentSetup) return;

// Setup default colour
currentSetup.embed.color = 0xff00a2;

// Create action components
let actionComponents = []
for (let component of currentSetup.components) {
actionComponents.push(new MessageButton()
.setCustomId(component.id)
.setLabel(component.label)
.setStyle(component.style as MessageButtonStyleResolvable)
.setDisabled(component.disabled)
);
}

// TO-DO: Support multiple rows
const row = new MessageActionRow().addComponents(actionComponents)

// Send the embed
let sentReply = msgInt.reply({
embeds: [currentSetup.embed],
components: [row]
})

// Create a collector
const collector = channel.createMessageComponentCollector({
max: 1,
time: 1000 * 15,
})

// TO-DO: Add a listener to the collector
/*
collector.on('collect', (i: ButtonInteraction) => {
// blah blah
})
*/
return sentReply;
}
}
4 replies