@gakr-gakr/matrix 0.1.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 +285 -0
- package/SPEC-SUPPORT.md +116 -0
- package/api.ts +38 -0
- package/auth-presence.ts +56 -0
- package/autobot.plugin.json +28 -0
- package/channel-plugin-api.ts +3 -0
- package/cli-metadata.ts +11 -0
- package/contract-api.ts +17 -0
- package/doctor-contract-api.ts +1 -0
- package/helper-api.ts +3 -0
- package/index.ts +55 -0
- package/package.json +101 -0
- package/plugin-entry.handlers.runtime.ts +1 -0
- package/runtime-api.ts +72 -0
- package/runtime-heavy-api.ts +1 -0
- package/runtime-setter-api.ts +3 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +17 -0
- package/setup-plugin-api.ts +3 -0
- package/src/account-selection.ts +223 -0
- package/src/actions.ts +346 -0
- package/src/approval-auth.ts +25 -0
- package/src/approval-handler.runtime.ts +595 -0
- package/src/approval-ids.ts +6 -0
- package/src/approval-native.ts +348 -0
- package/src/approval-reaction-auth.ts +45 -0
- package/src/approval-reactions.ts +313 -0
- package/src/auth-precedence.ts +61 -0
- package/src/channel-account-paths.ts +97 -0
- package/src/channel.runtime.ts +17 -0
- package/src/channel.setup.ts +48 -0
- package/src/channel.ts +667 -0
- package/src/cli-metadata.ts +19 -0
- package/src/cli.ts +2298 -0
- package/src/config-adapter.ts +41 -0
- package/src/config-schema.ts +159 -0
- package/src/config-ui-hints.ts +56 -0
- package/src/directory-live.ts +238 -0
- package/src/doctor-contract.ts +287 -0
- package/src/doctor.ts +262 -0
- package/src/env-vars.ts +92 -0
- package/src/exec-approval-resolver.ts +23 -0
- package/src/exec-approvals.ts +293 -0
- package/src/group-mentions.ts +41 -0
- package/src/legacy-crypto-inspector-availability.ts +60 -0
- package/src/legacy-crypto.ts +531 -0
- package/src/legacy-state.ts +156 -0
- package/src/matrix/account-config.ts +175 -0
- package/src/matrix/accounts.ts +194 -0
- package/src/matrix/actions/client.ts +31 -0
- package/src/matrix/actions/devices.ts +34 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.ts +129 -0
- package/src/matrix/actions/pins.ts +63 -0
- package/src/matrix/actions/polls.ts +109 -0
- package/src/matrix/actions/profile.ts +37 -0
- package/src/matrix/actions/reactions.ts +59 -0
- package/src/matrix/actions/room.ts +71 -0
- package/src/matrix/actions/summary.ts +88 -0
- package/src/matrix/actions/types.ts +63 -0
- package/src/matrix/actions/verification.ts +589 -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 +124 -0
- package/src/matrix/client/config-runtime-api.ts +9 -0
- package/src/matrix/client/config-secret-input.runtime.ts +1 -0
- package/src/matrix/client/config.ts +853 -0
- package/src/matrix/client/create-client.ts +105 -0
- package/src/matrix/client/env-auth.ts +95 -0
- package/src/matrix/client/file-sync-store.ts +289 -0
- package/src/matrix/client/logging.ts +140 -0
- package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
- package/src/matrix/client/private-network-host.ts +1 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.ts +316 -0
- package/src/matrix/client/storage.ts +543 -0
- package/src/matrix/client/types.ts +50 -0
- package/src/matrix/client/url-validation.ts +76 -0
- package/src/matrix/client-bootstrap.ts +173 -0
- package/src/matrix/client.ts +23 -0
- package/src/matrix/config-paths.ts +31 -0
- package/src/matrix/config-update.ts +292 -0
- package/src/matrix/credentials-read.ts +207 -0
- package/src/matrix/credentials-write.runtime.ts +35 -0
- package/src/matrix/credentials.ts +95 -0
- package/src/matrix/deps.ts +309 -0
- package/src/matrix/device-health.ts +31 -0
- package/src/matrix/direct-management.ts +349 -0
- package/src/matrix/direct-room.ts +128 -0
- package/src/matrix/draft-stream.ts +225 -0
- package/src/matrix/encryption-guidance.ts +24 -0
- package/src/matrix/errors.ts +21 -0
- package/src/matrix/format.ts +426 -0
- package/src/matrix/legacy-crypto-inspector.ts +95 -0
- package/src/matrix/media-errors.ts +20 -0
- package/src/matrix/media-text.ts +162 -0
- package/src/matrix/monitor/access-state.ts +145 -0
- package/src/matrix/monitor/ack-config.ts +27 -0
- package/src/matrix/monitor/allowlist.ts +92 -0
- package/src/matrix/monitor/auto-join.ts +86 -0
- package/src/matrix/monitor/config.ts +569 -0
- package/src/matrix/monitor/context-summary.ts +43 -0
- package/src/matrix/monitor/direct.ts +296 -0
- package/src/matrix/monitor/events.ts +397 -0
- package/src/matrix/monitor/handler.ts +2271 -0
- package/src/matrix/monitor/inbound-dedupe.ts +267 -0
- package/src/matrix/monitor/index.ts +540 -0
- package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
- package/src/matrix/monitor/location.ts +108 -0
- package/src/matrix/monitor/media.ts +119 -0
- package/src/matrix/monitor/mentions.ts +256 -0
- package/src/matrix/monitor/reaction-events.ts +197 -0
- package/src/matrix/monitor/recent-invite.ts +30 -0
- package/src/matrix/monitor/replies.ts +136 -0
- package/src/matrix/monitor/reply-context.ts +92 -0
- package/src/matrix/monitor/room-history.ts +301 -0
- package/src/matrix/monitor/room-info.ts +126 -0
- package/src/matrix/monitor/rooms.ts +52 -0
- package/src/matrix/monitor/route.ts +179 -0
- package/src/matrix/monitor/runtime-api.ts +28 -0
- package/src/matrix/monitor/startup-verification.ts +237 -0
- package/src/matrix/monitor/startup.ts +218 -0
- package/src/matrix/monitor/status.ts +120 -0
- package/src/matrix/monitor/sync-lifecycle.ts +91 -0
- package/src/matrix/monitor/task-runner.ts +38 -0
- package/src/matrix/monitor/test-events.ts +21 -0
- package/src/matrix/monitor/thread-context.ts +108 -0
- package/src/matrix/monitor/threads.ts +85 -0
- package/src/matrix/monitor/types.ts +30 -0
- package/src/matrix/monitor/verification-events.ts +643 -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.ts +429 -0
- package/src/matrix/probe.runtime.ts +4 -0
- package/src/matrix/probe.ts +97 -0
- package/src/matrix/profile.ts +184 -0
- package/src/matrix/reaction-common.ts +147 -0
- package/src/matrix/sdk/crypto-bootstrap.ts +438 -0
- package/src/matrix/sdk/crypto-facade.ts +242 -0
- package/src/matrix/sdk/crypto-node.runtime.ts +17 -0
- package/src/matrix/sdk/crypto-runtime.ts +14 -0
- package/src/matrix/sdk/decrypt-bridge.ts +410 -0
- package/src/matrix/sdk/event-helpers.ts +83 -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.ts +286 -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.ts +453 -0
- package/src/matrix/sdk/timeout-abort-signal.ts +1 -0
- package/src/matrix/sdk/transport-runtime-api.ts +18 -0
- package/src/matrix/sdk/transport.ts +352 -0
- package/src/matrix/sdk/types.ts +245 -0
- package/src/matrix/sdk/verification-manager.ts +795 -0
- package/src/matrix/sdk/verification-status.ts +23 -0
- package/src/matrix/sdk.ts +2152 -0
- package/src/matrix/send/client.ts +93 -0
- package/src/matrix/send/formatting.ts +189 -0
- package/src/matrix/send/media.ts +244 -0
- package/src/matrix/send/targets.ts +104 -0
- package/src/matrix/send/types.ts +131 -0
- package/src/matrix/send.ts +660 -0
- package/src/matrix/session-store-metadata.ts +108 -0
- package/src/matrix/startup-abort.ts +44 -0
- package/src/matrix/subagent-hooks.ts +308 -0
- package/src/matrix/sync-state.ts +27 -0
- package/src/matrix/target-ids.ts +79 -0
- package/src/matrix/thread-bindings-shared.ts +206 -0
- package/src/matrix/thread-bindings.ts +580 -0
- package/src/matrix-migration.runtime.ts +9 -0
- package/src/migration-config.ts +243 -0
- package/src/migration-snapshot-backup.ts +116 -0
- package/src/migration-snapshot.ts +53 -0
- package/src/onboarding.ts +775 -0
- package/src/outbound.ts +248 -0
- package/src/plugin-entry.runtime.js +115 -0
- package/src/plugin-entry.runtime.ts +70 -0
- package/src/profile-update.ts +71 -0
- package/src/record-shared.ts +3 -0
- package/src/resolve-targets.ts +175 -0
- package/src/resolver.runtime.ts +5 -0
- package/src/resolver.ts +21 -0
- package/src/runtime-api.ts +106 -0
- package/src/runtime.ts +13 -0
- package/src/secret-contract.ts +174 -0
- package/src/session-route.ts +126 -0
- package/src/setup-bootstrap.ts +102 -0
- package/src/setup-config.ts +222 -0
- package/src/setup-contract.ts +90 -0
- package/src/setup-core.ts +146 -0
- package/src/setup-dm-policy.ts +15 -0
- package/src/setup-surface.ts +4 -0
- package/src/startup-maintenance.ts +114 -0
- package/src/storage-paths.ts +92 -0
- package/src/thread-binding-api.ts +23 -0
- package/src/tool-actions.runtime.ts +1 -0
- package/src/tool-actions.ts +498 -0
- package/src/types.ts +257 -0
- package/subagent-hooks-api.ts +31 -0
- package/test-api.ts +21 -0
- package/thread-binding-api.ts +4 -0
- package/thread-bindings-runtime.ts +4 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty } from "autobot/plugin-sdk/string-coerce-runtime";
|
|
2
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
3
|
+
import type { MatrixClient } from "../sdk.js";
|
|
4
|
+
import { chunkMatrixText, sendMessageMatrix } from "../send.js";
|
|
5
|
+
import type { MarkdownTableMode, AutoBotConfig, ReplyPayload, RuntimeEnv } from "./runtime-api.js";
|
|
6
|
+
|
|
7
|
+
const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
|
|
8
|
+
const THINKING_BLOCK_RE =
|
|
9
|
+
/<\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>[\s\S]*?<\s*\/\s*(?:think(?:ing)?|thought|antthinking)\s*>/gi;
|
|
10
|
+
|
|
11
|
+
function shouldSuppressReasoningReplyText(text?: string): boolean {
|
|
12
|
+
if (typeof text !== "string") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const trimmedStart = text.trimStart();
|
|
16
|
+
if (!trimmedStart) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (normalizeLowercaseStringOrEmpty(trimmedStart).startsWith("reasoning:")) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
THINKING_TAG_RE.lastIndex = 0;
|
|
23
|
+
if (!THINKING_TAG_RE.test(text)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
THINKING_BLOCK_RE.lastIndex = 0;
|
|
27
|
+
const withoutThinkingBlocks = text.replace(THINKING_BLOCK_RE, "");
|
|
28
|
+
THINKING_TAG_RE.lastIndex = 0;
|
|
29
|
+
return !withoutThinkingBlocks.replace(THINKING_TAG_RE, "").trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deliverMatrixReplies(params: {
|
|
33
|
+
cfg: AutoBotConfig;
|
|
34
|
+
replies: ReplyPayload[];
|
|
35
|
+
roomId: string;
|
|
36
|
+
client: MatrixClient;
|
|
37
|
+
runtime: RuntimeEnv;
|
|
38
|
+
textLimit: number;
|
|
39
|
+
replyToMode: "off" | "first" | "all" | "batched";
|
|
40
|
+
threadId?: string;
|
|
41
|
+
accountId?: string;
|
|
42
|
+
mediaLocalRoots?: readonly string[];
|
|
43
|
+
tableMode?: MarkdownTableMode;
|
|
44
|
+
}): Promise<boolean> {
|
|
45
|
+
const core = getMatrixRuntime();
|
|
46
|
+
const tableMode =
|
|
47
|
+
params.tableMode ??
|
|
48
|
+
core.channel.text.resolveMarkdownTableMode({
|
|
49
|
+
cfg: params.cfg,
|
|
50
|
+
channel: "matrix",
|
|
51
|
+
accountId: params.accountId,
|
|
52
|
+
});
|
|
53
|
+
const logVerbose = (message: string) => {
|
|
54
|
+
if (core.logging.shouldLogVerbose()) {
|
|
55
|
+
params.runtime.log?.(message);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
let hasReplied = false;
|
|
59
|
+
let deliveredAny = false;
|
|
60
|
+
for (const reply of params.replies) {
|
|
61
|
+
if (reply.isReasoning === true || shouldSuppressReasoningReplyText(reply.text)) {
|
|
62
|
+
logVerbose("matrix reply suppressed as reasoning-only");
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
|
|
66
|
+
if (!reply?.text && !hasMedia) {
|
|
67
|
+
if (reply?.audioAsVoice) {
|
|
68
|
+
logVerbose("matrix reply has audioAsVoice without media/text; skipping");
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
params.runtime.error?.("matrix reply missing text/media");
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const replyToIdRaw = reply.replyToId?.trim();
|
|
75
|
+
const replyToId = params.threadId || params.replyToMode === "off" ? undefined : replyToIdRaw;
|
|
76
|
+
const rawText = reply.text ?? "";
|
|
77
|
+
const mediaList = reply.mediaUrls?.length
|
|
78
|
+
? reply.mediaUrls
|
|
79
|
+
: reply.mediaUrl
|
|
80
|
+
? [reply.mediaUrl]
|
|
81
|
+
: [];
|
|
82
|
+
|
|
83
|
+
const shouldIncludeReply = (id?: string) =>
|
|
84
|
+
Boolean(id) && (params.replyToMode === "all" || !hasReplied);
|
|
85
|
+
const replyToIdForReply = shouldIncludeReply(replyToId) ? replyToId : undefined;
|
|
86
|
+
|
|
87
|
+
if (mediaList.length === 0) {
|
|
88
|
+
let sentTextChunk = false;
|
|
89
|
+
const { chunks } = chunkMatrixText(rawText, {
|
|
90
|
+
cfg: params.cfg,
|
|
91
|
+
accountId: params.accountId,
|
|
92
|
+
tableMode,
|
|
93
|
+
});
|
|
94
|
+
for (const chunk of chunks) {
|
|
95
|
+
const trimmed = chunk.trim();
|
|
96
|
+
if (!trimmed) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
await sendMessageMatrix(params.roomId, trimmed, {
|
|
100
|
+
client: params.client,
|
|
101
|
+
cfg: params.cfg,
|
|
102
|
+
replyToId: replyToIdForReply,
|
|
103
|
+
threadId: params.threadId,
|
|
104
|
+
accountId: params.accountId,
|
|
105
|
+
});
|
|
106
|
+
deliveredAny = true;
|
|
107
|
+
sentTextChunk = true;
|
|
108
|
+
}
|
|
109
|
+
if (replyToIdForReply && !hasReplied && sentTextChunk) {
|
|
110
|
+
hasReplied = true;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let first = true;
|
|
116
|
+
for (const mediaUrl of mediaList) {
|
|
117
|
+
const caption = first ? rawText : "";
|
|
118
|
+
await sendMessageMatrix(params.roomId, caption, {
|
|
119
|
+
client: params.client,
|
|
120
|
+
cfg: params.cfg,
|
|
121
|
+
mediaUrl,
|
|
122
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
123
|
+
replyToId: replyToIdForReply,
|
|
124
|
+
threadId: params.threadId,
|
|
125
|
+
audioAsVoice: reply.audioAsVoice,
|
|
126
|
+
accountId: params.accountId,
|
|
127
|
+
});
|
|
128
|
+
deliveredAny = true;
|
|
129
|
+
first = false;
|
|
130
|
+
}
|
|
131
|
+
if (replyToIdForReply && !hasReplied) {
|
|
132
|
+
hasReplied = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return deliveredAny;
|
|
136
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { MatrixClient } from "../sdk.js";
|
|
2
|
+
import { summarizeMatrixMessageContextEvent, trimMatrixMaybeString } from "./context-summary.js";
|
|
3
|
+
import type { MatrixRawEvent } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const MAX_CACHED_REPLY_CONTEXTS = 256;
|
|
6
|
+
const MAX_REPLY_BODY_LENGTH = 500;
|
|
7
|
+
|
|
8
|
+
type MatrixReplyContext = {
|
|
9
|
+
replyToBody?: string;
|
|
10
|
+
replyToSender?: string;
|
|
11
|
+
replyToSenderId?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function truncateReplyBody(value: string): string {
|
|
15
|
+
if (value.length <= MAX_REPLY_BODY_LENGTH) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return `${value.slice(0, MAX_REPLY_BODY_LENGTH - 3)}...`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function summarizeMatrixReplyEvent(event: MatrixRawEvent): string | undefined {
|
|
22
|
+
const body = summarizeMatrixMessageContextEvent(event);
|
|
23
|
+
return body ? truncateReplyBody(body) : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a cached resolver that fetches the body and sender of a replied-to
|
|
28
|
+
* Matrix event. This allows the agent to see the content of the message being
|
|
29
|
+
* replied to, not just its event ID.
|
|
30
|
+
*/
|
|
31
|
+
export function createMatrixReplyContextResolver(params: {
|
|
32
|
+
client: MatrixClient;
|
|
33
|
+
getMemberDisplayName: (roomId: string, userId: string) => Promise<string>;
|
|
34
|
+
logVerboseMessage: (message: string) => void;
|
|
35
|
+
}) {
|
|
36
|
+
const cache = new Map<string, MatrixReplyContext>();
|
|
37
|
+
|
|
38
|
+
const remember = (key: string, value: MatrixReplyContext): MatrixReplyContext => {
|
|
39
|
+
cache.set(key, value);
|
|
40
|
+
if (cache.size > MAX_CACHED_REPLY_CONTEXTS) {
|
|
41
|
+
const oldest = cache.keys().next().value;
|
|
42
|
+
if (typeof oldest === "string") {
|
|
43
|
+
cache.delete(oldest);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return async (input: { roomId: string; eventId: string }): Promise<MatrixReplyContext> => {
|
|
50
|
+
const cacheKey = `${input.roomId}:${input.eventId}`;
|
|
51
|
+
const cached = cache.get(cacheKey);
|
|
52
|
+
if (cached) {
|
|
53
|
+
// Move to end for LRU semantics so frequently accessed entries survive eviction.
|
|
54
|
+
cache.delete(cacheKey);
|
|
55
|
+
cache.set(cacheKey, cached);
|
|
56
|
+
return cached;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const event = await params.client.getEvent(input.roomId, input.eventId).catch((err) => {
|
|
60
|
+
params.logVerboseMessage(
|
|
61
|
+
`matrix: failed resolving reply context room=${input.roomId} id=${input.eventId}: ${String(err)}`,
|
|
62
|
+
);
|
|
63
|
+
return null;
|
|
64
|
+
});
|
|
65
|
+
if (!event) {
|
|
66
|
+
// Do not cache failures so transient errors can be retried on the next
|
|
67
|
+
// message that references the same event.
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rawEvent = event as MatrixRawEvent;
|
|
72
|
+
if (rawEvent.unsigned?.redacted_because) {
|
|
73
|
+
return remember(cacheKey, {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const replyToBody = summarizeMatrixReplyEvent(rawEvent);
|
|
77
|
+
if (!replyToBody) {
|
|
78
|
+
return remember(cacheKey, {});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const senderId = trimMatrixMaybeString(rawEvent.sender);
|
|
82
|
+
const senderName =
|
|
83
|
+
senderId &&
|
|
84
|
+
(await params.getMemberDisplayName(input.roomId, senderId).catch(() => undefined));
|
|
85
|
+
|
|
86
|
+
return remember(cacheKey, {
|
|
87
|
+
replyToBody,
|
|
88
|
+
replyToSender: senderName ?? senderId,
|
|
89
|
+
replyToSenderId: senderId,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-room group chat history tracking for Matrix.
|
|
3
|
+
*
|
|
4
|
+
* Maintains a shared per-room message queue and per-(agentId, roomId) watermarks so
|
|
5
|
+
* each agent independently tracks which messages it has already consumed. This design
|
|
6
|
+
* lets multiple agents in the same room see independent history windows:
|
|
7
|
+
*
|
|
8
|
+
* - dev replies to @dev msgB (watermark advances to B) → room queue still has [A, B]
|
|
9
|
+
* - spark replies to @spark msgC → spark watermark starts at 0 and sees [A, B, C]
|
|
10
|
+
*
|
|
11
|
+
* Race-condition safety: the watermark only advances to the snapshot index taken at
|
|
12
|
+
* dispatch time, NOT to the queue's end at reply time. Messages that land in the queue
|
|
13
|
+
* while the agent is processing stay visible to the next trigger for that agent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { HistoryEntry } from "autobot/plugin-sdk/reply-history";
|
|
17
|
+
|
|
18
|
+
/** Maximum entries retained per room (hard cap to bound memory). */
|
|
19
|
+
const DEFAULT_MAX_QUEUE_SIZE = 200;
|
|
20
|
+
/** Maximum number of rooms to retain queues for (FIFO eviction beyond this). */
|
|
21
|
+
const DEFAULT_MAX_ROOM_QUEUES = 1000;
|
|
22
|
+
/** Maximum number of (agentId, roomId) watermark entries to retain. */
|
|
23
|
+
const MAX_WATERMARK_ENTRIES = 5000;
|
|
24
|
+
/** Maximum prepared trigger snapshots retained per room for retry reuse. */
|
|
25
|
+
const MAX_PREPARED_TRIGGER_ENTRIES = 500;
|
|
26
|
+
|
|
27
|
+
export type { HistoryEntry };
|
|
28
|
+
|
|
29
|
+
type HistorySnapshotToken = {
|
|
30
|
+
snapshotIdx: number;
|
|
31
|
+
queueGeneration: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type PreparedTriggerResult = {
|
|
35
|
+
history: HistoryEntry[];
|
|
36
|
+
} & HistorySnapshotToken;
|
|
37
|
+
|
|
38
|
+
type RoomHistoryTracker = {
|
|
39
|
+
/**
|
|
40
|
+
* Record a non-trigger message for future context.
|
|
41
|
+
* Call this when a room message arrives but does not mention the bot.
|
|
42
|
+
*/
|
|
43
|
+
recordPending: (roomId: string, entry: HistoryEntry) => void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Capture pending history and append the trigger as one idempotent operation.
|
|
47
|
+
* Retries of the same Matrix event reuse the original prepared history window.
|
|
48
|
+
*/
|
|
49
|
+
prepareTrigger: (
|
|
50
|
+
agentId: string,
|
|
51
|
+
roomId: string,
|
|
52
|
+
limit: number,
|
|
53
|
+
entry: HistoryEntry,
|
|
54
|
+
) => PreparedTriggerResult;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Advance the agent's watermark to the snapshot index returned by prepareTrigger
|
|
58
|
+
* (or the lower-level recordTrigger helper used in tests).
|
|
59
|
+
* Only messages appended after that snapshot remain visible on the next trigger.
|
|
60
|
+
*/
|
|
61
|
+
consumeHistory: (
|
|
62
|
+
agentId: string,
|
|
63
|
+
roomId: string,
|
|
64
|
+
snapshot: HistorySnapshotToken,
|
|
65
|
+
messageId?: string,
|
|
66
|
+
) => void;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type RoomHistoryTrackerTestApi = RoomHistoryTracker & {
|
|
70
|
+
/**
|
|
71
|
+
* Test-only helper for inspecting pending room history directly.
|
|
72
|
+
*/
|
|
73
|
+
getPendingHistory: (agentId: string, roomId: string, limit: number) => HistoryEntry[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Test-only helper for manually appending a trigger entry and snapshot index.
|
|
77
|
+
*/
|
|
78
|
+
recordTrigger: (roomId: string, entry: HistoryEntry) => HistorySnapshotToken;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type RoomQueue = {
|
|
82
|
+
entries: HistoryEntry[];
|
|
83
|
+
/** Absolute index of entries[0] — increases as old entries are trimmed. */
|
|
84
|
+
baseIndex: number;
|
|
85
|
+
generation: number;
|
|
86
|
+
preparedTriggers: Map<string, PreparedTriggerResult>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function createRoomHistoryTrackerInternal(
|
|
90
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
91
|
+
maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES,
|
|
92
|
+
maxWatermarkEntries = MAX_WATERMARK_ENTRIES,
|
|
93
|
+
maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES,
|
|
94
|
+
): RoomHistoryTrackerTestApi {
|
|
95
|
+
const roomQueues = new Map<string, RoomQueue>();
|
|
96
|
+
/** Maps `${agentId}:${roomId}` → absolute consumed-up-to index */
|
|
97
|
+
const agentWatermarks = new Map<string, number>();
|
|
98
|
+
let nextQueueGeneration = 1;
|
|
99
|
+
|
|
100
|
+
function clearRoomWatermarks(roomId: string): void {
|
|
101
|
+
const roomSuffix = `:${roomId}`;
|
|
102
|
+
for (const key of agentWatermarks.keys()) {
|
|
103
|
+
if (key.endsWith(roomSuffix)) {
|
|
104
|
+
agentWatermarks.delete(key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getOrCreateQueue(roomId: string): RoomQueue {
|
|
110
|
+
let queue = roomQueues.get(roomId);
|
|
111
|
+
if (!queue) {
|
|
112
|
+
queue = {
|
|
113
|
+
entries: [],
|
|
114
|
+
baseIndex: 0,
|
|
115
|
+
generation: nextQueueGeneration++,
|
|
116
|
+
preparedTriggers: new Map(),
|
|
117
|
+
};
|
|
118
|
+
roomQueues.set(roomId, queue);
|
|
119
|
+
// FIFO eviction to prevent unbounded growth across many rooms
|
|
120
|
+
if (roomQueues.size > maxRoomQueues) {
|
|
121
|
+
const oldest = roomQueues.keys().next().value;
|
|
122
|
+
if (oldest !== undefined) {
|
|
123
|
+
roomQueues.delete(oldest);
|
|
124
|
+
clearRoomWatermarks(oldest);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return queue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function appendToQueue(queue: RoomQueue, entry: HistoryEntry): HistorySnapshotToken {
|
|
132
|
+
queue.entries.push(entry);
|
|
133
|
+
if (queue.entries.length > maxQueueSize) {
|
|
134
|
+
const overflow = queue.entries.length - maxQueueSize;
|
|
135
|
+
queue.entries.splice(0, overflow);
|
|
136
|
+
queue.baseIndex += overflow;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
snapshotIdx: queue.baseIndex + queue.entries.length,
|
|
140
|
+
queueGeneration: queue.generation,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wmKey(agentId: string, roomId: string): string {
|
|
145
|
+
return `${agentId}:${roomId}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function preparedTriggerKey(agentId: string, messageId?: string): string | null {
|
|
149
|
+
if (!messageId?.trim()) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return `${agentId}:${messageId.trim()}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rememberWatermark(key: string, snapshotIdx: number): void {
|
|
156
|
+
const nextSnapshotIdx = Math.max(agentWatermarks.get(key) ?? 0, snapshotIdx);
|
|
157
|
+
if (agentWatermarks.has(key)) {
|
|
158
|
+
// Refresh insertion order so capped-map eviction removes the stalest pair, not an active one.
|
|
159
|
+
agentWatermarks.delete(key);
|
|
160
|
+
}
|
|
161
|
+
agentWatermarks.set(key, nextSnapshotIdx);
|
|
162
|
+
if (agentWatermarks.size > maxWatermarkEntries) {
|
|
163
|
+
const oldest = agentWatermarks.keys().next().value;
|
|
164
|
+
if (oldest !== undefined) {
|
|
165
|
+
agentWatermarks.delete(oldest);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function rememberPreparedTrigger(
|
|
171
|
+
queue: RoomQueue,
|
|
172
|
+
retryKey: string,
|
|
173
|
+
prepared: PreparedTriggerResult,
|
|
174
|
+
): PreparedTriggerResult {
|
|
175
|
+
if (queue.preparedTriggers.has(retryKey)) {
|
|
176
|
+
// Refresh insertion order so capped eviction keeps actively retried events hot.
|
|
177
|
+
queue.preparedTriggers.delete(retryKey);
|
|
178
|
+
}
|
|
179
|
+
queue.preparedTriggers.set(retryKey, prepared);
|
|
180
|
+
if (queue.preparedTriggers.size > maxPreparedTriggerEntries) {
|
|
181
|
+
const oldest = queue.preparedTriggers.keys().next().value;
|
|
182
|
+
if (oldest !== undefined) {
|
|
183
|
+
queue.preparedTriggers.delete(oldest);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return prepared;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function computePendingHistory(
|
|
190
|
+
queue: RoomQueue,
|
|
191
|
+
agentId: string,
|
|
192
|
+
roomId: string,
|
|
193
|
+
limit: number,
|
|
194
|
+
): HistoryEntry[] {
|
|
195
|
+
if (limit <= 0 || queue.entries.length === 0) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const wm = agentWatermarks.get(wmKey(agentId, roomId)) ?? 0;
|
|
199
|
+
// startAbs: the first absolute index the agent hasn't seen yet
|
|
200
|
+
const startAbs = Math.max(wm, queue.baseIndex);
|
|
201
|
+
const startRel = startAbs - queue.baseIndex;
|
|
202
|
+
const available = queue.entries.slice(startRel);
|
|
203
|
+
return available.length > limit ? available.slice(-limit) : available;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
recordPending(roomId, entry) {
|
|
208
|
+
const queue = getOrCreateQueue(roomId);
|
|
209
|
+
appendToQueue(queue, entry);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
getPendingHistory(agentId, roomId, limit) {
|
|
213
|
+
const queue = roomQueues.get(roomId);
|
|
214
|
+
if (!queue) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
return computePendingHistory(queue, agentId, roomId, limit);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
recordTrigger(roomId, entry) {
|
|
221
|
+
const queue = getOrCreateQueue(roomId);
|
|
222
|
+
return appendToQueue(queue, entry);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
prepareTrigger(agentId, roomId, limit, entry) {
|
|
226
|
+
const queue = getOrCreateQueue(roomId);
|
|
227
|
+
const retryKey = preparedTriggerKey(agentId, entry.messageId);
|
|
228
|
+
if (retryKey) {
|
|
229
|
+
const prepared = queue.preparedTriggers.get(retryKey);
|
|
230
|
+
if (prepared) {
|
|
231
|
+
return rememberPreparedTrigger(queue, retryKey, prepared);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const prepared = {
|
|
235
|
+
history: computePendingHistory(queue, agentId, roomId, limit),
|
|
236
|
+
...appendToQueue(queue, entry),
|
|
237
|
+
};
|
|
238
|
+
if (retryKey) {
|
|
239
|
+
return rememberPreparedTrigger(queue, retryKey, prepared);
|
|
240
|
+
}
|
|
241
|
+
return prepared;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
consumeHistory(agentId, roomId, snapshot, messageId) {
|
|
245
|
+
const key = wmKey(agentId, roomId);
|
|
246
|
+
const queue = roomQueues.get(roomId);
|
|
247
|
+
if (!queue) {
|
|
248
|
+
// The room was evicted while this trigger was in flight. Keep eviction authoritative
|
|
249
|
+
// so a late completion cannot recreate a stale watermark against a fresh queue.
|
|
250
|
+
agentWatermarks.delete(key);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (queue.generation !== snapshot.queueGeneration) {
|
|
254
|
+
// The room was evicted and recreated before this trigger completed. Reject the stale
|
|
255
|
+
// snapshot so it cannot advance or erase state for the new queue generation.
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Monotone write: never regress an already-advanced watermark.
|
|
259
|
+
// Guards against out-of-order completion when two triggers for the same
|
|
260
|
+
// (agentId, roomId) are in-flight concurrently.
|
|
261
|
+
rememberWatermark(key, snapshot.snapshotIdx);
|
|
262
|
+
const retryKey = preparedTriggerKey(agentId, messageId);
|
|
263
|
+
if (queue && retryKey) {
|
|
264
|
+
queue.preparedTriggers.delete(retryKey);
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createRoomHistoryTracker(
|
|
271
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
272
|
+
maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES,
|
|
273
|
+
maxWatermarkEntries = MAX_WATERMARK_ENTRIES,
|
|
274
|
+
maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES,
|
|
275
|
+
): RoomHistoryTracker {
|
|
276
|
+
const tracker = createRoomHistoryTrackerInternal(
|
|
277
|
+
maxQueueSize,
|
|
278
|
+
maxRoomQueues,
|
|
279
|
+
maxWatermarkEntries,
|
|
280
|
+
maxPreparedTriggerEntries,
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
recordPending: tracker.recordPending,
|
|
284
|
+
prepareTrigger: tracker.prepareTrigger,
|
|
285
|
+
consumeHistory: tracker.consumeHistory,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function createRoomHistoryTrackerForTests(
|
|
290
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
291
|
+
maxRoomQueues = DEFAULT_MAX_ROOM_QUEUES,
|
|
292
|
+
maxWatermarkEntries = MAX_WATERMARK_ENTRIES,
|
|
293
|
+
maxPreparedTriggerEntries = MAX_PREPARED_TRIGGER_ENTRIES,
|
|
294
|
+
): RoomHistoryTrackerTestApi {
|
|
295
|
+
return createRoomHistoryTrackerInternal(
|
|
296
|
+
maxQueueSize,
|
|
297
|
+
maxRoomQueues,
|
|
298
|
+
maxWatermarkEntries,
|
|
299
|
+
maxPreparedTriggerEntries,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { isMatrixNotFoundError } from "../errors.js";
|
|
2
|
+
import type { MatrixClient } from "../sdk.js";
|
|
3
|
+
|
|
4
|
+
export type MatrixRoomInfo = {
|
|
5
|
+
name?: string;
|
|
6
|
+
canonicalAlias?: string;
|
|
7
|
+
altAliases: string[];
|
|
8
|
+
nameResolved: boolean;
|
|
9
|
+
aliasesResolved: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const MAX_TRACKED_ROOM_INFO = 1024;
|
|
13
|
+
const MAX_TRACKED_MEMBER_DISPLAY_NAMES = 4096;
|
|
14
|
+
|
|
15
|
+
function rememberBounded<T>(map: Map<string, T>, key: string, value: T, maxEntries: number): void {
|
|
16
|
+
map.set(key, value);
|
|
17
|
+
if (map.size > maxEntries) {
|
|
18
|
+
const oldest = map.keys().next().value;
|
|
19
|
+
if (typeof oldest === "string") {
|
|
20
|
+
map.delete(oldest);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createMatrixRoomInfoResolver(client: MatrixClient) {
|
|
26
|
+
const roomNameCache = new Map<string, Pick<MatrixRoomInfo, "name" | "nameResolved">>();
|
|
27
|
+
const roomAliasCache = new Map<
|
|
28
|
+
string,
|
|
29
|
+
Pick<MatrixRoomInfo, "canonicalAlias" | "altAliases" | "aliasesResolved">
|
|
30
|
+
>();
|
|
31
|
+
const memberDisplayNameCache = new Map<string, string>();
|
|
32
|
+
|
|
33
|
+
const getRoomName = async (
|
|
34
|
+
roomId: string,
|
|
35
|
+
): Promise<Pick<MatrixRoomInfo, "name" | "nameResolved">> => {
|
|
36
|
+
if (roomNameCache.has(roomId)) {
|
|
37
|
+
return roomNameCache.get(roomId) ?? { nameResolved: false };
|
|
38
|
+
}
|
|
39
|
+
let name: string | undefined;
|
|
40
|
+
let nameResolved = false;
|
|
41
|
+
try {
|
|
42
|
+
const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "");
|
|
43
|
+
nameResolved = true;
|
|
44
|
+
if (nameState && typeof nameState.name === "string") {
|
|
45
|
+
name = nameState.name;
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (isMatrixNotFoundError(err)) {
|
|
49
|
+
nameResolved = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const info = { name, nameResolved };
|
|
53
|
+
if (nameResolved) {
|
|
54
|
+
rememberBounded(roomNameCache, roomId, info, MAX_TRACKED_ROOM_INFO);
|
|
55
|
+
}
|
|
56
|
+
return info;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getRoomAliases = async (
|
|
60
|
+
roomId: string,
|
|
61
|
+
): Promise<Pick<MatrixRoomInfo, "canonicalAlias" | "altAliases" | "aliasesResolved">> => {
|
|
62
|
+
const cached = roomAliasCache.get(roomId);
|
|
63
|
+
if (cached) {
|
|
64
|
+
return cached;
|
|
65
|
+
}
|
|
66
|
+
let canonicalAlias: string | undefined;
|
|
67
|
+
let altAliases: string[] = [];
|
|
68
|
+
let aliasesResolved = false;
|
|
69
|
+
try {
|
|
70
|
+
const aliasState = await client.getRoomStateEvent(roomId, "m.room.canonical_alias", "");
|
|
71
|
+
aliasesResolved = true;
|
|
72
|
+
if (aliasState && typeof aliasState.alias === "string") {
|
|
73
|
+
canonicalAlias = aliasState.alias;
|
|
74
|
+
}
|
|
75
|
+
const rawAliases = aliasState?.alt_aliases;
|
|
76
|
+
if (Array.isArray(rawAliases)) {
|
|
77
|
+
altAliases = rawAliases.filter((entry): entry is string => typeof entry === "string");
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (isMatrixNotFoundError(err)) {
|
|
81
|
+
aliasesResolved = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const info = { canonicalAlias, altAliases, aliasesResolved };
|
|
85
|
+
if (aliasesResolved) {
|
|
86
|
+
rememberBounded(roomAliasCache, roomId, info, MAX_TRACKED_ROOM_INFO);
|
|
87
|
+
}
|
|
88
|
+
return info;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getRoomInfo = async (
|
|
92
|
+
roomId: string,
|
|
93
|
+
opts: { includeAliases?: boolean } = {},
|
|
94
|
+
): Promise<MatrixRoomInfo> => {
|
|
95
|
+
const { name, nameResolved } = await getRoomName(roomId);
|
|
96
|
+
if (!opts.includeAliases) {
|
|
97
|
+
return { name, altAliases: [], nameResolved, aliasesResolved: false };
|
|
98
|
+
}
|
|
99
|
+
const aliases = await getRoomAliases(roomId);
|
|
100
|
+
return { name, nameResolved, ...aliases };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const getMemberDisplayName = async (roomId: string, userId: string): Promise<string> => {
|
|
104
|
+
const cacheKey = `${roomId}:${userId}`;
|
|
105
|
+
if (memberDisplayNameCache.has(cacheKey)) {
|
|
106
|
+
return memberDisplayNameCache.get(cacheKey) ?? userId;
|
|
107
|
+
}
|
|
108
|
+
const memberState = await client
|
|
109
|
+
.getRoomStateEvent(roomId, "m.room.member", userId)
|
|
110
|
+
.catch(() => null);
|
|
111
|
+
const displayName =
|
|
112
|
+
memberState && typeof memberState.displayname === "string" ? memberState.displayname : userId;
|
|
113
|
+
rememberBounded(
|
|
114
|
+
memberDisplayNameCache,
|
|
115
|
+
cacheKey,
|
|
116
|
+
displayName,
|
|
117
|
+
MAX_TRACKED_MEMBER_DISPLAY_NAMES,
|
|
118
|
+
);
|
|
119
|
+
return displayName;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
getRoomInfo,
|
|
124
|
+
getMemberDisplayName,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { MatrixRoomConfig } from "../../types.js";
|
|
2
|
+
import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "./runtime-api.js";
|
|
3
|
+
|
|
4
|
+
type MatrixRoomConfigResolved = {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
allowlistConfigured: boolean;
|
|
7
|
+
config?: MatrixRoomConfig;
|
|
8
|
+
matchKey?: string;
|
|
9
|
+
matchSource?: "direct" | "wildcard";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function readLegacyRoomAllowAlias(room: MatrixRoomConfig | undefined): boolean | undefined {
|
|
13
|
+
const rawRoom = room as Record<string, unknown> | undefined;
|
|
14
|
+
return typeof rawRoom?.allow === "boolean" ? rawRoom.allow : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveMatrixRoomConfig(params: {
|
|
18
|
+
rooms?: Record<string, MatrixRoomConfig>;
|
|
19
|
+
roomId: string;
|
|
20
|
+
aliases: string[];
|
|
21
|
+
}): MatrixRoomConfigResolved {
|
|
22
|
+
const rooms = params.rooms ?? {};
|
|
23
|
+
const keys = Object.keys(rooms);
|
|
24
|
+
const allowlistConfigured = keys.length > 0;
|
|
25
|
+
const candidates = buildChannelKeyCandidates(
|
|
26
|
+
params.roomId,
|
|
27
|
+
`room:${params.roomId}`,
|
|
28
|
+
...params.aliases,
|
|
29
|
+
);
|
|
30
|
+
const {
|
|
31
|
+
entry: matched,
|
|
32
|
+
key: matchedKey,
|
|
33
|
+
wildcardEntry,
|
|
34
|
+
wildcardKey,
|
|
35
|
+
} = resolveChannelEntryMatch({
|
|
36
|
+
entries: rooms,
|
|
37
|
+
keys: candidates,
|
|
38
|
+
wildcardKey: "*",
|
|
39
|
+
});
|
|
40
|
+
const resolved = matched ?? wildcardEntry;
|
|
41
|
+
const legacyAllow = readLegacyRoomAllowAlias(resolved);
|
|
42
|
+
const allowed = resolved ? resolved.enabled !== false && legacyAllow !== false : false;
|
|
43
|
+
const matchKey = matchedKey ?? wildcardKey;
|
|
44
|
+
const matchSource = matched ? "direct" : wildcardEntry ? "wildcard" : undefined;
|
|
45
|
+
return {
|
|
46
|
+
allowed,
|
|
47
|
+
allowlistConfigured,
|
|
48
|
+
config: resolved,
|
|
49
|
+
matchKey,
|
|
50
|
+
matchSource,
|
|
51
|
+
};
|
|
52
|
+
}
|