From 2ab4f9f37099dcab944f23770d4c61136b9de4bf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 15 Jul 2026 21:29:34 -0700 Subject: [PATCH] fix(discord): resolve forum thread activity targets (#108603) * fix(discord): resolve activity thread targets * chore: keep changelog release-owned --- .../discord/src/activities/tool.test.ts | 49 +++++++++++++++++++ extensions/discord/src/activities/tool.ts | 8 ++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/extensions/discord/src/activities/tool.test.ts b/extensions/discord/src/activities/tool.test.ts index 4af08cf5ed14..a47ea6fb9637 100644 --- a/extensions/discord/src/activities/tool.test.ts +++ b/extensions/discord/src/activities/tool.test.ts @@ -71,6 +71,36 @@ describe("discord_widget", () => { }); }); + it("resolves a provider-prefixed forum thread target", async () => { + const runtime = createActivityTestRuntime(); + const send = vi.fn(async (..._args: Parameters) => ({ + messageId: "message-1", + channelId: "987654321", + receipt: {}, + })); + const tool = createDiscordWidgetTool( + discordContext({ + nativeChannelId: undefined, + deliveryContext: { channel: "discord", to: "discord:channel:987654321" }, + }), + { + runtime, + sendMessage: send as unknown as typeof sendMessageDiscord, + }, + ); + if (!tool) { + throw new Error("expected Discord widget tool"); + } + + const result = await tool.execute("forum-widget", { + html: "

Forum widget

", + title: "Forum widget", + }); + + expect(result.details).toMatchObject({ channelId: "987654321" }); + expect(send).toHaveBeenCalledWith("channel:987654321", "Forum widget", expect.any(Object)); + }); + it("keeps full documents unchanged and rejects oversized HTML", async () => { const document = "full"; const runtime = createActivityTestRuntime(); @@ -139,4 +169,23 @@ describe("discord_widget", () => { tool.execute("missing-channel", { html: "hello", title: "No channel" }), ).rejects.toThrow("requires a concrete Discord channel"); }); + + it("rejects direct-message targets without a channel", async () => { + const tool = createDiscordWidgetTool( + discordContext({ + nativeChannelId: undefined, + deliveryContext: { channel: "discord", to: "discord:user:987654321" }, + }), + { + runtime: createActivityTestRuntime(), + sendMessage: vi.fn() as unknown as typeof sendMessageDiscord, + }, + ); + if (!tool) { + throw new Error("expected Discord widget tool"); + } + await expect(tool.execute("dm-target", { html: "hello", title: "No channel" })).rejects.toThrow( + "requires a concrete Discord channel", + ); + }); }); diff --git a/extensions/discord/src/activities/tool.ts b/extensions/discord/src/activities/tool.ts index f65e2da04d31..879d4f41559b 100644 --- a/extensions/discord/src/activities/tool.ts +++ b/extensions/discord/src/activities/tool.ts @@ -7,6 +7,7 @@ import { resolveDiscordAccount } from "../accounts.js"; import { buildDiscordActivityCustomId } from "../component-custom-id.js"; import { Button, Row } from "../internal/discord.js"; import { sendMessageDiscord } from "../send.js"; +import { resolveDiscordChannelId as resolveDiscordTargetChannelId } from "../target-parsing.js"; import type { DiscordActivitiesRuntime } from "./runtime.js"; const DISCORD_WIDGET_HTML_MAX_BYTES = 48 * 1024; @@ -47,8 +48,11 @@ function resolveDiscordChannelId(context: OpenClawPluginToolContext): string | u if (!raw) { return undefined; } - const normalized = raw.replace(/^channel:/i, ""); - return /^\d+$/.test(normalized) ? normalized : undefined; + try { + return resolveDiscordTargetChannelId(raw); + } catch { + return undefined; + } } function buildDiscordWidgetDocument(title: string, html: string): string {