mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 06:07:28 +00:00
refactor(discord): dedupe voice command runtime checks
This commit is contained in:
99
src/discord/voice/command.test.ts
Normal file
99
src/discord/voice/command.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { CommandInteraction, CommandWithSubcommands } from "@buape/carbon";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createDiscordVoiceCommand } from "./command.js";
|
||||
import type { DiscordVoiceManager } from "./manager.js";
|
||||
|
||||
function findVoiceSubcommand(command: CommandWithSubcommands, name: string) {
|
||||
const subcommands = (
|
||||
command as unknown as { subcommands?: Array<{ name: string; run: unknown }> }
|
||||
).subcommands;
|
||||
const subcommand = subcommands?.find((entry) => entry.name === name) as
|
||||
| { run: (interaction: CommandInteraction) => Promise<void> }
|
||||
| undefined;
|
||||
if (!subcommand) {
|
||||
throw new Error(`Missing vc ${name} subcommand`);
|
||||
}
|
||||
return subcommand;
|
||||
}
|
||||
|
||||
function createVoiceCommandHarness(manager: DiscordVoiceManager | null = null) {
|
||||
const command = createDiscordVoiceCommand({
|
||||
cfg: {},
|
||||
discordConfig: {},
|
||||
accountId: "default",
|
||||
groupPolicy: "open",
|
||||
useAccessGroups: false,
|
||||
getManager: () => manager,
|
||||
ephemeralDefault: true,
|
||||
});
|
||||
return {
|
||||
command,
|
||||
leave: findVoiceSubcommand(command, "leave"),
|
||||
status: findVoiceSubcommand(command, "status"),
|
||||
};
|
||||
}
|
||||
|
||||
function createInteraction(overrides?: Partial<CommandInteraction>): {
|
||||
interaction: CommandInteraction;
|
||||
reply: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const reply = vi.fn(async () => undefined);
|
||||
const interaction = {
|
||||
guild: undefined,
|
||||
user: { id: "u1", username: "tester" },
|
||||
rawData: { member: { roles: [] } },
|
||||
reply,
|
||||
...overrides,
|
||||
} as unknown as CommandInteraction;
|
||||
return { interaction, reply };
|
||||
}
|
||||
|
||||
describe("createDiscordVoiceCommand", () => {
|
||||
it("vc leave reports missing guild before manager lookup", async () => {
|
||||
const { leave } = createVoiceCommandHarness(null);
|
||||
const { interaction, reply } = createInteraction();
|
||||
|
||||
await leave.run(interaction);
|
||||
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "Unable to resolve guild for this command.",
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("vc status reports unavailable voice manager", async () => {
|
||||
const { status } = createVoiceCommandHarness(null);
|
||||
const { interaction, reply } = createInteraction({
|
||||
guild: { id: "g1" } as CommandInteraction["guild"],
|
||||
});
|
||||
|
||||
await status.run(interaction);
|
||||
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "Voice manager is not available yet.",
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("vc status reports no active sessions when manager has none", async () => {
|
||||
const statusSpy = vi.fn(() => []);
|
||||
const manager = {
|
||||
status: statusSpy,
|
||||
} as unknown as DiscordVoiceManager;
|
||||
const { status } = createVoiceCommandHarness(manager);
|
||||
const { interaction, reply } = createInteraction({
|
||||
guild: { id: "g1", name: "Guild" } as CommandInteraction["guild"],
|
||||
});
|
||||
|
||||
await status.run(interaction);
|
||||
|
||||
expect(statusSpy).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledWith({
|
||||
content: "No active voice sessions.",
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,6 +48,11 @@ type VoiceCommandChannelOverride = {
|
||||
parentId?: string;
|
||||
};
|
||||
|
||||
type VoiceCommandRuntimeContext = {
|
||||
guildId: string;
|
||||
manager: DiscordVoiceManager;
|
||||
};
|
||||
|
||||
async function authorizeVoiceCommand(
|
||||
interaction: CommandInteraction,
|
||||
params: VoiceCommandContext,
|
||||
@@ -185,6 +190,47 @@ async function authorizeVoiceCommand(
|
||||
return { ok: true, guildId: interaction.guild.id };
|
||||
}
|
||||
|
||||
async function resolveVoiceCommandRuntimeContext(
|
||||
interaction: CommandInteraction,
|
||||
params: Pick<VoiceCommandContext, "getManager">,
|
||||
): Promise<VoiceCommandRuntimeContext | null> {
|
||||
const guildId = interaction.guild?.id;
|
||||
if (!guildId) {
|
||||
await interaction.reply({
|
||||
content: "Unable to resolve guild for this command.",
|
||||
ephemeral: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
const manager = params.getManager();
|
||||
if (!manager) {
|
||||
await interaction.reply({
|
||||
content: "Voice manager is not available yet.",
|
||||
ephemeral: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
return { guildId, manager };
|
||||
}
|
||||
|
||||
async function ensureVoiceCommandAccess(params: {
|
||||
interaction: CommandInteraction;
|
||||
context: VoiceCommandContext;
|
||||
channelOverride?: VoiceCommandChannelOverride;
|
||||
}): Promise<boolean> {
|
||||
const access = await authorizeVoiceCommand(params.interaction, params.context, {
|
||||
channelOverride: params.channelOverride,
|
||||
});
|
||||
if (access.ok) {
|
||||
return true;
|
||||
}
|
||||
await params.interaction.reply({
|
||||
content: access.message ?? "Not authorized.",
|
||||
ephemeral: true,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export function createDiscordVoiceCommand(params: VoiceCommandContext): CommandWithSubcommands {
|
||||
const resolveSessionChannelId = (manager: DiscordVoiceManager, guildId: string) =>
|
||||
manager.status().find((entry) => entry.guildId === guildId)?.channelId;
|
||||
@@ -259,31 +305,23 @@ export function createDiscordVoiceCommand(params: VoiceCommandContext): CommandW
|
||||
ephemeral = params.ephemeralDefault;
|
||||
|
||||
async run(interaction: CommandInteraction) {
|
||||
const guildId = interaction.guild?.id;
|
||||
if (!guildId) {
|
||||
await interaction.reply({
|
||||
content: "Unable to resolve guild for this command.",
|
||||
ephemeral: true,
|
||||
});
|
||||
const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
|
||||
if (!runtimeContext) {
|
||||
return;
|
||||
}
|
||||
const manager = params.getManager();
|
||||
if (!manager) {
|
||||
await interaction.reply({
|
||||
content: "Voice manager is not available yet.",
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const sessionChannelId = resolveSessionChannelId(manager, guildId);
|
||||
const access = await authorizeVoiceCommand(interaction, params, {
|
||||
const sessionChannelId = resolveSessionChannelId(
|
||||
runtimeContext.manager,
|
||||
runtimeContext.guildId,
|
||||
);
|
||||
const authorized = await ensureVoiceCommandAccess({
|
||||
interaction,
|
||||
context: params,
|
||||
channelOverride: sessionChannelId ? { id: sessionChannelId } : undefined,
|
||||
});
|
||||
if (!access.ok) {
|
||||
await interaction.reply({ content: access.message ?? "Not authorized.", ephemeral: true });
|
||||
if (!authorized) {
|
||||
return;
|
||||
}
|
||||
const result = await manager.leave({ guildId });
|
||||
const result = await runtimeContext.manager.leave({ guildId: runtimeContext.guildId });
|
||||
await interaction.reply({ content: result.message, ephemeral: true });
|
||||
}
|
||||
}
|
||||
@@ -295,29 +333,20 @@ export function createDiscordVoiceCommand(params: VoiceCommandContext): CommandW
|
||||
ephemeral = params.ephemeralDefault;
|
||||
|
||||
async run(interaction: CommandInteraction) {
|
||||
const guildId = interaction.guild?.id;
|
||||
if (!guildId) {
|
||||
await interaction.reply({
|
||||
content: "Unable to resolve guild for this command.",
|
||||
ephemeral: true,
|
||||
});
|
||||
const runtimeContext = await resolveVoiceCommandRuntimeContext(interaction, params);
|
||||
if (!runtimeContext) {
|
||||
return;
|
||||
}
|
||||
const manager = params.getManager();
|
||||
if (!manager) {
|
||||
await interaction.reply({
|
||||
content: "Voice manager is not available yet.",
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const sessions = manager.status().filter((entry) => entry.guildId === guildId);
|
||||
const sessions = runtimeContext.manager
|
||||
.status()
|
||||
.filter((entry) => entry.guildId === runtimeContext.guildId);
|
||||
const sessionChannelId = sessions[0]?.channelId;
|
||||
const access = await authorizeVoiceCommand(interaction, params, {
|
||||
const authorized = await ensureVoiceCommandAccess({
|
||||
interaction,
|
||||
context: params,
|
||||
channelOverride: sessionChannelId ? { id: sessionChannelId } : undefined,
|
||||
});
|
||||
if (!access.ok) {
|
||||
await interaction.reply({ content: access.message ?? "Not authorized.", ephemeral: true });
|
||||
if (!authorized) {
|
||||
return;
|
||||
}
|
||||
if (sessions.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user