From dff8692613d586dd9e4759b6a3efe66509d811a9 Mon Sep 17 00:00:00 2001 From: Sebastian <19554889+sebslight@users.noreply.github.com> Date: Tue, 17 Feb 2026 08:45:12 -0500 Subject: [PATCH] fix(discord): normalize command allowFrom prefixes --- CHANGELOG.md | 1 + src/auto-reply/command-control.test.ts | 51 ++++++++++++++++++++++++++ src/channels/dock.ts | 27 ++++++++------ 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fbc8b379..0ddac6fa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai - Telegram: preserve private-chat topic `message_thread_id` on outbound sends (message/sticker/poll), keep thread-not-found retry fallback, and avoid masking `chat not found` routing errors. (#18993) Thanks @obviyus. - Discord: prevent duplicate media delivery when the model uses the `message send` tool with media, by skipping media extraction from messaging tool results since the tool already sent the message directly. (#18270) - Discord: route `audioAsVoice` auto-replies through the voice message API so opt-in audio renders as voice messages. (#18041) Thanks @zerone0x. +- Discord/Commands: normalize `commands.allowFrom` entries with `user:`/`discord:`/`pk:` prefixes and `<@id>` mentions so command authorization matches Discord allowlist behavior. (#18042) - Telegram: keep draft-stream preview replies attached to the user message for `replyToMode: "all"` in groups and DMs, preserving threaded reply context from preview through finalization. (#17880) Thanks @yinghaosang. - Telegram: prevent streaming final replies from being overwritten by later final/error payloads, and suppress fallback tool-error warnings when a recovered assistant answer already exists after tool calls. (#17883) Thanks @Marvae and @obviyus. - Telegram: debounce the first draft-stream preview update (30-char threshold) and finalize short responses by editing the stop-time preview message, improving first push notifications and avoiding duplicate final sends. (#18148) Thanks @Marvae. diff --git a/src/auto-reply/command-control.test.ts b/src/auto-reply/command-control.test.ts index b8c04a48e..d322acadd 100644 --- a/src/auto-reply/command-control.test.ts +++ b/src/auto-reply/command-control.test.ts @@ -253,6 +253,15 @@ describe("resolveCommandAuthorization", () => { } as MsgContext; } + function makeDiscordContext(senderId: string, fromOverride?: string): MsgContext { + return { + Provider: "discord", + Surface: "discord", + From: fromOverride ?? `discord:${senderId}`, + SenderId: senderId, + } as MsgContext; + } + function resolveWithCommandsAllowFrom(senderId: string, commandAuthorized: boolean) { return resolveCommandAuthorization({ ctx: makeWhatsAppContext(senderId), @@ -372,6 +381,48 @@ describe("resolveCommandAuthorization", () => { expect(auth.isAuthorizedSender).toBe(true); }); + + it("normalizes Discord commands.allowFrom prefixes and mentions", () => { + const cfg = { + commands: { + allowFrom: { + discord: ["user:123", "<@!456>", "pk:member-1"], + }, + }, + } as OpenClawConfig; + + const userAuth = resolveCommandAuthorization({ + ctx: makeDiscordContext("123"), + cfg, + commandAuthorized: false, + }); + + expect(userAuth.isAuthorizedSender).toBe(true); + + const mentionAuth = resolveCommandAuthorization({ + ctx: makeDiscordContext("456"), + cfg, + commandAuthorized: false, + }); + + expect(mentionAuth.isAuthorizedSender).toBe(true); + + const pkAuth = resolveCommandAuthorization({ + ctx: makeDiscordContext("member-1", "discord:999"), + cfg, + commandAuthorized: false, + }); + + expect(pkAuth.isAuthorizedSender).toBe(true); + + const deniedAuth = resolveCommandAuthorization({ + ctx: makeDiscordContext("other"), + cfg, + commandAuthorized: false, + }); + + expect(deniedAuth.isAuthorizedSender).toBe(false); + }); }); }); diff --git a/src/channels/dock.ts b/src/channels/dock.ts index 32ccf835b..5473cf7cd 100644 --- a/src/channels/dock.ts +++ b/src/channels/dock.ts @@ -82,6 +82,21 @@ const formatLower = (allowFrom: Array) => .filter(Boolean) .map((entry) => entry.toLowerCase()); +const formatDiscordAllowFrom = (allowFrom: Array) => + allowFrom + .map((entry) => + String(entry) + .trim() + .replace(/^<@!?/, "") + .replace(/>$/, "") + .replace(/^discord:/i, "") + .replace(/^user:/i, "") + .replace(/^pk:/i, "") + .trim() + .toLowerCase(), + ) + .filter(Boolean); + function buildDirectOrGroupThreadToolContext(params: { context: ChannelThreadingContext; hasRepliedRef: ChannelThreadingToolContext["hasRepliedRef"]; @@ -218,17 +233,7 @@ const DOCKS: Record = { String(entry), ); }, - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => - entry - .replace(/^discord:/i, "") - .replace(/^user:/i, "") - .replace(/^pk:/i, "") - .toLowerCase(), - ), + formatAllowFrom: ({ allowFrom }) => formatDiscordAllowFrom(allowFrom), }, groups: { resolveRequireMention: resolveDiscordGroupRequireMention,