@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.
- package/CHANGELOG.md +12 -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
|
@@ -5,6 +5,21 @@ import { sendReadReceiptMatrix } from "../send.js";
|
|
|
5
5
|
import type { MatrixRawEvent } from "./types.js";
|
|
6
6
|
import { EventType } from "./types.js";
|
|
7
7
|
|
|
8
|
+
const matrixMonitorListenerRegistry = (() => {
|
|
9
|
+
// Prevent duplicate listener registration when both bundled and extension
|
|
10
|
+
// paths attempt to start monitors against the same shared client.
|
|
11
|
+
const registeredClients = new WeakSet<object>();
|
|
12
|
+
return {
|
|
13
|
+
tryRegister(client: object): boolean {
|
|
14
|
+
if (registeredClients.has(client)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
registeredClients.add(client);
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
})();
|
|
22
|
+
|
|
8
23
|
function createSelfUserIdResolver(client: Pick<MatrixClient, "getUserId">) {
|
|
9
24
|
let selfUserId: string | undefined;
|
|
10
25
|
let selfUserIdLookup: Promise<string | undefined> | undefined;
|
|
@@ -41,6 +56,11 @@ export function registerMatrixMonitorEvents(params: {
|
|
|
41
56
|
formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"];
|
|
42
57
|
onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise<void>;
|
|
43
58
|
}): void {
|
|
59
|
+
if (!matrixMonitorListenerRegistry.tryRegister(params.client)) {
|
|
60
|
+
params.logVerboseMessage("matrix: skipping duplicate listener registration for client");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
const {
|
|
45
65
|
client,
|
|
46
66
|
auth,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createMatrixRoomMessageHandler } from "./handler.js";
|
|
5
|
+
import { EventType, type MatrixRawEvent } from "./types.js";
|
|
6
|
+
|
|
7
|
+
describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => {
|
|
8
|
+
it("stores sender-labeled BodyForAgent for group thread messages", async () => {
|
|
9
|
+
const recordInboundSession = vi.fn().mockResolvedValue(undefined);
|
|
10
|
+
const formatInboundEnvelope = vi
|
|
11
|
+
.fn()
|
|
12
|
+
.mockImplementation((params: { senderLabel?: string; body: string }) => params.body);
|
|
13
|
+
const finalizeInboundContext = vi
|
|
14
|
+
.fn()
|
|
15
|
+
.mockImplementation((ctx: Record<string, unknown>) => ctx);
|
|
16
|
+
|
|
17
|
+
const core = {
|
|
18
|
+
channel: {
|
|
19
|
+
pairing: {
|
|
20
|
+
readAllowFromStore: vi.fn().mockResolvedValue([]),
|
|
21
|
+
},
|
|
22
|
+
routing: {
|
|
23
|
+
resolveAgentRoute: vi.fn().mockReturnValue({
|
|
24
|
+
agentId: "main",
|
|
25
|
+
accountId: undefined,
|
|
26
|
+
sessionKey: "agent:main:matrix:channel:!room:example.org",
|
|
27
|
+
mainSessionKey: "agent:main:main",
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
session: {
|
|
31
|
+
resolveStorePath: vi.fn().mockReturnValue("/tmp/openclaw-test-session.json"),
|
|
32
|
+
readSessionUpdatedAt: vi.fn().mockReturnValue(123),
|
|
33
|
+
recordInboundSession,
|
|
34
|
+
},
|
|
35
|
+
reply: {
|
|
36
|
+
resolveEnvelopeFormatOptions: vi.fn().mockReturnValue({}),
|
|
37
|
+
formatInboundEnvelope,
|
|
38
|
+
formatAgentEnvelope: vi
|
|
39
|
+
.fn()
|
|
40
|
+
.mockImplementation((params: { body: string }) => params.body),
|
|
41
|
+
finalizeInboundContext,
|
|
42
|
+
resolveHumanDelayConfig: vi.fn().mockReturnValue(undefined),
|
|
43
|
+
createReplyDispatcherWithTyping: vi.fn().mockReturnValue({
|
|
44
|
+
dispatcher: {},
|
|
45
|
+
replyOptions: {},
|
|
46
|
+
markDispatchIdle: vi.fn(),
|
|
47
|
+
}),
|
|
48
|
+
withReplyDispatcher: vi
|
|
49
|
+
.fn()
|
|
50
|
+
.mockResolvedValue({ queuedFinal: false, counts: { final: 0, partial: 0, tool: 0 } }),
|
|
51
|
+
},
|
|
52
|
+
commands: {
|
|
53
|
+
shouldHandleTextCommands: vi.fn().mockReturnValue(true),
|
|
54
|
+
},
|
|
55
|
+
text: {
|
|
56
|
+
hasControlCommand: vi.fn().mockReturnValue(false),
|
|
57
|
+
resolveMarkdownTableMode: vi.fn().mockReturnValue("code"),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
system: {
|
|
61
|
+
enqueueSystemEvent: vi.fn(),
|
|
62
|
+
},
|
|
63
|
+
} as unknown as PluginRuntime;
|
|
64
|
+
|
|
65
|
+
const runtime = {
|
|
66
|
+
error: vi.fn(),
|
|
67
|
+
} as unknown as RuntimeEnv;
|
|
68
|
+
const logger = {
|
|
69
|
+
info: vi.fn(),
|
|
70
|
+
warn: vi.fn(),
|
|
71
|
+
} as unknown as RuntimeLogger;
|
|
72
|
+
const logVerboseMessage = vi.fn();
|
|
73
|
+
|
|
74
|
+
const client = {
|
|
75
|
+
getUserId: vi.fn().mockResolvedValue("@bot:matrix.example.org"),
|
|
76
|
+
} as unknown as MatrixClient;
|
|
77
|
+
|
|
78
|
+
const handler = createMatrixRoomMessageHandler({
|
|
79
|
+
client,
|
|
80
|
+
core,
|
|
81
|
+
cfg: {},
|
|
82
|
+
runtime,
|
|
83
|
+
logger,
|
|
84
|
+
logVerboseMessage,
|
|
85
|
+
allowFrom: [],
|
|
86
|
+
roomsConfig: undefined,
|
|
87
|
+
mentionRegexes: [],
|
|
88
|
+
groupPolicy: "open",
|
|
89
|
+
replyToMode: "first",
|
|
90
|
+
threadReplies: "inbound",
|
|
91
|
+
dmEnabled: true,
|
|
92
|
+
dmPolicy: "open",
|
|
93
|
+
textLimit: 4000,
|
|
94
|
+
mediaMaxBytes: 5 * 1024 * 1024,
|
|
95
|
+
startupMs: Date.now(),
|
|
96
|
+
startupGraceMs: 60_000,
|
|
97
|
+
directTracker: {
|
|
98
|
+
isDirectMessage: vi.fn().mockResolvedValue(false),
|
|
99
|
+
},
|
|
100
|
+
getRoomInfo: vi.fn().mockResolvedValue({
|
|
101
|
+
name: "Dev Room",
|
|
102
|
+
canonicalAlias: "#dev:matrix.example.org",
|
|
103
|
+
altAliases: [],
|
|
104
|
+
}),
|
|
105
|
+
getMemberDisplayName: vi.fn().mockResolvedValue("Bu"),
|
|
106
|
+
accountId: undefined,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const event = {
|
|
110
|
+
type: EventType.RoomMessage,
|
|
111
|
+
event_id: "$event1",
|
|
112
|
+
sender: "@bu:matrix.example.org",
|
|
113
|
+
origin_server_ts: Date.now(),
|
|
114
|
+
content: {
|
|
115
|
+
msgtype: "m.text",
|
|
116
|
+
body: "show me my commits",
|
|
117
|
+
"m.mentions": { user_ids: ["@bot:matrix.example.org"] },
|
|
118
|
+
"m.relates_to": {
|
|
119
|
+
rel_type: "m.thread",
|
|
120
|
+
event_id: "$thread-root",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
} as unknown as MatrixRawEvent;
|
|
124
|
+
|
|
125
|
+
await handler("!room:example.org", event);
|
|
126
|
+
|
|
127
|
+
expect(formatInboundEnvelope).toHaveBeenCalledWith(
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
chatType: "channel",
|
|
130
|
+
senderLabel: "Bu (bu)",
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
expect(recordInboundSession).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
ctx: expect.objectContaining({
|
|
136
|
+
ChatType: "thread",
|
|
137
|
+
BodyForAgent: "Bu (bu): show me my commits",
|
|
138
|
+
}),
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
createScopedPairingAccess,
|
|
3
5
|
createReplyPrefixOptions,
|
|
4
6
|
createTypingCallbacks,
|
|
5
7
|
formatAllowlistMatchMeta,
|
|
@@ -19,11 +21,17 @@ import {
|
|
|
19
21
|
type PollStartContent,
|
|
20
22
|
} from "../poll-types.js";
|
|
21
23
|
import { reactMatrixMessage, sendMessageMatrix, sendTypingMatrix } from "../send.js";
|
|
24
|
+
import { enforceMatrixDirectMessageAccess, resolveMatrixAccessState } from "./access-policy.js";
|
|
22
25
|
import {
|
|
23
26
|
normalizeMatrixAllowList,
|
|
24
27
|
resolveMatrixAllowListMatch,
|
|
25
28
|
resolveMatrixAllowListMatches,
|
|
26
29
|
} from "./allowlist.js";
|
|
30
|
+
import {
|
|
31
|
+
resolveMatrixBodyForAgent,
|
|
32
|
+
resolveMatrixInboundSenderLabel,
|
|
33
|
+
resolveMatrixSenderUsername,
|
|
34
|
+
} from "./inbound-body.js";
|
|
27
35
|
import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js";
|
|
28
36
|
import { downloadMatrixMedia } from "./media.js";
|
|
29
37
|
import { resolveMentions } from "./mentions.js";
|
|
@@ -91,6 +99,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
91
99
|
getMemberDisplayName,
|
|
92
100
|
accountId,
|
|
93
101
|
} = params;
|
|
102
|
+
const resolvedAccountId = accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
|
103
|
+
const pairing = createScopedPairingAccess({
|
|
104
|
+
core,
|
|
105
|
+
channel: "matrix",
|
|
106
|
+
accountId: resolvedAccountId,
|
|
107
|
+
});
|
|
94
108
|
|
|
95
109
|
return async (roomId: string, event: MatrixRawEvent) => {
|
|
96
110
|
try {
|
|
@@ -213,62 +227,42 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
const senderName = await getMemberDisplayName(roomId, senderId);
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
const senderUsername = resolveMatrixSenderUsername(senderId);
|
|
231
|
+
const senderLabel = resolveMatrixInboundSenderLabel({
|
|
232
|
+
senderName,
|
|
233
|
+
senderId,
|
|
234
|
+
senderUsername,
|
|
235
|
+
});
|
|
221
236
|
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
|
222
|
-
const effectiveGroupAllowFrom =
|
|
223
|
-
|
|
237
|
+
const { access, effectiveAllowFrom, effectiveGroupAllowFrom, groupAllowConfigured } =
|
|
238
|
+
await resolveMatrixAccessState({
|
|
239
|
+
isDirectMessage,
|
|
240
|
+
resolvedAccountId,
|
|
241
|
+
dmPolicy,
|
|
242
|
+
groupPolicy,
|
|
243
|
+
allowFrom,
|
|
244
|
+
groupAllowFrom,
|
|
245
|
+
senderId,
|
|
246
|
+
readStoreForDmPolicy: pairing.readStoreForDmPolicy,
|
|
247
|
+
});
|
|
224
248
|
|
|
225
249
|
if (isDirectMessage) {
|
|
226
|
-
|
|
250
|
+
const allowedDirectMessage = await enforceMatrixDirectMessageAccess({
|
|
251
|
+
dmEnabled,
|
|
252
|
+
dmPolicy,
|
|
253
|
+
accessDecision: access.decision,
|
|
254
|
+
senderId,
|
|
255
|
+
senderName,
|
|
256
|
+
effectiveAllowFrom,
|
|
257
|
+
upsertPairingRequest: pairing.upsertPairingRequest,
|
|
258
|
+
sendPairingReply: async (text) => {
|
|
259
|
+
await sendMessageMatrix(`room:${roomId}`, text, { client });
|
|
260
|
+
},
|
|
261
|
+
logVerboseMessage,
|
|
262
|
+
});
|
|
263
|
+
if (!allowedDirectMessage) {
|
|
227
264
|
return;
|
|
228
265
|
}
|
|
229
|
-
if (dmPolicy !== "open") {
|
|
230
|
-
const allowMatch = resolveMatrixAllowListMatch({
|
|
231
|
-
allowList: effectiveAllowFrom,
|
|
232
|
-
userId: senderId,
|
|
233
|
-
});
|
|
234
|
-
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
|
235
|
-
if (!allowMatch.allowed) {
|
|
236
|
-
if (dmPolicy === "pairing") {
|
|
237
|
-
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
|
238
|
-
channel: "matrix",
|
|
239
|
-
id: senderId,
|
|
240
|
-
meta: { name: senderName },
|
|
241
|
-
});
|
|
242
|
-
if (created) {
|
|
243
|
-
logVerboseMessage(
|
|
244
|
-
`matrix pairing request sender=${senderId} name=${senderName ?? "unknown"} (${allowMatchMeta})`,
|
|
245
|
-
);
|
|
246
|
-
try {
|
|
247
|
-
await sendMessageMatrix(
|
|
248
|
-
`room:${roomId}`,
|
|
249
|
-
[
|
|
250
|
-
"OpenClaw: access not configured.",
|
|
251
|
-
"",
|
|
252
|
-
`Pairing code: ${code}`,
|
|
253
|
-
"",
|
|
254
|
-
"Ask the bot owner to approve with:",
|
|
255
|
-
"openclaw pairing approve matrix <code>",
|
|
256
|
-
].join("\n"),
|
|
257
|
-
{ client },
|
|
258
|
-
);
|
|
259
|
-
} catch (err) {
|
|
260
|
-
logVerboseMessage(`matrix pairing reply failed for ${senderId}: ${String(err)}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (dmPolicy !== "pairing") {
|
|
265
|
-
logVerboseMessage(
|
|
266
|
-
`matrix: blocked dm sender ${senderId} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
266
|
}
|
|
273
267
|
|
|
274
268
|
const roomUsers = roomConfig?.users ?? [];
|
|
@@ -286,7 +280,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
286
280
|
return;
|
|
287
281
|
}
|
|
288
282
|
}
|
|
289
|
-
if (isRoom &&
|
|
283
|
+
if (isRoom && roomUsers.length === 0 && groupAllowConfigured && access.decision !== "allow") {
|
|
290
284
|
const groupAllowMatch = resolveMatrixAllowListMatch({
|
|
291
285
|
allowList: effectiveGroupAllowFrom,
|
|
292
286
|
userId: senderId,
|
|
@@ -498,19 +492,25 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
498
492
|
storePath,
|
|
499
493
|
sessionKey: route.sessionKey,
|
|
500
494
|
});
|
|
501
|
-
const body = core.channel.reply.
|
|
495
|
+
const body = core.channel.reply.formatInboundEnvelope({
|
|
502
496
|
channel: "Matrix",
|
|
503
497
|
from: envelopeFrom,
|
|
504
498
|
timestamp: eventTs ?? undefined,
|
|
505
499
|
previousTimestamp,
|
|
506
500
|
envelope: envelopeOptions,
|
|
507
501
|
body: textWithId,
|
|
502
|
+
chatType: isDirectMessage ? "direct" : "channel",
|
|
503
|
+
senderLabel,
|
|
508
504
|
});
|
|
509
505
|
|
|
510
506
|
const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined;
|
|
511
507
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
512
508
|
Body: body,
|
|
513
|
-
BodyForAgent:
|
|
509
|
+
BodyForAgent: resolveMatrixBodyForAgent({
|
|
510
|
+
isDirectMessage,
|
|
511
|
+
bodyText,
|
|
512
|
+
senderLabel,
|
|
513
|
+
}),
|
|
514
514
|
RawBody: bodyText,
|
|
515
515
|
CommandBody: bodyText,
|
|
516
516
|
From: isDirectMessage ? `matrix:${senderId}` : `matrix:channel:${roomId}`,
|
|
@@ -521,7 +521,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
521
521
|
ConversationLabel: envelopeFrom,
|
|
522
522
|
SenderName: senderName,
|
|
523
523
|
SenderId: senderId,
|
|
524
|
-
SenderUsername:
|
|
524
|
+
SenderUsername: senderUsername,
|
|
525
525
|
GroupSubject: isRoom ? (roomName ?? roomId) : undefined,
|
|
526
526
|
GroupChannel: isRoom ? (roomInfo.canonicalAlias ?? roomId) : undefined,
|
|
527
527
|
GroupSystemPrompt: isRoom ? groupSystemPrompt : undefined,
|
|
@@ -655,17 +655,23 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
655
655
|
},
|
|
656
656
|
});
|
|
657
657
|
|
|
658
|
-
const { queuedFinal, counts } = await core.channel.reply.
|
|
659
|
-
ctx: ctxPayload,
|
|
660
|
-
cfg,
|
|
658
|
+
const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
|
|
661
659
|
dispatcher,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
skillFilter: roomConfig?.skills,
|
|
665
|
-
onModelSelected,
|
|
660
|
+
onSettled: () => {
|
|
661
|
+
markDispatchIdle();
|
|
666
662
|
},
|
|
663
|
+
run: () =>
|
|
664
|
+
core.channel.reply.dispatchReplyFromConfig({
|
|
665
|
+
ctx: ctxPayload,
|
|
666
|
+
cfg,
|
|
667
|
+
dispatcher,
|
|
668
|
+
replyOptions: {
|
|
669
|
+
...replyOptions,
|
|
670
|
+
skillFilter: roomConfig?.skills,
|
|
671
|
+
onModelSelected,
|
|
672
|
+
},
|
|
673
|
+
}),
|
|
667
674
|
});
|
|
668
|
-
markDispatchIdle();
|
|
669
675
|
if (!queuedFinal) {
|
|
670
676
|
return;
|
|
671
677
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolveMatrixBodyForAgent,
|
|
4
|
+
resolveMatrixInboundSenderLabel,
|
|
5
|
+
resolveMatrixSenderUsername,
|
|
6
|
+
} from "./inbound-body.js";
|
|
7
|
+
|
|
8
|
+
describe("resolveMatrixSenderUsername", () => {
|
|
9
|
+
it("extracts localpart without leading @", () => {
|
|
10
|
+
expect(resolveMatrixSenderUsername("@bu:matrix.example.org")).toBe("bu");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("resolveMatrixInboundSenderLabel", () => {
|
|
15
|
+
it("uses provided senderUsername when present", () => {
|
|
16
|
+
expect(
|
|
17
|
+
resolveMatrixInboundSenderLabel({
|
|
18
|
+
senderName: "Bu",
|
|
19
|
+
senderId: "@bu:matrix.example.org",
|
|
20
|
+
senderUsername: "BU_CUSTOM",
|
|
21
|
+
}),
|
|
22
|
+
).toBe("Bu (BU_CUSTOM)");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("includes sender username when it differs from display name", () => {
|
|
26
|
+
expect(
|
|
27
|
+
resolveMatrixInboundSenderLabel({
|
|
28
|
+
senderName: "Bu",
|
|
29
|
+
senderId: "@bu:matrix.example.org",
|
|
30
|
+
}),
|
|
31
|
+
).toBe("Bu (bu)");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("falls back to sender username when display name is blank", () => {
|
|
35
|
+
expect(
|
|
36
|
+
resolveMatrixInboundSenderLabel({
|
|
37
|
+
senderName: " ",
|
|
38
|
+
senderId: "@zhang:matrix.example.org",
|
|
39
|
+
}),
|
|
40
|
+
).toBe("zhang");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("falls back to sender id when username cannot be parsed", () => {
|
|
44
|
+
expect(
|
|
45
|
+
resolveMatrixInboundSenderLabel({
|
|
46
|
+
senderName: "",
|
|
47
|
+
senderId: "matrix-user-without-colon",
|
|
48
|
+
}),
|
|
49
|
+
).toBe("matrix-user-without-colon");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("resolveMatrixBodyForAgent", () => {
|
|
54
|
+
it("keeps direct message body unchanged", () => {
|
|
55
|
+
expect(
|
|
56
|
+
resolveMatrixBodyForAgent({
|
|
57
|
+
isDirectMessage: true,
|
|
58
|
+
bodyText: "show me my commits",
|
|
59
|
+
senderLabel: "Bu (bu)",
|
|
60
|
+
}),
|
|
61
|
+
).toBe("show me my commits");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("prefixes non-direct message body with sender label", () => {
|
|
65
|
+
expect(
|
|
66
|
+
resolveMatrixBodyForAgent({
|
|
67
|
+
isDirectMessage: false,
|
|
68
|
+
bodyText: "show me my commits",
|
|
69
|
+
senderLabel: "Bu (bu)",
|
|
70
|
+
}),
|
|
71
|
+
).toBe("Bu (bu): show me my commits");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function resolveMatrixSenderUsername(senderId: string): string | undefined {
|
|
2
|
+
const username = senderId.split(":")[0]?.replace(/^@/, "").trim();
|
|
3
|
+
return username ? username : undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function resolveMatrixInboundSenderLabel(params: {
|
|
7
|
+
senderName: string;
|
|
8
|
+
senderId: string;
|
|
9
|
+
senderUsername?: string;
|
|
10
|
+
}): string {
|
|
11
|
+
const senderName = params.senderName.trim();
|
|
12
|
+
const senderUsername = params.senderUsername ?? resolveMatrixSenderUsername(params.senderId);
|
|
13
|
+
if (senderName && senderUsername && senderName !== senderUsername) {
|
|
14
|
+
return `${senderName} (${senderUsername})`;
|
|
15
|
+
}
|
|
16
|
+
return senderName || senderUsername || params.senderId;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveMatrixBodyForAgent(params: {
|
|
20
|
+
isDirectMessage: boolean;
|
|
21
|
+
bodyText: string;
|
|
22
|
+
senderLabel: string;
|
|
23
|
+
}): string {
|
|
24
|
+
if (params.isDirectMessage) {
|
|
25
|
+
return params.bodyText;
|
|
26
|
+
}
|
|
27
|
+
return `${params.senderLabel}: ${params.bodyText}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { DEFAULT_STARTUP_GRACE_MS, isConfiguredMatrixRoomEntry } from "./index.js";
|
|
3
|
+
|
|
4
|
+
describe("monitorMatrixProvider helpers", () => {
|
|
5
|
+
it("treats !-prefixed room IDs as configured room entries", () => {
|
|
6
|
+
expect(isConfiguredMatrixRoomEntry("!abc123")).toBe(true);
|
|
7
|
+
expect(isConfiguredMatrixRoomEntry("!RoomMixedCase")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("requires a homeserver suffix for # aliases", () => {
|
|
11
|
+
expect(isConfiguredMatrixRoomEntry("#alias:example.org")).toBe(true);
|
|
12
|
+
expect(isConfiguredMatrixRoomEntry("#alias")).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("uses a non-zero startup grace window", () => {
|
|
16
|
+
expect(DEFAULT_STARTUP_GRACE_MS).toBe(5000);
|
|
17
|
+
});
|
|
18
|
+
});
|