diff --git a/src/channels/plugins/outbound/discord.ts b/src/channels/plugins/outbound/discord.ts index bc8126d4d3d..fbb1415326d 100644 --- a/src/channels/plugins/outbound/discord.ts +++ b/src/channels/plugins/outbound/discord.ts @@ -6,22 +6,24 @@ export const discordOutbound: ChannelOutboundAdapter = { chunker: null, textChunkLimit: 2000, pollMaxOptions: 10, - sendText: async ({ to, text, accountId, deps, replyToId }) => { + sendText: async ({ to, text, accountId, deps, replyToId, silent }) => { const send = deps?.sendDiscord ?? sendMessageDiscord; const result = await send(to, text, { verbose: false, replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, + silent: silent ?? undefined, }); return { channel: "discord", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId }) => { + sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, silent }) => { const send = deps?.sendDiscord ?? sendMessageDiscord; const result = await send(to, text, { verbose: false, mediaUrl, replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, + silent: silent ?? undefined, }); return { channel: "discord", ...result }; }, diff --git a/src/channels/plugins/types.adapters.ts b/src/channels/plugins/types.adapters.ts index ab1473bf1ef..d9ad5a0a527 100644 --- a/src/channels/plugins/types.adapters.ts +++ b/src/channels/plugins/types.adapters.ts @@ -80,6 +80,7 @@ export type ChannelOutboundContext = { threadId?: string | number | null; accountId?: string | null; deps?: OutboundSendDeps; + silent?: boolean; }; export type ChannelOutboundPayloadContext = ChannelOutboundContext & { diff --git a/src/discord/send.shared.ts b/src/discord/send.shared.ts index 7e3b059363e..872cfb9668c 100644 --- a/src/discord/send.shared.ts +++ b/src/discord/send.shared.ts @@ -278,6 +278,9 @@ async function resolveChannelId( return { channelId: dmChannel.id, dm: true }; } +// Discord message flag for silent/suppress notifications +const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12; + export function buildDiscordTextChunks( text: string, opts: { maxLinesPerMessage?: number; chunkMode?: ChunkMode; maxChars?: number } = {}, @@ -305,11 +308,13 @@ async function sendDiscordText( maxLinesPerMessage?: number, embeds?: unknown[], chunkMode?: ChunkMode, + silent?: boolean, ) { if (!text.trim()) { throw new Error("Message must be non-empty for Discord sends"); } const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined; + const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined; const chunks = buildDiscordTextChunks(text, { maxLinesPerMessage, chunkMode }); if (chunks.length === 1) { const res = (await request( @@ -319,6 +324,7 @@ async function sendDiscordText( content: chunks[0], message_reference: messageReference, ...(embeds?.length ? { embeds } : {}), + ...(flags ? { flags } : {}), }, }) as Promise<{ id: string; channel_id: string }>, "text", @@ -335,6 +341,7 @@ async function sendDiscordText( content: chunk, message_reference: isFirst ? messageReference : undefined, ...(isFirst && embeds?.length ? { embeds } : {}), + ...(flags ? { flags } : {}), }, }) as Promise<{ id: string; channel_id: string }>, "text", @@ -357,12 +364,14 @@ async function sendDiscordMedia( maxLinesPerMessage?: number, embeds?: unknown[], chunkMode?: ChunkMode, + silent?: boolean, ) { const media = await loadWebMedia(mediaUrl); const chunks = text ? buildDiscordTextChunks(text, { maxLinesPerMessage, chunkMode }) : []; const caption = chunks[0] ?? ""; const hasCaption = caption.trim().length > 0; const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined; + const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined; const res = (await request( () => rest.post(Routes.channelMessages(channelId), { @@ -373,6 +382,7 @@ async function sendDiscordMedia( ...(hasCaption ? { content: caption } : {}), ...(messageReference ? { message_reference: messageReference } : {}), ...(embeds?.length ? { embeds } : {}), + ...(flags ? { flags } : {}), files: [ { data: media.buffer, @@ -396,6 +406,7 @@ async function sendDiscordMedia( maxLinesPerMessage, undefined, chunkMode, + silent, ); } return res; diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index a9872530f5a..6460efc01a0 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -86,6 +86,7 @@ async function createChannelHandler(params: { threadId?: string | number | null; deps?: OutboundSendDeps; gifPlayback?: boolean; + silent?: boolean; }): Promise { const outbound = await loadChannelOutboundAdapter(params.channel); if (!outbound?.sendText || !outbound?.sendMedia) { @@ -101,6 +102,7 @@ async function createChannelHandler(params: { threadId: params.threadId, deps: params.deps, gifPlayback: params.gifPlayback, + silent: params.silent, }); if (!handler) { throw new Error(`Outbound not configured for channel: ${params.channel}`); @@ -118,6 +120,7 @@ function createPluginHandler(params: { threadId?: string | number | null; deps?: OutboundSendDeps; gifPlayback?: boolean; + silent?: boolean; }): ChannelHandler | null { const outbound = params.outbound; if (!outbound?.sendText || !outbound?.sendMedia) { @@ -143,6 +146,7 @@ function createPluginHandler(params: { threadId: params.threadId, gifPlayback: params.gifPlayback, deps: params.deps, + silent: params.silent, payload, }) : undefined, @@ -156,6 +160,7 @@ function createPluginHandler(params: { threadId: params.threadId, gifPlayback: params.gifPlayback, deps: params.deps, + silent: params.silent, }), sendMedia: async (caption, mediaUrl) => sendMedia({ @@ -168,6 +173,7 @@ function createPluginHandler(params: { threadId: params.threadId, gifPlayback: params.gifPlayback, deps: params.deps, + silent: params.silent, }), }; } @@ -192,6 +198,7 @@ export async function deliverOutboundPayloads(params: { text?: string; mediaUrls?: string[]; }; + silent?: boolean; }): Promise { const { cfg, channel, to, payloads } = params; const accountId = params.accountId; @@ -208,6 +215,7 @@ export async function deliverOutboundPayloads(params: { replyToId: params.replyToId, threadId: params.threadId, gifPlayback: params.gifPlayback, + silent: params.silent, }); const textLimit = handler.chunker ? resolveTextChunkLimit(cfg, channel, accountId, { diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index bf9c33265da..a86bdc31ed6 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -820,6 +820,7 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise): unknown { @@ -128,6 +129,7 @@ export async function executeSendAction(params: { gateway: params.ctx.gateway, mirror: params.ctx.mirror, abortSignal: params.ctx.abortSignal, + silent: params.ctx.silent, }); return {