diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index f8fc9576e6..4db082e32e 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -16,6 +16,7 @@ import { migrateBaseNameToDefaultAccount, normalizeAccountId, normalizeDiscordMessagingTarget, + normalizeDiscordOutboundTarget, PAIRING_APPROVED_MESSAGE, resolveDiscordAccount, resolveDefaultDiscordAccountId, @@ -291,6 +292,7 @@ export const discordPlugin: ChannelPlugin = { chunker: null, textChunkLimit: 2000, pollMaxOptions: 10, + resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), sendText: async ({ to, text, accountId, deps, replyToId, silent }) => { const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord; const result = await send(to, text, { diff --git a/src/channels/plugins/normalize/discord.ts b/src/channels/plugins/normalize/discord.ts index 43c3f67dda..1885582500 100644 --- a/src/channels/plugins/normalize/discord.ts +++ b/src/channels/plugins/normalize/discord.ts @@ -6,6 +6,29 @@ export function normalizeDiscordMessagingTarget(raw: string): string | undefined return target?.normalized; } +/** + * Normalize a Discord outbound target for delivery. Bare numeric IDs are + * prefixed with "channel:" to avoid the ambiguous-target error in + * parseDiscordTarget. All other formats pass through unchanged. + */ +export function normalizeDiscordOutboundTarget( + to?: string, +): { ok: true; to: string } | { ok: false; error: Error } { + const trimmed = to?.trim(); + if (!trimmed) { + return { + ok: false, + error: new Error( + 'Discord recipient is required. Use "channel:" for channels or "user:" for DMs.', + ), + }; + } + if (/^\d+$/.test(trimmed)) { + return { ok: true, to: `channel:${trimmed}` }; + } + return { ok: true, to: trimmed }; +} + export function looksLikeDiscordTargetId(raw: string): boolean { const trimmed = raw.trim(); if (!trimmed) { diff --git a/src/channels/plugins/outbound/discord.test.ts b/src/channels/plugins/outbound/discord.test.ts new file mode 100644 index 0000000000..dc80bd18ed --- /dev/null +++ b/src/channels/plugins/outbound/discord.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { normalizeDiscordOutboundTarget } from "../normalize/discord.js"; + +describe("normalizeDiscordOutboundTarget", () => { + it("normalizes bare numeric IDs to channel: prefix", () => { + expect(normalizeDiscordOutboundTarget("1470130713209602050")).toEqual({ + ok: true, + to: "channel:1470130713209602050", + }); + }); + + it("passes through channel: prefixed targets", () => { + expect(normalizeDiscordOutboundTarget("channel:123")).toEqual({ ok: true, to: "channel:123" }); + }); + + it("passes through user: prefixed targets", () => { + expect(normalizeDiscordOutboundTarget("user:123")).toEqual({ ok: true, to: "user:123" }); + }); + + it("passes through channel name strings", () => { + expect(normalizeDiscordOutboundTarget("general")).toEqual({ ok: true, to: "general" }); + }); + + it("returns error for empty target", () => { + expect(normalizeDiscordOutboundTarget("").ok).toBe(false); + }); + + it("returns error for undefined target", () => { + expect(normalizeDiscordOutboundTarget(undefined).ok).toBe(false); + }); + + it("trims whitespace", () => { + expect(normalizeDiscordOutboundTarget(" 123 ")).toEqual({ ok: true, to: "channel:123" }); + }); +}); diff --git a/src/channels/plugins/outbound/discord.ts b/src/channels/plugins/outbound/discord.ts index 37affea5e0..82fbdd757b 100644 --- a/src/channels/plugins/outbound/discord.ts +++ b/src/channels/plugins/outbound/discord.ts @@ -1,11 +1,13 @@ import type { ChannelOutboundAdapter } from "../types.js"; import { sendMessageDiscord, sendPollDiscord } from "../../../discord/send.js"; +import { normalizeDiscordOutboundTarget } from "../normalize/discord.js"; export const discordOutbound: ChannelOutboundAdapter = { deliveryMode: "direct", chunker: null, textChunkLimit: 2000, pollMaxOptions: 10, + resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), sendText: async ({ to, text, accountId, deps, replyToId, silent }) => { const send = deps?.sendDiscord ?? sendMessageDiscord; const result = await send(to, text, { diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index e415f44189..bd0a06036f 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -328,6 +328,7 @@ export { discordOnboardingAdapter } from "../channels/plugins/onboarding/discord export { looksLikeDiscordTargetId, normalizeDiscordMessagingTarget, + normalizeDiscordOutboundTarget, } from "../channels/plugins/normalize/discord.js"; export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js";