@openclaw/matrix 2026.1.29 → 2026.2.2
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 +28 -0
- package/index.ts +0 -1
- package/openclaw.plugin.json +1 -3
- package/package.json +12 -12
- package/src/actions.ts +16 -6
- package/src/channel.directory.test.ts +13 -5
- package/src/channel.ts +61 -39
- package/src/directory-live.ts +21 -8
- package/src/group-mentions.ts +10 -5
- package/src/matrix/accounts.test.ts +0 -1
- package/src/matrix/accounts.ts +4 -2
- package/src/matrix/actions/client.ts +8 -4
- package/src/matrix/actions/messages.ts +17 -9
- package/src/matrix/actions/pins.ts +12 -6
- package/src/matrix/actions/reactions.ts +24 -12
- package/src/matrix/actions/room.ts +10 -13
- package/src/matrix/actions/summary.ts +4 -6
- package/src/matrix/client/config.ts +4 -9
- package/src/matrix/client/create-client.ts +12 -16
- package/src/matrix/client/logging.ts +17 -16
- package/src/matrix/client/shared.ts +6 -5
- package/src/matrix/client/storage.ts +12 -12
- package/src/matrix/client.test.ts +0 -1
- package/src/matrix/client.ts +1 -5
- package/src/matrix/credentials.ts +7 -5
- package/src/matrix/deps.ts +8 -5
- package/src/matrix/format.test.ts +0 -1
- package/src/matrix/monitor/allowlist.test.ts +45 -0
- package/src/matrix/monitor/allowlist.ts +62 -17
- package/src/matrix/monitor/auto-join.ts +7 -4
- package/src/matrix/monitor/direct.ts +11 -12
- package/src/matrix/monitor/events.ts +1 -3
- package/src/matrix/monitor/handler.ts +69 -53
- package/src/matrix/monitor/index.ts +118 -59
- package/src/matrix/monitor/location.ts +27 -10
- package/src/matrix/monitor/media.test.ts +1 -2
- package/src/matrix/monitor/media.ts +8 -8
- package/src/matrix/monitor/replies.ts +4 -3
- package/src/matrix/monitor/room-info.ts +5 -8
- package/src/matrix/monitor/rooms.test.ts +39 -0
- package/src/matrix/monitor/rooms.ts +7 -3
- package/src/matrix/monitor/threads.ts +6 -2
- package/src/matrix/poll-types.test.ts +0 -1
- package/src/matrix/poll-types.ts +16 -7
- package/src/matrix/send/client.ts +7 -4
- package/src/matrix/send/formatting.ts +14 -17
- package/src/matrix/send/media.ts +17 -8
- package/src/matrix/send/targets.test.ts +7 -11
- package/src/matrix/send/targets.ts +19 -27
- package/src/matrix/send.test.ts +1 -2
- package/src/matrix/send.ts +9 -4
- package/src/onboarding.ts +24 -14
- package/src/outbound.ts +1 -2
- package/src/resolve-targets.test.ts +48 -0
- package/src/resolve-targets.ts +55 -9
- package/src/tool-actions.ts +15 -11
- package/src/types.ts +5 -5
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { format } from "node:util";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
mergeAllowlist,
|
|
5
|
-
summarizeMapping,
|
|
6
|
-
type RuntimeEnv,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
2
|
+
import { mergeAllowlist, summarizeMapping, type RuntimeEnv } from "openclaw/plugin-sdk";
|
|
8
3
|
import type { CoreConfig, ReplyToMode } from "../../types.js";
|
|
4
|
+
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
|
5
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
9
6
|
import { setActiveMatrixClient } from "../active-client.js";
|
|
10
7
|
import {
|
|
11
8
|
isBunRuntime,
|
|
@@ -13,13 +10,12 @@ import {
|
|
|
13
10
|
resolveSharedMatrixClient,
|
|
14
11
|
stopSharedClient,
|
|
15
12
|
} from "../client.js";
|
|
13
|
+
import { normalizeMatrixUserId } from "./allowlist.js";
|
|
16
14
|
import { registerMatrixAutoJoin } from "./auto-join.js";
|
|
17
15
|
import { createDirectRoomTracker } from "./direct.js";
|
|
18
16
|
import { registerMatrixMonitorEvents } from "./events.js";
|
|
19
17
|
import { createMatrixRoomMessageHandler } from "./handler.js";
|
|
20
18
|
import { createMatrixRoomInfoResolver } from "./room-info.js";
|
|
21
|
-
import { resolveMatrixTargets } from "../../resolve-targets.js";
|
|
22
|
-
import { getMatrixRuntime } from "../../runtime.js";
|
|
23
19
|
|
|
24
20
|
export type MonitorMatrixOpts = {
|
|
25
21
|
runtime?: RuntimeEnv;
|
|
@@ -38,7 +34,9 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
38
34
|
}
|
|
39
35
|
const core = getMatrixRuntime();
|
|
40
36
|
let cfg = core.config.loadConfig() as CoreConfig;
|
|
41
|
-
if (cfg.channels?.matrix?.enabled === false)
|
|
37
|
+
if (cfg.channels?.matrix?.enabled === false) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
42
40
|
|
|
43
41
|
const logger = core.logging.getChildLogger({ module: "matrix-auto-reply" });
|
|
44
42
|
const formatRuntimeMessage = (...args: Parameters<RuntimeEnv["log"]>) => format(...args);
|
|
@@ -54,75 +52,111 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
54
52
|
},
|
|
55
53
|
};
|
|
56
54
|
const logVerboseMessage = (message: string) => {
|
|
57
|
-
if (!core.logging.shouldLogVerbose())
|
|
55
|
+
if (!core.logging.shouldLogVerbose()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
58
|
logger.debug(message);
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const normalizeUserEntry = (raw: string) =>
|
|
62
|
-
raw
|
|
62
|
+
raw
|
|
63
|
+
.replace(/^matrix:/i, "")
|
|
64
|
+
.replace(/^user:/i, "")
|
|
65
|
+
.trim();
|
|
63
66
|
const normalizeRoomEntry = (raw: string) =>
|
|
64
|
-
raw
|
|
67
|
+
raw
|
|
68
|
+
.replace(/^matrix:/i, "")
|
|
69
|
+
.replace(/^(room|channel):/i, "")
|
|
70
|
+
.trim();
|
|
65
71
|
const isMatrixUserId = (value: string) => value.startsWith("@") && value.includes(":");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
const resolveUserAllowlist = async (
|
|
73
|
+
label: string,
|
|
74
|
+
list?: Array<string | number>,
|
|
75
|
+
): Promise<string[]> => {
|
|
76
|
+
let allowList = list ?? [];
|
|
77
|
+
if (allowList.length === 0) {
|
|
78
|
+
return allowList;
|
|
79
|
+
}
|
|
80
|
+
const entries = allowList
|
|
73
81
|
.map((entry) => normalizeUserEntry(String(entry)))
|
|
74
82
|
.filter((entry) => entry && entry !== "*");
|
|
75
|
-
if (entries.length
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
if (entries.length === 0) {
|
|
84
|
+
return allowList;
|
|
85
|
+
}
|
|
86
|
+
const mapping: string[] = [];
|
|
87
|
+
const unresolved: string[] = [];
|
|
88
|
+
const additions: string[] = [];
|
|
89
|
+
const pending: string[] = [];
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (isMatrixUserId(entry)) {
|
|
92
|
+
additions.push(normalizeMatrixUserId(entry));
|
|
93
|
+
continue;
|
|
86
94
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
95
|
+
pending.push(entry);
|
|
96
|
+
}
|
|
97
|
+
if (pending.length > 0) {
|
|
98
|
+
const resolved = await resolveMatrixTargets({
|
|
99
|
+
cfg,
|
|
100
|
+
inputs: pending,
|
|
101
|
+
kind: "user",
|
|
102
|
+
runtime,
|
|
103
|
+
});
|
|
104
|
+
for (const entry of resolved) {
|
|
105
|
+
if (entry.resolved && entry.id) {
|
|
106
|
+
const normalizedId = normalizeMatrixUserId(entry.id);
|
|
107
|
+
additions.push(normalizedId);
|
|
108
|
+
mapping.push(`${entry.input}→${normalizedId}`);
|
|
109
|
+
} else {
|
|
110
|
+
unresolved.push(entry.input);
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
|
-
allowFrom = mergeAllowlist({ existing: allowFrom, additions });
|
|
104
|
-
summarizeMapping("matrix users", mapping, unresolved, runtime);
|
|
105
113
|
}
|
|
106
|
-
|
|
114
|
+
allowList = mergeAllowlist({ existing: allowList, additions });
|
|
115
|
+
summarizeMapping(label, mapping, unresolved, runtime);
|
|
116
|
+
if (unresolved.length > 0) {
|
|
117
|
+
runtime.log?.(
|
|
118
|
+
`${label} entries must be full Matrix IDs (example: @user:server). Unresolved entries are ignored.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return allowList;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true;
|
|
125
|
+
let allowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
|
|
126
|
+
let groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
|
127
|
+
let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms;
|
|
128
|
+
|
|
129
|
+
allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom);
|
|
130
|
+
groupAllowFrom = await resolveUserAllowlist("matrix group allowlist", groupAllowFrom);
|
|
107
131
|
|
|
108
132
|
if (roomsConfig && Object.keys(roomsConfig).length > 0) {
|
|
109
|
-
const entries = Object.keys(roomsConfig).filter((key) => key !== "*");
|
|
110
133
|
const mapping: string[] = [];
|
|
111
134
|
const unresolved: string[] = [];
|
|
112
|
-
const nextRooms
|
|
113
|
-
|
|
114
|
-
|
|
135
|
+
const nextRooms: Record<string, (typeof roomsConfig)[string]> = {};
|
|
136
|
+
if (roomsConfig["*"]) {
|
|
137
|
+
nextRooms["*"] = roomsConfig["*"];
|
|
138
|
+
}
|
|
139
|
+
const pending: Array<{ input: string; query: string; config: (typeof roomsConfig)[string] }> =
|
|
140
|
+
[];
|
|
141
|
+
for (const [entry, roomConfig] of Object.entries(roomsConfig)) {
|
|
142
|
+
if (entry === "*") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
115
145
|
const trimmed = entry.trim();
|
|
116
|
-
if (!trimmed)
|
|
146
|
+
if (!trimmed) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
117
149
|
const cleaned = normalizeRoomEntry(trimmed);
|
|
118
|
-
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
|
150
|
+
if ((cleaned.startsWith("!") || cleaned.startsWith("#")) && cleaned.includes(":")) {
|
|
119
151
|
if (!nextRooms[cleaned]) {
|
|
120
|
-
nextRooms[cleaned] =
|
|
152
|
+
nextRooms[cleaned] = roomConfig;
|
|
153
|
+
}
|
|
154
|
+
if (cleaned !== entry) {
|
|
155
|
+
mapping.push(`${entry}→${cleaned}`);
|
|
121
156
|
}
|
|
122
|
-
mapping.push(`${entry}→${cleaned}`);
|
|
123
157
|
continue;
|
|
124
158
|
}
|
|
125
|
-
pending.push({ input: entry, query: trimmed });
|
|
159
|
+
pending.push({ input: entry, query: trimmed, config: roomConfig });
|
|
126
160
|
}
|
|
127
161
|
if (pending.length > 0) {
|
|
128
162
|
const resolved = await resolveMatrixTargets({
|
|
@@ -133,10 +167,12 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
133
167
|
});
|
|
134
168
|
resolved.forEach((entry, index) => {
|
|
135
169
|
const source = pending[index];
|
|
136
|
-
if (!source)
|
|
170
|
+
if (!source) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
137
173
|
if (entry.resolved && entry.id) {
|
|
138
174
|
if (!nextRooms[entry.id]) {
|
|
139
|
-
nextRooms[entry.id] =
|
|
175
|
+
nextRooms[entry.id] = source.config;
|
|
140
176
|
}
|
|
141
177
|
mapping.push(`${source.input}→${entry.id}`);
|
|
142
178
|
} else {
|
|
@@ -146,6 +182,25 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
146
182
|
}
|
|
147
183
|
roomsConfig = nextRooms;
|
|
148
184
|
summarizeMapping("matrix rooms", mapping, unresolved, runtime);
|
|
185
|
+
if (unresolved.length > 0) {
|
|
186
|
+
runtime.log?.(
|
|
187
|
+
"matrix rooms must be room IDs or aliases (example: !room:server or #alias:server). Unresolved entries are ignored.",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (roomsConfig && Object.keys(roomsConfig).length > 0) {
|
|
192
|
+
const nextRooms = { ...roomsConfig };
|
|
193
|
+
for (const [roomKey, roomConfig] of Object.entries(roomsConfig)) {
|
|
194
|
+
const users = roomConfig?.users ?? [];
|
|
195
|
+
if (users.length === 0) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const resolvedUsers = await resolveUserAllowlist(`matrix room users (${roomKey})`, users);
|
|
199
|
+
if (resolvedUsers !== users) {
|
|
200
|
+
nextRooms[roomKey] = { ...roomConfig, users: resolvedUsers };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
roomsConfig = nextRooms;
|
|
149
204
|
}
|
|
150
205
|
|
|
151
206
|
cfg = {
|
|
@@ -158,6 +213,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
158
213
|
...cfg.channels?.matrix?.dm,
|
|
159
214
|
allowFrom,
|
|
160
215
|
},
|
|
216
|
+
...(groupAllowFrom.length > 0 ? { groupAllowFrom } : {}),
|
|
161
217
|
...(roomsConfig ? { groups: roomsConfig } : {}),
|
|
162
218
|
},
|
|
163
219
|
},
|
|
@@ -256,7 +312,10 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
|
|
256
312
|
logger.info("matrix: device verification requested - please verify in another client");
|
|
257
313
|
}
|
|
258
314
|
} catch (err) {
|
|
259
|
-
logger.debug(
|
|
315
|
+
logger.debug(
|
|
316
|
+
{ error: String(err) },
|
|
317
|
+
"Device verification request failed (may already be verified)",
|
|
318
|
+
);
|
|
260
319
|
}
|
|
261
320
|
}
|
|
262
321
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { LocationMessageEventContent } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
|
|
3
2
|
import {
|
|
4
3
|
formatLocationText,
|
|
5
4
|
toLocationContext,
|
|
@@ -20,25 +19,37 @@ type GeoUriParams = {
|
|
|
20
19
|
|
|
21
20
|
function parseGeoUri(value: string): GeoUriParams | null {
|
|
22
21
|
const trimmed = value.trim();
|
|
23
|
-
if (!trimmed)
|
|
24
|
-
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!trimmed.toLowerCase().startsWith("geo:")) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
25
28
|
const payload = trimmed.slice(4);
|
|
26
29
|
const [coordsPart, ...paramParts] = payload.split(";");
|
|
27
30
|
const coords = coordsPart.split(",");
|
|
28
|
-
if (coords.length < 2)
|
|
31
|
+
if (coords.length < 2) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
29
34
|
const latitude = Number.parseFloat(coords[0] ?? "");
|
|
30
35
|
const longitude = Number.parseFloat(coords[1] ?? "");
|
|
31
|
-
if (!Number.isFinite(latitude) || !Number.isFinite(longitude))
|
|
36
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
32
39
|
|
|
33
40
|
const params = new Map<string, string>();
|
|
34
41
|
for (const part of paramParts) {
|
|
35
42
|
const segment = part.trim();
|
|
36
|
-
if (!segment)
|
|
43
|
+
if (!segment) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
37
46
|
const eqIndex = segment.indexOf("=");
|
|
38
47
|
const rawKey = eqIndex === -1 ? segment : segment.slice(0, eqIndex);
|
|
39
48
|
const rawValue = eqIndex === -1 ? "" : segment.slice(eqIndex + 1);
|
|
40
49
|
const key = rawKey.trim().toLowerCase();
|
|
41
|
-
if (!key)
|
|
50
|
+
if (!key) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
42
53
|
const valuePart = rawValue.trim();
|
|
43
54
|
params.set(key, valuePart ? decodeURIComponent(valuePart) : "");
|
|
44
55
|
}
|
|
@@ -61,11 +72,17 @@ export function resolveMatrixLocation(params: {
|
|
|
61
72
|
const isLocation =
|
|
62
73
|
eventType === EventType.Location ||
|
|
63
74
|
(eventType === EventType.RoomMessage && content.msgtype === EventType.Location);
|
|
64
|
-
if (!isLocation)
|
|
75
|
+
if (!isLocation) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
65
78
|
const geoUri = typeof content.geo_uri === "string" ? content.geo_uri.trim() : "";
|
|
66
|
-
if (!geoUri)
|
|
79
|
+
if (!geoUri) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
67
82
|
const parsed = parseGeoUri(geoUri);
|
|
68
|
-
if (!parsed)
|
|
83
|
+
if (!parsed) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
69
86
|
const caption = typeof content.body === "string" ? content.body.trim() : "";
|
|
70
87
|
const location: NormalizedLocation = {
|
|
71
88
|
latitude: parsed.latitude,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
3
|
import { setMatrixRuntime } from "../../runtime.js";
|
|
5
4
|
import { downloadMatrixMedia } from "./media.js";
|
|
6
5
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
|
|
3
2
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
3
|
|
|
5
4
|
// Type for encrypted file info
|
|
@@ -24,7 +23,9 @@ async function fetchMatrixMediaBuffer(params: {
|
|
|
24
23
|
}): Promise<{ buffer: Buffer; headerType?: string } | null> {
|
|
25
24
|
// @vector-im/matrix-bot-sdk provides mxcToHttp helper
|
|
26
25
|
const url = params.client.mxcToHttp(params.mxcUrl);
|
|
27
|
-
if (!url)
|
|
26
|
+
if (!url) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
28
29
|
|
|
29
30
|
// Use the client's download method which handles auth
|
|
30
31
|
try {
|
|
@@ -34,7 +35,7 @@ async function fetchMatrixMediaBuffer(params: {
|
|
|
34
35
|
}
|
|
35
36
|
return { buffer: Buffer.from(buffer) };
|
|
36
37
|
} catch (err) {
|
|
37
|
-
throw new Error(`Matrix media download failed: ${String(err)}
|
|
38
|
+
throw new Error(`Matrix media download failed: ${String(err)}`, { cause: err });
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -74,10 +75,7 @@ export async function downloadMatrixMedia(params: {
|
|
|
74
75
|
placeholder: string;
|
|
75
76
|
} | null> {
|
|
76
77
|
let fetched: { buffer: Buffer; headerType?: string } | null;
|
|
77
|
-
if (
|
|
78
|
-
typeof params.sizeBytes === "number" &&
|
|
79
|
-
params.sizeBytes > params.maxBytes
|
|
80
|
-
) {
|
|
78
|
+
if (typeof params.sizeBytes === "number" && params.sizeBytes > params.maxBytes) {
|
|
81
79
|
throw new Error("Matrix media exceeds configured size limit");
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -97,7 +95,9 @@ export async function downloadMatrixMedia(params: {
|
|
|
97
95
|
});
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
if (!fetched)
|
|
98
|
+
if (!fetched) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
101
|
const headerType = fetched.headerType ?? params.contentType ?? undefined;
|
|
102
102
|
const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(
|
|
103
103
|
fetched.buffer,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
|
|
3
2
|
import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
4
|
-
import { sendMessageMatrix } from "../send.js";
|
|
5
3
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
|
+
import { sendMessageMatrix } from "../send.js";
|
|
6
5
|
|
|
7
6
|
export async function deliverMatrixReplies(params: {
|
|
8
7
|
replies: ReplyPayload[];
|
|
@@ -62,7 +61,9 @@ export async function deliverMatrixReplies(params: {
|
|
|
62
61
|
chunkMode,
|
|
63
62
|
)) {
|
|
64
63
|
const trimmed = chunk.trim();
|
|
65
|
-
if (!trimmed)
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
66
67
|
await sendMessageMatrix(params.roomId, trimmed, {
|
|
67
68
|
client: params.client,
|
|
68
69
|
replyToId: shouldIncludeReply(replyToId) ? replyToId : undefined,
|
|
@@ -11,14 +11,14 @@ export function createMatrixRoomInfoResolver(client: MatrixClient) {
|
|
|
11
11
|
|
|
12
12
|
const getRoomInfo = async (roomId: string): Promise<MatrixRoomInfo> => {
|
|
13
13
|
const cached = roomInfoCache.get(roomId);
|
|
14
|
-
if (cached)
|
|
14
|
+
if (cached) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
15
17
|
let name: string | undefined;
|
|
16
18
|
let canonicalAlias: string | undefined;
|
|
17
19
|
let altAliases: string[] = [];
|
|
18
20
|
try {
|
|
19
|
-
const nameState = await client
|
|
20
|
-
.getRoomStateEvent(roomId, "m.room.name", "")
|
|
21
|
-
.catch(() => null);
|
|
21
|
+
const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "").catch(() => null);
|
|
22
22
|
name = nameState?.name;
|
|
23
23
|
} catch {
|
|
24
24
|
// ignore
|
|
@@ -37,10 +37,7 @@ export function createMatrixRoomInfoResolver(client: MatrixClient) {
|
|
|
37
37
|
return info;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const getMemberDisplayName = async (
|
|
41
|
-
roomId: string,
|
|
42
|
-
userId: string,
|
|
43
|
-
): Promise<string> => {
|
|
40
|
+
const getMemberDisplayName = async (roomId: string, userId: string): Promise<string> => {
|
|
44
41
|
try {
|
|
45
42
|
const memberState = await client
|
|
46
43
|
.getRoomStateEvent(roomId, "m.room.member", userId)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveMatrixRoomConfig } from "./rooms.js";
|
|
3
|
+
|
|
4
|
+
describe("resolveMatrixRoomConfig", () => {
|
|
5
|
+
it("matches room IDs and aliases, not names", () => {
|
|
6
|
+
const rooms = {
|
|
7
|
+
"!room:example.org": { allow: true },
|
|
8
|
+
"#alias:example.org": { allow: true },
|
|
9
|
+
"Project Room": { allow: true },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const byId = resolveMatrixRoomConfig({
|
|
13
|
+
rooms,
|
|
14
|
+
roomId: "!room:example.org",
|
|
15
|
+
aliases: [],
|
|
16
|
+
name: "Project Room",
|
|
17
|
+
});
|
|
18
|
+
expect(byId.allowed).toBe(true);
|
|
19
|
+
expect(byId.matchKey).toBe("!room:example.org");
|
|
20
|
+
|
|
21
|
+
const byAlias = resolveMatrixRoomConfig({
|
|
22
|
+
rooms,
|
|
23
|
+
roomId: "!other:example.org",
|
|
24
|
+
aliases: ["#alias:example.org"],
|
|
25
|
+
name: "Other Room",
|
|
26
|
+
});
|
|
27
|
+
expect(byAlias.allowed).toBe(true);
|
|
28
|
+
expect(byAlias.matchKey).toBe("#alias:example.org");
|
|
29
|
+
|
|
30
|
+
const byName = resolveMatrixRoomConfig({
|
|
31
|
+
rooms: { "Project Room": { allow: true } },
|
|
32
|
+
roomId: "!different:example.org",
|
|
33
|
+
aliases: [],
|
|
34
|
+
name: "Project Room",
|
|
35
|
+
});
|
|
36
|
+
expect(byName.allowed).toBe(false);
|
|
37
|
+
expect(byName.config).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MatrixRoomConfig } from "../../types.js";
|
|
2
1
|
import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { MatrixRoomConfig } from "../../types.js";
|
|
3
3
|
|
|
4
4
|
export type MatrixRoomConfigResolved = {
|
|
5
5
|
allowed: boolean;
|
|
@@ -22,9 +22,13 @@ export function resolveMatrixRoomConfig(params: {
|
|
|
22
22
|
params.roomId,
|
|
23
23
|
`room:${params.roomId}`,
|
|
24
24
|
...params.aliases,
|
|
25
|
-
params.name ?? "",
|
|
26
25
|
);
|
|
27
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
entry: matched,
|
|
28
|
+
key: matchedKey,
|
|
29
|
+
wildcardEntry,
|
|
30
|
+
wildcardKey,
|
|
31
|
+
} = resolveChannelEntryMatch({
|
|
28
32
|
entries: rooms,
|
|
29
33
|
keys: candidates,
|
|
30
34
|
wildcardKey: "*",
|
|
@@ -28,7 +28,9 @@ export function resolveMatrixThreadTarget(params: {
|
|
|
28
28
|
isThreadRoot?: boolean;
|
|
29
29
|
}): string | undefined {
|
|
30
30
|
const { threadReplies, messageId, threadRootId } = params;
|
|
31
|
-
if (threadReplies === "off")
|
|
31
|
+
if (threadReplies === "off") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
32
34
|
const isThreadRoot = params.isThreadRoot === true;
|
|
33
35
|
const hasInboundThread = Boolean(threadRootId && threadRootId !== messageId && !isThreadRoot);
|
|
34
36
|
if (threadReplies === "inbound") {
|
|
@@ -45,7 +47,9 @@ export function resolveMatrixThreadRootId(params: {
|
|
|
45
47
|
content: RoomMessageEventContent;
|
|
46
48
|
}): string | undefined {
|
|
47
49
|
const relates = params.content["m.relates_to"];
|
|
48
|
-
if (!relates || typeof relates !== "object")
|
|
50
|
+
if (!relates || typeof relates !== "object") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
49
53
|
if ("rel_type" in relates && relates.rel_type === RelationType.Thread) {
|
|
50
54
|
if ("event_id" in relates && typeof relates.event_id === "string") {
|
|
51
55
|
return relates.event_id;
|
package/src/matrix/poll-types.ts
CHANGED
|
@@ -77,18 +77,25 @@ export function isPollStartType(eventType: string): boolean {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export function getTextContent(text?: TextContent): string {
|
|
80
|
-
if (!text)
|
|
80
|
+
if (!text) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
81
83
|
return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? "";
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
|
|
85
|
-
const poll =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const poll =
|
|
88
|
+
(content as Record<string, PollStartSubtype | undefined>)[M_POLL_START] ??
|
|
89
|
+
(content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START] ??
|
|
90
|
+
(content as Record<string, PollStartSubtype | undefined>)["m.poll"];
|
|
91
|
+
if (!poll) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
89
94
|
|
|
90
95
|
const question = getTextContent(poll.question);
|
|
91
|
-
if (!question)
|
|
96
|
+
if (!question) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
92
99
|
|
|
93
100
|
const answers = poll.answers
|
|
94
101
|
.map((answer) => getTextContent(answer))
|
|
@@ -124,7 +131,9 @@ function buildTextContent(body: string): TextContent {
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
function buildPollFallbackText(question: string, answers: string[]): string {
|
|
127
|
-
if (answers.length === 0)
|
|
134
|
+
if (answers.length === 0) {
|
|
135
|
+
return question;
|
|
136
|
+
}
|
|
128
137
|
return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
|
|
129
138
|
}
|
|
130
139
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
|
|
2
|
+
import type { CoreConfig } from "../types.js";
|
|
3
3
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
4
|
import { getActiveMatrixClient } from "../active-client.js";
|
|
5
5
|
import {
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
resolveMatrixAuth,
|
|
9
9
|
resolveSharedMatrixClient,
|
|
10
10
|
} from "../client.js";
|
|
11
|
-
import type { CoreConfig } from "../types.js";
|
|
12
11
|
|
|
13
12
|
const getCore = () => getMatrixRuntime();
|
|
14
13
|
|
|
@@ -31,9 +30,13 @@ export async function resolveMatrixClient(opts: {
|
|
|
31
30
|
timeoutMs?: number;
|
|
32
31
|
}): Promise<{ client: MatrixClient; stopOnDone: boolean }> {
|
|
33
32
|
ensureNodeRuntime();
|
|
34
|
-
if (opts.client)
|
|
33
|
+
if (opts.client) {
|
|
34
|
+
return { client: opts.client, stopOnDone: false };
|
|
35
|
+
}
|
|
35
36
|
const active = getActiveMatrixClient();
|
|
36
|
-
if (active)
|
|
37
|
+
if (active) {
|
|
38
|
+
return { client: active, stopOnDone: false };
|
|
39
|
+
}
|
|
37
40
|
const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT);
|
|
38
41
|
if (shouldShareClient) {
|
|
39
42
|
const client = await resolveSharedMatrixClient({
|