@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,5 +1,5 @@
|
|
|
1
|
-
import { markdownToMatrixHtml } from "../format.js";
|
|
2
1
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
2
|
+
import { markdownToMatrixHtml } from "../format.js";
|
|
3
3
|
import {
|
|
4
4
|
MsgType,
|
|
5
5
|
RelationType,
|
|
@@ -13,10 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const getCore = () => getMatrixRuntime();
|
|
15
15
|
|
|
16
|
-
export function buildTextContent(
|
|
17
|
-
body: string,
|
|
18
|
-
relation?: MatrixRelation,
|
|
19
|
-
): MatrixTextContent {
|
|
16
|
+
export function buildTextContent(body: string, relation?: MatrixRelation): MatrixTextContent {
|
|
20
17
|
const content: MatrixTextContent = relation
|
|
21
18
|
? {
|
|
22
19
|
msgtype: MsgType.Text,
|
|
@@ -33,34 +30,32 @@ export function buildTextContent(
|
|
|
33
30
|
|
|
34
31
|
export function applyMatrixFormatting(content: MatrixFormattedContent, body: string): void {
|
|
35
32
|
const formatted = markdownToMatrixHtml(body ?? "");
|
|
36
|
-
if (!formatted)
|
|
33
|
+
if (!formatted) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
37
36
|
content.format = "org.matrix.custom.html";
|
|
38
37
|
content.formatted_body = formatted;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
export function buildReplyRelation(replyToId?: string): MatrixReplyRelation | undefined {
|
|
42
41
|
const trimmed = replyToId?.trim();
|
|
43
|
-
if (!trimmed)
|
|
42
|
+
if (!trimmed) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
44
45
|
return { "m.in_reply_to": { event_id: trimmed } };
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export function buildThreadRelation(
|
|
48
|
-
threadId: string,
|
|
49
|
-
replyToId?: string,
|
|
50
|
-
): MatrixThreadRelation {
|
|
48
|
+
export function buildThreadRelation(threadId: string, replyToId?: string): MatrixThreadRelation {
|
|
51
49
|
const trimmed = threadId.trim();
|
|
52
50
|
return {
|
|
53
51
|
rel_type: RelationType.Thread,
|
|
54
52
|
event_id: trimmed,
|
|
55
53
|
is_falling_back: true,
|
|
56
|
-
"m.in_reply_to": { event_id:
|
|
54
|
+
"m.in_reply_to": { event_id: replyToId?.trim() || trimmed },
|
|
57
55
|
};
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
export function resolveMatrixMsgType(
|
|
61
|
-
contentType?: string,
|
|
62
|
-
_fileName?: string,
|
|
63
|
-
): MatrixMediaMsgType {
|
|
58
|
+
export function resolveMatrixMsgType(contentType?: string, _fileName?: string): MatrixMediaMsgType {
|
|
64
59
|
const kind = getCore().media.mediaKindFromMime(contentType ?? "");
|
|
65
60
|
switch (kind) {
|
|
66
61
|
case "image":
|
|
@@ -79,7 +74,9 @@ export function resolveMatrixVoiceDecision(opts: {
|
|
|
79
74
|
contentType?: string;
|
|
80
75
|
fileName?: string;
|
|
81
76
|
}): { useVoice: boolean } {
|
|
82
|
-
if (!opts.wantsVoice)
|
|
77
|
+
if (!opts.wantsVoice) {
|
|
78
|
+
return { useVoice: false };
|
|
79
|
+
}
|
|
83
80
|
if (
|
|
84
81
|
getCore().media.isVoiceCompatibleAudio({
|
|
85
82
|
contentType: opts.contentType,
|
package/src/matrix/send/media.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type {
|
|
|
7
7
|
VideoFileInfo,
|
|
8
8
|
} from "@vector-im/matrix-bot-sdk";
|
|
9
9
|
import { parseBuffer, type IFileInfo } from "music-metadata";
|
|
10
|
-
|
|
11
10
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
11
|
+
import { applyMatrixFormatting } from "./formatting.js";
|
|
12
12
|
import {
|
|
13
13
|
type MatrixMediaContent,
|
|
14
14
|
type MatrixMediaInfo,
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
type MatrixRelation,
|
|
17
17
|
type MediaKind,
|
|
18
18
|
} from "./types.js";
|
|
19
|
-
import { applyMatrixFormatting } from "./formatting.js";
|
|
20
19
|
|
|
21
20
|
const getCore = () => getMatrixRuntime();
|
|
22
21
|
|
|
@@ -54,7 +53,9 @@ export function buildMatrixMediaInfo(params: {
|
|
|
54
53
|
};
|
|
55
54
|
return timedInfo;
|
|
56
55
|
}
|
|
57
|
-
if (Object.keys(base).length === 0)
|
|
56
|
+
if (Object.keys(base).length === 0) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
58
59
|
return base;
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -113,8 +114,12 @@ export async function prepareImageInfo(params: {
|
|
|
113
114
|
buffer: Buffer;
|
|
114
115
|
client: MatrixClient;
|
|
115
116
|
}): Promise<DimensionalFileInfo | undefined> {
|
|
116
|
-
const meta = await getCore()
|
|
117
|
-
|
|
117
|
+
const meta = await getCore()
|
|
118
|
+
.media.getImageMetadata(params.buffer)
|
|
119
|
+
.catch(() => null);
|
|
120
|
+
if (!meta) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
118
123
|
const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height };
|
|
119
124
|
const maxDim = Math.max(meta.width, meta.height);
|
|
120
125
|
if (maxDim > THUMBNAIL_MAX_SIDE) {
|
|
@@ -125,7 +130,9 @@ export async function prepareImageInfo(params: {
|
|
|
125
130
|
quality: THUMBNAIL_QUALITY,
|
|
126
131
|
withoutEnlargement: true,
|
|
127
132
|
});
|
|
128
|
-
const thumbMeta = await getCore()
|
|
133
|
+
const thumbMeta = await getCore()
|
|
134
|
+
.media.getImageMetadata(thumbBuffer)
|
|
135
|
+
.catch(() => null);
|
|
129
136
|
const thumbUri = await params.client.uploadContent(
|
|
130
137
|
thumbBuffer,
|
|
131
138
|
"image/jpeg",
|
|
@@ -153,7 +160,9 @@ export async function resolveMediaDurationMs(params: {
|
|
|
153
160
|
fileName?: string;
|
|
154
161
|
kind: MediaKind;
|
|
155
162
|
}): Promise<number | undefined> {
|
|
156
|
-
if (params.kind !== "audio" && params.kind !== "video")
|
|
163
|
+
if (params.kind !== "audio" && params.kind !== "video") {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
157
166
|
try {
|
|
158
167
|
const fileInfo: IFileInfo | string | undefined =
|
|
159
168
|
params.contentType || params.fileName
|
|
@@ -201,7 +210,7 @@ export async function uploadMediaMaybeEncrypted(
|
|
|
201
210
|
},
|
|
202
211
|
): Promise<{ url: string; file?: EncryptedFile }> {
|
|
203
212
|
// Check if room is encrypted and crypto is available
|
|
204
|
-
const isEncrypted = client.crypto && await client.crypto.isRoomEncrypted(roomId);
|
|
213
|
+
const isEncrypted = client.crypto && (await client.crypto.isRoomEncrypted(roomId));
|
|
205
214
|
|
|
206
215
|
if (isEncrypted && client.crypto) {
|
|
207
216
|
// Encrypt the media before uploading
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
3
|
import { EventType } from "./types.js";
|
|
5
4
|
|
|
6
5
|
let resolveMatrixRoomId: typeof import("./targets.js").resolveMatrixRoomId;
|
|
@@ -26,7 +25,9 @@ describe("resolveMatrixRoomId", () => {
|
|
|
26
25
|
const roomId = await resolveMatrixRoomId(client, userId);
|
|
27
26
|
|
|
28
27
|
expect(roomId).toBe("!room:example.org");
|
|
28
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
29
29
|
expect(client.getJoinedRooms).not.toHaveBeenCalled();
|
|
30
|
+
// oxlint-disable-next-line typescript/unbound-method
|
|
30
31
|
expect(client.setAccountData).not.toHaveBeenCalled();
|
|
31
32
|
});
|
|
32
33
|
|
|
@@ -37,10 +38,7 @@ describe("resolveMatrixRoomId", () => {
|
|
|
37
38
|
const client = {
|
|
38
39
|
getAccountData: vi.fn().mockRejectedValue(new Error("nope")),
|
|
39
40
|
getJoinedRooms: vi.fn().mockResolvedValue([roomId]),
|
|
40
|
-
getJoinedRoomMembers: vi.fn().mockResolvedValue([
|
|
41
|
-
"@bot:example.org",
|
|
42
|
-
userId,
|
|
43
|
-
]),
|
|
41
|
+
getJoinedRoomMembers: vi.fn().mockResolvedValue(["@bot:example.org", userId]),
|
|
44
42
|
setAccountData,
|
|
45
43
|
} as unknown as MatrixClient;
|
|
46
44
|
|
|
@@ -80,11 +78,9 @@ describe("resolveMatrixRoomId", () => {
|
|
|
80
78
|
const client = {
|
|
81
79
|
getAccountData: vi.fn().mockRejectedValue(new Error("nope")),
|
|
82
80
|
getJoinedRooms: vi.fn().mockResolvedValue([roomId]),
|
|
83
|
-
getJoinedRoomMembers: vi
|
|
84
|
-
|
|
85
|
-
userId,
|
|
86
|
-
"@extra:example.org",
|
|
87
|
-
]),
|
|
81
|
+
getJoinedRoomMembers: vi
|
|
82
|
+
.fn()
|
|
83
|
+
.mockResolvedValue(["@bot:example.org", userId, "@extra:example.org"]),
|
|
88
84
|
setAccountData: vi.fn().mockResolvedValue(undefined),
|
|
89
85
|
} as unknown as MatrixClient;
|
|
90
86
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
|
|
3
2
|
import { EventType, type MatrixDirectAccountData } from "./types.js";
|
|
4
3
|
|
|
5
4
|
function normalizeTarget(raw: string): string {
|
|
@@ -11,7 +10,9 @@ function normalizeTarget(raw: string): string {
|
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export function normalizeThreadId(raw?: string | number | null): string | null {
|
|
14
|
-
if (raw === undefined || raw === null)
|
|
13
|
+
if (raw === undefined || raw === null) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
15
16
|
const trimmed = String(raw).trim();
|
|
16
17
|
return trimmed ? trimmed : null;
|
|
17
18
|
}
|
|
@@ -25,16 +26,15 @@ async function persistDirectRoom(
|
|
|
25
26
|
): Promise<void> {
|
|
26
27
|
let directContent: MatrixDirectAccountData | null = null;
|
|
27
28
|
try {
|
|
28
|
-
directContent =
|
|
29
|
-
EventType.Direct,
|
|
30
|
-
)) as MatrixDirectAccountData | null;
|
|
29
|
+
directContent = await client.getAccountData(EventType.Direct);
|
|
31
30
|
} catch {
|
|
32
31
|
// Ignore fetch errors and fall back to an empty map.
|
|
33
32
|
}
|
|
34
|
-
const existing =
|
|
35
|
-
directContent && !Array.isArray(directContent) ? directContent : {};
|
|
33
|
+
const existing = directContent && !Array.isArray(directContent) ? directContent : {};
|
|
36
34
|
const current = Array.isArray(existing[userId]) ? existing[userId] : [];
|
|
37
|
-
if (current[0] === roomId)
|
|
35
|
+
if (current[0] === roomId) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
38
|
const next = [roomId, ...current.filter((id) => id !== roomId)];
|
|
39
39
|
try {
|
|
40
40
|
await client.setAccountData(EventType.Direct, {
|
|
@@ -46,28 +46,21 @@ async function persistDirectRoom(
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async function resolveDirectRoomId(
|
|
50
|
-
client: MatrixClient,
|
|
51
|
-
userId: string,
|
|
52
|
-
): Promise<string> {
|
|
49
|
+
async function resolveDirectRoomId(client: MatrixClient, userId: string): Promise<string> {
|
|
53
50
|
const trimmed = userId.trim();
|
|
54
51
|
if (!trimmed.startsWith("@")) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
`Matrix user IDs must be fully qualified (got "${trimmed}")`,
|
|
57
|
-
);
|
|
52
|
+
throw new Error(`Matrix user IDs must be fully qualified (got "${trimmed}")`);
|
|
58
53
|
}
|
|
59
54
|
|
|
60
55
|
const cached = directRoomCache.get(trimmed);
|
|
61
|
-
if (cached)
|
|
56
|
+
if (cached) {
|
|
57
|
+
return cached;
|
|
58
|
+
}
|
|
62
59
|
|
|
63
60
|
// 1) Fast path: use account data (m.direct) for *this* logged-in user (the bot).
|
|
64
61
|
try {
|
|
65
|
-
const directContent =
|
|
66
|
-
|
|
67
|
-
)) as MatrixDirectAccountData | null;
|
|
68
|
-
const list = Array.isArray(directContent?.[trimmed])
|
|
69
|
-
? directContent[trimmed]
|
|
70
|
-
: [];
|
|
62
|
+
const directContent = await client.getAccountData(EventType.Direct);
|
|
63
|
+
const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : [];
|
|
71
64
|
if (list.length > 0) {
|
|
72
65
|
directRoomCache.set(trimmed, list[0]);
|
|
73
66
|
return list[0];
|
|
@@ -88,7 +81,9 @@ async function resolveDirectRoomId(
|
|
|
88
81
|
} catch {
|
|
89
82
|
continue;
|
|
90
83
|
}
|
|
91
|
-
if (!members.includes(trimmed))
|
|
84
|
+
if (!members.includes(trimmed)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
92
87
|
// Prefer classic 1:1 rooms, but allow larger rooms if requested.
|
|
93
88
|
if (members.length === 2) {
|
|
94
89
|
directRoomCache.set(trimmed, roomId);
|
|
@@ -112,10 +107,7 @@ async function resolveDirectRoomId(
|
|
|
112
107
|
throw new Error(`No direct room found for ${trimmed} (m.direct missing)`);
|
|
113
108
|
}
|
|
114
109
|
|
|
115
|
-
export async function resolveMatrixRoomId(
|
|
116
|
-
client: MatrixClient,
|
|
117
|
-
raw: string,
|
|
118
|
-
): Promise<string> {
|
|
110
|
+
export async function resolveMatrixRoomId(client: MatrixClient, raw: string): Promise<string> {
|
|
119
111
|
const target = normalizeTarget(raw);
|
|
120
112
|
const lowered = target.toLowerCase();
|
|
121
113
|
if (lowered.startsWith("matrix:")) {
|
package/src/matrix/send.test.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
3
|
import { setMatrixRuntime } from "../runtime.js";
|
|
5
4
|
|
|
6
5
|
vi.mock("@vector-im/matrix-bot-sdk", () => ({
|
package/src/matrix/send.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
|
|
3
2
|
import type { PollInput } from "openclaw/plugin-sdk";
|
|
4
3
|
import { getMatrixRuntime } from "../runtime.js";
|
|
5
4
|
import { buildPollStartContent, M_POLL_START } from "./poll-types.js";
|
|
@@ -123,7 +122,9 @@ export async function sendMessageMatrix(
|
|
|
123
122
|
const followupRelation = threadId ? relation : undefined;
|
|
124
123
|
for (const chunk of textChunks) {
|
|
125
124
|
const text = chunk.trim();
|
|
126
|
-
if (!text)
|
|
125
|
+
if (!text) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
127
128
|
const followup = buildTextContent(text, followupRelation);
|
|
128
129
|
const followupEventId = await sendContent(followup);
|
|
129
130
|
lastMessageId = followupEventId ?? lastMessageId;
|
|
@@ -131,7 +132,9 @@ export async function sendMessageMatrix(
|
|
|
131
132
|
} else {
|
|
132
133
|
for (const chunk of chunks.length ? chunks : [""]) {
|
|
133
134
|
const text = chunk.trim();
|
|
134
|
-
if (!text)
|
|
135
|
+
if (!text) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
135
138
|
const content = buildTextContent(text, relation);
|
|
136
139
|
const eventId = await sendContent(content);
|
|
137
140
|
lastMessageId = eventId ?? lastMessageId;
|
|
@@ -211,7 +214,9 @@ export async function sendReadReceiptMatrix(
|
|
|
211
214
|
eventId: string,
|
|
212
215
|
client?: MatrixClient,
|
|
213
216
|
): Promise<void> {
|
|
214
|
-
if (!eventId?.trim())
|
|
217
|
+
if (!eventId?.trim()) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
215
220
|
const { client: resolved, stopOnDone } = await resolveMatrixClient({
|
|
216
221
|
client,
|
|
217
222
|
});
|
package/src/onboarding.ts
CHANGED
|
@@ -6,16 +6,17 @@ import {
|
|
|
6
6
|
type ChannelOnboardingDmPolicy,
|
|
7
7
|
type WizardPrompter,
|
|
8
8
|
} from "openclaw/plugin-sdk";
|
|
9
|
+
import type { CoreConfig, DmPolicy } from "./types.js";
|
|
9
10
|
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
|
10
11
|
import { listMatrixDirectoryPeersLive } from "./directory-live.js";
|
|
11
12
|
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
|
12
13
|
import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
|
|
13
|
-
import type { CoreConfig, DmPolicy } from "./types.js";
|
|
14
14
|
|
|
15
15
|
const channel = "matrix" as const;
|
|
16
16
|
|
|
17
17
|
function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
|
|
18
|
-
const allowFrom =
|
|
18
|
+
const allowFrom =
|
|
19
|
+
policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined;
|
|
19
20
|
return {
|
|
20
21
|
...cfg,
|
|
21
22
|
channels: {
|
|
@@ -248,8 +249,12 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
248
249
|
initialValue: existing.homeserver ?? envHomeserver,
|
|
249
250
|
validate: (value) => {
|
|
250
251
|
const raw = String(value ?? "").trim();
|
|
251
|
-
if (!raw)
|
|
252
|
-
|
|
252
|
+
if (!raw) {
|
|
253
|
+
return "Required";
|
|
254
|
+
}
|
|
255
|
+
if (!/^https?:\/\//i.test(raw)) {
|
|
256
|
+
return "Use a full URL (https://...)";
|
|
257
|
+
}
|
|
253
258
|
return undefined;
|
|
254
259
|
},
|
|
255
260
|
}),
|
|
@@ -273,13 +278,13 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
273
278
|
|
|
274
279
|
if (!accessToken && !password) {
|
|
275
280
|
// Ask auth method FIRST before asking for user ID
|
|
276
|
-
const authMode =
|
|
281
|
+
const authMode = await prompter.select({
|
|
277
282
|
message: "Matrix auth method",
|
|
278
283
|
options: [
|
|
279
284
|
{ value: "token", label: "Access token (user ID fetched automatically)" },
|
|
280
285
|
{ value: "password", label: "Password (requires user ID)" },
|
|
281
286
|
],
|
|
282
|
-
})
|
|
287
|
+
});
|
|
283
288
|
|
|
284
289
|
if (authMode === "token") {
|
|
285
290
|
accessToken = String(
|
|
@@ -299,9 +304,15 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
299
304
|
initialValue: existing.userId ?? envUserId,
|
|
300
305
|
validate: (value) => {
|
|
301
306
|
const raw = String(value ?? "").trim();
|
|
302
|
-
if (!raw)
|
|
303
|
-
|
|
304
|
-
|
|
307
|
+
if (!raw) {
|
|
308
|
+
return "Required";
|
|
309
|
+
}
|
|
310
|
+
if (!raw.startsWith("@")) {
|
|
311
|
+
return "Matrix user IDs should start with @";
|
|
312
|
+
}
|
|
313
|
+
if (!raw.includes(":")) {
|
|
314
|
+
return "Matrix user IDs should include a server (:server)";
|
|
315
|
+
}
|
|
305
316
|
return undefined;
|
|
306
317
|
},
|
|
307
318
|
}),
|
|
@@ -369,7 +380,9 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
369
380
|
const unresolved: string[] = [];
|
|
370
381
|
for (const entry of accessConfig.entries) {
|
|
371
382
|
const trimmed = entry.trim();
|
|
372
|
-
if (!trimmed)
|
|
383
|
+
if (!trimmed) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
373
386
|
const cleaned = trimmed.replace(/^(room|channel):/i, "").trim();
|
|
374
387
|
if (cleaned.startsWith("!") && cleaned.includes(":")) {
|
|
375
388
|
resolvedIds.push(cleaned);
|
|
@@ -390,10 +403,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
390
403
|
unresolved.push(entry);
|
|
391
404
|
}
|
|
392
405
|
}
|
|
393
|
-
roomKeys = [
|
|
394
|
-
...resolvedIds,
|
|
395
|
-
...unresolved.map((entry) => entry.trim()).filter(Boolean),
|
|
396
|
-
];
|
|
406
|
+
roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
|
|
397
407
|
if (resolvedIds.length > 0 || unresolved.length > 0) {
|
|
398
408
|
await prompter.note(
|
|
399
409
|
[
|
package/src/outbound.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
import { getMatrixRuntime } from "./runtime.js";
|
|
4
2
|
import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js";
|
|
3
|
+
import { getMatrixRuntime } from "./runtime.js";
|
|
5
4
|
|
|
6
5
|
export const matrixOutbound: ChannelOutboundAdapter = {
|
|
7
6
|
deliveryMode: "direct",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
|
+
import { listMatrixDirectoryPeersLive } from "./directory-live.js";
|
|
4
|
+
import { resolveMatrixTargets } from "./resolve-targets.js";
|
|
5
|
+
|
|
6
|
+
vi.mock("./directory-live.js", () => ({
|
|
7
|
+
listMatrixDirectoryPeersLive: vi.fn(),
|
|
8
|
+
listMatrixDirectoryGroupsLive: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("resolveMatrixTargets (users)", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.mocked(listMatrixDirectoryPeersLive).mockReset();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("resolves exact unique display name matches", async () => {
|
|
17
|
+
const matches: ChannelDirectoryEntry[] = [
|
|
18
|
+
{ kind: "user", id: "@alice:example.org", name: "Alice" },
|
|
19
|
+
];
|
|
20
|
+
vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
|
|
21
|
+
|
|
22
|
+
const [result] = await resolveMatrixTargets({
|
|
23
|
+
cfg: {},
|
|
24
|
+
inputs: ["Alice"],
|
|
25
|
+
kind: "user",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(result?.resolved).toBe(true);
|
|
29
|
+
expect(result?.id).toBe("@alice:example.org");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("does not resolve ambiguous or non-exact matches", async () => {
|
|
33
|
+
const matches: ChannelDirectoryEntry[] = [
|
|
34
|
+
{ kind: "user", id: "@alice:example.org", name: "Alice" },
|
|
35
|
+
{ kind: "user", id: "@alice:evil.example", name: "Alice" },
|
|
36
|
+
];
|
|
37
|
+
vi.mocked(listMatrixDirectoryPeersLive).mockResolvedValue(matches);
|
|
38
|
+
|
|
39
|
+
const [result] = await resolveMatrixTargets({
|
|
40
|
+
cfg: {},
|
|
41
|
+
inputs: ["Alice"],
|
|
42
|
+
kind: "user",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result?.resolved).toBe(false);
|
|
46
|
+
expect(result?.note).toMatch(/use full Matrix ID/i);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/resolve-targets.ts
CHANGED
|
@@ -4,17 +4,15 @@ import type {
|
|
|
4
4
|
ChannelResolveResult,
|
|
5
5
|
RuntimeEnv,
|
|
6
6
|
} from "openclaw/plugin-sdk";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
listMatrixDirectoryGroupsLive,
|
|
10
|
-
listMatrixDirectoryPeersLive,
|
|
11
|
-
} from "./directory-live.js";
|
|
7
|
+
import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
|
|
12
8
|
|
|
13
9
|
function pickBestGroupMatch(
|
|
14
10
|
matches: ChannelDirectoryEntry[],
|
|
15
11
|
query: string,
|
|
16
12
|
): ChannelDirectoryEntry | undefined {
|
|
17
|
-
if (matches.length === 0)
|
|
13
|
+
if (matches.length === 0) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
18
16
|
const normalized = query.trim().toLowerCase();
|
|
19
17
|
if (normalized) {
|
|
20
18
|
const exact = matches.find((match) => {
|
|
@@ -23,11 +21,59 @@ function pickBestGroupMatch(
|
|
|
23
21
|
const id = match.id.trim().toLowerCase();
|
|
24
22
|
return name === normalized || handle === normalized || id === normalized;
|
|
25
23
|
});
|
|
26
|
-
if (exact)
|
|
24
|
+
if (exact) {
|
|
25
|
+
return exact;
|
|
26
|
+
}
|
|
27
27
|
}
|
|
28
28
|
return matches[0];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function pickBestUserMatch(
|
|
32
|
+
matches: ChannelDirectoryEntry[],
|
|
33
|
+
query: string,
|
|
34
|
+
): ChannelDirectoryEntry | undefined {
|
|
35
|
+
if (matches.length === 0) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const normalized = query.trim().toLowerCase();
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const exact = matches.filter((match) => {
|
|
43
|
+
const id = match.id.trim().toLowerCase();
|
|
44
|
+
const name = match.name?.trim().toLowerCase();
|
|
45
|
+
const handle = match.handle?.trim().toLowerCase();
|
|
46
|
+
return normalized === id || normalized === name || normalized === handle;
|
|
47
|
+
});
|
|
48
|
+
if (exact.length === 1) {
|
|
49
|
+
return exact[0];
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function describeUserMatchFailure(matches: ChannelDirectoryEntry[], query: string): string {
|
|
55
|
+
if (matches.length === 0) {
|
|
56
|
+
return "no matches";
|
|
57
|
+
}
|
|
58
|
+
const normalized = query.trim().toLowerCase();
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return "empty input";
|
|
61
|
+
}
|
|
62
|
+
const exact = matches.filter((match) => {
|
|
63
|
+
const id = match.id.trim().toLowerCase();
|
|
64
|
+
const name = match.name?.trim().toLowerCase();
|
|
65
|
+
const handle = match.handle?.trim().toLowerCase();
|
|
66
|
+
return normalized === id || normalized === name || normalized === handle;
|
|
67
|
+
});
|
|
68
|
+
if (exact.length === 0) {
|
|
69
|
+
return "no exact match; use full Matrix ID";
|
|
70
|
+
}
|
|
71
|
+
if (exact.length > 1) {
|
|
72
|
+
return "multiple exact matches; use full Matrix ID";
|
|
73
|
+
}
|
|
74
|
+
return "no exact match; use full Matrix ID";
|
|
75
|
+
}
|
|
76
|
+
|
|
31
77
|
export async function resolveMatrixTargets(params: {
|
|
32
78
|
cfg: unknown;
|
|
33
79
|
inputs: string[];
|
|
@@ -52,13 +98,13 @@ export async function resolveMatrixTargets(params: {
|
|
|
52
98
|
query: trimmed,
|
|
53
99
|
limit: 5,
|
|
54
100
|
});
|
|
55
|
-
const best = matches
|
|
101
|
+
const best = pickBestUserMatch(matches, trimmed);
|
|
56
102
|
results.push({
|
|
57
103
|
input,
|
|
58
104
|
resolved: Boolean(best?.id),
|
|
59
105
|
id: best?.id,
|
|
60
106
|
name: best?.name,
|
|
61
|
-
note:
|
|
107
|
+
note: best ? undefined : describeUserMatchFailure(matches, trimmed),
|
|
62
108
|
});
|
|
63
109
|
} catch (err) {
|
|
64
110
|
params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
|
package/src/tool-actions.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
2
|
-
|
|
2
|
+
import {
|
|
3
|
+
createActionGate,
|
|
4
|
+
jsonResult,
|
|
5
|
+
readNumberParam,
|
|
6
|
+
readReactionParams,
|
|
7
|
+
readStringParam,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
3
9
|
import type { CoreConfig } from "./types.js";
|
|
4
10
|
import {
|
|
5
11
|
deleteMatrixMessage,
|
|
@@ -15,13 +21,6 @@ import {
|
|
|
15
21
|
unpinMatrixMessage,
|
|
16
22
|
} from "./matrix/actions.js";
|
|
17
23
|
import { reactMatrixMessage } from "./matrix/send.js";
|
|
18
|
-
import {
|
|
19
|
-
createActionGate,
|
|
20
|
-
jsonResult,
|
|
21
|
-
readNumberParam,
|
|
22
|
-
readReactionParams,
|
|
23
|
-
readStringParam,
|
|
24
|
-
} from "openclaw/plugin-sdk";
|
|
25
24
|
|
|
26
25
|
const messageActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
|
|
27
26
|
const reactionActions = new Set(["react", "reactions"]);
|
|
@@ -29,8 +28,12 @@ const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]);
|
|
|
29
28
|
|
|
30
29
|
function readRoomId(params: Record<string, unknown>, required = true): string {
|
|
31
30
|
const direct = readStringParam(params, "roomId") ?? readStringParam(params, "channelId");
|
|
32
|
-
if (direct)
|
|
33
|
-
|
|
31
|
+
if (direct) {
|
|
32
|
+
return direct;
|
|
33
|
+
}
|
|
34
|
+
if (!required) {
|
|
35
|
+
return readStringParam(params, "to") ?? "";
|
|
36
|
+
}
|
|
34
37
|
return readStringParam(params, "to", { required: true });
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -76,7 +79,8 @@ export async function handleMatrixAction(
|
|
|
76
79
|
allowEmpty: true,
|
|
77
80
|
});
|
|
78
81
|
const mediaUrl = readStringParam(params, "mediaUrl");
|
|
79
|
-
const replyToId =
|
|
82
|
+
const replyToId =
|
|
83
|
+
readStringParam(params, "replyToId") ?? readStringParam(params, "replyTo");
|
|
80
84
|
const threadId = readStringParam(params, "threadId");
|
|
81
85
|
const result = await sendMatrixMessage(to, content, {
|
|
82
86
|
mediaUrl: mediaUrl ?? undefined,
|
package/src/types.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type MatrixDmConfig = {
|
|
|
7
7
|
enabled?: boolean;
|
|
8
8
|
/** Direct message access policy (default: pairing). */
|
|
9
9
|
policy?: DmPolicy;
|
|
10
|
-
/** Allowlist for DM senders (matrix user IDs
|
|
10
|
+
/** Allowlist for DM senders (matrix user IDs or "*"). */
|
|
11
11
|
allowFrom?: Array<string | number>;
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ export type MatrixRoomConfig = {
|
|
|
22
22
|
tools?: { allow?: string[]; deny?: string[] };
|
|
23
23
|
/** If true, reply without mention requirements. */
|
|
24
24
|
autoReply?: boolean;
|
|
25
|
-
/** Optional allowlist for room senders (user IDs
|
|
25
|
+
/** Optional allowlist for room senders (matrix user IDs). */
|
|
26
26
|
users?: Array<string | number>;
|
|
27
27
|
/** Optional skill filter for this room. */
|
|
28
28
|
skills?: string[];
|
|
@@ -61,7 +61,7 @@ export type MatrixConfig = {
|
|
|
61
61
|
allowlistOnly?: boolean;
|
|
62
62
|
/** Group message policy (default: allowlist). */
|
|
63
63
|
groupPolicy?: GroupPolicy;
|
|
64
|
-
/** Allowlist for group senders (user IDs
|
|
64
|
+
/** Allowlist for group senders (matrix user IDs). */
|
|
65
65
|
groupAllowFrom?: Array<string | number>;
|
|
66
66
|
/** Control reply threading when reply tags are present (off|first|all). */
|
|
67
67
|
replyToMode?: ReplyToMode;
|
|
@@ -79,9 +79,9 @@ export type MatrixConfig = {
|
|
|
79
79
|
autoJoinAllowlist?: Array<string | number>;
|
|
80
80
|
/** Direct message policy + allowlist overrides. */
|
|
81
81
|
dm?: MatrixDmConfig;
|
|
82
|
-
/** Room config allowlist keyed by room ID
|
|
82
|
+
/** Room config allowlist keyed by room ID or alias (names resolved to IDs when possible). */
|
|
83
83
|
groups?: Record<string, MatrixRoomConfig>;
|
|
84
|
-
/** Room config allowlist keyed by room ID
|
|
84
|
+
/** Room config allowlist keyed by room ID or alias. Legacy; use groups. */
|
|
85
85
|
rooms?: Record<string, MatrixRoomConfig>;
|
|
86
86
|
/** Per-action tool gating (default: true for all). */
|
|
87
87
|
actions?: MatrixActionConfig;
|