@openclaw/matrix 2026.2.24 → 2026.3.1
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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/config-schema.ts +2 -0
- package/src/directory-live.test.ts +11 -0
- package/src/directory-live.ts +2 -1
- package/src/matrix/accounts.test.ts +50 -1
- package/src/matrix/accounts.ts +13 -1
- package/src/matrix/client/shared.test.ts +85 -0
- package/src/matrix/client/shared.ts +8 -1
- package/src/matrix/client/startup.test.ts +49 -0
- package/src/matrix/client/startup.ts +29 -0
- package/src/matrix/client-bootstrap.ts +9 -2
- package/src/matrix/monitor/access-policy.ts +127 -0
- package/src/matrix/monitor/direct.test.ts +65 -0
- package/src/matrix/monitor/direct.ts +20 -7
- package/src/matrix/monitor/events.test.ts +31 -0
- package/src/matrix/monitor/events.ts +20 -0
- package/src/matrix/monitor/handler.body-for-agent.test.ts +142 -0
- package/src/matrix/monitor/handler.ts +69 -63
- package/src/matrix/monitor/inbound-body.test.ts +73 -0
- package/src/matrix/monitor/inbound-body.ts +28 -0
- package/src/matrix/monitor/index.test.ts +18 -0
- package/src/matrix/monitor/index.ts +204 -147
- package/src/types.ts +3 -1
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "openclaw/plugin-sdk";
|
|
11
11
|
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
|
12
12
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
13
|
-
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
|
13
|
+
import type { CoreConfig, MatrixConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js";
|
|
14
14
|
import { resolveMatrixAccount } from "../accounts.js";
|
|
15
15
|
import { setActiveMatrixClient } from "../active-client.js";
|
|
16
16
|
import {
|
|
@@ -36,6 +36,199 @@ export type MonitorMatrixOpts = {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const DEFAULT_MEDIA_MAX_MB = 20;
|
|
39
|
+
export const DEFAULT_STARTUP_GRACE_MS = 5000;
|
|
40
|
+
|
|
41
|
+
export function isConfiguredMatrixRoomEntry(entry: string): boolean {
|
|
42
|
+
return entry.startsWith("!") || (entry.startsWith("#") && entry.includes(":"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeMatrixUserEntry(raw: string): string {
|
|
46
|
+
return raw
|
|
47
|
+
.replace(/^matrix:/i, "")
|
|
48
|
+
.replace(/^user:/i, "")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeMatrixRoomEntry(raw: string): string {
|
|
53
|
+
return raw
|
|
54
|
+
.replace(/^matrix:/i, "")
|
|
55
|
+
.replace(/^(room|channel):/i, "")
|
|
56
|
+
.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isMatrixUserId(value: string): boolean {
|
|
60
|
+
return value.startsWith("@") && value.includes(":");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function resolveMatrixUserAllowlist(params: {
|
|
64
|
+
cfg: CoreConfig;
|
|
65
|
+
runtime: RuntimeEnv;
|
|
66
|
+
label: string;
|
|
67
|
+
list?: Array<string | number>;
|
|
68
|
+
}): Promise<string[]> {
|
|
69
|
+
let allowList = params.list ?? [];
|
|
70
|
+
if (allowList.length === 0) {
|
|
71
|
+
return allowList.map(String);
|
|
72
|
+
}
|
|
73
|
+
const entries = allowList
|
|
74
|
+
.map((entry) => normalizeMatrixUserEntry(String(entry)))
|
|
75
|
+
.filter((entry) => entry && entry !== "*");
|
|
76
|
+
if (entries.length === 0) {
|
|
77
|
+
return allowList.map(String);
|
|
78
|
+
}
|
|
79
|
+
const mapping: string[] = [];
|
|
80
|
+
const unresolved: string[] = [];
|
|
81
|
+
const additions: string[] = [];
|
|
82
|
+
const pending: string[] = [];
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (isMatrixUserId(entry)) {
|
|
85
|
+
additions.push(normalizeMatrixUserId(entry));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
pending.push(entry);
|
|
89
|
+
}
|
|
90
|
+
if (pending.length > 0) {
|
|
91
|
+
const resolved = await resolveMatrixTargets({
|
|
92
|
+
cfg: params.cfg,
|
|
93
|
+
inputs: pending,
|
|
94
|
+
kind: "user",
|
|
95
|
+
runtime: params.runtime,
|
|
96
|
+
});
|
|
97
|
+
for (const entry of resolved) {
|
|
98
|
+
if (entry.resolved && entry.id) {
|
|
99
|
+
const normalizedId = normalizeMatrixUserId(entry.id);
|
|
100
|
+
additions.push(normalizedId);
|
|
101
|
+
mapping.push(`${entry.input}→${normalizedId}`);
|
|
102
|
+
} else {
|
|
103
|
+
unresolved.push(entry.input);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
allowList = mergeAllowlist({ existing: allowList, additions });
|
|
108
|
+
summarizeMapping(params.label, mapping, unresolved, params.runtime);
|
|
109
|
+
if (unresolved.length > 0) {
|
|
110
|
+
params.runtime.log?.(
|
|
111
|
+
`${params.label} entries must be full Matrix IDs (example: @user:server). Unresolved entries are ignored.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return allowList.map(String);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function resolveMatrixRoomsConfig(params: {
|
|
118
|
+
cfg: CoreConfig;
|
|
119
|
+
runtime: RuntimeEnv;
|
|
120
|
+
roomsConfig?: Record<string, MatrixRoomConfig>;
|
|
121
|
+
}): Promise<Record<string, MatrixRoomConfig> | undefined> {
|
|
122
|
+
let roomsConfig = params.roomsConfig;
|
|
123
|
+
if (!roomsConfig || Object.keys(roomsConfig).length === 0) {
|
|
124
|
+
return roomsConfig;
|
|
125
|
+
}
|
|
126
|
+
const mapping: string[] = [];
|
|
127
|
+
const unresolved: string[] = [];
|
|
128
|
+
const nextRooms: Record<string, MatrixRoomConfig> = {};
|
|
129
|
+
if (roomsConfig["*"]) {
|
|
130
|
+
nextRooms["*"] = roomsConfig["*"];
|
|
131
|
+
}
|
|
132
|
+
const pending: Array<{ input: string; query: string; config: MatrixRoomConfig }> = [];
|
|
133
|
+
for (const [entry, roomConfig] of Object.entries(roomsConfig)) {
|
|
134
|
+
if (entry === "*") {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const trimmed = entry.trim();
|
|
138
|
+
if (!trimmed) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const cleaned = normalizeMatrixRoomEntry(trimmed);
|
|
142
|
+
if (isConfiguredMatrixRoomEntry(cleaned)) {
|
|
143
|
+
if (!nextRooms[cleaned]) {
|
|
144
|
+
nextRooms[cleaned] = roomConfig;
|
|
145
|
+
}
|
|
146
|
+
if (cleaned !== entry) {
|
|
147
|
+
mapping.push(`${entry}→${cleaned}`);
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
pending.push({ input: entry, query: trimmed, config: roomConfig });
|
|
152
|
+
}
|
|
153
|
+
if (pending.length > 0) {
|
|
154
|
+
const resolved = await resolveMatrixTargets({
|
|
155
|
+
cfg: params.cfg,
|
|
156
|
+
inputs: pending.map((entry) => entry.query),
|
|
157
|
+
kind: "group",
|
|
158
|
+
runtime: params.runtime,
|
|
159
|
+
});
|
|
160
|
+
resolved.forEach((entry, index) => {
|
|
161
|
+
const source = pending[index];
|
|
162
|
+
if (!source) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (entry.resolved && entry.id) {
|
|
166
|
+
if (!nextRooms[entry.id]) {
|
|
167
|
+
nextRooms[entry.id] = source.config;
|
|
168
|
+
}
|
|
169
|
+
mapping.push(`${source.input}→${entry.id}`);
|
|
170
|
+
} else {
|
|
171
|
+
unresolved.push(source.input);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
roomsConfig = nextRooms;
|
|
176
|
+
summarizeMapping("matrix rooms", mapping, unresolved, params.runtime);
|
|
177
|
+
if (unresolved.length > 0) {
|
|
178
|
+
params.runtime.log?.(
|
|
179
|
+
"matrix rooms must be room IDs or aliases (example: !room:server or #alias:server). Unresolved entries are ignored.",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (Object.keys(roomsConfig).length === 0) {
|
|
183
|
+
return roomsConfig;
|
|
184
|
+
}
|
|
185
|
+
const nextRoomsWithUsers = { ...roomsConfig };
|
|
186
|
+
for (const [roomKey, roomConfig] of Object.entries(roomsConfig)) {
|
|
187
|
+
const users = roomConfig?.users ?? [];
|
|
188
|
+
if (users.length === 0) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const resolvedUsers = await resolveMatrixUserAllowlist({
|
|
192
|
+
cfg: params.cfg,
|
|
193
|
+
runtime: params.runtime,
|
|
194
|
+
label: `matrix room users (${roomKey})`,
|
|
195
|
+
list: users,
|
|
196
|
+
});
|
|
197
|
+
if (resolvedUsers !== users) {
|
|
198
|
+
nextRoomsWithUsers[roomKey] = { ...roomConfig, users: resolvedUsers };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return nextRoomsWithUsers;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function resolveMatrixMonitorConfig(params: {
|
|
205
|
+
cfg: CoreConfig;
|
|
206
|
+
runtime: RuntimeEnv;
|
|
207
|
+
accountConfig: MatrixConfig;
|
|
208
|
+
}): Promise<{
|
|
209
|
+
allowFrom: string[];
|
|
210
|
+
groupAllowFrom: string[];
|
|
211
|
+
roomsConfig?: Record<string, MatrixRoomConfig>;
|
|
212
|
+
}> {
|
|
213
|
+
const allowFrom = await resolveMatrixUserAllowlist({
|
|
214
|
+
cfg: params.cfg,
|
|
215
|
+
runtime: params.runtime,
|
|
216
|
+
label: "matrix dm allowlist",
|
|
217
|
+
list: params.accountConfig.dm?.allowFrom ?? [],
|
|
218
|
+
});
|
|
219
|
+
const groupAllowFrom = await resolveMatrixUserAllowlist({
|
|
220
|
+
cfg: params.cfg,
|
|
221
|
+
runtime: params.runtime,
|
|
222
|
+
label: "matrix group allowlist",
|
|
223
|
+
list: params.accountConfig.groupAllowFrom ?? [],
|
|
224
|
+
});
|
|
225
|
+
const roomsConfig = await resolveMatrixRoomsConfig({
|
|
226
|
+
cfg: params.cfg,
|
|
227
|
+
runtime: params.runtime,
|
|
228
|
+
roomsConfig: params.accountConfig.groups ?? params.accountConfig.rooms,
|
|
229
|
+
});
|
|
230
|
+
return { allowFrom, groupAllowFrom, roomsConfig };
|
|
231
|
+
}
|
|
39
232
|
|
|
40
233
|
export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promise<void> {
|
|
41
234
|
if (isBunRuntime()) {
|
|
@@ -60,154 +253,15 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
60
253
|
logger.debug?.(message);
|
|
61
254
|
};
|
|
62
255
|
|
|
63
|
-
const normalizeUserEntry = (raw: string) =>
|
|
64
|
-
raw
|
|
65
|
-
.replace(/^matrix:/i, "")
|
|
66
|
-
.replace(/^user:/i, "")
|
|
67
|
-
.trim();
|
|
68
|
-
const normalizeRoomEntry = (raw: string) =>
|
|
69
|
-
raw
|
|
70
|
-
.replace(/^matrix:/i, "")
|
|
71
|
-
.replace(/^(room|channel):/i, "")
|
|
72
|
-
.trim();
|
|
73
|
-
const isMatrixUserId = (value: string) => value.startsWith("@") && value.includes(":");
|
|
74
|
-
const resolveUserAllowlist = async (
|
|
75
|
-
label: string,
|
|
76
|
-
list?: Array<string | number>,
|
|
77
|
-
): Promise<string[]> => {
|
|
78
|
-
let allowList = list ?? [];
|
|
79
|
-
if (allowList.length === 0) {
|
|
80
|
-
return allowList.map(String);
|
|
81
|
-
}
|
|
82
|
-
const entries = allowList
|
|
83
|
-
.map((entry) => normalizeUserEntry(String(entry)))
|
|
84
|
-
.filter((entry) => entry && entry !== "*");
|
|
85
|
-
if (entries.length === 0) {
|
|
86
|
-
return allowList.map(String);
|
|
87
|
-
}
|
|
88
|
-
const mapping: string[] = [];
|
|
89
|
-
const unresolved: string[] = [];
|
|
90
|
-
const additions: string[] = [];
|
|
91
|
-
const pending: string[] = [];
|
|
92
|
-
for (const entry of entries) {
|
|
93
|
-
if (isMatrixUserId(entry)) {
|
|
94
|
-
additions.push(normalizeMatrixUserId(entry));
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
pending.push(entry);
|
|
98
|
-
}
|
|
99
|
-
if (pending.length > 0) {
|
|
100
|
-
const resolved = await resolveMatrixTargets({
|
|
101
|
-
cfg,
|
|
102
|
-
inputs: pending,
|
|
103
|
-
kind: "user",
|
|
104
|
-
runtime,
|
|
105
|
-
});
|
|
106
|
-
for (const entry of resolved) {
|
|
107
|
-
if (entry.resolved && entry.id) {
|
|
108
|
-
const normalizedId = normalizeMatrixUserId(entry.id);
|
|
109
|
-
additions.push(normalizedId);
|
|
110
|
-
mapping.push(`${entry.input}→${normalizedId}`);
|
|
111
|
-
} else {
|
|
112
|
-
unresolved.push(entry.input);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
allowList = mergeAllowlist({ existing: allowList, additions });
|
|
117
|
-
summarizeMapping(label, mapping, unresolved, runtime);
|
|
118
|
-
if (unresolved.length > 0) {
|
|
119
|
-
runtime.log?.(
|
|
120
|
-
`${label} entries must be full Matrix IDs (example: @user:server). Unresolved entries are ignored.`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
return allowList.map(String);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
256
|
// Resolve account-specific config for multi-account support
|
|
127
257
|
const account = resolveMatrixAccount({ cfg, accountId: opts.accountId });
|
|
128
258
|
const accountConfig = account.config;
|
|
129
|
-
|
|
130
259
|
const allowlistOnly = accountConfig.allowlistOnly === true;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
groupAllowFrom = await resolveUserAllowlist("matrix group allowlist", groupAllowFrom);
|
|
137
|
-
|
|
138
|
-
if (roomsConfig && Object.keys(roomsConfig).length > 0) {
|
|
139
|
-
const mapping: string[] = [];
|
|
140
|
-
const unresolved: string[] = [];
|
|
141
|
-
const nextRooms: Record<string, (typeof roomsConfig)[string]> = {};
|
|
142
|
-
if (roomsConfig["*"]) {
|
|
143
|
-
nextRooms["*"] = roomsConfig["*"];
|
|
144
|
-
}
|
|
145
|
-
const pending: Array<{ input: string; query: string; config: (typeof roomsConfig)[string] }> =
|
|
146
|
-
[];
|
|
147
|
-
for (const [entry, roomConfig] of Object.entries(roomsConfig)) {
|
|
148
|
-
if (entry === "*") {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
const trimmed = entry.trim();
|
|
152
|
-
if (!trimmed) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const cleaned = normalizeRoomEntry(trimmed);
|
|
156
|
-
if ((cleaned.startsWith("!") || cleaned.startsWith("#")) && cleaned.includes(":")) {
|
|
157
|
-
if (!nextRooms[cleaned]) {
|
|
158
|
-
nextRooms[cleaned] = roomConfig;
|
|
159
|
-
}
|
|
160
|
-
if (cleaned !== entry) {
|
|
161
|
-
mapping.push(`${entry}→${cleaned}`);
|
|
162
|
-
}
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
pending.push({ input: entry, query: trimmed, config: roomConfig });
|
|
166
|
-
}
|
|
167
|
-
if (pending.length > 0) {
|
|
168
|
-
const resolved = await resolveMatrixTargets({
|
|
169
|
-
cfg,
|
|
170
|
-
inputs: pending.map((entry) => entry.query),
|
|
171
|
-
kind: "group",
|
|
172
|
-
runtime,
|
|
173
|
-
});
|
|
174
|
-
resolved.forEach((entry, index) => {
|
|
175
|
-
const source = pending[index];
|
|
176
|
-
if (!source) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (entry.resolved && entry.id) {
|
|
180
|
-
if (!nextRooms[entry.id]) {
|
|
181
|
-
nextRooms[entry.id] = source.config;
|
|
182
|
-
}
|
|
183
|
-
mapping.push(`${source.input}→${entry.id}`);
|
|
184
|
-
} else {
|
|
185
|
-
unresolved.push(source.input);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
roomsConfig = nextRooms;
|
|
190
|
-
summarizeMapping("matrix rooms", mapping, unresolved, runtime);
|
|
191
|
-
if (unresolved.length > 0) {
|
|
192
|
-
runtime.log?.(
|
|
193
|
-
"matrix rooms must be room IDs or aliases (example: !room:server or #alias:server). Unresolved entries are ignored.",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (roomsConfig && Object.keys(roomsConfig).length > 0) {
|
|
198
|
-
const nextRooms = { ...roomsConfig };
|
|
199
|
-
for (const [roomKey, roomConfig] of Object.entries(roomsConfig)) {
|
|
200
|
-
const users = roomConfig?.users ?? [];
|
|
201
|
-
if (users.length === 0) {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
const resolvedUsers = await resolveUserAllowlist(`matrix room users (${roomKey})`, users);
|
|
205
|
-
if (resolvedUsers !== users) {
|
|
206
|
-
nextRooms[roomKey] = { ...roomConfig, users: resolvedUsers };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
roomsConfig = nextRooms;
|
|
210
|
-
}
|
|
260
|
+
const { allowFrom, groupAllowFrom, roomsConfig } = await resolveMatrixMonitorConfig({
|
|
261
|
+
cfg,
|
|
262
|
+
runtime,
|
|
263
|
+
accountConfig,
|
|
264
|
+
});
|
|
211
265
|
|
|
212
266
|
cfg = {
|
|
213
267
|
...cfg,
|
|
@@ -268,8 +322,11 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
268
322
|
const mediaMaxMb = opts.mediaMaxMb ?? accountConfig.mediaMaxMb ?? DEFAULT_MEDIA_MAX_MB;
|
|
269
323
|
const mediaMaxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
|
|
270
324
|
const startupMs = Date.now();
|
|
271
|
-
const startupGraceMs =
|
|
272
|
-
const directTracker = createDirectRoomTracker(client, {
|
|
325
|
+
const startupGraceMs = DEFAULT_STARTUP_GRACE_MS;
|
|
326
|
+
const directTracker = createDirectRoomTracker(client, {
|
|
327
|
+
log: logVerboseMessage,
|
|
328
|
+
includeMemberCountInLogs: core.logging.shouldLogVerbose(),
|
|
329
|
+
});
|
|
273
330
|
registerMatrixAutoJoin({ client, cfg, runtime });
|
|
274
331
|
const warnedEncryptedRooms = new Set<string>();
|
|
275
332
|
const warnedCryptoMissingRooms = new Set<string>();
|
package/src/types.ts
CHANGED
|
@@ -49,6 +49,8 @@ export type MatrixConfig = {
|
|
|
49
49
|
enabled?: boolean;
|
|
50
50
|
/** Multi-account configuration keyed by account ID. */
|
|
51
51
|
accounts?: Record<string, MatrixAccountConfig>;
|
|
52
|
+
/** Optional default account id when multiple accounts are configured. */
|
|
53
|
+
defaultAccount?: string;
|
|
52
54
|
/** Matrix homeserver URL (https://matrix.example.org). */
|
|
53
55
|
homeserver?: string;
|
|
54
56
|
/** Matrix user id (@user:server). */
|
|
@@ -110,7 +112,7 @@ export type CoreConfig = {
|
|
|
110
112
|
};
|
|
111
113
|
messages?: {
|
|
112
114
|
ackReaction?: string;
|
|
113
|
-
ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all";
|
|
115
|
+
ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all" | "off" | "none";
|
|
114
116
|
};
|
|
115
117
|
[key: string]: unknown;
|
|
116
118
|
};
|