diff --git a/README.md b/README.md index d83e592..eb0e433 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ To add the bot to your server, click [here](https://discord.com/oauth2/authorize - un-signs you up for a voice channel, so you no longer get pinged - **/ring** - pings someone to join the voice channel you're currently in + - also available by right-clicking someone and picking Apps > Ring - **/block** - blocks someone, which prevents them from ringing you and makes it so you don't get pinged if they start a call - **/unblock** diff --git a/package.json b/package.json index aa4dfac..ae78ad2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@supabase/supabase-js": "^2.110.2", - "discord-embed-router": "^1.4.2", + "discord-embed-router": "^1.5.0", "discord.js": "^14.22.1", "dotenv": "^17.2.2", "is-online": "^11.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc9daa4..f3b73c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^2.110.2 version: 2.111.0 discord-embed-router: - specifier: ^1.4.2 - version: 1.4.2(discord.js@14.27.0) + specifier: ^1.5.0 + version: 1.5.0(discord.js@14.27.0) discord.js: specifier: ^14.22.1 version: 14.27.0 @@ -1153,8 +1153,8 @@ packages: discord-api-types@0.38.52: resolution: {integrity: sha512-uwe9EKfbjsmgWc2fdFjvDbj+dQqx3lp7wqDCmIha0jInuU+xeQjkCK9tMMn+p7RXfdVQORCInq4cD3U2ymDmyg==} - discord-embed-router@1.4.2: - resolution: {integrity: sha512-6FS9M9+MDnnQfTpD5mdYHgGx7qEiOs36sDLQLcmdWnhbN3A7FLDRDDfl510GTIylGQs7qhFvnUG6AIqhH6UTJQ==} + discord-embed-router@1.5.0: + resolution: {integrity: sha512-JBoR/3kcByeM125DztBt+CFbXFf2RczkzXYZswSXJRwb34tO02H1mteZe7RzDO5vV9Nk8m9SJYMufTdL7zTveg==} engines: {node: '>=18'} peerDependencies: discord.js: ^14.26.4 @@ -2801,7 +2801,7 @@ snapshots: discord-api-types@0.38.52: {} - discord-embed-router@1.4.2(discord.js@14.27.0): + discord-embed-router@1.5.0(discord.js@14.27.0): dependencies: discord.js: 14.27.0 path-to-regexp: 8.4.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2ce8045..bc0e53a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,4 @@ allowBuilds: esbuild: true minimumReleaseAgeExclude: - - discord-embed-router@1.4.2 + - discord-embed-router@1.5.0 diff --git a/src/commands/commands.ts b/src/commands/commands.ts deleted file mode 100644 index 6517b7c..0000000 --- a/src/commands/commands.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ChatInputCommandInteraction, SharedSlashCommand } from "discord.js"; - -import { about } from "@commands/about"; -import { catalog } from "@commands/catalog"; -import { defaultRingees } from "@commands/defaultRingees"; -import { defaultRingRecipients } from "@commands/defaultRingRecipient"; -import { deleteData } from "@commands/deleteData"; -import { block } from "@commands/filter/block"; -import { filter } from "@commands/filter/filter"; -import { unblock } from "@commands/filter/unblock"; -import { unwhitelist } from "@commands/filter/unwhitelist"; -import { whitelist } from "@commands/filter/whitelist"; -import { help } from "@commands/help"; -import { mode } from "@commands/mode"; -import { quit } from "@commands/quit"; -import { ring } from "@commands/ring"; -import { ringDefaults } from "@commands/ringDefaults"; -import { signup } from "@commands/signup"; -import { signuprole } from "@commands/signuprole"; -import { unsignup } from "@commands/unsignup"; -import { unsignuprole } from "@commands/unsignuprole"; -import { RingRouter } from "@routes/types"; - -// commands are thin adapters: they parse their options and dispatch into -// the router, where all business rules live -export type CommandImplementation = { - data: SharedSlashCommand; - execute: ( - router: RingRouter, - interaction: ChatInputCommandInteraction, - ) => Promise; -}; - -export const commands: CommandImplementation[] = [ - help, - catalog, - about, - deleteData, - ring, - ringDefaults, - signup, - unsignup, - signuprole, - unsignuprole, - quit, - mode, - // filter commands - block, - unblock, - whitelist, - unwhitelist, - filter, - // default ringees - defaultRingees, - defaultRingRecipients, -]; diff --git a/src/commands/context/ringUser.ts b/src/commands/context/ringUser.ts new file mode 100644 index 0000000..abd1df4 --- /dev/null +++ b/src/commands/context/ringUser.ts @@ -0,0 +1,24 @@ +import { + ApplicationCommandType, + ContextMenuCommandBuilder, + MessageFlags, + UserContextMenuCommandInteraction, +} from "discord.js"; + +import { RingRouter } from "@routes/types"; + +export const ringUser = { + data: new ContextMenuCommandBuilder() + .setName("Ring") + .setType(ApplicationCommandType.User), + async execute( + router: RingRouter, + interaction: UserContextMenuCommandInteraction, + ) { + await router.dispatch(interaction, "/ring/user", { + method: "POST", + queryParams: { id: interaction.targetUser.id }, + flags: [MessageFlags.Ephemeral], + }); + }, +}; diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..e952228 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,50 @@ +import { ringUser } from "@commands/context/ringUser"; +import { about } from "@commands/slash/about"; +import { catalog } from "@commands/slash/catalog"; +import { defaultRingees } from "@commands/slash/defaultRingees"; +import { defaultRingRecipients } from "@commands/slash/defaultRingRecipient"; +import { deleteData } from "@commands/slash/deleteData"; +import { block } from "@commands/slash/filter/block"; +import { filter } from "@commands/slash/filter/filter"; +import { unblock } from "@commands/slash/filter/unblock"; +import { unwhitelist } from "@commands/slash/filter/unwhitelist"; +import { whitelist } from "@commands/slash/filter/whitelist"; +import { help } from "@commands/slash/help"; +import { mode } from "@commands/slash/mode"; +import { quit } from "@commands/slash/quit"; +import { ring } from "@commands/slash/ring"; +import { ringDefaults } from "@commands/slash/ringDefaults"; +import { signup } from "@commands/slash/signup"; +import { signuprole } from "@commands/slash/signuprole"; +import { unsignup } from "@commands/slash/unsignup"; +import { unsignuprole } from "@commands/slash/unsignuprole"; +import { + CommandImplementation, + ContextMenuImplementation, +} from "@commands/types"; + +export const commands: CommandImplementation[] = [ + help, + catalog, + about, + deleteData, + ring, + ringDefaults, + signup, + unsignup, + signuprole, + unsignuprole, + quit, + mode, + // filter commands + block, + unblock, + whitelist, + unwhitelist, + filter, + // default ringees + defaultRingees, + defaultRingRecipients, +]; + +export const contextMenuCommands: ContextMenuImplementation[] = [ringUser]; diff --git a/src/commands/about.ts b/src/commands/slash/about.ts similarity index 100% rename from src/commands/about.ts rename to src/commands/slash/about.ts diff --git a/src/commands/catalog.ts b/src/commands/slash/catalog.ts similarity index 100% rename from src/commands/catalog.ts rename to src/commands/slash/catalog.ts diff --git a/src/commands/defaultRingRecipient.ts b/src/commands/slash/defaultRingRecipient.ts similarity index 82% rename from src/commands/defaultRingRecipient.ts rename to src/commands/slash/defaultRingRecipient.ts index 290e088..8a4c185 100644 --- a/src/commands/defaultRingRecipient.ts +++ b/src/commands/slash/defaultRingRecipient.ts @@ -1,4 +1,4 @@ -import { defaultRingeesCommand } from "@commands/defaultRingees"; +import { defaultRingeesCommand } from "@commands/slash/defaultRingees"; // the pre-rename name of /default_ringees: still registered so existing muscle // memory works, but left out of /catalog and /help. Delete this file, its entry diff --git a/src/commands/defaultRingees.ts b/src/commands/slash/defaultRingees.ts similarity index 100% rename from src/commands/defaultRingees.ts rename to src/commands/slash/defaultRingees.ts diff --git a/src/commands/deleteData.ts b/src/commands/slash/deleteData.ts similarity index 100% rename from src/commands/deleteData.ts rename to src/commands/slash/deleteData.ts diff --git a/src/commands/filter/block.ts b/src/commands/slash/filter/block.ts similarity index 100% rename from src/commands/filter/block.ts rename to src/commands/slash/filter/block.ts diff --git a/src/commands/filter/filter.ts b/src/commands/slash/filter/filter.ts similarity index 100% rename from src/commands/filter/filter.ts rename to src/commands/slash/filter/filter.ts diff --git a/src/commands/filter/unblock.ts b/src/commands/slash/filter/unblock.ts similarity index 100% rename from src/commands/filter/unblock.ts rename to src/commands/slash/filter/unblock.ts diff --git a/src/commands/filter/unwhitelist.ts b/src/commands/slash/filter/unwhitelist.ts similarity index 100% rename from src/commands/filter/unwhitelist.ts rename to src/commands/slash/filter/unwhitelist.ts diff --git a/src/commands/filter/whitelist.ts b/src/commands/slash/filter/whitelist.ts similarity index 100% rename from src/commands/filter/whitelist.ts rename to src/commands/slash/filter/whitelist.ts diff --git a/src/commands/help.ts b/src/commands/slash/help.ts similarity index 100% rename from src/commands/help.ts rename to src/commands/slash/help.ts diff --git a/src/commands/mode.ts b/src/commands/slash/mode.ts similarity index 100% rename from src/commands/mode.ts rename to src/commands/slash/mode.ts diff --git a/src/commands/quit.ts b/src/commands/slash/quit.ts similarity index 93% rename from src/commands/quit.ts rename to src/commands/slash/quit.ts index 9406ac8..2bef192 100644 --- a/src/commands/quit.ts +++ b/src/commands/slash/quit.ts @@ -5,7 +5,7 @@ import { SlashCommandBuilder, } from "discord.js"; -import { unsignup } from "@commands/unsignup"; +import { unsignup } from "@commands/slash/unsignup"; import { RingRouter } from "@routes/types"; export const quit = { diff --git a/src/commands/ring.ts b/src/commands/slash/ring.ts similarity index 100% rename from src/commands/ring.ts rename to src/commands/slash/ring.ts diff --git a/src/commands/ringDefaults.ts b/src/commands/slash/ringDefaults.ts similarity index 100% rename from src/commands/ringDefaults.ts rename to src/commands/slash/ringDefaults.ts diff --git a/src/commands/signup.ts b/src/commands/slash/signup.ts similarity index 100% rename from src/commands/signup.ts rename to src/commands/slash/signup.ts diff --git a/src/commands/signuprole.ts b/src/commands/slash/signuprole.ts similarity index 100% rename from src/commands/signuprole.ts rename to src/commands/slash/signuprole.ts diff --git a/src/commands/unsignup.ts b/src/commands/slash/unsignup.ts similarity index 100% rename from src/commands/unsignup.ts rename to src/commands/slash/unsignup.ts diff --git a/src/commands/unsignuprole.ts b/src/commands/slash/unsignuprole.ts similarity index 100% rename from src/commands/unsignuprole.ts rename to src/commands/slash/unsignuprole.ts diff --git a/src/commands/types.ts b/src/commands/types.ts new file mode 100644 index 0000000..e233998 --- /dev/null +++ b/src/commands/types.ts @@ -0,0 +1,26 @@ +import { + ChatInputCommandInteraction, + ContextMenuCommandBuilder, + SharedSlashCommand, + UserContextMenuCommandInteraction, +} from "discord.js"; + +import { RingRouter } from "@routes/types"; + +// commands are thin adapters: they parse their options and dispatch into +// the router, where all business rules live +export type CommandImplementation = { + data: SharedSlashCommand; + execute: ( + router: RingRouter, + interaction: ChatInputCommandInteraction, + ) => Promise; +}; + +export type ContextMenuImplementation = { + data: ContextMenuCommandBuilder; + execute: ( + router: RingRouter, + interaction: UserContextMenuCommandInteraction, + ) => Promise; +}; diff --git a/src/deploy-commands.ts b/src/deploy-commands.ts index 7d7ecc2..1d736e5 100644 --- a/src/deploy-commands.ts +++ b/src/deploy-commands.ts @@ -1,6 +1,6 @@ import { REST, Routes } from "discord.js"; -import { commands } from "@commands/commands"; +import { commands, contextMenuCommands } from "@commands/index"; import { DISCORD_CLIENT_ID, DISCORD_TOKEN } from "@config"; // Construct and prepare an instance of the REST module @@ -9,17 +9,18 @@ const rest = new REST().setToken(DISCORD_TOKEN); // and deploy your commands! (async () => { try { + const allCommands = [...commands, ...contextMenuCommands]; console.log( - `Started refreshing ${commands.length} application (/) commands.`, + `Started refreshing ${allCommands.length} application commands.`, ); // The put method is used to fully refresh all commands in the guild with the current set const data = await rest.put(Routes.applicationCommands(DISCORD_CLIENT_ID), { - body: commands.map((command) => command.data.toJSON()), + body: allCommands.map((command) => command.data.toJSON()), }); console.log( - `Successfully reloaded ${(data as Array).length} application (/) commands.`, + `Successfully reloaded ${(data as Array).length} application commands.`, ); } catch (error) { console.error(error); diff --git a/src/index.ts b/src/index.ts index 12dfddb..cf21e39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { EmbedRouter } from "discord-embed-router"; import { - ChatInputCommandInteraction, + ApplicationCommandType, Client, Collection, GatewayIntentBits, @@ -10,9 +10,13 @@ import { import { CommandName, isCommandName } from "@commands/commandNames"; import { - CommandImplementation, commands as commandsArray, -} from "@commands/commands"; + contextMenuCommands as contextMenuCommandsArray, +} from "@commands/index"; +import { + CommandImplementation, + ContextMenuImplementation, +} from "@commands/types"; import { DISCORD_TOKEN } from "@config"; import { recordError, recordUsage } from "@db/diagnostics"; import { observeRouter } from "@main/diagnostics"; @@ -59,6 +63,10 @@ const commands = new Collection(); for (const command of commandsArray) { commands.set(command.data.name, command); } +const contextMenuCommands = new Collection(); +for (const command of contextMenuCommandsArray) { + contextMenuCommands.set(command.data.name, command); +} // When the client is ready, set status client.once("clientReady", async () => { @@ -69,6 +77,8 @@ client.once("clientReady", async () => { }); (await client.application?.commands.fetch())?.forEach((command, key) => { + // only chat input commands are mentionable, so only they need ids + if (command.type !== ApplicationCommandType.ChatInput) return; if (isCommandName(command.name)) { commandIds.set(command.name, key); } else { @@ -93,17 +103,22 @@ client.on("shardResume", () => { client.on("interactionCreate", async (interaction) => { if (!interaction.isCommand()) return; - const command = commands.get(interaction.commandName); - - if (!command) return; + const label = `COMMAND ${interaction.isChatInputCommand() ? "/" : ""}${interaction.commandName}`; try { - if (interaction instanceof ChatInputCommandInteraction) { - recordUsage(`COMMAND /${interaction.commandName}`); + if (interaction.isChatInputCommand()) { + const command = commands.get(interaction.commandName); + if (!command) return; + recordUsage(label); + await command.execute(router, interaction); + } else if (interaction.isUserContextMenuCommand()) { + const command = contextMenuCommands.get(interaction.commandName); + if (!command) return; + recordUsage(label); await command.execute(router, interaction); } } catch (error) { - recordError(`COMMAND /${interaction.commandName}`, error); + recordError(label, error); console.error(error); await interaction .reply({ diff --git a/src/routes/about/about.test.ts b/src/routes/about/about.test.ts index 06c2068..21cd558 100644 --- a/src/routes/about/about.test.ts +++ b/src/routes/about/about.test.ts @@ -11,6 +11,7 @@ vi.mock("@db/feedback", () => ({ const interaction = { user: { id: "caller" }, + isCommand: () => false, isChatInputCommand: () => false, } as unknown as Interaction; diff --git a/src/routes/delete-data/delete-data.test.ts b/src/routes/delete-data/delete-data.test.ts index e2f6934..ae70bc4 100644 --- a/src/routes/delete-data/delete-data.test.ts +++ b/src/routes/delete-data/delete-data.test.ts @@ -12,6 +12,7 @@ vi.mock("@db/users", () => ({ const interaction = { user: { id: "caller" }, + isCommand: () => false, isChatInputCommand: () => false, } as unknown as Interaction; diff --git a/src/routes/filter/filter.test.ts b/src/routes/filter/filter.test.ts index de0d393..2835d44 100644 --- a/src/routes/filter/filter.test.ts +++ b/src/routes/filter/filter.test.ts @@ -24,6 +24,7 @@ vi.mock("@db/filters", async (importOriginal) => ({ const interaction = { user: { id: "caller" }, + isCommand: () => false, isChatInputCommand: () => false, } as unknown as Interaction; diff --git a/src/routes/help/get.ts b/src/routes/help/get.ts index 4f9dfa0..7b9e37a 100644 --- a/src/routes/help/get.ts +++ b/src/routes/help/get.ts @@ -13,23 +13,23 @@ export const helpGet: Handler<"GET"> = (router, interaction, state) => { const mention = (name: CommandName) => commandMention(state.globals, name); const lines = [ - `**Ring** · ${mention("ring")} pings someone to join the voice channel you're in. Save default people and ping them with ${mention("ring_defaults")}, or turn on auto-ring to ping them automatically whenever you start a call.`, - `**Sign up** · ${mention("signup")} in a voice channel's text chat signs you up, so you get pinged when someone starts a call there (joins it while it's empty). Manage several from the Signups panel.`, + `**Ring**\nRight-click someone and pick Apps > Ring to ping them into the voice channel you're in, or run ${mention("ring")}. Save your default ringees with ${mention("default_ringees")} and ping them with ${mention("ring_defaults")}, or turn on auto-ring to ping them automatically whenever you start a call.`, + `**Sign up**\n${mention("signup")} in a voice channel's text chat signs you up, so you get pinged when someone starts a call there (joins it while it's empty). Manage several from the Signups panel.`, // role signups are a Manage Roles action, so the guidance only shows to // members who can use it ...(canManageRoleSignups(interaction) ? [ - `**Role signups** · with Manage Roles, sign a whole role up for a channel via ${mention("signuprole")} so its members get pinged.`, + `**Role signups**\nwith Manage Roles, sign a whole role up for a channel via ${mention("signuprole")} so its members get pinged.`, ] : []), - `**Filters** · ${mention("block")} stops someone from ringing you, and ${mention("whitelist")} restricts ringing to only the people you list.`, - `**Modes** · Normal pings your signed-up people when you start a call, Stealth pings no one, Auto goes stealth only while you're invisible. Set it with ${mention("mode")}.`, + `**Filters**\n${mention("block")} stops someone from ringing you, and ${mention("whitelist")} restricts ringing to only the people you list.`, + `**Modes**\nNormal pings your signed-up people when you start a call, Stealth pings no one, Auto goes stealth only while you're invisible. Set it with ${mention("mode")}.`, ]; const description = withFlash( state.queryParams, "RingVC replicates group-chat calls in Discord servers. A server can't ring people the way a group chat does, so RingVC pings them in the voice channel's text chat instead.\n\n" + - `${lines.join("\n")}\n\n` + + `${lines.join("\n\n")}\n\n` + "-# Full command list on the Catalog tab. Jump to any panel with the section menu below.", ); diff --git a/src/routes/lib/flash.test.ts b/src/routes/lib/flash.test.ts index 1bef58e..f688ec3 100644 --- a/src/routes/lib/flash.test.ts +++ b/src/routes/lib/flash.test.ts @@ -4,9 +4,18 @@ import { expect, test } from "vitest"; import { flashIcon, flashLine, flashRedirect, NOTICE } from "@routes/lib/flash"; const component = { + isCommand: () => false, + isChatInputCommand: () => false, +} as unknown as Interaction; +const command = { + isCommand: () => true, + isChatInputCommand: () => true, +} as unknown as Interaction; +// no panel behind it either, so its outcome belongs on the notice +const contextMenuCommand = { + isCommand: () => true, isChatInputCommand: () => false, } as unknown as Interaction; -const command = { isChatInputCommand: () => true } as unknown as Interaction; test("flashRedirect carries the flash text and level as redirect query params", () => { expect( @@ -37,6 +46,15 @@ test("a slash-command mutation redirects to the notice with the panel as target" }); }); +test("a context-menu mutation redirects to the notice like a slash command", () => { + expect( + flashRedirect(contextMenuCommand, "/ring", "Ringed <@1>", "success"), + ).toStrictEqual({ + redirect: NOTICE, + queryParams: { flash: "Ringed <@1>", level: "success", to: "/ring" }, + }); +}); + test("a slash-command mutation folds extra params into the notice target", () => { expect( flashRedirect(command, "/signups", "Signed up", "success", { page: "2" }), diff --git a/src/routes/lib/flash.ts b/src/routes/lib/flash.ts index 54b9b9c..77477aa 100644 --- a/src/routes/lib/flash.ts +++ b/src/routes/lib/flash.ts @@ -3,13 +3,13 @@ import { Interaction } from "discord.js"; export type FlashLevel = "success" | "warn" | "info"; -// the compact outcome view slash-command mutations land on (routes/notice) +// the compact outcome view command mutations land on (routes/notice) export const NOTICE = "/notice"; // the redirect a mutation handler returns so its outcome shows as a notice. // Component and modal interactions come from a panel, so the notice rides the // target panel's in-flight query params only — the components the GET builds -// don't carry it, so it clears on the next interaction. Slash commands never +// don't carry it, so it clears on the next interaction. Commands never // showed a panel, so they land on the compact notice view instead, with the // panel (and its extra params) folded into its `to` target export const flashRedirect = ( @@ -19,7 +19,7 @@ export const flashRedirect = ( level: FlashLevel, extraParams: Record = {}, ): RouteRedirect => { - if (interaction.isChatInputCommand()) { + if (interaction.isCommand()) { const query = new URLSearchParams(extraParams).toString(); return { redirect: NOTICE, diff --git a/src/routes/mode/mode.test.ts b/src/routes/mode/mode.test.ts index 12ba257..2071683 100644 --- a/src/routes/mode/mode.test.ts +++ b/src/routes/mode/mode.test.ts @@ -12,6 +12,7 @@ vi.mock("@db/users", () => ({ const interaction = { user: { id: "caller" }, + isCommand: () => false, isChatInputCommand: () => false, } as unknown as Interaction; diff --git a/src/routes/page-jump/page-jump.test.ts b/src/routes/page-jump/page-jump.test.ts index 2ca88c1..302abfb 100644 --- a/src/routes/page-jump/page-jump.test.ts +++ b/src/routes/page-jump/page-jump.test.ts @@ -5,6 +5,7 @@ import { pageJumpPost } from "./post"; const interaction = { user: { id: "caller" }, + isCommand: () => false, isChatInputCommand: () => false, } as unknown as Interaction; diff --git a/src/routes/ring/default/post.ts b/src/routes/ring/default/post.ts index c99ad4f..adfebd9 100644 --- a/src/routes/ring/default/post.ts +++ b/src/routes/ring/default/post.ts @@ -34,11 +34,11 @@ export const ringDefaultPost: Handler<"POST"> = async ( } catch (err) { const message = getErrorMessage(err); return message === "no default users to ring" - ? // from a slash command the notice's button leads to the default-ringees + ? // from a command the notice's button leads to the default-ringees // panel, where the missing ringees get added flashRedirect( interaction, - interaction.isChatInputCommand() ? "/ringees/global" : PANEL, + interaction.isCommand() ? "/ringees/global" : PANEL, `You have no default ringees. Add some in the Default ringees panel`, "warn", ) diff --git a/src/routes/ring/ring.test.ts b/src/routes/ring/ring.test.ts index 529efc7..b498610 100644 --- a/src/routes/ring/ring.test.ts +++ b/src/routes/ring/ring.test.ts @@ -21,6 +21,7 @@ const makeInteraction = (inVoice: boolean) => user: { id: "caller" }, member: { voice: { channel: inVoice ? voiceChannel : null } }, inGuild: () => true, + isCommand: () => false, isChatInputCommand: () => false, }) as unknown as Interaction; @@ -73,6 +74,7 @@ test("ringing defaults with none saved points the notice at the default ringees const command = { ...(makeInteraction(true) as object), + isCommand: () => true, isChatInputCommand: () => true, } as unknown as Interaction; const result = await ringDefaultPost( @@ -142,6 +144,7 @@ test("ringing from a DM flashes the server-only hint and rings nobody", async () user: { id: "caller" }, member: null, inGuild: () => false, + isCommand: () => false, isChatInputCommand: () => false, } as unknown as Interaction; const result = await ringUsersPost( diff --git a/src/routes/ringees/ringees.test.ts b/src/routes/ringees/ringees.test.ts index 84ccf1d..4cdd006 100644 --- a/src/routes/ringees/ringees.test.ts +++ b/src/routes/ringees/ringees.test.ts @@ -20,6 +20,7 @@ vi.mock("@db/default-ringees", () => ({ const interaction = { user: { id: "caller" }, + isCommand: () => false, isChatInputCommand: () => false, } as unknown as Interaction; diff --git a/src/routes/signups/signups.test.ts b/src/routes/signups/signups.test.ts index 81cbd11..484126d 100644 --- a/src/routes/signups/signups.test.ts +++ b/src/routes/signups/signups.test.ts @@ -29,6 +29,7 @@ vi.mock("@db/voice-chats", () => ({ const makeInteraction = (managesRoles: boolean) => ({ user: { id: "caller" }, + isCommand: () => false, isChatInputCommand: () => false, memberPermissions: new PermissionsBitField( managesRoles ? PermissionsBitField.Flags.ManageRoles : 0n,