From 355b4c62bcc2f33a502121edcb7467fef7066df7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 03:14:17 +0000 Subject: [PATCH] fix(mattermost): land #30891 route private channels as group (@BlueBirdBack) Landed from contributor PR #30891 by @BlueBirdBack. Co-authored-by: BlueBirdBack --- CHANGELOG.md | 1 + .../mattermost/monitor.channel-kind.test.ts | 20 +++++++++++++++++++ .../mattermost/src/mattermost/monitor.ts | 13 +++++++++--- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 extensions/mattermost/src/mattermost/monitor.channel-kind.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ae511ee2a06..f571692b2ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Mattermost/Private channel policy routing: map Mattermost private channel type `P` to group chat type so `groupPolicy`/`groupAllowFrom` gates apply correctly instead of being treated as open public channels. Landed from contributor PR #30891 by @BlueBirdBack. Thanks @BlueBirdBack. - Models/Custom provider keys: trim custom provider map keys during normalization so image-capable models remain discoverable when provider keys are configured with leading/trailing whitespace. Landed from contributor PR #31202 by @stakeswky. Thanks @stakeswky. - Discord/Agent component interactions: accept Components v2 `cid` payloads alongside legacy `componentId`, and safely decode percent-encoded IDs without throwing on malformed `%` sequences. Landed from contributor PR #29013 by @Jacky1n7. Thanks @Jacky1n7. - Matrix/Directory room IDs: preserve original room-ID casing for direct `!roomId` group lookups (without `:server`) so allowlist checks do not fail on case-sensitive IDs. Landed from contributor PR #31201 by @williamos-dev. Thanks @williamos-dev. diff --git a/extensions/mattermost/src/mattermost/monitor.channel-kind.test.ts b/extensions/mattermost/src/mattermost/monitor.channel-kind.test.ts new file mode 100644 index 00000000000..0928ef31c12 --- /dev/null +++ b/extensions/mattermost/src/mattermost/monitor.channel-kind.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { mapMattermostChannelTypeToChatType } from "./monitor.js"; + +describe("mapMattermostChannelTypeToChatType", () => { + it("maps direct and group dm channel types", () => { + expect(mapMattermostChannelTypeToChatType("D")).toBe("direct"); + expect(mapMattermostChannelTypeToChatType("g")).toBe("group"); + }); + + it("maps private channels to group", () => { + expect(mapMattermostChannelTypeToChatType("P")).toBe("group"); + expect(mapMattermostChannelTypeToChatType(" p ")).toBe("group"); + }); + + it("keeps public channels and unknown values as channel", () => { + expect(mapMattermostChannelTypeToChatType("O")).toBe("channel"); + expect(mapMattermostChannelTypeToChatType("x")).toBe("channel"); + expect(mapMattermostChannelTypeToChatType(undefined)).toBe("channel"); + }); +}); diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 71b02afff5f..9d16dfedacb 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -110,10 +110,11 @@ function isSystemPost(post: MattermostPost): boolean { return Boolean(type); } -function channelKind(channelType?: string | null): ChatType { +export function mapMattermostChannelTypeToChatType(channelType?: string | null): ChatType { if (!channelType) { return "channel"; } + // Mattermost channel types: D=direct, G=group DM, O=public channel, P=private channel. const normalized = channelType.trim().toUpperCase(); if (normalized === "D") { return "direct"; @@ -121,6 +122,12 @@ function channelKind(channelType?: string | null): ChatType { if (normalized === "G") { return "group"; } + if (normalized === "P") { + // Private channels are invitation-restricted spaces; route as "group" so + // groupPolicy / groupAllowFrom can gate access separately from open public + // channels (type "O"), and the From prefix becomes mattermost:group:. + return "group"; + } return "channel"; } @@ -352,7 +359,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const channelInfo = await resolveChannelInfo(channelId); const channelType = payload.data?.channel_type ?? channelInfo?.type ?? undefined; - const kind = channelKind(channelType); + const kind = mapMattermostChannelTypeToChatType(channelType); const chatType = channelChatType(kind); const senderName = @@ -863,7 +870,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} logVerboseMessage(`mattermost: drop reaction (cannot resolve channel type for ${channelId})`); return; } - const kind = channelKind(channelInfo.type); + const kind = mapMattermostChannelTypeToChatType(channelInfo.type); // Enforce DM/group policy and allowlist checks (same as normal messages) const dmPolicy = account.config.dmPolicy ?? "pairing";