@openclaw/matrix 2026.2.25 → 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.
@@ -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
- let allowFrom: string[] = (accountConfig.dm?.allowFrom ?? []).map(String);
132
- let groupAllowFrom: string[] = (accountConfig.groupAllowFrom ?? []).map(String);
133
- let roomsConfig = accountConfig.groups ?? accountConfig.rooms;
134
-
135
- allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
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 = 0;
272
- const directTracker = createDirectRoomTracker(client, { log: logVerboseMessage });
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
  };