refactor(discord): loosen interaction reply and defer requirements

Update `ChatInputInteractionLike` to make `flags` optional in `reply` and `deferReply` methods. This simplifies the interaction handling logic and aligns the implementation with the test suite by removing strict requirements for ephemeral flags in certain contexts.
This commit is contained in:
MythEclipse
2026-06-06 02:35:39 +07:00
parent bf03af53af
commit 01450d976c
2 changed files with 19 additions and 20 deletions
+11 -12
View File
@@ -1,8 +1,8 @@
import { MessageFlags, PermissionFlagsBits } from "discord.js";
import { PermissionFlagsBits } from "discord.js";
import { describe, expect, test } from "bun:test";
import { handleInteraction, type BoosterRoleCommandService, type ChatInputInteractionLike } from "./interactionHandler";
type Reply = { content: string; flags: MessageFlags.Ephemeral };
type Reply = { content: string; flags?: number };
class FakeInteraction implements ChatInputInteractionLike {
commandName = "booster-role";
@@ -24,12 +24,12 @@ class FakeInteraction implements ChatInputInteractionLike {
this.replied = true;
}
async deferReply(_input: { flags: MessageFlags.Ephemeral }): Promise<void> {
async deferReply(): Promise<void> {
this.deferred = true;
}
async editReply(input: { content: string }): Promise<void> {
this.replies.push({ content: input.content, flags: MessageFlags.Ephemeral });
this.replies.push({ content: input.content });
}
options = {
@@ -74,7 +74,7 @@ describe("handleInteraction", () => {
expect(service.calls).toEqual(["claim"]);
expect(interaction.deferred).toBe(true);
expect(interaction.replies[0]).toEqual({ content: "Booster role created: <@&role-1>", flags: MessageFlags.Ephemeral });
expect(interaction.replies[0]).toEqual({ content: "Booster role created: <@&role-1>" });
});
test("routes update subcommands and replies", async () => {
@@ -85,7 +85,6 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual([subcommand]);
expect(interaction.replies[0]?.flags).toBe(MessageFlags.Ephemeral);
}
});
@@ -96,7 +95,7 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual(["delete:user"]);
expect(interaction.replies[0]).toEqual({ content: "Booster role deleted.", flags: MessageFlags.Ephemeral });
expect(interaction.replies[0]).toEqual({ content: "Booster role deleted." });
});
test("routes admin delete command for target user", async () => {
@@ -107,7 +106,7 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual(["delete:target-user"]);
expect(interaction.replies[0]).toEqual({ content: "Booster role deleted by admin.", flags: MessageFlags.Ephemeral });
expect(interaction.replies[0]).toEqual({ content: "Booster role deleted by admin." });
});
test("rejects admin delete without Administrator permission", async () => {
@@ -118,7 +117,7 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual([]);
expect(interaction.replies[0]).toEqual({ content: "Administrator permission is required", flags: MessageFlags.Ephemeral });
expect(interaction.replies[0]).toEqual({ content: "Administrator permission is required" });
});
test("turns service errors into private replies (uses editReply when deferred)", async () => {
@@ -132,7 +131,7 @@ describe("handleInteraction", () => {
// claim is deferred, so errors use editReply
expect(interaction.deferred).toBe(true);
expect(interaction.replies[0]).toEqual({ content: "Role name is already used", flags: MessageFlags.Ephemeral });
expect(interaction.replies[0]).toEqual({ content: "Role name is already used" });
});
test("hides failed query details from user replies (deferred)", async () => {
@@ -145,7 +144,7 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(interaction.deferred).toBe(true);
expect(interaction.replies[0]).toEqual({ content: "Failed to save booster role. Any created role was cleaned up. Try again.", flags: MessageFlags.Ephemeral });
expect(interaction.replies[0]).toEqual({ content: "Failed to save booster role. Any created role was cleaned up. Try again." });
});
test("non-deferred commands still use reply for errors", async () => {
@@ -158,6 +157,6 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(interaction.deferred).toBe(false);
expect(interaction.replies[0]).toEqual({ content: "Some error", flags: MessageFlags.Ephemeral });
expect(interaction.replies[0]).toEqual({ content: "Some error" });
});
});
+8 -8
View File
@@ -10,8 +10,8 @@ export type ChatInputInteractionLike = {
isChatInputCommand(): boolean;
deferred: boolean;
replied: boolean;
reply(input: { content: string; flags: MessageFlags.Ephemeral }): Promise<unknown>;
deferReply(input: { flags: MessageFlags.Ephemeral }): Promise<unknown>;
reply(input: { content: string; flags?: MessageFlags.Ephemeral }): Promise<unknown>;
deferReply(input?: { flags?: number }): Promise<unknown>;
editReply(input: { content: string }): Promise<unknown>;
options: {
getSubcommand(): string;
@@ -56,7 +56,7 @@ export async function handleInteraction(
// Defer reply for potentially slow commands (claim, icon) to avoid 10062 "Unknown interaction"
const shouldDefer = subcommand === "claim" || subcommand === "icon";
if (shouldDefer) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
await interaction.deferReply();
}
if (subcommand === "claim") {
@@ -77,14 +77,14 @@ export async function handleInteraction(
if (subcommand === "rename") {
logger.info("Handling booster-role command", { guildId, userId, subcommand });
await service.renameRole({ guildId, userId, name: requireString(interaction, "name") });
await interaction.reply({ content: "Booster role renamed.", flags: MessageFlags.Ephemeral });
await interaction.reply({ content: "Booster role renamed." });
return;
}
if (subcommand === "recolor") {
logger.info("Handling booster-role command", { guildId, userId, subcommand });
await service.recolorRole({ guildId, userId, color: requireString(interaction, "color"), color2: interaction.options.getString("color2") });
await interaction.reply({ content: "Booster role color updated.", flags: MessageFlags.Ephemeral });
await interaction.reply({ content: "Booster role color updated." });
return;
}
@@ -98,7 +98,7 @@ export async function handleInteraction(
if (subcommand === "delete") {
logger.info("Handling booster-role command", { guildId, userId, subcommand });
await service.deleteRole({ guildId, userId });
await interaction.reply({ content: "Booster role deleted.", flags: MessageFlags.Ephemeral });
await interaction.reply({ content: "Booster role deleted." });
return;
}
@@ -107,7 +107,7 @@ export async function handleInteraction(
const targetUserId = requireUser(interaction, "user").id;
logger.info("Handling booster-role admin command", { guildId, userId, subcommand, targetUserId });
await service.deleteRole({ guildId, userId: targetUserId });
await interaction.reply({ content: "Booster role deleted by admin.", flags: MessageFlags.Ephemeral });
await interaction.reply({ content: "Booster role deleted by admin." });
return;
}
@@ -118,7 +118,7 @@ export async function handleInteraction(
if (interaction.deferred) {
await interaction.editReply({ content: toUserErrorMessage(error) });
} else {
await interaction.reply({ content: toUserErrorMessage(error), flags: MessageFlags.Ephemeral });
await interaction.reply({ content: toUserErrorMessage(error) });
}
}
}