@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.
@@ -0,0 +1,221 @@
1
+ import type { ChannelAccountSnapshot, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ import WebSocket from "ws";
3
+ import type { MattermostPost } from "./client.js";
4
+ import { rawDataToString } from "./monitor-helpers.js";
5
+
6
+ export type MattermostEventPayload = {
7
+ event?: string;
8
+ data?: {
9
+ post?: string;
10
+ reaction?: string;
11
+ channel_id?: string;
12
+ channel_name?: string;
13
+ channel_display_name?: string;
14
+ channel_type?: string;
15
+ sender_name?: string;
16
+ team_id?: string;
17
+ };
18
+ broadcast?: {
19
+ channel_id?: string;
20
+ team_id?: string;
21
+ user_id?: string;
22
+ };
23
+ };
24
+
25
+ export type MattermostWebSocketLike = {
26
+ on(event: "open", listener: () => void): void;
27
+ on(event: "message", listener: (data: WebSocket.RawData) => void | Promise<void>): void;
28
+ on(event: "close", listener: (code: number, reason: Buffer) => void): void;
29
+ on(event: "error", listener: (err: unknown) => void): void;
30
+ send(data: string): void;
31
+ close(): void;
32
+ terminate(): void;
33
+ };
34
+
35
+ export type MattermostWebSocketFactory = (url: string) => MattermostWebSocketLike;
36
+
37
+ export class WebSocketClosedBeforeOpenError extends Error {
38
+ constructor(
39
+ public readonly code: number,
40
+ public readonly reason?: string,
41
+ ) {
42
+ super(`websocket closed before open (code ${code})`);
43
+ this.name = "WebSocketClosedBeforeOpenError";
44
+ }
45
+ }
46
+
47
+ type CreateMattermostConnectOnceOpts = {
48
+ wsUrl: string;
49
+ botToken: string;
50
+ abortSignal?: AbortSignal;
51
+ statusSink?: (patch: Partial<ChannelAccountSnapshot>) => void;
52
+ runtime: RuntimeEnv;
53
+ nextSeq: () => number;
54
+ onPosted: (post: MattermostPost, payload: MattermostEventPayload) => Promise<void>;
55
+ onReaction?: (payload: MattermostEventPayload) => Promise<void>;
56
+ webSocketFactory?: MattermostWebSocketFactory;
57
+ };
58
+
59
+ export const defaultMattermostWebSocketFactory: MattermostWebSocketFactory = (url) =>
60
+ new WebSocket(url) as MattermostWebSocketLike;
61
+
62
+ export function parsePostedPayload(
63
+ payload: MattermostEventPayload,
64
+ ): { payload: MattermostEventPayload; post: MattermostPost } | null {
65
+ if (payload.event !== "posted") {
66
+ return null;
67
+ }
68
+ const postData = payload.data?.post;
69
+ if (!postData) {
70
+ return null;
71
+ }
72
+ let post: MattermostPost | null = null;
73
+ if (typeof postData === "string") {
74
+ try {
75
+ post = JSON.parse(postData) as MattermostPost;
76
+ } catch {
77
+ return null;
78
+ }
79
+ } else if (typeof postData === "object") {
80
+ post = postData as MattermostPost;
81
+ }
82
+ if (!post) {
83
+ return null;
84
+ }
85
+ return { payload, post };
86
+ }
87
+
88
+ export function parsePostedEvent(
89
+ data: WebSocket.RawData,
90
+ ): { payload: MattermostEventPayload; post: MattermostPost } | null {
91
+ const raw = rawDataToString(data);
92
+ let payload: MattermostEventPayload;
93
+ try {
94
+ payload = JSON.parse(raw) as MattermostEventPayload;
95
+ } catch {
96
+ return null;
97
+ }
98
+ return parsePostedPayload(payload);
99
+ }
100
+
101
+ export function createMattermostConnectOnce(
102
+ opts: CreateMattermostConnectOnceOpts,
103
+ ): () => Promise<void> {
104
+ const webSocketFactory = opts.webSocketFactory ?? defaultMattermostWebSocketFactory;
105
+ return async () => {
106
+ const ws = webSocketFactory(opts.wsUrl);
107
+ const onAbort = () => ws.terminate();
108
+ opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
109
+
110
+ try {
111
+ return await new Promise<void>((resolve, reject) => {
112
+ let opened = false;
113
+ let settled = false;
114
+ const resolveOnce = () => {
115
+ if (settled) {
116
+ return;
117
+ }
118
+ settled = true;
119
+ resolve();
120
+ };
121
+ const rejectOnce = (error: Error) => {
122
+ if (settled) {
123
+ return;
124
+ }
125
+ settled = true;
126
+ reject(error);
127
+ };
128
+
129
+ ws.on("open", () => {
130
+ opened = true;
131
+ opts.statusSink?.({
132
+ connected: true,
133
+ lastConnectedAt: Date.now(),
134
+ lastError: null,
135
+ });
136
+ ws.send(
137
+ JSON.stringify({
138
+ seq: opts.nextSeq(),
139
+ action: "authentication_challenge",
140
+ data: { token: opts.botToken },
141
+ }),
142
+ );
143
+ });
144
+
145
+ ws.on("message", async (data) => {
146
+ const raw = rawDataToString(data);
147
+ let payload: MattermostEventPayload;
148
+ try {
149
+ payload = JSON.parse(raw) as MattermostEventPayload;
150
+ } catch {
151
+ return;
152
+ }
153
+
154
+ if (payload.event === "reaction_added" || payload.event === "reaction_removed") {
155
+ if (!opts.onReaction) {
156
+ return;
157
+ }
158
+ try {
159
+ await opts.onReaction(payload);
160
+ } catch (err) {
161
+ opts.runtime.error?.(`mattermost reaction handler failed: ${String(err)}`);
162
+ }
163
+ return;
164
+ }
165
+
166
+ if (payload.event !== "posted") {
167
+ return;
168
+ }
169
+ const parsed = parsePostedPayload(payload);
170
+ if (!parsed) {
171
+ return;
172
+ }
173
+ try {
174
+ await opts.onPosted(parsed.post, parsed.payload);
175
+ } catch (err) {
176
+ opts.runtime.error?.(`mattermost handler failed: ${String(err)}`);
177
+ }
178
+ });
179
+
180
+ ws.on("close", (code, reason) => {
181
+ const message = reasonToString(reason);
182
+ opts.statusSink?.({
183
+ connected: false,
184
+ lastDisconnect: {
185
+ at: Date.now(),
186
+ status: code,
187
+ error: message || undefined,
188
+ },
189
+ });
190
+ if (opened) {
191
+ resolveOnce();
192
+ return;
193
+ }
194
+ rejectOnce(new WebSocketClosedBeforeOpenError(code, message || undefined));
195
+ });
196
+
197
+ ws.on("error", (err) => {
198
+ opts.runtime.error?.(`mattermost websocket error: ${String(err)}`);
199
+ opts.statusSink?.({
200
+ lastError: String(err),
201
+ });
202
+ try {
203
+ ws.close();
204
+ } catch {}
205
+ });
206
+ });
207
+ } finally {
208
+ opts.abortSignal?.removeEventListener("abort", onAbort);
209
+ }
210
+ };
211
+ }
212
+
213
+ function reasonToString(reason: Buffer | string | undefined): string {
214
+ if (!reason) {
215
+ return "";
216
+ }
217
+ if (typeof reason === "string") {
218
+ return reason;
219
+ }
220
+ return reason.length > 0 ? reason.toString("utf8") : "";
221
+ }