fix(slack): scope assistant self-event bypass

This commit is contained in:
Peter Steinberger
2026-04-25 00:03:23 +01:00
parent 893a18ff5c
commit 2a4fa8ffe8
2 changed files with 91 additions and 1 deletions

View File

@@ -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<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: 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 };
}

View File

@@ -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<string, unknown>;
middleware: unknown[] = [];
constructor(args: Record<string, unknown>) {
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);
});
});