mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 15:41:40 +00:00
Fix Discord native commands bypassing group DM channel allowlist (#57735)
* Fix Discord native commands bypassing group DM channel allowlist * Fix linting * Update tests
This commit is contained in:
@@ -43,10 +43,20 @@ function createNativeCommand(
|
||||
if (!command) {
|
||||
throw new Error(`missing native command: ${name}`);
|
||||
}
|
||||
const cfg = (opts?.cfg ?? {}) as ReturnType<typeof loadConfig>;
|
||||
const discordConfig = (opts?.discordConfig ?? {}) as NonNullable<
|
||||
const baseCfg = (opts?.cfg ?? {}) as ReturnType<typeof loadConfig>;
|
||||
const discordConfig = (opts?.discordConfig ?? baseCfg.channels?.discord ?? {}) as NonNullable<
|
||||
OpenClawConfig["channels"]
|
||||
>["discord"];
|
||||
const cfg =
|
||||
opts?.discordConfig === undefined
|
||||
? baseCfg
|
||||
: ({
|
||||
...baseCfg,
|
||||
channels: {
|
||||
...baseCfg.channels,
|
||||
discord: discordConfig,
|
||||
},
|
||||
} as ReturnType<typeof loadConfig>);
|
||||
return createDiscordNativeCommand({
|
||||
command,
|
||||
cfg,
|
||||
@@ -199,6 +209,57 @@ describe("createDiscordNativeCommand option wiring", () => {
|
||||
expect(respond).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it("returns no autocomplete choices for group DMs outside dm.groupChannels", async () => {
|
||||
const discordConfig = {
|
||||
dm: {
|
||||
enabled: true,
|
||||
policy: "open",
|
||||
groupEnabled: true,
|
||||
groupChannels: ["allowed-group"],
|
||||
},
|
||||
} satisfies NonNullable<OpenClawConfig["channels"]>["discord"];
|
||||
const command = createNativeCommand("think", {
|
||||
cfg: {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
discord: ["user:allowed-user"],
|
||||
},
|
||||
},
|
||||
} as ReturnType<typeof loadConfig>,
|
||||
discordConfig,
|
||||
});
|
||||
const level = requireOption(command, "level");
|
||||
const autocomplete = readAutocomplete(level);
|
||||
if (typeof autocomplete !== "function") {
|
||||
throw new Error("think level option did not wire autocomplete");
|
||||
}
|
||||
const respond = vi.fn(async (_choices: unknown[]) => undefined);
|
||||
|
||||
await autocomplete({
|
||||
user: {
|
||||
id: "allowed-user",
|
||||
username: "allowed",
|
||||
globalName: "Allowed",
|
||||
},
|
||||
channel: {
|
||||
type: ChannelType.GroupDM,
|
||||
id: "blocked-group",
|
||||
name: "Blocked Group",
|
||||
},
|
||||
guild: undefined,
|
||||
rawData: {
|
||||
member: { roles: [] },
|
||||
},
|
||||
options: {
|
||||
getFocused: () => ({ value: "xh" }),
|
||||
},
|
||||
respond,
|
||||
client: {},
|
||||
} as never);
|
||||
|
||||
expect(respond).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it("truncates Discord command and option descriptions to Discord's limit", () => {
|
||||
const longDescription = "x".repeat(140);
|
||||
const cfg = {} as ReturnType<typeof loadConfig>;
|
||||
|
||||
@@ -466,6 +466,41 @@ describe("Discord native plugin command dispatch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects group DM slash commands outside dm.groupChannels before dispatch", async () => {
|
||||
const cfg = {
|
||||
commands: {
|
||||
allowFrom: {
|
||||
discord: ["user:owner"],
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
dm: {
|
||||
enabled: true,
|
||||
policy: "open",
|
||||
groupEnabled: true,
|
||||
groupChannels: ["allowed-group"],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const interaction = createInteraction({
|
||||
channelType: ChannelType.GroupDM,
|
||||
channelId: "blocked-group",
|
||||
});
|
||||
const dispatchSpy = createDispatchSpy();
|
||||
const command = await createStatusCommand(cfg);
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(dispatchSpy).not.toHaveBeenCalled();
|
||||
expect(interaction.reply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: "This group DM is not allowed.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("executes matched plugin commands directly without invoking the agent dispatcher", async () => {
|
||||
const cfg = createConfig();
|
||||
const commandSpec: NativeCommandSpec = {
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
isDiscordGroupAllowedByPolicy,
|
||||
normalizeDiscordAllowList,
|
||||
normalizeDiscordSlug,
|
||||
resolveGroupDmAllow,
|
||||
resolveDiscordChannelConfigWithFallback,
|
||||
resolveDiscordAllowListMatch,
|
||||
resolveDiscordGuildEntry,
|
||||
@@ -283,6 +284,33 @@ function shouldBypassConfiguredAcpEnsure(commandName: string): boolean {
|
||||
return normalized === "acp" || normalized === "new" || normalized === "reset";
|
||||
}
|
||||
|
||||
function resolveDiscordNativeGroupDmAccess(params: {
|
||||
isGroupDm: boolean;
|
||||
groupEnabled?: boolean;
|
||||
groupChannels?: string[];
|
||||
channelId: string;
|
||||
channelName?: string;
|
||||
channelSlug: string;
|
||||
}): { allowed: true } | { allowed: false; reason: "disabled" | "not-allowlisted" } {
|
||||
if (!params.isGroupDm) {
|
||||
return { allowed: true };
|
||||
}
|
||||
if (params.groupEnabled === false) {
|
||||
return { allowed: false, reason: "disabled" };
|
||||
}
|
||||
if (
|
||||
!resolveGroupDmAllow({
|
||||
channels: params.groupChannels,
|
||||
channelId: params.channelId,
|
||||
channelName: params.channelName,
|
||||
channelSlug: params.channelSlug,
|
||||
})
|
||||
) {
|
||||
return { allowed: false, reason: "not-allowlisted" };
|
||||
}
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
async function resolveDiscordNativeAutocompleteAuthorized(params: {
|
||||
interaction: AutocompleteInteraction;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
@@ -421,7 +449,15 @@ async function resolveDiscordNativeAutocompleteAuthorized(params: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isGroupDm && discordConfig?.dm?.groupEnabled === false) {
|
||||
const groupDmAccess = resolveDiscordNativeGroupDmAccess({
|
||||
isGroupDm,
|
||||
groupEnabled: discordConfig?.dm?.groupEnabled,
|
||||
groupChannels: discordConfig?.dm?.groupChannels,
|
||||
channelId: rawChannelId,
|
||||
channelName,
|
||||
channelSlug,
|
||||
});
|
||||
if (!groupDmAccess.allowed) {
|
||||
return false;
|
||||
}
|
||||
if (!isDirectMessage) {
|
||||
@@ -832,6 +868,22 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const groupDmAccess = resolveDiscordNativeGroupDmAccess({
|
||||
isGroupDm,
|
||||
groupEnabled: discordConfig?.dm?.groupEnabled,
|
||||
groupChannels: discordConfig?.dm?.groupChannels,
|
||||
channelId: rawChannelId,
|
||||
channelName,
|
||||
channelSlug,
|
||||
});
|
||||
if (!groupDmAccess.allowed) {
|
||||
await respond(
|
||||
groupDmAccess.reason === "disabled"
|
||||
? "Discord group DMs are disabled."
|
||||
: "This group DM is not allowed.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isDirectMessage) {
|
||||
const { hasAccessRestrictions, memberAllowed } = resolveDiscordMemberAccessState({
|
||||
channelConfig,
|
||||
@@ -866,10 +918,6 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isGroupDm && discordConfig?.dm?.groupEnabled === false) {
|
||||
await respond("Discord group DMs are disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const menu = resolveCommandArgMenu({
|
||||
command,
|
||||
|
||||
Reference in New Issue
Block a user