From 3efb4440028b45f6521dba48223c5723af64b7b7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 02:36:39 +0100 Subject: [PATCH] fix(discord): skip disabled reaction fetches --- CHANGELOG.md | 1 + extensions/discord/src/monitor.test.ts | 28 ++++++++++ extensions/discord/src/monitor/listeners.ts | 59 +++++++++++++++++++-- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75689643dba..466ad10d07b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai - CLI/status: show skipped fast-path memory checks as `not checked` and report active custom memory plugin runtime status from `status --json --all` without requiring built-in `agents.defaults.memorySearch`, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius. - Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler. - Plugins/package: force nested bundled-plugin runtime dependency installs out of inherited npm dry-run mode during prepack and package smoke checks, so packed installs materialize required plugin modules instead of reporting missing bundled files. Refs #73128. Thanks @Adam-Researchh. +- Discord: skip reaction events before REST channel fetch when notifications are off, guild reactions are disabled, or allowlist mode cannot match without channel overrides, reducing reconnect bursts that caused slow listener warnings. Fixes #73133. Thanks @isaacsummers. - Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts. - Agents/models: classify empty, reasoning-only, and planning-only terminal agent runs before accepting a model fallback candidate, so invalid or incompatible models can advance to the next configured fallback instead of returning a 30-second terminal failure. Fixes #73115. Thanks @vdruts. - Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin `embedding.apiKey`, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai. diff --git a/extensions/discord/src/monitor.test.ts b/extensions/discord/src/monitor.test.ts index d89b1c46a01..e58eda2f55c 100644 --- a/extensions/discord/src/monitor.test.ts +++ b/extensions/discord/src/monitor.test.ts @@ -985,6 +985,10 @@ function makeReactionClient(options?: { } as unknown as DiscordReactionClient; } +function getReactionClientFetchChannelMock(client: DiscordReactionClient) { + return (client as unknown as { fetchChannel: ReturnType }).fetchChannel; +} + function makeReactionListenerParams(overrides?: { botUserId?: string; dmEnabled?: boolean; @@ -1127,6 +1131,7 @@ describe("discord DM reaction handling", () => { await listener.handle(data, client); + expect(getReactionClientFetchChannelMock(client)).not.toHaveBeenCalled(); expect(enqueueSystemEventSpy).not.toHaveBeenCalled(); }); @@ -1191,6 +1196,7 @@ describe("discord DM reaction handling", () => { await listener.handle(data, client); + expect(getReactionClientFetchChannelMock(client)).toHaveBeenCalled(); expect(enqueueSystemEventSpy).toHaveBeenCalledOnce(); const [text] = enqueueSystemEventSpy.mock.calls[0]; expect(text).toContain("Discord reaction added"); @@ -1251,6 +1257,7 @@ describe("discord reaction notification modes", () => { channelId: string | undefined; parentId: string | undefined; messageAuthorId: string; + expectedFetchChannelCalls: number; expectedMessageFetchCalls: number; expectedEnqueueCalls: number; }>([ @@ -1263,6 +1270,7 @@ describe("discord reaction notification modes", () => { channelId: undefined, parentId: undefined, messageAuthorId: "other-user", + expectedFetchChannelCalls: 0, expectedMessageFetchCalls: 0, expectedEnqueueCalls: 0, }, @@ -1275,6 +1283,7 @@ describe("discord reaction notification modes", () => { channelId: undefined, parentId: undefined, messageAuthorId: "other-user", + expectedFetchChannelCalls: 1, expectedMessageFetchCalls: 0, expectedEnqueueCalls: 1, }, @@ -1287,9 +1296,23 @@ describe("discord reaction notification modes", () => { channelId: undefined, parentId: undefined, messageAuthorId: "other-user", + expectedFetchChannelCalls: 1, expectedMessageFetchCalls: 0, expectedEnqueueCalls: 1, }, + { + name: "allowlist mode denied without channel overrides", + reactionNotifications: "allowlist" as const, + users: ["trusted-user"] as string[], + userId: "untrusted-user", + channelType: ChannelType.GuildText, + channelId: undefined, + parentId: undefined, + messageAuthorId: "other-user", + expectedFetchChannelCalls: 0, + expectedMessageFetchCalls: 0, + expectedEnqueueCalls: 0, + }, { name: "own mode", reactionNotifications: "own" as const, @@ -1299,6 +1322,7 @@ describe("discord reaction notification modes", () => { channelId: undefined, parentId: undefined, messageAuthorId: "bot-1", + expectedFetchChannelCalls: 1, expectedMessageFetchCalls: 1, expectedEnqueueCalls: 1, }, @@ -1311,6 +1335,7 @@ describe("discord reaction notification modes", () => { channelId: "thread-1", parentId: "parent-1", messageAuthorId: "other-user", + expectedFetchChannelCalls: 2, expectedMessageFetchCalls: 0, expectedEnqueueCalls: 1, }, @@ -1344,6 +1369,9 @@ describe("discord reaction notification modes", () => { await listener.handle(data, client); + expect(getReactionClientFetchChannelMock(client), testCase.name).toHaveBeenCalledTimes( + testCase.expectedFetchChannelCalls, + ); expect(messageFetch, testCase.name).toHaveBeenCalledTimes(testCase.expectedMessageFetchCalls); expect(enqueueSystemEventSpy, testCase.name).toHaveBeenCalledTimes( testCase.expectedEnqueueCalls, diff --git a/extensions/discord/src/monitor/listeners.ts b/extensions/discord/src/monitor/listeners.ts index e92fc9bb81b..3d533d67f5c 100644 --- a/extensions/discord/src/monitor/listeners.ts +++ b/extensions/discord/src/monitor/listeners.ts @@ -521,6 +521,42 @@ async function handleDiscordChannelReactionNotification(params: { params.emitReactionWithAuthor(message); } +function hasDiscordGuildChannelOverrides( + guildInfo: import("./allow-list.js").DiscordGuildEntryResolved | null, +) { + return Boolean(guildInfo?.channels && Object.keys(guildInfo.channels).length > 0); +} + +function shouldSkipGuildReactionBeforeChannelFetch(params: { + reactionMode: DiscordReactionMode; + guildInfo: import("./allow-list.js").DiscordGuildEntryResolved | null; + groupPolicy: DiscordReactionRoutingParams["groupPolicy"]; + memberRoleIds: string[]; + user: User; + botUserId?: string; + allowNameMatching: boolean; +}) { + if (params.reactionMode === "off" || params.groupPolicy === "disabled") { + return true; + } + if (params.reactionMode !== "allowlist") { + return false; + } + if (hasDiscordGuildChannelOverrides(params.guildInfo)) { + return false; + } + return !shouldEmitDiscordReactionNotification({ + mode: params.reactionMode, + botId: params.botUserId, + userId: params.user.id, + userName: params.user.username, + userTag: formatDiscordUserTag(params.user), + guildInfo: params.guildInfo, + memberRoleIds: params.memberRoleIds, + allowNameMatching: params.allowNameMatching, + }); +} + async function handleDiscordReactionEvent( params: { data: DiscordReactionEvent; @@ -556,6 +592,24 @@ async function handleDiscordReactionEvent( if (isGuildMessage && guildEntries && Object.keys(guildEntries).length > 0 && !guildInfo) { return; } + const memberRoleIds = Array.isArray(data.rawMember?.roles) + ? data.rawMember.roles.map((roleId: string) => roleId) + : []; + const reactionMode = guildInfo?.reactionNotifications ?? "own"; + if ( + isGuildMessage && + shouldSkipGuildReactionBeforeChannelFetch({ + reactionMode, + guildInfo, + groupPolicy: params.groupPolicy, + memberRoleIds, + user, + botUserId, + allowNameMatching: params.allowNameMatching, + }) + ) { + return; + } const channel = await client.fetchChannel(data.channel_id); if (!channel) { @@ -572,9 +626,6 @@ async function handleDiscordReactionEvent( const isDirectMessage = channelType === ChannelType.DM; const isGroupDm = channelType === ChannelType.GroupDM; const isThreadChannel = channelContext.isThreadChannel; - const memberRoleIds = Array.isArray(data.rawMember?.roles) - ? data.rawMember.roles.map((roleId: string) => roleId) - : []; const reactionIngressBase: Omit = { accountId: params.accountId, user, @@ -695,7 +746,6 @@ async function handleDiscordReactionEvent( }; if (isThreadChannel) { - const reactionMode = guildInfo?.reactionNotifications ?? "own"; await handleDiscordThreadReactionNotification({ reactionMode, message: data.message, @@ -720,7 +770,6 @@ async function handleDiscordReactionEvent( parentSlug, scope: "channel", }); - const reactionMode = guildInfo?.reactionNotifications ?? "own"; await handleDiscordChannelReactionNotification({ isGuildMessage, reactionMode,