diff --git a/extensions/slack/src/monitor/provider-support.ts b/extensions/slack/src/monitor/provider-support.ts index bdfa8393012..5882d176a13 100644 --- a/extensions/slack/src/monitor/provider-support.ts +++ b/extensions/slack/src/monitor/provider-support.ts @@ -16,6 +16,14 @@ type SlackSocketShutdownClient = { shuttingDown?: boolean; }; type Constructor = abstract new (...args: never[]) => unknown; +type SlackSelfFilterArgs = { + context?: { + botId?: string; + botUserId?: string; + }; + event?: unknown; + message?: unknown; +}; function isConstructorFunction< // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Constructor guard preserves the requested concrete Slack constructor type. @@ -118,6 +126,39 @@ export function publishSlackDisconnectedStatus( }); } +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +export function shouldSkipOpenClawSlackSelfEvent(args: SlackSelfFilterArgs): boolean { + const botId = args.context?.botId; + const botUserId = args.context?.botUserId; + const message = asRecord(args.message); + if (message?.subtype === "bot_message" && botId && message.bot_id === botId) { + return true; + } + + const event = asRecord(args.event); + if ( + event?.type === "message" && + event.subtype === "message_changed" && + event.user === botUserId + ) { + return false; + } + + const eventsWhichShouldBeKept = new Set(["member_joined_channel", "member_left_channel"]); + return Boolean( + botUserId && + event && + event.user === botUserId && + typeof event.type === "string" && + !eventsWhichShouldBeKept.has(event.type), + ); +} + export function createSlackBoltApp(params: { interop: SlackBoltResolvedExports; slackMode: "socket" | "http"; @@ -146,6 +187,12 @@ export function createSlackBoltApp(params: { clientOptions: params.clientOptions, ignoreSelf: false, }); + app.use(async (args) => { + if (shouldSkipOpenClawSlackSelfEvent(args)) { + return; + } + await args.next(); + }); return { app, receiver }; } diff --git a/extensions/slack/src/monitor/provider.interop.test.ts b/extensions/slack/src/monitor/provider.interop.test.ts index 5e030dd4b01..0015c6fe5f9 100644 --- a/extensions/slack/src/monitor/provider.interop.test.ts +++ b/extensions/slack/src/monitor/provider.interop.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { createSlackBoltApp, resolveSlackBoltInterop } from "./provider-support.js"; +import { + createSlackBoltApp, + resolveSlackBoltInterop, + shouldSkipOpenClawSlackSelfEvent, +} from "./provider-support.js"; describe("resolveSlackBoltInterop", () => { function FakeApp() {} @@ -107,10 +111,16 @@ describe("resolveSlackBoltInterop", () => { describe("createSlackBoltApp", () => { class FakeApp { args: Record; + middleware: unknown[] = []; constructor(args: Record) { this.args = args; } + + use(middleware: unknown) { + this.middleware.push(middleware); + return this; + } } class FakeHTTPReceiver { @@ -159,6 +169,7 @@ describe("createSlackBoltApp", () => { clientOptions, ignoreSelf: false, }); + expect((app as unknown as FakeApp).middleware).toHaveLength(1); }); it("uses HTTPReceiver for webhook mode", () => { @@ -188,5 +199,37 @@ describe("createSlackBoltApp", () => { clientOptions, ignoreSelf: false, }); + expect((app as unknown as FakeApp).middleware).toHaveLength(1); + }); + + it("keeps Bolt self filtering except assistant message_changed events", () => { + expect( + shouldSkipOpenClawSlackSelfEvent({ + context: { botUserId: "U_BOT", botId: "B_BOT" }, + event: { type: "reaction_added", user: "U_BOT" }, + }), + ).toBe(true); + + expect( + shouldSkipOpenClawSlackSelfEvent({ + context: { botUserId: "U_BOT", botId: "B_BOT" }, + event: { type: "message", subtype: "message_changed", user: "U_BOT" }, + }), + ).toBe(false); + + expect( + shouldSkipOpenClawSlackSelfEvent({ + context: { botUserId: "U_BOT", botId: "B_BOT" }, + event: { type: "message", user: "U_BOT" }, + }), + ).toBe(true); + + expect( + shouldSkipOpenClawSlackSelfEvent({ + context: { botUserId: "U_BOT", botId: "B_BOT" }, + event: { type: "message", user: "U_OTHER" }, + message: { subtype: "bot_message", bot_id: "B_BOT" }, + }), + ).toBe(true); }); });