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 { describe, expect, test } from "bun:test";
import { handleInteraction, type BoosterRoleCommandService, type ChatInputInteractionLike } from "./interactionHandler"; 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 { class FakeInteraction implements ChatInputInteractionLike {
commandName = "booster-role"; commandName = "booster-role";
@@ -24,12 +24,12 @@ class FakeInteraction implements ChatInputInteractionLike {
this.replied = true; this.replied = true;
} }
async deferReply(_input: { flags: MessageFlags.Ephemeral }): Promise<void> { async deferReply(): Promise<void> {
this.deferred = true; this.deferred = true;
} }
async editReply(input: { content: string }): Promise<void> { async editReply(input: { content: string }): Promise<void> {
this.replies.push({ content: input.content, flags: MessageFlags.Ephemeral }); this.replies.push({ content: input.content });
} }
options = { options = {
@@ -74,7 +74,7 @@ describe("handleInteraction", () => {
expect(service.calls).toEqual(["claim"]); expect(service.calls).toEqual(["claim"]);
expect(interaction.deferred).toBe(true); 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 () => { test("routes update subcommands and replies", async () => {
@@ -85,7 +85,6 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true }); await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual([subcommand]); 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 }); await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual(["delete:user"]); 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 () => { test("routes admin delete command for target user", async () => {
@@ -107,7 +106,7 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true }); await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual(["delete:target-user"]); 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 () => { test("rejects admin delete without Administrator permission", async () => {
@@ -118,7 +117,7 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true }); await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(service.calls).toEqual([]); 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 () => { 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 // claim is deferred, so errors use editReply
expect(interaction.deferred).toBe(true); 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 () => { test("hides failed query details from user replies (deferred)", async () => {
@@ -145,7 +144,7 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true }); await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(interaction.deferred).toBe(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 () => { test("non-deferred commands still use reply for errors", async () => {
@@ -158,6 +157,6 @@ describe("handleInteraction", () => {
await handleInteraction(interaction, service, { isBoosting: async () => true }); await handleInteraction(interaction, service, { isBoosting: async () => true });
expect(interaction.deferred).toBe(false); 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; isChatInputCommand(): boolean;
deferred: boolean; deferred: boolean;
replied: boolean; replied: boolean;
reply(input: { content: string; flags: MessageFlags.Ephemeral }): Promise<unknown>; reply(input: { content: string; flags?: MessageFlags.Ephemeral }): Promise<unknown>;
deferReply(input: { flags: MessageFlags.Ephemeral }): Promise<unknown>; deferReply(input?: { flags?: number }): Promise<unknown>;
editReply(input: { content: string }): Promise<unknown>; editReply(input: { content: string }): Promise<unknown>;
options: { options: {
getSubcommand(): string; getSubcommand(): string;
@@ -56,7 +56,7 @@ export async function handleInteraction(
// Defer reply for potentially slow commands (claim, icon) to avoid 10062 "Unknown interaction" // Defer reply for potentially slow commands (claim, icon) to avoid 10062 "Unknown interaction"
const shouldDefer = subcommand === "claim" || subcommand === "icon"; const shouldDefer = subcommand === "claim" || subcommand === "icon";
if (shouldDefer) { if (shouldDefer) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral }); await interaction.deferReply();
} }
if (subcommand === "claim") { if (subcommand === "claim") {
@@ -77,14 +77,14 @@ export async function handleInteraction(
if (subcommand === "rename") { if (subcommand === "rename") {
logger.info("Handling booster-role command", { guildId, userId, subcommand }); logger.info("Handling booster-role command", { guildId, userId, subcommand });
await service.renameRole({ guildId, userId, name: requireString(interaction, "name") }); 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; return;
} }
if (subcommand === "recolor") { if (subcommand === "recolor") {
logger.info("Handling booster-role command", { guildId, userId, subcommand }); logger.info("Handling booster-role command", { guildId, userId, subcommand });
await service.recolorRole({ guildId, userId, color: requireString(interaction, "color"), color2: interaction.options.getString("color2") }); 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; return;
} }
@@ -98,7 +98,7 @@ export async function handleInteraction(
if (subcommand === "delete") { if (subcommand === "delete") {
logger.info("Handling booster-role command", { guildId, userId, subcommand }); logger.info("Handling booster-role command", { guildId, userId, subcommand });
await service.deleteRole({ guildId, userId }); await service.deleteRole({ guildId, userId });
await interaction.reply({ content: "Booster role deleted.", flags: MessageFlags.Ephemeral }); await interaction.reply({ content: "Booster role deleted." });
return; return;
} }
@@ -107,7 +107,7 @@ export async function handleInteraction(
const targetUserId = requireUser(interaction, "user").id; const targetUserId = requireUser(interaction, "user").id;
logger.info("Handling booster-role admin command", { guildId, userId, subcommand, targetUserId }); logger.info("Handling booster-role admin command", { guildId, userId, subcommand, targetUserId });
await service.deleteRole({ guildId, userId: 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; return;
} }
@@ -118,7 +118,7 @@ export async function handleInteraction(
if (interaction.deferred) { if (interaction.deferred) {
await interaction.editReply({ content: toUserErrorMessage(error) }); await interaction.editReply({ content: toUserErrorMessage(error) });
} else { } else {
await interaction.reply({ content: toUserErrorMessage(error), flags: MessageFlags.Ephemeral }); await interaction.reply({ content: toUserErrorMessage(error) });
} }
} }
} }