@archipelagolab/lobi 1.0.0
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 +164 -0
- package/ENDOFFILE +0 -0
- package/EOF +0 -0
- package/LICENSE +21 -0
- package/SPEC-SUPPORT.md +116 -0
- package/YAMLEND +0 -0
- package/api.ts +18 -0
- package/archipelagolab-lobi-1.0.0.tgz +0 -0
- package/auth-presence.ts +56 -0
- package/channel-plugin-api.ts +3 -0
- package/cli-metadata.ts +11 -0
- package/contract-api.ts +17 -0
- package/docs/CHECKLIST.md +83 -0
- package/docs/FORK_SDK_GUIDE.md +279 -0
- package/helper-api.ts +3 -0
- package/index.test.ts +61 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +52 -0
- package/plugin-entry.handlers.runtime.ts +1 -0
- package/runtime-api.ts +54 -0
- package/runtime-heavy-api.ts +1 -0
- package/scripts/migrate-to-lobi.sh +72 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/account-selection.test.ts +124 -0
- package/src/account-selection.ts +226 -0
- package/src/actions.account-propagation.test.ts +251 -0
- package/src/actions.test.ts +251 -0
- package/src/actions.ts +336 -0
- package/src/approval-auth.test.ts +23 -0
- package/src/approval-auth.ts +25 -0
- package/src/approval-handler.runtime.test.ts +46 -0
- package/src/approval-handler.runtime.ts +400 -0
- package/src/approval-ids.ts +6 -0
- package/src/approval-native.test.ts +329 -0
- package/src/approval-native.ts +336 -0
- package/src/approval-reactions.test.ts +107 -0
- package/src/approval-reactions.ts +158 -0
- package/src/auth-precedence.ts +61 -0
- package/src/channel-account-paths.ts +92 -0
- package/src/channel.account-paths.test.ts +102 -0
- package/src/channel.directory.test.ts +601 -0
- package/src/channel.resolve.test.ts +38 -0
- package/src/channel.runtime.ts +16 -0
- package/src/channel.setup.test.ts +269 -0
- package/src/channel.ts +570 -0
- package/src/cli-metadata.ts +19 -0
- package/src/cli.test.ts +1015 -0
- package/src/cli.ts +1198 -0
- package/src/config-adapter.ts +41 -0
- package/src/config-schema.test.ts +90 -0
- package/src/config-schema.ts +114 -0
- package/src/directory-live.test.ts +200 -0
- package/src/directory-live.ts +238 -0
- package/src/doctor-contract.ts +287 -0
- package/src/doctor.test.ts +440 -0
- package/src/doctor.ts +262 -0
- package/src/env-vars.ts +92 -0
- package/src/exec-approval-resolver.test.ts +68 -0
- package/src/exec-approval-resolver.ts +23 -0
- package/src/exec-approvals.test.ts +483 -0
- package/src/exec-approvals.ts +290 -0
- package/src/group-mentions.ts +41 -0
- package/src/legacy-crypto-inspector-availability.test.ts +81 -0
- package/src/legacy-crypto-inspector-availability.ts +60 -0
- package/src/legacy-crypto.test.ts +234 -0
- package/src/legacy-crypto.ts +549 -0
- package/src/legacy-state.test.ts +86 -0
- package/src/legacy-state.ts +156 -0
- package/src/matrix/account-config.ts +150 -0
- package/src/matrix/accounts.readiness.test.ts +27 -0
- package/src/matrix/accounts.test.ts +757 -0
- package/src/matrix/accounts.ts +194 -0
- package/src/matrix/actions/client.test.ts +215 -0
- package/src/matrix/actions/client.ts +31 -0
- package/src/matrix/actions/devices.test.ts +114 -0
- package/src/matrix/actions/devices.ts +34 -0
- package/src/matrix/actions/limits.test.ts +15 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.test.ts +289 -0
- package/src/matrix/actions/messages.ts +123 -0
- package/src/matrix/actions/pins.test.ts +74 -0
- package/src/matrix/actions/pins.ts +64 -0
- package/src/matrix/actions/polls.test.ts +71 -0
- package/src/matrix/actions/polls.ts +109 -0
- package/src/matrix/actions/profile.test.ts +109 -0
- package/src/matrix/actions/profile.ts +37 -0
- package/src/matrix/actions/reactions.test.ts +135 -0
- package/src/matrix/actions/reactions.ts +59 -0
- package/src/matrix/actions/room.test.ts +79 -0
- package/src/matrix/actions/room.ts +71 -0
- package/src/matrix/actions/summary.test.ts +87 -0
- package/src/matrix/actions/summary.ts +88 -0
- package/src/matrix/actions/types.ts +82 -0
- package/src/matrix/actions/verification.test.ts +105 -0
- package/src/matrix/actions/verification.ts +237 -0
- package/src/matrix/actions.ts +37 -0
- package/src/matrix/active-client.ts +26 -0
- package/src/matrix/async-lock.ts +18 -0
- package/src/matrix/backup-health.ts +115 -0
- package/src/matrix/client/config-runtime-api.ts +14 -0
- package/src/matrix/client/config-secret-input.runtime.ts +1 -0
- package/src/matrix/client/config.ts +982 -0
- package/src/matrix/client/create-client.test.ts +115 -0
- package/src/matrix/client/create-client.ts +101 -0
- package/src/matrix/client/env-auth.ts +6 -0
- package/src/matrix/client/file-sync-store.test.ts +265 -0
- package/src/matrix/client/file-sync-store.ts +289 -0
- package/src/matrix/client/logging.ts +123 -0
- package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
- package/src/matrix/client/private-network-host.ts +56 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.test.ts +344 -0
- package/src/matrix/client/shared.ts +306 -0
- package/src/matrix/client/storage.test.ts +634 -0
- package/src/matrix/client/storage.ts +544 -0
- package/src/matrix/client/types.ts +50 -0
- package/src/matrix/client-bootstrap.test.ts +84 -0
- package/src/matrix/client-bootstrap.ts +164 -0
- package/src/matrix/client-resolver.test-helpers.ts +147 -0
- package/src/matrix/client.test.ts +1521 -0
- package/src/matrix/client.ts +23 -0
- package/src/matrix/config-paths.ts +31 -0
- package/src/matrix/config-update.test.ts +237 -0
- package/src/matrix/config-update.ts +291 -0
- package/src/matrix/credentials-read.ts +206 -0
- package/src/matrix/credentials-write.runtime.ts +26 -0
- package/src/matrix/credentials.test.ts +501 -0
- package/src/matrix/credentials.ts +95 -0
- package/src/matrix/deps.test.ts +74 -0
- package/src/matrix/deps.ts +225 -0
- package/src/matrix/device-health.test.ts +45 -0
- package/src/matrix/device-health.ts +31 -0
- package/src/matrix/direct-management.test.ts +350 -0
- package/src/matrix/direct-management.ts +347 -0
- package/src/matrix/direct-room.test.ts +61 -0
- package/src/matrix/direct-room.ts +128 -0
- package/src/matrix/draft-stream.test.ts +406 -0
- package/src/matrix/draft-stream.ts +216 -0
- package/src/matrix/encryption-guidance.ts +27 -0
- package/src/matrix/errors.ts +21 -0
- package/src/matrix/format.test.ts +340 -0
- package/src/matrix/format.ts +428 -0
- package/src/matrix/legacy-crypto-inspector.ts +95 -0
- package/src/matrix/media-errors.ts +20 -0
- package/src/matrix/media-text.ts +169 -0
- package/src/matrix/monitor/access-state.test.ts +45 -0
- package/src/matrix/monitor/access-state.ts +77 -0
- package/src/matrix/monitor/ack-config.test.ts +57 -0
- package/src/matrix/monitor/ack-config.ts +26 -0
- package/src/matrix/monitor/allowlist.test.ts +45 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.test.ts +203 -0
- package/src/matrix/monitor/auto-join.ts +86 -0
- package/src/matrix/monitor/config.test.ts +197 -0
- package/src/matrix/monitor/config.ts +303 -0
- package/src/matrix/monitor/context-summary.ts +43 -0
- package/src/matrix/monitor/direct.test.ts +529 -0
- package/src/matrix/monitor/direct.ts +270 -0
- package/src/matrix/monitor/events.test.ts +1524 -0
- package/src/matrix/monitor/events.ts +213 -0
- package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
- package/src/matrix/monitor/handler.group-history.test.ts +648 -0
- package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
- package/src/matrix/monitor/handler.test-helpers.ts +308 -0
- package/src/matrix/monitor/handler.test.ts +2952 -0
- package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
- package/src/matrix/monitor/handler.ts +1679 -0
- package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
- package/src/matrix/monitor/inbound-dedupe.ts +267 -0
- package/src/matrix/monitor/index.test.ts +920 -0
- package/src/matrix/monitor/index.ts +434 -0
- package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
- package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.test.ts +159 -0
- package/src/matrix/monitor/media.ts +119 -0
- package/src/matrix/monitor/mentions.test.ts +289 -0
- package/src/matrix/monitor/mentions.ts +177 -0
- package/src/matrix/monitor/reaction-events.test.ts +326 -0
- package/src/matrix/monitor/reaction-events.ts +187 -0
- package/src/matrix/monitor/recent-invite.test.ts +92 -0
- package/src/matrix/monitor/recent-invite.ts +30 -0
- package/src/matrix/monitor/replies.test.ts +265 -0
- package/src/matrix/monitor/replies.ts +136 -0
- package/src/matrix/monitor/reply-context.test.ts +276 -0
- package/src/matrix/monitor/reply-context.ts +92 -0
- package/src/matrix/monitor/room-history.test.ts +258 -0
- package/src/matrix/monitor/room-history.ts +301 -0
- package/src/matrix/monitor/room-info.test.ts +201 -0
- package/src/matrix/monitor/room-info.ts +126 -0
- package/src/matrix/monitor/rooms.test.ts +121 -0
- package/src/matrix/monitor/rooms.ts +52 -0
- package/src/matrix/monitor/route.test.ts +255 -0
- package/src/matrix/monitor/route.ts +178 -0
- package/src/matrix/monitor/runtime-api.ts +31 -0
- package/src/matrix/monitor/startup-verification.test.ts +294 -0
- package/src/matrix/monitor/startup-verification.ts +237 -0
- package/src/matrix/monitor/startup.test.ts +257 -0
- package/src/matrix/monitor/startup.ts +218 -0
- package/src/matrix/monitor/status.ts +111 -0
- package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
- package/src/matrix/monitor/sync-lifecycle.ts +91 -0
- package/src/matrix/monitor/task-runner.ts +38 -0
- package/src/matrix/monitor/thread-context.test.ts +149 -0
- package/src/matrix/monitor/thread-context.ts +108 -0
- package/src/matrix/monitor/threads.test.ts +68 -0
- package/src/matrix/monitor/threads.ts +85 -0
- package/src/matrix/monitor/types.ts +30 -0
- package/src/matrix/monitor/verification-events.ts +627 -0
- package/src/matrix/monitor/verification-utils.test.ts +47 -0
- package/src/matrix/monitor/verification-utils.ts +46 -0
- package/src/matrix/outbound-media-runtime.ts +1 -0
- package/src/matrix/poll-summary.ts +110 -0
- package/src/matrix/poll-types.test.ts +205 -0
- package/src/matrix/poll-types.ts +433 -0
- package/src/matrix/probe.runtime.ts +4 -0
- package/src/matrix/probe.test.ts +154 -0
- package/src/matrix/probe.ts +96 -0
- package/src/matrix/profile.test.ts +154 -0
- package/src/matrix/profile.ts +184 -0
- package/src/matrix/reaction-common.test.ts +96 -0
- package/src/matrix/reaction-common.ts +147 -0
- package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
- package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
- package/src/matrix/sdk/crypto-facade.test.ts +197 -0
- package/src/matrix/sdk/crypto-facade.ts +207 -0
- package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
- package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
- package/src/matrix/sdk/crypto-runtime.ts +11 -0
- package/src/matrix/sdk/decrypt-bridge.ts +356 -0
- package/src/matrix/sdk/event-helpers.test.ts +60 -0
- package/src/matrix/sdk/event-helpers.ts +71 -0
- package/src/matrix/sdk/http-client.test.ts +134 -0
- package/src/matrix/sdk/http-client.ts +87 -0
- package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
- package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
- package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
- package/src/matrix/sdk/idb-persistence.test.ts +149 -0
- package/src/matrix/sdk/idb-persistence.ts +283 -0
- package/src/matrix/sdk/logger.test.ts +25 -0
- package/src/matrix/sdk/logger.ts +108 -0
- package/src/matrix/sdk/read-response-with-limit.ts +19 -0
- package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
- package/src/matrix/sdk/recovery-key-store.ts +430 -0
- package/src/matrix/sdk/transport.test.ts +161 -0
- package/src/matrix/sdk/transport.ts +344 -0
- package/src/matrix/sdk/types.ts +236 -0
- package/src/matrix/sdk/verification-manager.test.ts +509 -0
- package/src/matrix/sdk/verification-manager.ts +694 -0
- package/src/matrix/sdk/verification-status.ts +23 -0
- package/src/matrix/sdk.test.ts +2568 -0
- package/src/matrix/sdk.ts +1789 -0
- package/src/matrix/send/client.test.ts +174 -0
- package/src/matrix/send/client.ts +90 -0
- package/src/matrix/send/formatting.ts +189 -0
- package/src/matrix/send/media.ts +244 -0
- package/src/matrix/send/targets.test.ts +254 -0
- package/src/matrix/send/targets.ts +104 -0
- package/src/matrix/send/types.ts +134 -0
- package/src/matrix/send.test.ts +958 -0
- package/src/matrix/send.ts +609 -0
- package/src/matrix/session-store-metadata.ts +108 -0
- package/src/matrix/startup-abort.ts +44 -0
- package/src/matrix/sync-state.ts +27 -0
- package/src/matrix/target-ids.ts +102 -0
- package/src/matrix/thread-bindings-shared.ts +201 -0
- package/src/matrix/thread-bindings.test.ts +673 -0
- package/src/matrix/thread-bindings.ts +577 -0
- package/src/matrix-migration.runtime.ts +9 -0
- package/src/migration-config.test.ts +228 -0
- package/src/migration-config.ts +243 -0
- package/src/migration-snapshot-backup.ts +117 -0
- package/src/migration-snapshot.test.ts +184 -0
- package/src/migration-snapshot.ts +55 -0
- package/src/onboarding.resolve.test.ts +55 -0
- package/src/onboarding.test-harness.ts +158 -0
- package/src/onboarding.test.ts +665 -0
- package/src/onboarding.ts +773 -0
- package/src/outbound.test.ts +173 -0
- package/src/outbound.ts +78 -0
- package/src/plugin-entry.runtime.js +159 -0
- package/src/plugin-entry.runtime.test.ts +108 -0
- package/src/plugin-entry.runtime.ts +68 -0
- package/src/profile-update.ts +68 -0
- package/src/record-shared.ts +3 -0
- package/src/resolve-targets.test.ts +178 -0
- package/src/resolve-targets.ts +175 -0
- package/src/resolver.ts +21 -0
- package/src/runtime-api.ts +144 -0
- package/src/runtime.ts +7 -0
- package/src/secret-contract.ts +174 -0
- package/src/session-route.test.ts +315 -0
- package/src/session-route.ts +113 -0
- package/src/setup-bootstrap.ts +94 -0
- package/src/setup-config.ts +222 -0
- package/src/setup-contract.ts +89 -0
- package/src/setup-core.test.ts +326 -0
- package/src/setup-core.ts +50 -0
- package/src/setup-surface.ts +4 -0
- package/src/startup-maintenance.test.ts +227 -0
- package/src/startup-maintenance.ts +114 -0
- package/src/storage-paths.ts +92 -0
- package/src/test-helpers.ts +42 -0
- package/src/test-mocks.ts +55 -0
- package/src/test-runtime.ts +72 -0
- package/src/test-support/monitor-route-test-support.ts +8 -0
- package/src/tool-actions.runtime.ts +1 -0
- package/src/tool-actions.test.ts +422 -0
- package/src/tool-actions.ts +498 -0
- package/src/types.ts +230 -0
- package/test-api.ts +2 -0
- package/thread-bindings-runtime.ts +4 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock the runtime before importing resolveMentions
|
|
4
|
+
vi.mock("../../runtime.js", () => ({
|
|
5
|
+
getMatrixRuntime: () => ({
|
|
6
|
+
channel: {
|
|
7
|
+
mentions: {
|
|
8
|
+
matchesMentionPatterns: (text: string, patterns: RegExp[]) =>
|
|
9
|
+
patterns.some((p) => p.test(text)),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { resolveMentions } from "./mentions.js";
|
|
16
|
+
|
|
17
|
+
describe("resolveMentions", () => {
|
|
18
|
+
const userId = "@bot:matrix.org";
|
|
19
|
+
const mentionRegexes = [/@bot/i];
|
|
20
|
+
|
|
21
|
+
describe("m.mentions field", () => {
|
|
22
|
+
it("detects mention via m.mentions.user_ids when the visible text also mentions the bot", () => {
|
|
23
|
+
const result = resolveMentions({
|
|
24
|
+
content: {
|
|
25
|
+
msgtype: "m.text",
|
|
26
|
+
body: "hello @bot",
|
|
27
|
+
"m.mentions": { user_ids: ["@bot:matrix.org"] },
|
|
28
|
+
},
|
|
29
|
+
userId,
|
|
30
|
+
text: "hello @bot",
|
|
31
|
+
mentionRegexes,
|
|
32
|
+
});
|
|
33
|
+
expect(result.wasMentioned).toBe(true);
|
|
34
|
+
expect(result.hasExplicitMention).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("does not trust forged m.mentions.user_ids without a visible mention", () => {
|
|
38
|
+
const result = resolveMentions({
|
|
39
|
+
content: {
|
|
40
|
+
msgtype: "m.text",
|
|
41
|
+
body: "hello",
|
|
42
|
+
"m.mentions": { user_ids: ["@bot:matrix.org"] },
|
|
43
|
+
},
|
|
44
|
+
userId,
|
|
45
|
+
text: "hello",
|
|
46
|
+
mentionRegexes,
|
|
47
|
+
});
|
|
48
|
+
expect(result.wasMentioned).toBe(false);
|
|
49
|
+
expect(result.hasExplicitMention).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("detects room mention via visible @room text", () => {
|
|
53
|
+
const result = resolveMentions({
|
|
54
|
+
content: {
|
|
55
|
+
msgtype: "m.text",
|
|
56
|
+
body: "@room hello everyone",
|
|
57
|
+
"m.mentions": { room: true },
|
|
58
|
+
},
|
|
59
|
+
userId,
|
|
60
|
+
text: "@room hello everyone",
|
|
61
|
+
mentionRegexes,
|
|
62
|
+
});
|
|
63
|
+
expect(result.wasMentioned).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does not trust forged m.mentions.room without visible @room text", () => {
|
|
67
|
+
const result = resolveMentions({
|
|
68
|
+
content: {
|
|
69
|
+
msgtype: "m.text",
|
|
70
|
+
body: "hello everyone",
|
|
71
|
+
"m.mentions": { room: true },
|
|
72
|
+
},
|
|
73
|
+
userId,
|
|
74
|
+
text: "hello everyone",
|
|
75
|
+
mentionRegexes,
|
|
76
|
+
});
|
|
77
|
+
expect(result.wasMentioned).toBe(false);
|
|
78
|
+
expect(result.hasExplicitMention).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("formatted_body matrix.to links", () => {
|
|
83
|
+
it("detects mention in formatted_body with plain user ID", () => {
|
|
84
|
+
const result = resolveMentions({
|
|
85
|
+
content: {
|
|
86
|
+
msgtype: "m.text",
|
|
87
|
+
body: "Bot: hello",
|
|
88
|
+
formatted_body: '<a href="https://matrix.to/#/@bot:matrix.org">Bot</a>: hello',
|
|
89
|
+
},
|
|
90
|
+
userId,
|
|
91
|
+
text: "Bot: hello",
|
|
92
|
+
mentionRegexes: [],
|
|
93
|
+
});
|
|
94
|
+
expect(result.wasMentioned).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("detects mention in formatted_body with URL-encoded user ID", () => {
|
|
98
|
+
const result = resolveMentions({
|
|
99
|
+
content: {
|
|
100
|
+
msgtype: "m.text",
|
|
101
|
+
body: "Bot: hello",
|
|
102
|
+
formatted_body: '<a href="https://matrix.to/#/%40bot%3Amatrix.org">Bot</a>: hello',
|
|
103
|
+
},
|
|
104
|
+
userId,
|
|
105
|
+
text: "Bot: hello",
|
|
106
|
+
mentionRegexes: [],
|
|
107
|
+
});
|
|
108
|
+
expect(result.wasMentioned).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("detects mention with single quotes in href", () => {
|
|
112
|
+
const result = resolveMentions({
|
|
113
|
+
content: {
|
|
114
|
+
msgtype: "m.text",
|
|
115
|
+
body: "Bot: hello",
|
|
116
|
+
formatted_body: "<a href='https://matrix.to/#/@bot:matrix.org'>Bot</a>: hello",
|
|
117
|
+
},
|
|
118
|
+
userId,
|
|
119
|
+
text: "Bot: hello",
|
|
120
|
+
mentionRegexes: [],
|
|
121
|
+
});
|
|
122
|
+
expect(result.wasMentioned).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("does not detect mention for different user ID", () => {
|
|
126
|
+
const result = resolveMentions({
|
|
127
|
+
content: {
|
|
128
|
+
msgtype: "m.text",
|
|
129
|
+
body: "Other: hello",
|
|
130
|
+
formatted_body: '<a href="https://matrix.to/#/@other:matrix.org">Other</a>: hello',
|
|
131
|
+
},
|
|
132
|
+
userId,
|
|
133
|
+
text: "Other: hello",
|
|
134
|
+
mentionRegexes: [],
|
|
135
|
+
});
|
|
136
|
+
expect(result.wasMentioned).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does not false-positive on partial user ID match", () => {
|
|
140
|
+
const result = resolveMentions({
|
|
141
|
+
content: {
|
|
142
|
+
msgtype: "m.text",
|
|
143
|
+
body: "Bot2: hello",
|
|
144
|
+
formatted_body: '<a href="https://matrix.to/#/@bot2:matrix.org">Bot2</a>: hello',
|
|
145
|
+
},
|
|
146
|
+
userId: "@bot:matrix.org",
|
|
147
|
+
text: "Bot2: hello",
|
|
148
|
+
mentionRegexes: [],
|
|
149
|
+
});
|
|
150
|
+
expect(result.wasMentioned).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("does not trust hidden matrix.to links behind unrelated visible text", () => {
|
|
154
|
+
const result = resolveMentions({
|
|
155
|
+
content: {
|
|
156
|
+
msgtype: "m.text",
|
|
157
|
+
body: "click here: hello",
|
|
158
|
+
formatted_body: '<a href="https://matrix.to/#/@bot:matrix.org">click here</a>: hello',
|
|
159
|
+
},
|
|
160
|
+
userId,
|
|
161
|
+
text: "click here: hello",
|
|
162
|
+
mentionRegexes: [],
|
|
163
|
+
});
|
|
164
|
+
expect(result.wasMentioned).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("detects mention when the visible label still names the bot", () => {
|
|
168
|
+
const result = resolveMentions({
|
|
169
|
+
content: {
|
|
170
|
+
msgtype: "m.text",
|
|
171
|
+
body: "@bot: hello",
|
|
172
|
+
formatted_body:
|
|
173
|
+
'<a href="https://matrix.to/#/@bot:matrix.org"><span>@bot</span></a>: hello',
|
|
174
|
+
},
|
|
175
|
+
userId,
|
|
176
|
+
text: "@bot: hello",
|
|
177
|
+
mentionRegexes: [],
|
|
178
|
+
});
|
|
179
|
+
expect(result.wasMentioned).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("detects mention when the visible label matches the bot's displayName", () => {
|
|
183
|
+
const result = resolveMentions({
|
|
184
|
+
content: {
|
|
185
|
+
msgtype: "m.text",
|
|
186
|
+
body: "Wonderful Bot: hello",
|
|
187
|
+
formatted_body: '<a href="https://matrix.to/#/@bot:matrix.org">Wonderful Bot</a>: hello',
|
|
188
|
+
},
|
|
189
|
+
userId,
|
|
190
|
+
displayName: "Wonderful Bot",
|
|
191
|
+
text: "Wonderful Bot: hello",
|
|
192
|
+
mentionRegexes: [],
|
|
193
|
+
});
|
|
194
|
+
expect(result.wasMentioned).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("detects mention when the visible label encodes the bot's displayName", () => {
|
|
198
|
+
const result = resolveMentions({
|
|
199
|
+
content: {
|
|
200
|
+
msgtype: "m.text",
|
|
201
|
+
body: "R&D Bot: hello",
|
|
202
|
+
formatted_body: '<a href="https://matrix.to/#/@bot:matrix.org">R&D Bot</a>: hello',
|
|
203
|
+
},
|
|
204
|
+
userId,
|
|
205
|
+
displayName: "R&D Bot",
|
|
206
|
+
text: "R&D Bot: hello",
|
|
207
|
+
mentionRegexes: [],
|
|
208
|
+
});
|
|
209
|
+
expect(result.wasMentioned).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("ignores out-of-range hexadecimal HTML entities in visible labels", () => {
|
|
213
|
+
expect(() =>
|
|
214
|
+
resolveMentions({
|
|
215
|
+
content: {
|
|
216
|
+
msgtype: "m.text",
|
|
217
|
+
body: "hello",
|
|
218
|
+
formatted_body: '<a href="https://matrix.to/#/@bot:matrix.org">�</a>: hello',
|
|
219
|
+
},
|
|
220
|
+
userId,
|
|
221
|
+
text: "hello",
|
|
222
|
+
mentionRegexes: [],
|
|
223
|
+
}),
|
|
224
|
+
).not.toThrow();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("ignores oversized decimal HTML entities in visible labels", () => {
|
|
228
|
+
expect(() =>
|
|
229
|
+
resolveMentions({
|
|
230
|
+
content: {
|
|
231
|
+
msgtype: "m.text",
|
|
232
|
+
body: "hello",
|
|
233
|
+
formatted_body:
|
|
234
|
+
'<a href="https://matrix.to/#/@bot:matrix.org">�</a>: hello',
|
|
235
|
+
},
|
|
236
|
+
userId,
|
|
237
|
+
text: "hello",
|
|
238
|
+
mentionRegexes: [],
|
|
239
|
+
}),
|
|
240
|
+
).not.toThrow();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("does not detect mention when displayName is spoofed", () => {
|
|
244
|
+
const result = resolveMentions({
|
|
245
|
+
content: {
|
|
246
|
+
msgtype: "m.text",
|
|
247
|
+
body: "Spoofed Bot: hello",
|
|
248
|
+
formatted_body: '<a href="https://matrix.to/#/@bot:matrix.org">Spoofed Bot</a>: hello',
|
|
249
|
+
},
|
|
250
|
+
userId,
|
|
251
|
+
displayName: "Alice",
|
|
252
|
+
text: "Spoofed Bot: hello",
|
|
253
|
+
mentionRegexes: [],
|
|
254
|
+
});
|
|
255
|
+
expect(result.wasMentioned).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("regex patterns", () => {
|
|
260
|
+
it("detects mention via regex pattern in body text", () => {
|
|
261
|
+
const result = resolveMentions({
|
|
262
|
+
content: {
|
|
263
|
+
msgtype: "m.text",
|
|
264
|
+
body: "hey @bot can you help?",
|
|
265
|
+
},
|
|
266
|
+
userId,
|
|
267
|
+
text: "hey @bot can you help?",
|
|
268
|
+
mentionRegexes,
|
|
269
|
+
});
|
|
270
|
+
expect(result.wasMentioned).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("no mention", () => {
|
|
275
|
+
it("returns false when no mention is present", () => {
|
|
276
|
+
const result = resolveMentions({
|
|
277
|
+
content: {
|
|
278
|
+
msgtype: "m.text",
|
|
279
|
+
body: "hello world",
|
|
280
|
+
},
|
|
281
|
+
userId,
|
|
282
|
+
text: "hello world",
|
|
283
|
+
mentionRegexes,
|
|
284
|
+
});
|
|
285
|
+
expect(result.wasMentioned).toBe(false);
|
|
286
|
+
expect(result.hasExplicitMention).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
3
|
+
import type { RoomMessageEventContent } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const HTML_ENTITY_REPLACEMENTS: Readonly<Record<string, string>> = {
|
|
6
|
+
amp: "&",
|
|
7
|
+
apos: "'",
|
|
8
|
+
gt: ">",
|
|
9
|
+
lt: "<",
|
|
10
|
+
nbsp: " ",
|
|
11
|
+
quot: '"',
|
|
12
|
+
};
|
|
13
|
+
const MAX_UNICODE_SCALAR_VALUE = 0x10ffff;
|
|
14
|
+
|
|
15
|
+
function decodeNumericHtmlEntity(match: string, rawValue: string, radix: 10 | 16): string {
|
|
16
|
+
const codePoint = Number.parseInt(rawValue, radix);
|
|
17
|
+
if (
|
|
18
|
+
!Number.isSafeInteger(codePoint) ||
|
|
19
|
+
codePoint < 0 ||
|
|
20
|
+
codePoint > MAX_UNICODE_SCALAR_VALUE ||
|
|
21
|
+
(codePoint >= 0xd800 && codePoint <= 0xdfff)
|
|
22
|
+
) {
|
|
23
|
+
return match;
|
|
24
|
+
}
|
|
25
|
+
return String.fromCodePoint(codePoint);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function decodeHtmlEntities(value: string): string {
|
|
29
|
+
return value.replace(/&(#x?[0-9a-f]+|\w+);/gi, (match, entity: string) => {
|
|
30
|
+
const normalized = normalizeLowercaseStringOrEmpty(entity);
|
|
31
|
+
if (normalized.startsWith("#x")) {
|
|
32
|
+
return decodeNumericHtmlEntity(match, normalized.slice(2), 16);
|
|
33
|
+
}
|
|
34
|
+
if (normalized.startsWith("#")) {
|
|
35
|
+
return decodeNumericHtmlEntity(match, normalized.slice(1), 10);
|
|
36
|
+
}
|
|
37
|
+
return HTML_ENTITY_REPLACEMENTS[normalized] ?? match;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeVisibleMentionText(value: string): string {
|
|
42
|
+
return normalizeLowercaseStringOrEmpty(
|
|
43
|
+
decodeHtmlEntities(
|
|
44
|
+
value.replace(/<[^>]+>/g, " ").replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, ""),
|
|
45
|
+
).replace(/\s+/g, " "),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function extractVisibleMentionText(value?: string): string {
|
|
50
|
+
return normalizeVisibleMentionText(value ?? "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveMatrixUserLocalpart(userId: string): string | null {
|
|
54
|
+
const trimmed = userId.trim();
|
|
55
|
+
if (!trimmed.startsWith("@")) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const colonIndex = trimmed.indexOf(":");
|
|
59
|
+
if (colonIndex <= 1) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return trimmed.slice(1, colonIndex).trim() || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isVisibleMentionLabel(params: {
|
|
66
|
+
text: string;
|
|
67
|
+
userId: string;
|
|
68
|
+
mentionRegexes: RegExp[];
|
|
69
|
+
displayName?: string | null;
|
|
70
|
+
}): boolean {
|
|
71
|
+
const cleaned = extractVisibleMentionText(params.text);
|
|
72
|
+
if (!cleaned) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (params.mentionRegexes.some((pattern) => pattern.test(cleaned))) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const localpart = resolveMatrixUserLocalpart(params.userId);
|
|
79
|
+
const candidates = [
|
|
80
|
+
extractVisibleMentionText(params.userId),
|
|
81
|
+
localpart ? extractVisibleMentionText(localpart) : null,
|
|
82
|
+
localpart ? extractVisibleMentionText(`@${localpart}`) : null,
|
|
83
|
+
params.displayName ? extractVisibleMentionText(params.displayName) : null,
|
|
84
|
+
].filter((value): value is string => Boolean(value));
|
|
85
|
+
return candidates.includes(cleaned);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasVisibleRoomMention(value?: string): boolean {
|
|
89
|
+
const cleaned = extractVisibleMentionText(value);
|
|
90
|
+
return /(^|[^a-z0-9_])@room\b/i.test(cleaned);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if formatted_body contains a matrix.to link whose visible label still
|
|
95
|
+
* looks like a real mention for the given user. Do not trust href alone, since
|
|
96
|
+
* senders can hide arbitrary matrix.to links behind unrelated link text.
|
|
97
|
+
* Many Matrix clients (including Element) use HTML links in formatted_body instead of
|
|
98
|
+
* or in addition to the m.mentions field.
|
|
99
|
+
*/
|
|
100
|
+
function checkFormattedBodyMention(params: {
|
|
101
|
+
formattedBody?: string;
|
|
102
|
+
userId: string;
|
|
103
|
+
displayName?: string | null;
|
|
104
|
+
mentionRegexes: RegExp[];
|
|
105
|
+
}): boolean {
|
|
106
|
+
if (!params.formattedBody || !params.userId) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const anchorPattern = /<a\b[^>]*href=(["'])(https:\/\/matrix\.to\/#[^"']+)\1[^>]*>(.*?)<\/a>/gis;
|
|
110
|
+
for (const match of params.formattedBody.matchAll(anchorPattern)) {
|
|
111
|
+
const href = match[2];
|
|
112
|
+
const visibleLabel = match[3] ?? "";
|
|
113
|
+
if (!href) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const parsed = new URL(href);
|
|
118
|
+
const fragmentTarget = decodeURIComponent(parsed.hash.replace(/^#\/?/, "").trim());
|
|
119
|
+
if (fragmentTarget !== params.userId.trim()) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
isVisibleMentionLabel({
|
|
124
|
+
text: visibleLabel,
|
|
125
|
+
userId: params.userId,
|
|
126
|
+
mentionRegexes: params.mentionRegexes,
|
|
127
|
+
displayName: params.displayName,
|
|
128
|
+
})
|
|
129
|
+
) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function resolveMentions(params: {
|
|
140
|
+
content: RoomMessageEventContent;
|
|
141
|
+
userId?: string | null;
|
|
142
|
+
displayName?: string | null;
|
|
143
|
+
text?: string;
|
|
144
|
+
mentionRegexes: RegExp[];
|
|
145
|
+
}) {
|
|
146
|
+
const mentions = params.content["m.mentions"];
|
|
147
|
+
const mentionedUsers = Array.isArray(mentions?.user_ids)
|
|
148
|
+
? new Set(mentions.user_ids)
|
|
149
|
+
: new Set<string>();
|
|
150
|
+
const textMentioned = getMatrixRuntime().channel.mentions.matchesMentionPatterns(
|
|
151
|
+
params.text ?? "",
|
|
152
|
+
params.mentionRegexes,
|
|
153
|
+
);
|
|
154
|
+
const visibleRoomMention =
|
|
155
|
+
hasVisibleRoomMention(params.text) || hasVisibleRoomMention(params.content.formatted_body);
|
|
156
|
+
|
|
157
|
+
// Check formatted_body for matrix.to mention links (legacy/alternative mention format)
|
|
158
|
+
const mentionedInFormattedBody = params.userId
|
|
159
|
+
? checkFormattedBodyMention({
|
|
160
|
+
formattedBody: params.content.formatted_body,
|
|
161
|
+
userId: params.userId,
|
|
162
|
+
displayName: params.displayName,
|
|
163
|
+
mentionRegexes: params.mentionRegexes,
|
|
164
|
+
})
|
|
165
|
+
: false;
|
|
166
|
+
const metadataBackedUserMention = Boolean(
|
|
167
|
+
params.userId &&
|
|
168
|
+
mentionedUsers.has(params.userId) &&
|
|
169
|
+
(mentionedInFormattedBody || textMentioned),
|
|
170
|
+
);
|
|
171
|
+
const metadataBackedRoomMention = Boolean(mentions?.room) && visibleRoomMention;
|
|
172
|
+
const explicitMention =
|
|
173
|
+
mentionedInFormattedBody || metadataBackedUserMention || metadataBackedRoomMention;
|
|
174
|
+
|
|
175
|
+
const wasMentioned = explicitMention || textMentioned || visibleRoomMention;
|
|
176
|
+
return { wasMentioned, hasExplicitMention: explicitMention };
|
|
177
|
+
}
|