Multiple Button Handlers, only 1 takes in a request

currently, I have 2 seperate files that both have button handlers. However, when a button is pressed, it only does parsing in 1 of the files. Both are in the interaction-handlers directory. src/interaction-handlers/operation.ts
import {
InteractionHandler,
InteractionHandlerTypes,
PieceContext,
} from "@sapphire/framework";
import type { ButtonInteraction, TextChannel } from "discord.js";

export class OperationButtonHandler extends InteractionHandler {
public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
super(ctx, {
...options,
interactionHandlerType: InteractionHandlerTypes.Button,
});
}

public override parse(interaction: ButtonInteraction) {
if (interaction.customId === "host-operation.post")
return this.some<boolean>(true);
if (interaction.customId === "host-operation.cancel")
return this.some<boolean>(false);

return this.none();
}

public async run(interaction: ButtonInteraction, post: boolean) {
if (post) {
const sessionChannel = await interaction.guild?.channels.fetch(
process.env.SESSION_CHANNEL
);

const pingChoice = interaction.message.content.includes("`everyone`")
? "@everyone"
: interaction.message.content.includes("`session`")
? "<@&1119004847077859328>"
: interaction.message.content.includes("`training`")
? "<@&856317590032220200>"
: "none";

try {
(sessionChannel as TextChannel).send({
content: pingChoice,
embeds: [interaction.message.embeds[0]],
});
} catch (error) {
return interaction.update({
content: `## Cancelled
Post has been cancelled due to an error.`,
components: [],
});
}

return interaction.update({
content: `## Posted
Post has been sent in <#${process.env.SESSION_CHANNEL}>!`,
components: [],
});
} else {
return interaction.update({
content: `## Cancelled
Post has been cancelled.`,
embeds: [],
components: [],
});
}
}
}
import {
InteractionHandler,
InteractionHandlerTypes,
PieceContext,
} from "@sapphire/framework";
import type { ButtonInteraction, TextChannel } from "discord.js";

export class OperationButtonHandler extends InteractionHandler {
public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
super(ctx, {
...options,
interactionHandlerType: InteractionHandlerTypes.Button,
});
}

public override parse(interaction: ButtonInteraction) {
if (interaction.customId === "host-operation.post")
return this.some<boolean>(true);
if (interaction.customId === "host-operation.cancel")
return this.some<boolean>(false);

return this.none();
}

public async run(interaction: ButtonInteraction, post: boolean) {
if (post) {
const sessionChannel = await interaction.guild?.channels.fetch(
process.env.SESSION_CHANNEL
);

const pingChoice = interaction.message.content.includes("`everyone`")
? "@everyone"
: interaction.message.content.includes("`session`")
? "<@&1119004847077859328>"
: interaction.message.content.includes("`training`")
? "<@&856317590032220200>"
: "none";

try {
(sessionChannel as TextChannel).send({
content: pingChoice,
embeds: [interaction.message.embeds[0]],
});
} catch (error) {
return interaction.update({
content: `## Cancelled
Post has been cancelled due to an error.`,
components: [],
});
}

return interaction.update({
content: `## Posted
Post has been sent in <#${process.env.SESSION_CHANNEL}>!`,
components: [],
});
} else {
return interaction.update({
content: `## Cancelled
Post has been cancelled.`,
embeds: [],
components: [],
});
}
}
}
src/interaction-handlers/profile.ts
import {
InteractionHandler,
InteractionHandlerTypes,
PieceContext,
} from "@sapphire/framework";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
TextChannel,
type StringSelectMenuInteraction,
StringSelectMenuBuilder,
APIStringSelectComponent,
} from "discord.js";
import embed from "../resources/templates/embed";

type ManageType = "OPERATING" | "TRAINING";
type UpdateButtonData = {
continue: boolean;
approve: boolean;
};

export class ManageProfileButtonHandler extends InteractionHandler {
public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
super(ctx, {
...options,
interactionHandlerType: InteractionHandlerTypes.Button,
});
}

public override parse(interaction: ButtonInteraction) {
if (interaction.customId === "profile.edit.update.approve")
return this.some<UpdateButtonData>({
continue: true,
approve: true,
});
if (interaction.customId === "profile.edit.update.deny")
return this.some<UpdateButtonData>({
continue: true,
approve: false,
});
if (interaction.customId === "profile.edit.cancel")
return this.some<UpdateButtonData>({
continue: false,
approve: false,
});

return this.none();
}

public async run(interaction: ButtonInteraction, data: UpdateButtonData) {
if (data.continue) {
} else {
console.log(1);
interaction.update({
components: [],
});
interaction.editReply({
components: [],
});
}
}
}
import {
InteractionHandler,
InteractionHandlerTypes,
PieceContext,
} from "@sapphire/framework";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
TextChannel,
type StringSelectMenuInteraction,
StringSelectMenuBuilder,
APIStringSelectComponent,
} from "discord.js";
import embed from "../resources/templates/embed";

type ManageType = "OPERATING" | "TRAINING";
type UpdateButtonData = {
continue: boolean;
approve: boolean;
};

export class ManageProfileButtonHandler extends InteractionHandler {
public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
super(ctx, {
...options,
interactionHandlerType: InteractionHandlerTypes.Button,
});
}

public override parse(interaction: ButtonInteraction) {
if (interaction.customId === "profile.edit.update.approve")
return this.some<UpdateButtonData>({
continue: true,
approve: true,
});
if (interaction.customId === "profile.edit.update.deny")
return this.some<UpdateButtonData>({
continue: true,
approve: false,
});
if (interaction.customId === "profile.edit.cancel")
return this.some<UpdateButtonData>({
continue: false,
approve: false,
});

return this.none();
}

public async run(interaction: ButtonInteraction, data: UpdateButtonData) {
if (data.continue) {
} else {
console.log(1);
interaction.update({
components: [],
});
interaction.editReply({
components: [],
});
}
}
}
16 Replies
fisher
fisher13mo ago
(also the first file is the one that does work)
Favna
Favna12mo ago
are you sure your custom IDs are set correctly? There is no reason why it shouldnt execute all. For example @Dragonite has many handlers and for that bot they are also all executed
Spinel
Spinel12mo ago
Discord bots that use @sapphire/framework v4 - Official Bot Examples ᴱ ᴰ ᴶˢ - Gemboard ᴱ ᴰ - Dragonite ᴱ ᴰ - Radon ᴱ ᴬ - Sapphire Application Commands Examples ᴱ - Archangel ᴱ ᴰ - Zeyr ᴰ ᴬ - Birthdayy ᴰ - KBot ᴱ ᴬ ᴰ Discord bots that use @sapphire/framework v3 - Arima ᴱ - Nino ᴱ ᴰ - Operator ᴱ ᴬ ᴰ - Spectera ᴬ Discord bots that use @sapphire/framework v2 - Materia ᴱ - RTByte ᴱ ᴬ - Skyra ᴬ ᴰ - YliasDiscordBot ᴬ : Uses ESM (if not specified then uses CJS) : Advanced bot (if not specified it is a simple bot, or not graded) : Uses Docker in production ᴶˢ: Written in JavaScript. If not specified then the bot is written in TypeScript.
fisher
fisher12mo ago
oh hi there! I did some testing and apprently it was because i had a button and select menu handler in the same file because they were both related, i did seperate them and it seemed to work fine... while i'm here though, im having troubles with a modal, im sending a modal after a chat input cmd interaction and it isnt displaying and says "interaction failed" on my client, no errors in the terminal and i have a console.log right before the showmodal, and it logs but doesn't seem to work? https://micro.sylo.digital/i/Dhmxgs
fisher
fisher12mo ago
public async chatInputApply(
interaction: Subcommand.ChatInputCommandInteraction
) {
console.log(0);
// interaction.deferReply();
console.log(1);

const { client } = this.container;
const args = interaction.options;

const discordUser = interaction.user;

let bloxlinkUser = "";
try {
bloxlinkUser = (
await bloxlinkGuild.DiscordToRoblox(
process.env.GUILD_ID,
discordUser.id
)
).robloxID;
} catch (error) {
return await interaction.editReply({
embeds: [
embed.err(
"You aren't linked with Bloxlink! Link your account, and then try again"
),
],
});
}

console.log(2);

let careerProfile = await findOne(database.getRepositories().careers, {
robloxId: bloxlinkUser,
});

if (
careerProfile &&
//@ts-ignore
careerProfile.applicationStatus[args.getString("position", true)] !==
"Not Applied"
) {
return await interaction.editReply({
embeds: [
embed.err(
"You already have an application submitted for this position!"
),
],
});
}

console.log(3);

if (!careerProfile) {
careerProfile = await create(database.getRepositories().careers, {
robloxId: bloxlinkUser,
applicationStatus: {
rideOperator: "Not Applied",
},
});
}

console.log(4);

const careerSystemProfile = await findOne(
database.getRepositories().careers,
{
robloxId: "system",
}
);

console.log(5);

if (
//@ts-ignore
careerSystemProfile?.applicationStatus[
args.getString("position", true)
] !== "Passed"
) {
return await interaction.editReply({
embeds: [embed.err("This position isn't hiring right now!")],
});
}

console.log(6);

const applicationModal = new ModalBuilder()
.setTitle("Atlas Parks Application")
.setCustomId(`careers.apply:${args.getString("position", true)}`)
.addComponents(
this.questionRow1,
this.questionRow2,
this.questionRow3,
this.questionRow4,
this.questionRow5,
this.questionRow6,
this.questionRow7,
this.questionRow8,
this.questionRow9
);

console.log(7);

await interaction.showModal(applicationModal);
}
}
public async chatInputApply(
interaction: Subcommand.ChatInputCommandInteraction
) {
console.log(0);
// interaction.deferReply();
console.log(1);

const { client } = this.container;
const args = interaction.options;

const discordUser = interaction.user;

let bloxlinkUser = "";
try {
bloxlinkUser = (
await bloxlinkGuild.DiscordToRoblox(
process.env.GUILD_ID,
discordUser.id
)
).robloxID;
} catch (error) {
return await interaction.editReply({
embeds: [
embed.err(
"You aren't linked with Bloxlink! Link your account, and then try again"
),
],
});
}

console.log(2);

let careerProfile = await findOne(database.getRepositories().careers, {
robloxId: bloxlinkUser,
});

if (
careerProfile &&
//@ts-ignore
careerProfile.applicationStatus[args.getString("position", true)] !==
"Not Applied"
) {
return await interaction.editReply({
embeds: [
embed.err(
"You already have an application submitted for this position!"
),
],
});
}

console.log(3);

if (!careerProfile) {
careerProfile = await create(database.getRepositories().careers, {
robloxId: bloxlinkUser,
applicationStatus: {
rideOperator: "Not Applied",
},
});
}

console.log(4);

const careerSystemProfile = await findOne(
database.getRepositories().careers,
{
robloxId: "system",
}
);

console.log(5);

if (
//@ts-ignore
careerSystemProfile?.applicationStatus[
args.getString("position", true)
] !== "Passed"
) {
return await interaction.editReply({
embeds: [embed.err("This position isn't hiring right now!")],
});
}

console.log(6);

const applicationModal = new ModalBuilder()
.setTitle("Atlas Parks Application")
.setCustomId(`careers.apply:${args.getString("position", true)}`)
.addComponents(
this.questionRow1,
this.questionRow2,
this.questionRow3,
this.questionRow4,
this.questionRow5,
this.questionRow6,
this.questionRow7,
this.questionRow8,
this.questionRow9
);

console.log(7);

await interaction.showModal(applicationModal);
}
}
the modal is correct and doesn't have any issues (some text being too long)
KB
KB12mo ago
Having multiple interaction-handlers in the same file is possible if you give them distinct names. That can be set when calling super. (The logic for registering pieces is dependent on the name property. If there's no name then it'll infer one from the file name.) For the modal submission, there's nothing wrong that stands out in the command. So double check that you're parsing/handling the custom ID properly in the interaction-handler. One thing I like to do is create custom IDs in a string Enum or function so that the format is the same every time. Makes life easier than having to remember the format between files.
fisher
fisher12mo ago
its the the itneraction handler for the modal its just it never sends the modal 💀
KB
KB12mo ago
Whats the code in the parse for that handler? Try putting a console.log for the custom ID before you return a none If there's nothing, then the handler isnt registering properly. If the received ID is different than what you're parsing, then theres the issue. Oh wait, you mean the modal isnt showing at all? Since you removed the defer for showing a modal, all of the editReply need to be changed to reply
fisher
fisher12mo ago
. its not the handler the modal never sends
KB
KB12mo ago
Evidently you didn't read my messages where I realized that, also it helps to not be rude to the people helping you. Anyways, I assume you didn't disable the command error listeners? Otherwise re-enable that. And you'll need to do that reply change as I said.
fisher
fisher12mo ago
i didnt think i was being rude, sorry i didnt disable any command error listeners, but i will do the reply change but yes the modal isnt showing at all
KB
KB12mo ago
What do the question rows look like?
fisher
fisher12mo ago
public question1 = new TextInputBuilder()
.setCustomId("age")
.setLabel("Are you over the age of 13?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("Yes!");

public question2 = new TextInputBuilder()
.setCustomId("howLong")
.setLabel("How long have you been at Atlas Parks")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("A few months");

public question3 = new TextInputBuilder()
.setCustomId("rules")
.setLabel("Have you reviewed the server rules?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("Yes!");

public question4 = new TextInputBuilder()
.setCustomId("whyWork")
.setLabel("Why do you want to work at Atlas Parks?")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setPlaceholder("Minimum of 3+ Sentences");

public question5 = new TextInputBuilder()
.setCustomId("whyPicked")
.setLabel("Why should you be picked over others?")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setPlaceholder("Minimum of 3+ Sentences");

public question6 = new TextInputBuilder()
.setCustomId("experience")
.setLabel("Have you had any ride op experience?")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setPlaceholder(
"This can be Roblox or Real Life experience (bonus points)"
);

public question7 = new TextInputBuilder()
.setCustomId("activity")
.setLabel("On a scale of 1-10, how active would you be?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("10")
.setMaxLength(2);

public question8 = new TextInputBuilder()
.setCustomId("mic")
.setLabel("Can you join Discord VC's & speak?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("I can join a Discord VC, but I can not speak.");

public question9 = new TextInputBuilder()
.setCustomId("operations")
.setLabel("Would you be able to be in 2 ops a month?")
.setStyle(TextInputStyle.Short)
.setRequired(true);

public questionRow1 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question1
);
public questionRow2 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question2
);
public questionRow3 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question3
);
public questionRow4 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question4
);
public questionRow5 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question5
);
public questionRow6 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question6
);
public questionRow7 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question7
);
public questionRow8 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question8
);
public questionRow9 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question9
);
public question1 = new TextInputBuilder()
.setCustomId("age")
.setLabel("Are you over the age of 13?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("Yes!");

public question2 = new TextInputBuilder()
.setCustomId("howLong")
.setLabel("How long have you been at Atlas Parks")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("A few months");

public question3 = new TextInputBuilder()
.setCustomId("rules")
.setLabel("Have you reviewed the server rules?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("Yes!");

public question4 = new TextInputBuilder()
.setCustomId("whyWork")
.setLabel("Why do you want to work at Atlas Parks?")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setPlaceholder("Minimum of 3+ Sentences");

public question5 = new TextInputBuilder()
.setCustomId("whyPicked")
.setLabel("Why should you be picked over others?")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setPlaceholder("Minimum of 3+ Sentences");

public question6 = new TextInputBuilder()
.setCustomId("experience")
.setLabel("Have you had any ride op experience?")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setPlaceholder(
"This can be Roblox or Real Life experience (bonus points)"
);

public question7 = new TextInputBuilder()
.setCustomId("activity")
.setLabel("On a scale of 1-10, how active would you be?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("10")
.setMaxLength(2);

public question8 = new TextInputBuilder()
.setCustomId("mic")
.setLabel("Can you join Discord VC's & speak?")
.setStyle(TextInputStyle.Short)
.setRequired(true)
.setPlaceholder("I can join a Discord VC, but I can not speak.");

public question9 = new TextInputBuilder()
.setCustomId("operations")
.setLabel("Would you be able to be in 2 ops a month?")
.setStyle(TextInputStyle.Short)
.setRequired(true);

public questionRow1 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question1
);
public questionRow2 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question2
);
public questionRow3 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question3
);
public questionRow4 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question4
);
public questionRow5 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question5
);
public questionRow6 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question6
);
public questionRow7 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question7
);
public questionRow8 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question8
);
public questionRow9 = new ActionRowBuilder<TextInputBuilder>().addComponents(
this.question9
);
KB
KB12mo ago
Thanks. It's cause you can only have 1-5 rows for modals, so 9 is too much. Not sure why you're not getting an error in logs for that. I got data.components[BASE_TYPE_BAD_LENGTH]: Must be between 1 and 5 in length. when running it myself. Maybe check your logging levels? Anyways you should be all good now.
fisher
fisher12mo ago
hm i agree i should have gotten some notice, never knew that i could only have 5 inputs total? i might have to split this into 2 phases will test this out later tonight and get back
KB
KB12mo ago
👍 Probably mark this as the answer, and open another forum submission if you still have the other issue