@openclaw/mattermost 2026.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +17 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +28 -0
- package/src/channel.test.ts +235 -0
- package/src/channel.ts +433 -0
- package/src/config-schema.ts +61 -0
- package/src/group-mentions.ts +15 -0
- package/src/mattermost/accounts.ts +128 -0
- package/src/mattermost/client.test.ts +19 -0
- package/src/mattermost/client.ts +230 -0
- package/src/mattermost/index.ts +9 -0
- package/src/mattermost/monitor-helpers.ts +93 -0
- package/src/mattermost/monitor-onchar.ts +25 -0
- package/src/mattermost/monitor-websocket.test.ts +232 -0
- package/src/mattermost/monitor-websocket.ts +221 -0
- package/src/mattermost/monitor.ts +1030 -0
- package/src/mattermost/probe.ts +73 -0
- package/src/mattermost/reactions.test-helpers.ts +83 -0
- package/src/mattermost/reactions.test.ts +103 -0
- package/src/mattermost/reactions.ts +124 -0
- package/src/mattermost/reconnect.test.ts +192 -0
- package/src/mattermost/reconnect.ts +103 -0
- package/src/mattermost/send.ts +231 -0
- package/src/normalize.ts +46 -0
- package/src/onboarding-helpers.ts +1 -0
- package/src/onboarding.ts +186 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +57 -0
package/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { mattermostPlugin } from "./src/channel.js";
|
|
4
|
+
import { setMattermostRuntime } from "./src/runtime.js";
|
|
5
|
+
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: "mattermost",
|
|
8
|
+
name: "Mattermost",
|
|
9
|
+
description: "Mattermost channel plugin",
|
|
10
|
+
configSchema: emptyPluginConfigSchema(),
|
|
11
|
+
register(api: OpenClawPluginApi) {
|
|
12
|
+
setMattermostRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: mattermostPlugin });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openclaw/mattermost",
|
|
3
|
+
"version": "2026.2.21",
|
|
4
|
+
"description": "OpenClaw Mattermost channel plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"openclaw": "workspace:*"
|
|
8
|
+
},
|
|
9
|
+
"openclaw": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./index.ts"
|
|
12
|
+
],
|
|
13
|
+
"channel": {
|
|
14
|
+
"id": "mattermost",
|
|
15
|
+
"label": "Mattermost",
|
|
16
|
+
"selectionLabel": "Mattermost (plugin)",
|
|
17
|
+
"docsPath": "/channels/mattermost",
|
|
18
|
+
"docsLabel": "mattermost",
|
|
19
|
+
"blurb": "self-hosted Slack-style chat; install the plugin to enable.",
|
|
20
|
+
"order": 65
|
|
21
|
+
},
|
|
22
|
+
"install": {
|
|
23
|
+
"npmSpec": "@openclaw/mattermost",
|
|
24
|
+
"localPath": "extensions/mattermost",
|
|
25
|
+
"defaultChoice": "npm"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import { createReplyPrefixOptions } from "openclaw/plugin-sdk";
|
|
3
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { mattermostPlugin } from "./channel.js";
|
|
5
|
+
import { resetMattermostReactionBotUserCacheForTests } from "./mattermost/reactions.js";
|
|
6
|
+
import {
|
|
7
|
+
createMattermostReactionFetchMock,
|
|
8
|
+
createMattermostTestConfig,
|
|
9
|
+
withMockedGlobalFetch,
|
|
10
|
+
} from "./mattermost/reactions.test-helpers.js";
|
|
11
|
+
|
|
12
|
+
describe("mattermostPlugin", () => {
|
|
13
|
+
describe("messaging", () => {
|
|
14
|
+
it("keeps @username targets", () => {
|
|
15
|
+
const normalize = mattermostPlugin.messaging?.normalizeTarget;
|
|
16
|
+
if (!normalize) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
expect(normalize("@Alice")).toBe("@Alice");
|
|
21
|
+
expect(normalize("@alice")).toBe("@alice");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("normalizes mattermost: prefix to user:", () => {
|
|
25
|
+
const normalize = mattermostPlugin.messaging?.normalizeTarget;
|
|
26
|
+
if (!normalize) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
expect(normalize("mattermost:USER123")).toBe("user:USER123");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("pairing", () => {
|
|
35
|
+
it("normalizes allowlist entries", () => {
|
|
36
|
+
const normalize = mattermostPlugin.pairing?.normalizeAllowEntry;
|
|
37
|
+
if (!normalize) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
expect(normalize("@Alice")).toBe("alice");
|
|
42
|
+
expect(normalize("user:USER123")).toBe("user123");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("capabilities", () => {
|
|
47
|
+
it("declares reactions support", () => {
|
|
48
|
+
expect(mattermostPlugin.capabilities?.reactions).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("messageActions", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
resetMattermostReactionBotUserCacheForTests();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("exposes react when mattermost is configured", () => {
|
|
58
|
+
const cfg: OpenClawConfig = {
|
|
59
|
+
channels: {
|
|
60
|
+
mattermost: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
botToken: "test-token",
|
|
63
|
+
baseUrl: "https://chat.example.com",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
|
69
|
+
expect(actions).toContain("react");
|
|
70
|
+
expect(actions).not.toContain("send");
|
|
71
|
+
expect(mattermostPlugin.actions?.supportsAction?.({ action: "react" })).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("hides react when mattermost is not configured", () => {
|
|
75
|
+
const cfg: OpenClawConfig = {
|
|
76
|
+
channels: {
|
|
77
|
+
mattermost: {
|
|
78
|
+
enabled: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
|
84
|
+
expect(actions).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("hides react when actions.reactions is false", () => {
|
|
88
|
+
const cfg: OpenClawConfig = {
|
|
89
|
+
channels: {
|
|
90
|
+
mattermost: {
|
|
91
|
+
enabled: true,
|
|
92
|
+
botToken: "test-token",
|
|
93
|
+
baseUrl: "https://chat.example.com",
|
|
94
|
+
actions: { reactions: false },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
|
100
|
+
expect(actions).not.toContain("react");
|
|
101
|
+
expect(actions).not.toContain("send");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("respects per-account actions.reactions in listActions", () => {
|
|
105
|
+
const cfg: OpenClawConfig = {
|
|
106
|
+
channels: {
|
|
107
|
+
mattermost: {
|
|
108
|
+
enabled: true,
|
|
109
|
+
actions: { reactions: false },
|
|
110
|
+
accounts: {
|
|
111
|
+
default: {
|
|
112
|
+
enabled: true,
|
|
113
|
+
botToken: "test-token",
|
|
114
|
+
baseUrl: "https://chat.example.com",
|
|
115
|
+
actions: { reactions: true },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const actions = mattermostPlugin.actions?.listActions?.({ cfg }) ?? [];
|
|
123
|
+
expect(actions).toContain("react");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("blocks react when default account disables reactions and accountId is omitted", async () => {
|
|
127
|
+
const cfg: OpenClawConfig = {
|
|
128
|
+
channels: {
|
|
129
|
+
mattermost: {
|
|
130
|
+
enabled: true,
|
|
131
|
+
actions: { reactions: true },
|
|
132
|
+
accounts: {
|
|
133
|
+
default: {
|
|
134
|
+
enabled: true,
|
|
135
|
+
botToken: "test-token",
|
|
136
|
+
baseUrl: "https://chat.example.com",
|
|
137
|
+
actions: { reactions: false },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await expect(
|
|
145
|
+
mattermostPlugin.actions?.handleAction?.({
|
|
146
|
+
channel: "mattermost",
|
|
147
|
+
action: "react",
|
|
148
|
+
params: { messageId: "POST1", emoji: "thumbsup" },
|
|
149
|
+
cfg,
|
|
150
|
+
} as any),
|
|
151
|
+
).rejects.toThrow("Mattermost reactions are disabled in config");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("handles react by calling Mattermost reactions API", async () => {
|
|
155
|
+
const cfg = createMattermostTestConfig();
|
|
156
|
+
const fetchImpl = createMattermostReactionFetchMock({
|
|
157
|
+
mode: "add",
|
|
158
|
+
postId: "POST1",
|
|
159
|
+
emojiName: "thumbsup",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = await withMockedGlobalFetch(fetchImpl as unknown as typeof fetch, async () => {
|
|
163
|
+
const result = await mattermostPlugin.actions?.handleAction?.({
|
|
164
|
+
channel: "mattermost",
|
|
165
|
+
action: "react",
|
|
166
|
+
params: { messageId: "POST1", emoji: "thumbsup" },
|
|
167
|
+
cfg,
|
|
168
|
+
accountId: "default",
|
|
169
|
+
} as any);
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(result?.content).toEqual([{ type: "text", text: "Reacted with :thumbsup: on POST1" }]);
|
|
175
|
+
expect(result?.details).toEqual({});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("only treats boolean remove flag as removal", async () => {
|
|
179
|
+
const cfg = createMattermostTestConfig();
|
|
180
|
+
const fetchImpl = createMattermostReactionFetchMock({
|
|
181
|
+
mode: "add",
|
|
182
|
+
postId: "POST1",
|
|
183
|
+
emojiName: "thumbsup",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const result = await withMockedGlobalFetch(fetchImpl as unknown as typeof fetch, async () => {
|
|
187
|
+
const result = await mattermostPlugin.actions?.handleAction?.({
|
|
188
|
+
channel: "mattermost",
|
|
189
|
+
action: "react",
|
|
190
|
+
params: { messageId: "POST1", emoji: "thumbsup", remove: "true" },
|
|
191
|
+
cfg,
|
|
192
|
+
accountId: "default",
|
|
193
|
+
} as any);
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result?.content).toEqual([{ type: "text", text: "Reacted with :thumbsup: on POST1" }]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("config", () => {
|
|
203
|
+
it("formats allowFrom entries", () => {
|
|
204
|
+
const formatAllowFrom = mattermostPlugin.config.formatAllowFrom!;
|
|
205
|
+
|
|
206
|
+
const formatted = formatAllowFrom({
|
|
207
|
+
cfg: {} as OpenClawConfig,
|
|
208
|
+
allowFrom: ["@Alice", "user:USER123", "mattermost:BOT999"],
|
|
209
|
+
});
|
|
210
|
+
expect(formatted).toEqual(["@alice", "user123", "bot999"]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("uses account responsePrefix overrides", () => {
|
|
214
|
+
const cfg: OpenClawConfig = {
|
|
215
|
+
channels: {
|
|
216
|
+
mattermost: {
|
|
217
|
+
responsePrefix: "[Channel]",
|
|
218
|
+
accounts: {
|
|
219
|
+
default: { responsePrefix: "[Account]" },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const prefixContext = createReplyPrefixOptions({
|
|
226
|
+
cfg,
|
|
227
|
+
agentId: "main",
|
|
228
|
+
channel: "mattermost",
|
|
229
|
+
accountId: "default",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(prefixContext.responsePrefix).toBe("[Account]");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|