From 01450d976c6d1ff637da9686b30c47c9f4fbcc20 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sat, 6 Jun 2026 02:35:39 +0700 Subject: [PATCH] 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. --- src/discord/interactionHandler.test.ts | 23 +++++++++++------------ src/discord/interactionHandler.ts | 16 ++++++++-------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/discord/interactionHandler.test.ts b/src/discord/interactionHandler.test.ts index 37f3ffb..6862328 100644 --- a/src/discord/interactionHandler.test.ts +++ b/src/discord/interactionHandler.test.ts @@ -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 { + async deferReply(): Promise { this.deferred = true; } async editReply(input: { content: string }): Promise { - 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" }); }); }); diff --git a/src/discord/interactionHandler.ts b/src/discord/interactionHandler.ts index 1c6039e..12b82a2 100644 --- a/src/discord/interactionHandler.ts +++ b/src/discord/interactionHandler.ts @@ -10,8 +10,8 @@ export type ChatInputInteractionLike = { isChatInputCommand(): boolean; deferred: boolean; replied: boolean; - reply(input: { content: string; flags: MessageFlags.Ephemeral }): Promise; - deferReply(input: { flags: MessageFlags.Ephemeral }): Promise; + reply(input: { content: string; flags?: MessageFlags.Ephemeral }): Promise; + deferReply(input?: { flags?: number }): Promise; editReply(input: { content: string }): Promise; 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) }); } } }