fix(discord): skip disabled reaction listeners

This commit is contained in:
Peter Steinberger
2026-05-02 04:56:07 +01:00
parent 09c0b138a3
commit 16d8dcbcfc
3 changed files with 157 additions and 31 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Discord/reactions: skip reaction listener registration when DMs and group DMs are disabled and every configured guild has `reactionNotifications: "off"`, avoiding needless reaction-event queue work. Fixes #47516. Thanks @x4v13r1120.
- Telegram/streaming: keep partial preview streaming enabled for plain reply-to replies, disabling drafts only for real native quote excerpts that require Telegram quote parameters. Fixes #73505. Thanks @choury.
- Config: log the "newer OpenClaw" version warning once per process instead of once per config snapshot read. (#75927) Thanks @romneyda.
- Telegram/message actions: treat benign delete-message 400s as no-op warnings instead of runtime errors, so stale or already-removed messages do not create noisy delete failures. Fixes #73726. Thanks @Avicennasis.

View File

@@ -67,12 +67,24 @@ vi.mock("./gateway-supervisor.js", () => ({
}));
vi.mock("./listeners.js", () => ({
DiscordMessageListener: function DiscordMessageListener() {},
DiscordInteractionListener: function DiscordInteractionListener() {},
DiscordPresenceListener: function DiscordPresenceListener() {},
DiscordReactionListener: function DiscordReactionListener() {},
DiscordReactionRemoveListener: function DiscordReactionRemoveListener() {},
DiscordThreadUpdateListener: function DiscordThreadUpdateListener() {},
DiscordMessageListener: function DiscordMessageListener() {
return { type: "message" };
},
DiscordInteractionListener: function DiscordInteractionListener() {
return { type: "interaction" };
},
DiscordPresenceListener: function DiscordPresenceListener() {
return { type: "presence" };
},
DiscordReactionListener: function DiscordReactionListener() {
return { type: "reaction-add" };
},
DiscordReactionRemoveListener: function DiscordReactionRemoveListener() {
return { type: "reaction-remove" };
},
DiscordThreadUpdateListener: function DiscordThreadUpdateListener() {
return { type: "thread-update" };
},
registerDiscordListener: vi.fn(),
}));
@@ -81,13 +93,19 @@ vi.mock("./presence.js", () => ({
}));
import { createDiscordRequestClient, DISCORD_REST_TIMEOUT_MS } from "../proxy-request-client.js";
import { createDiscordMonitorClient, fetchDiscordBotIdentity } from "./provider.startup.js";
import { registerDiscordListener } from "./listeners.js";
import {
createDiscordMonitorClient,
fetchDiscordBotIdentity,
registerDiscordMonitorListeners,
} from "./provider.startup.js";
describe("createDiscordMonitorClient", () => {
beforeEach(() => {
registerVoiceClientSpy.mockReset();
waitForDiscordGatewayPluginRegistrationMock.mockReset().mockReturnValue(undefined);
vi.mocked(createDiscordRequestClient).mockClear();
vi.mocked(registerDiscordListener).mockClear();
});
function createRuntime() {
@@ -296,6 +314,92 @@ describe("createDiscordMonitorClient", () => {
});
});
describe("registerDiscordMonitorListeners", () => {
beforeEach(() => {
vi.mocked(registerDiscordListener).mockClear();
});
function createRuntime() {
return {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
}
function createListenerParams(
overrides: Partial<Parameters<typeof registerDiscordMonitorListeners>[0]> = {},
): Parameters<typeof registerDiscordMonitorListeners>[0] {
return {
cfg: {},
client: { listeners: [] },
accountId: "default",
discordConfig: {},
runtime: createRuntime(),
botUserId: "bot-1",
dmEnabled: false,
groupDmEnabled: false,
groupDmChannels: [],
dmPolicy: "disabled",
allowFrom: [],
groupPolicy: "allowlist",
guildEntries: {
"guild-1": {
id: "guild-1",
reactionNotifications: "off",
},
},
logger: {},
messageHandler: {},
...overrides,
} as Parameters<typeof registerDiscordMonitorListeners>[0];
}
function registeredListenerTypes() {
return vi.mocked(registerDiscordListener).mock.calls.map((call) => {
const listener = call[1] as { type?: string };
return listener.type;
});
}
it("skips reaction listeners when every configured guild disables reactions and DMs are off", () => {
registerDiscordMonitorListeners(createListenerParams());
expect(registeredListenerTypes()).toEqual(["interaction", "message", "thread-update"]);
});
it("keeps reaction listeners when direct messages can emit reaction notifications", () => {
registerDiscordMonitorListeners(
createListenerParams({
dmEnabled: true,
}),
);
expect(registeredListenerTypes()).toContain("reaction-add");
expect(registeredListenerTypes()).toContain("reaction-remove");
});
it("keeps reaction listeners when a configured guild enables reaction notifications", () => {
registerDiscordMonitorListeners(
createListenerParams({
guildEntries: {
"guild-1": {
id: "guild-1",
reactionNotifications: "off",
},
"guild-2": {
id: "guild-2",
reactionNotifications: "own",
},
},
}),
);
expect(registeredListenerTypes()).toContain("reaction-add");
expect(registeredListenerTypes()).toContain("reaction-remove");
});
});
describe("fetchDiscordBotIdentity", () => {
it("derives the bot id from a Discord bot token without calling /users/@me", async () => {
const fetchUser = vi.fn(async () => {

View File

@@ -263,30 +263,32 @@ export function registerDiscordMonitorListeners(params: {
new DiscordMessageListener(params.messageHandler, params.logger, params.trackInboundEvent),
);
const reactionListenerOptions: ConstructorParameters<typeof DiscordReactionListener>[0] = {
cfg: params.cfg,
accountId: params.accountId,
runtime: params.runtime,
botUserId: params.botUserId,
dmEnabled: params.dmEnabled,
groupDmEnabled: params.groupDmEnabled,
groupDmChannels: params.groupDmChannels ?? [],
dmPolicy: params.dmPolicy,
allowFrom: params.allowFrom ?? [],
groupPolicy: params.groupPolicy,
allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig),
guildEntries: params.guildEntries,
logger: params.logger,
onEvent: params.trackInboundEvent,
};
registerDiscordListener(
params.client.listeners,
new DiscordReactionListener(reactionListenerOptions),
);
registerDiscordListener(
params.client.listeners,
new DiscordReactionRemoveListener(reactionListenerOptions),
);
if (shouldRegisterDiscordReactionListeners(params)) {
const reactionListenerOptions: ConstructorParameters<typeof DiscordReactionListener>[0] = {
cfg: params.cfg,
accountId: params.accountId,
runtime: params.runtime,
botUserId: params.botUserId,
dmEnabled: params.dmEnabled,
groupDmEnabled: params.groupDmEnabled,
groupDmChannels: params.groupDmChannels ?? [],
dmPolicy: params.dmPolicy,
allowFrom: params.allowFrom ?? [],
groupPolicy: params.groupPolicy,
allowNameMatching: isDangerousNameMatchingEnabled(params.discordConfig),
guildEntries: params.guildEntries,
logger: params.logger,
onEvent: params.trackInboundEvent,
};
registerDiscordListener(
params.client.listeners,
new DiscordReactionListener(reactionListenerOptions),
);
registerDiscordListener(
params.client.listeners,
new DiscordReactionRemoveListener(reactionListenerOptions),
);
}
registerDiscordListener(
params.client.listeners,
new DiscordThreadUpdateListener(params.cfg, params.accountId, params.logger),
@@ -300,3 +302,22 @@ export function registerDiscordMonitorListeners(params: {
params.runtime.log?.("discord: GuildPresences intent enabled — presence listener registered");
}
}
function shouldRegisterDiscordReactionListeners(params: {
dmEnabled: boolean;
groupDmEnabled: boolean;
groupPolicy: "open" | "allowlist" | "disabled";
guildEntries?: Record<string, DiscordGuildEntryResolved>;
}): boolean {
if (params.dmEnabled || params.groupDmEnabled) {
return true;
}
if (params.groupPolicy === "disabled") {
return false;
}
const guildEntries = Object.values(params.guildEntries ?? {});
if (guildEntries.length === 0) {
return true;
}
return guildEntries.some((entry) => entry.reactionNotifications !== "off");
}