mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 03:40:21 +00:00
fix(slack): override wrong channel_type for D-prefix DM channels
This commit is contained in:
committed by
Peter Steinberger
parent
8cc841766c
commit
f33d0a884e
@@ -38,15 +38,20 @@ export function normalizeSlackChannelType(
|
||||
channelId?: string | null,
|
||||
): SlackMessageEvent["channel_type"] {
|
||||
const normalized = channelType?.trim().toLowerCase();
|
||||
const inferred = inferSlackChannelType(channelId);
|
||||
if (
|
||||
normalized === "im" ||
|
||||
normalized === "mpim" ||
|
||||
normalized === "channel" ||
|
||||
normalized === "group"
|
||||
) {
|
||||
// D-prefix channel IDs are always DMs — override a contradicting channel_type.
|
||||
if (inferred === "im" && normalized !== "im") {
|
||||
return "im";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
return inferSlackChannelType(channelId) ?? "channel";
|
||||
return inferred ?? "channel";
|
||||
}
|
||||
|
||||
export type SlackMonitorContext = {
|
||||
|
||||
@@ -264,6 +264,164 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
expect(untrusted).toContain("Do dangerous things");
|
||||
});
|
||||
|
||||
it("classifies D-prefix DMs correctly even when channel_type is wrong", async () => {
|
||||
const slackCtx = createSlackMonitorContext({
|
||||
cfg: {
|
||||
channels: { slack: { enabled: true } },
|
||||
session: { dmScope: "main" },
|
||||
} as OpenClawConfig,
|
||||
accountId: "default",
|
||||
botToken: "token",
|
||||
app: { client: {} } as App,
|
||||
runtime: {} as RuntimeEnv,
|
||||
botUserId: "B1",
|
||||
teamId: "T1",
|
||||
apiAppId: "A1",
|
||||
historyLimit: 0,
|
||||
sessionScope: "per-sender",
|
||||
mainKey: "main",
|
||||
dmEnabled: true,
|
||||
dmPolicy: "open",
|
||||
allowFrom: [],
|
||||
groupDmEnabled: true,
|
||||
groupDmChannels: [],
|
||||
defaultRequireMention: true,
|
||||
groupPolicy: "open",
|
||||
useAccessGroups: false,
|
||||
reactionMode: "off",
|
||||
reactionAllowlist: [],
|
||||
replyToMode: "off",
|
||||
threadHistoryScope: "thread",
|
||||
threadInheritParent: false,
|
||||
slashCommand: {
|
||||
enabled: false,
|
||||
name: "openclaw",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true,
|
||||
},
|
||||
textLimit: 4000,
|
||||
ackReactionScope: "group-mentions",
|
||||
mediaMaxBytes: 1024,
|
||||
removeAckAfterReply: false,
|
||||
});
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
// Simulate API returning correct type for DM channel
|
||||
slackCtx.resolveChannelName = async () => ({ name: undefined, type: "im" as const });
|
||||
|
||||
const account: ResolvedSlackAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
botTokenSource: "config",
|
||||
appTokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
|
||||
// Bug scenario: D-prefix channel but Slack event says channel_type: "channel"
|
||||
const message: SlackMessageEvent = {
|
||||
channel: "D0ACP6B1T8V",
|
||||
channel_type: "channel",
|
||||
user: "U1",
|
||||
text: "hello from DM",
|
||||
ts: "1.000",
|
||||
} as SlackMessageEvent;
|
||||
|
||||
const prepared = await prepareSlackMessage({
|
||||
ctx: slackCtx,
|
||||
account,
|
||||
message,
|
||||
opts: { source: "message" },
|
||||
});
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
expectInboundContextContract(prepared!.ctxPayload as any);
|
||||
// Should be classified as DM, not channel
|
||||
expect(prepared!.isDirectMessage).toBe(true);
|
||||
// DM with dmScope: "main" should route to the main session
|
||||
expect(prepared!.route.sessionKey).toBe("agent:main:main");
|
||||
// ChatType should be "direct", not "channel"
|
||||
expect(prepared!.ctxPayload.ChatType).toBe("direct");
|
||||
// From should use user ID (DM pattern), not channel ID
|
||||
expect(prepared!.ctxPayload.From).toContain("slack:U1");
|
||||
});
|
||||
|
||||
it("classifies D-prefix DMs when channel_type is missing", async () => {
|
||||
const slackCtx = createSlackMonitorContext({
|
||||
cfg: {
|
||||
channels: { slack: { enabled: true } },
|
||||
session: { dmScope: "main" },
|
||||
} as OpenClawConfig,
|
||||
accountId: "default",
|
||||
botToken: "token",
|
||||
app: { client: {} } as App,
|
||||
runtime: {} as RuntimeEnv,
|
||||
botUserId: "B1",
|
||||
teamId: "T1",
|
||||
apiAppId: "A1",
|
||||
historyLimit: 0,
|
||||
sessionScope: "per-sender",
|
||||
mainKey: "main",
|
||||
dmEnabled: true,
|
||||
dmPolicy: "open",
|
||||
allowFrom: [],
|
||||
groupDmEnabled: true,
|
||||
groupDmChannels: [],
|
||||
defaultRequireMention: true,
|
||||
groupPolicy: "open",
|
||||
useAccessGroups: false,
|
||||
reactionMode: "off",
|
||||
reactionAllowlist: [],
|
||||
replyToMode: "off",
|
||||
threadHistoryScope: "thread",
|
||||
threadInheritParent: false,
|
||||
slashCommand: {
|
||||
enabled: false,
|
||||
name: "openclaw",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true,
|
||||
},
|
||||
textLimit: 4000,
|
||||
ackReactionScope: "group-mentions",
|
||||
mediaMaxBytes: 1024,
|
||||
removeAckAfterReply: false,
|
||||
});
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
// Simulate API returning correct type for DM channel
|
||||
slackCtx.resolveChannelName = async () => ({ name: undefined, type: "im" as const });
|
||||
|
||||
const account: ResolvedSlackAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
botTokenSource: "config",
|
||||
appTokenSource: "config",
|
||||
config: {},
|
||||
};
|
||||
|
||||
// channel_type missing — should infer from D-prefix
|
||||
const message: SlackMessageEvent = {
|
||||
channel: "D0ACP6B1T8V",
|
||||
user: "U1",
|
||||
text: "hello from DM",
|
||||
ts: "1.000",
|
||||
} as SlackMessageEvent;
|
||||
|
||||
const prepared = await prepareSlackMessage({
|
||||
ctx: slackCtx,
|
||||
account,
|
||||
message,
|
||||
opts: { source: "message" },
|
||||
});
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
expectInboundContextContract(prepared!.ctxPayload as any);
|
||||
expect(prepared!.isDirectMessage).toBe(true);
|
||||
expect(prepared!.route.sessionKey).toBe("agent:main:main");
|
||||
expect(prepared!.ctxPayload.ChatType).toBe("direct");
|
||||
});
|
||||
|
||||
it("sets MessageThreadId for top-level messages when replyToMode=all", async () => {
|
||||
const slackCtx = createInboundSlackCtx({
|
||||
cfg: {
|
||||
|
||||
@@ -134,6 +134,25 @@ describe("normalizeSlackChannelType", () => {
|
||||
it("prefers explicit channel_type values", () => {
|
||||
expect(normalizeSlackChannelType("mpim", "C123")).toBe("mpim");
|
||||
});
|
||||
|
||||
it("overrides wrong channel_type for D-prefix DM channels", () => {
|
||||
// Slack DM channel IDs always start with "D" — if the event
|
||||
// reports a wrong channel_type, the D-prefix should win.
|
||||
expect(normalizeSlackChannelType("channel", "D123")).toBe("im");
|
||||
expect(normalizeSlackChannelType("group", "D456")).toBe("im");
|
||||
expect(normalizeSlackChannelType("mpim", "D789")).toBe("im");
|
||||
});
|
||||
|
||||
it("preserves correct channel_type for D-prefix DM channels", () => {
|
||||
expect(normalizeSlackChannelType("im", "D123")).toBe("im");
|
||||
});
|
||||
|
||||
it("does not override G-prefix channel_type (ambiguous prefix)", () => {
|
||||
// G-prefix can be either "group" (private channel) or "mpim" (group DM)
|
||||
// — trust the provided channel_type since the prefix is ambiguous.
|
||||
expect(normalizeSlackChannelType("group", "G123")).toBe("group");
|
||||
expect(normalizeSlackChannelType("mpim", "G456")).toBe("mpim");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSlackSystemEventSessionKey", () => {
|
||||
|
||||
Reference in New Issue
Block a user