@openclaw/matrix 2026.7.2-beta.7 → 2026.8.1-beta.3
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/dist/{account-selection-kqquRzCf.js → account-selection-CWuhuFn1.js} +7 -8
- package/dist/{accounts-DfngqebB.js → accounts-BCzyubQt.js} +3 -3
- package/dist/api.js +8 -11
- package/dist/{approval-handler.runtime-DSeaIsGa.js → approval-handler.runtime-CSvueU9P.js} +7 -6
- package/dist/{approval-ids-BdJGPAJT.js → approval-ids-CEcf-EiM.js} +1 -1
- package/dist/{approval-reaction-auth-BjOpELxu.js → approval-reaction-auth-CziKXatm.js} +2 -2
- package/dist/{approval-reactions-D-BxBK_L.js → approval-reactions-tXh9KYrW.js} +2 -8
- package/dist/auth-presence.js +2 -2
- package/dist/{channel-vj788dNZ.js → channel-CzQw5OZN.js} +376 -148
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.runtime-CLoF2S9T.js → channel.runtime-I9pBRQb3.js} +79 -55
- package/dist/{config-schema-Djxw3C6b.js → channel.setup-aNaa3Uyn.js} +51 -10
- package/dist/{cli-DMqfDE-q.js → cli-iR6QJ5U0.js} +16 -16
- package/dist/{cli-metadata-C7D53SND.js → cli-metadata-BmUYWjuM.js} +1 -1
- package/dist/cli-metadata.js +1 -1
- package/dist/client-BBQdBG7t.js +799 -0
- package/dist/{client-6eRENr7k.js → client-D8XghqmX.js} +4 -4
- package/dist/{client-bootstrap-XsIkHNt0.js → client-bootstrap-Co3RlItb.js} +25 -61
- package/dist/client-support-DLj4vDeu.js +93 -0
- package/dist/config-update-Bor_Pjsv.js +150 -0
- package/dist/contract-api.js +2 -2
- package/dist/{credentials-W2EmwRhu.js → credentials-BdAaWuiR.js} +2 -1
- package/dist/credentials-read-B37cQknZ.js +51 -0
- package/dist/credentials-state-B3cuUFwZ.js +27 -0
- package/dist/{credentials-write.runtime-Be9zNSUw.js → credentials-write.runtime-nAV2-5uR.js} +1 -1
- package/dist/{crypto-runtime-D9_6J5tZ.js → crypto-runtime-Bnz3zEcS.js} +55 -32
- package/dist/direct-room-RinHjtqX.js +71 -0
- package/dist/{directory-live-C8dVBiCc.js → directory-live-DLQzel12.js} +2 -3
- package/dist/{doctor-CM8ijNpW.js → doctor-EWf9CDso.js} +2 -2
- package/dist/{doctor-contract-BMSJkkAE.js → doctor-contract-DEt4O4Zt.js} +2 -3
- package/dist/doctor-contract-api.js +209 -91
- package/dist/{draft-stream-CtvOXq0Z.js → draft-stream-DQ7QNC-l.js} +20 -23
- package/dist/{encryption-guidance-CbEFe38F.js → encryption-guidance-DmLY8NiO.js} +1 -1
- package/dist/{env-auth-CQsYbz-p.js → env-auth-MVS-Vv41.js} +4 -3
- package/dist/{env-vars-B0NxQnPk.js → env-vars-dGqak9dN.js} +1 -1
- package/dist/{errors-C47hvAF8.js → errors-DKVS3_DO.js} +2 -5
- package/dist/{file-sync-store-DiiFsayX.js → file-sync-store-DjskBX2o.js} +24 -3
- package/dist/helper-api.js +2 -2
- package/dist/{idb-persistence-B6x7dMsF.js → idb-persistence-BM0P27Zg.js} +3 -2
- package/dist/{inbound-dedupe-CaBLblef.js → inbound-dedupe-RvSLqdsz.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{logger-D0GCSDQq.js → logger-B_71hN6W.js} +1 -1
- package/dist/{logging-DV-p52N0.js → logging-DRjvUOIh.js} +1 -1
- package/dist/{media-text-BTwzADS_.js → media-text-Dh-DpTc3.js} +4 -1
- package/dist/{messages-DxQJkhxs.js → messages-DEqg7HVy.js} +31 -11
- package/dist/{monitor-VyWlSLwo.js → monitor-FNrfkuWL.js} +413 -405
- package/dist/plugin-entry.handlers.runtime.js +3 -3
- package/dist/probe.runtime-NIJZYqc2.js +2 -0
- package/dist/profile-D-Wubo0A.js +108 -0
- package/dist/{profile-update-CffmyW_4.js → profile-update-BQlE5s2J.js} +3 -2
- package/dist/{reaction-events-B00WSi98.js → reaction-events-BKOLlnCo.js} +10 -7
- package/dist/{recovery-key-store-BNnHvXdk.js → recovery-key-store-CognuDys.js} +5 -4
- package/dist/{resolve-targets-nm2L48sh.js → resolve-targets-e7AI0P75.js} +2 -2
- package/dist/{resolver.runtime-DS6uPaEe.js → resolver.runtime-V8u0pHEb.js} +1 -1
- package/dist/{room-info-CAFZ3Z9J.js → room-info-BXQA7y10.js} +12 -3
- package/dist/runtime-api.js +7 -4
- package/dist/{sdk-bUytQHx7.js → sdk-NxK3vzMa.js} +242 -119
- package/dist/{secret-contract-M3hRmGO-.js → secret-contract-CufozuyJ.js} +1 -1
- package/dist/secret-contract-api.js +1 -1
- package/dist/{send-BldGYG8g.js → send-ClHgDZLT.js} +1477 -1210
- package/dist/session-binding-contract-api.js +2 -3
- package/dist/{setup-bootstrap-Deleiibz.js → setup-bootstrap-BGPjbA5N.js} +4 -4
- package/dist/{setup-core-BVfBv2ai.js → setup-core-DigrLebY.js} +17 -266
- package/dist/setup-plugin-api.js +1 -44
- package/dist/{setup-surface-BBq3WeqX.js → setup-surface-Dtn8sAWs.js} +15 -16
- package/dist/{startup-verification-wm3_i88s.js → startup-verification-Cs08ca8u.js} +3 -3
- package/dist/{storage-BvAkgzvT.js → storage-BsGuHngK.js} +3 -3
- package/dist/subagent-hooks-CMoZaARG.js +48 -0
- package/dist/subagent-hooks-api.js +1 -1
- package/dist/{target-ids-B-5aQxwn.js → target-ids-Nwx7cMVE.js} +11 -1
- package/dist/{thread-binding-api-DRbBZlhS.js → thread-binding-api-CzkeLZCU.js} +1 -1
- package/dist/thread-binding-api.js +1 -1
- package/dist/{thread-bindings-DGbl3vDW.js → thread-bindings-BYEGfw3h.js} +17 -10
- package/dist/thread-bindings-runtime.js +1 -1
- package/dist/{thread-bindings-shared-CKnY4LSd.js → thread-bindings-shared-oNPiyMWY.js} +1 -6
- package/dist/{tool-actions.runtime-uu9blSqO.js → tool-actions.runtime-ck6IJyP0.js} +36 -19
- package/dist/{verification-DYMQ1QpI.js → verification-CabtBTtB.js} +2 -2
- package/openclaw.plugin.json +61 -1
- package/package.json +4 -4
- package/dist/active-client-DXvz2gCo.js +0 -20
- package/dist/client-Dbso3-0V.js +0 -30
- package/dist/create-client-BmSU-q-D.js +0 -61
- package/dist/credentials-read-DVmFCm7X.js +0 -77
- package/dist/exec-approval-resolver-BCgqRtZS.js +0 -16
- package/dist/preflight-audio.runtime-BZxxQsxY.js +0 -11
- package/dist/probe.runtime-BuieqmS3.js +0 -3
- package/dist/record-shared-CWWx6fvI.js +0 -2
- package/dist/route-DY6at4qJ.js +0 -161
- package/dist/shared-CUMWxcMz.js +0 -673
- package/dist/subagent-hooks-dkPAF6Et.js +0 -143
- package/dist/sync-state-CWbp0QSY.js +0 -12
- package/dist/{runtime-api-D1xI_cvh.js → runtime-api-BGO16Fu8.js} +3 -3
|
@@ -1,249 +1,834 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
2
|
import { a as resolveMatrixAccountConfig } from "./account-config-BZWN9tGS.js";
|
|
3
3
|
import { t as getMatrixRuntime } from "./runtime-6S3DNFNv.js";
|
|
4
|
-
import {
|
|
4
|
+
import { i as normalizeMatrixResolvableTarget, t as isMatrixQualifiedUserId } from "./target-ids-Nwx7cMVE.js";
|
|
5
5
|
import { n as MATRIX_REACTION_EVENT_TYPE, r as buildMatrixReactionContent, t as MATRIX_ANNOTATION_RELATION_TYPE } from "./reaction-common-DkrQdBSZ.js";
|
|
6
|
+
import { a as isStrictDirectRoom, r as inspectMatrixDirectRoomEvidence } from "./direct-room-RinHjtqX.js";
|
|
6
7
|
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
7
8
|
import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
|
|
8
|
-
import {
|
|
9
|
+
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
9
10
|
import { FormatCapabilityProfile, convertMarkdownTables, findCodeRegions, isInsideCode, markdownToIR, renderMarkdownWithMarkers, tokenizeHtmlTags } from "openclaw/plugin-sdk/text-chunking";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
10
13
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
14
|
+
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
11
15
|
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
12
16
|
import { normalizePollInput } from "openclaw/plugin-sdk/poll-runtime";
|
|
13
17
|
import MarkdownIt from "markdown-it";
|
|
14
18
|
import { isAutoLinkedFileRef } from "openclaw/plugin-sdk/text-autolink-runtime";
|
|
15
19
|
import { isVoiceMessageCompatibleAudio } from "openclaw/plugin-sdk/media-runtime";
|
|
16
20
|
import { parseBuffer } from "music-metadata";
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
//#region extensions/matrix/src/matrix/send/client.ts
|
|
22
|
+
const loadMatrixSendClientRuntime = createLazyRuntimeModule(() => import("./client-bootstrap-Co3RlItb.js").then((n) => n.t));
|
|
23
|
+
function resolveMediaMaxBytes(accountId, cfg) {
|
|
24
|
+
if (!cfg) throw new Error("Matrix media limits requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.");
|
|
25
|
+
const mediaMaxMb = resolveMatrixAccountConfig({
|
|
26
|
+
cfg: requireRuntimeConfig(cfg, "Matrix media limits"),
|
|
27
|
+
accountId
|
|
28
|
+
}).mediaMaxMb;
|
|
29
|
+
return typeof mediaMaxMb === "number" && mediaMaxMb > 0 ? mediaMaxMb * 1024 * 1024 : void 0;
|
|
30
|
+
}
|
|
31
|
+
async function withResolvedMatrixSendClient(opts, run) {
|
|
32
|
+
return await withResolvedMatrixClient({
|
|
33
|
+
...opts,
|
|
34
|
+
readiness: "started"
|
|
35
|
+
}, run, "persist");
|
|
36
|
+
}
|
|
37
|
+
async function withResolvedMatrixControlClient(opts, run) {
|
|
38
|
+
return await withResolvedMatrixClient({
|
|
39
|
+
...opts,
|
|
40
|
+
readiness: "none"
|
|
41
|
+
}, run);
|
|
42
|
+
}
|
|
43
|
+
async function withResolvedMatrixClient(opts, run, shutdownBehavior) {
|
|
44
|
+
if (opts.client) return await run(opts.client);
|
|
45
|
+
const { withResolvedRuntimeMatrixClient } = await loadMatrixSendClientRuntime();
|
|
46
|
+
return await withResolvedRuntimeMatrixClient(opts, run, shutdownBehavior);
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region extensions/matrix/src/matrix/send/types.ts
|
|
50
|
+
const MsgType = {
|
|
51
|
+
Text: "m.text",
|
|
52
|
+
Image: "m.image",
|
|
53
|
+
Audio: "m.audio",
|
|
54
|
+
Video: "m.video",
|
|
55
|
+
File: "m.file",
|
|
56
|
+
Notice: "m.notice"
|
|
57
|
+
};
|
|
58
|
+
const RelationType = {
|
|
59
|
+
Annotation: MATRIX_ANNOTATION_RELATION_TYPE,
|
|
60
|
+
Replace: "m.replace",
|
|
61
|
+
Thread: "m.thread"
|
|
62
|
+
};
|
|
63
|
+
const EventType = {
|
|
64
|
+
Direct: "m.direct",
|
|
65
|
+
Reaction: MATRIX_REACTION_EVENT_TYPE,
|
|
66
|
+
RoomMessage: "m.room.message"
|
|
67
|
+
};
|
|
68
|
+
const MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY = "com.openclaw.finalized_preview";
|
|
19
69
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
70
|
+
* MSC4357 live marker key.
|
|
71
|
+
* When present on event content, signals that the message is still being
|
|
72
|
+
* streamed (e.g. an LLM generating a response). Supporting clients render
|
|
73
|
+
* the message with a streaming animation until an edit without this marker
|
|
74
|
+
* arrives, indicating the stream is complete.
|
|
75
|
+
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/4357
|
|
26
76
|
*/
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return POLL_START_TYPES.includes(eventType);
|
|
77
|
+
const MSC4357_LIVE_KEY = "org.matrix.msc4357.live";
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region extensions/matrix/src/matrix/direct-management.ts
|
|
80
|
+
var direct_management_exports = /* @__PURE__ */ __exportAll({
|
|
81
|
+
inspectMatrixDirectRooms: () => inspectMatrixDirectRooms,
|
|
82
|
+
persistMatrixDirectRoomMapping: () => persistMatrixDirectRoomMapping,
|
|
83
|
+
promoteMatrixDirectRoomCandidate: () => promoteMatrixDirectRoomCandidate,
|
|
84
|
+
repairMatrixDirectRooms: () => repairMatrixDirectRooms
|
|
85
|
+
});
|
|
86
|
+
const DIRECT_ACCOUNT_DATA_QUEUE_KEY = EventType.Direct;
|
|
87
|
+
const directAccountDataWriteQueues = /* @__PURE__ */ new WeakMap();
|
|
88
|
+
async function readMatrixDirectAccountData(client) {
|
|
89
|
+
try {
|
|
90
|
+
const direct = await client.getAccountData(EventType.Direct);
|
|
91
|
+
return direct && typeof direct === "object" && !Array.isArray(direct) ? direct : {};
|
|
92
|
+
} catch {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
46
95
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
96
|
+
function normalizeRemoteUserId(remoteUserId) {
|
|
97
|
+
const normalized = normalizeOptionalString(remoteUserId) ?? "";
|
|
98
|
+
if (!isMatrixQualifiedUserId(normalized)) throw new Error(`Matrix user IDs must be fully qualified (got "${remoteUserId}")`);
|
|
99
|
+
return normalized;
|
|
49
100
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
101
|
+
function normalizeMappedRoomIds(direct, remoteUserId) {
|
|
102
|
+
const current = direct[remoteUserId];
|
|
103
|
+
if (!Array.isArray(current)) return [];
|
|
104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
105
|
+
const normalized = [];
|
|
106
|
+
for (const value of current) {
|
|
107
|
+
const roomId = normalizeOptionalString(value) ?? "";
|
|
108
|
+
if (!roomId || seen.has(roomId)) continue;
|
|
109
|
+
seen.add(roomId);
|
|
110
|
+
normalized.push(roomId);
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
52
113
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
114
|
+
function normalizeRoomIdList(values) {
|
|
115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
116
|
+
const normalized = [];
|
|
117
|
+
for (const value of values) {
|
|
118
|
+
const roomId = value.trim();
|
|
119
|
+
if (!roomId || seen.has(roomId)) continue;
|
|
120
|
+
seen.add(roomId);
|
|
121
|
+
normalized.push(roomId);
|
|
122
|
+
}
|
|
123
|
+
return normalized;
|
|
55
124
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
125
|
+
function hasMatrixDirectRoomMappings(params) {
|
|
126
|
+
const current = normalizeMappedRoomIds(params.directContent, params.remoteUserId);
|
|
127
|
+
const next = normalizeRoomIdList([...params.roomIds, ...current]);
|
|
128
|
+
return current.length === next.length && current.every((roomId, index) => roomId === next[index]);
|
|
59
129
|
}
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
130
|
+
function resolveDirectAccountDataWriteQueue(client) {
|
|
131
|
+
const existing = directAccountDataWriteQueues.get(client);
|
|
132
|
+
if (existing) return existing;
|
|
133
|
+
const created = new KeyedAsyncQueue();
|
|
134
|
+
directAccountDataWriteQueues.set(client, created);
|
|
135
|
+
return created;
|
|
136
|
+
}
|
|
137
|
+
async function writeMatrixDirectRoomMappings(params) {
|
|
138
|
+
return await resolveDirectAccountDataWriteQueue(params.client).enqueue(DIRECT_ACCOUNT_DATA_QUEUE_KEY, async () => {
|
|
139
|
+
const directContentBefore = await readMatrixDirectAccountData(params.client);
|
|
140
|
+
const directContentAfter = buildNextDirectContent({
|
|
141
|
+
directContent: directContentBefore,
|
|
142
|
+
remoteUserId: params.remoteUserId,
|
|
143
|
+
roomIds: params.roomIds
|
|
144
|
+
});
|
|
145
|
+
const changed = !hasMatrixDirectRoomMappings({
|
|
146
|
+
directContent: directContentBefore,
|
|
147
|
+
remoteUserId: params.remoteUserId,
|
|
148
|
+
roomIds: params.roomIds
|
|
149
|
+
});
|
|
150
|
+
if (changed) await params.client.setAccountData(EventType.Direct, directContentAfter);
|
|
151
|
+
return {
|
|
152
|
+
changed,
|
|
153
|
+
directContentBefore,
|
|
154
|
+
directContentAfter
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async function classifyDirectRoomCandidate(params) {
|
|
159
|
+
const evidence = await inspectMatrixDirectRoomEvidence({
|
|
160
|
+
client: params.client,
|
|
161
|
+
roomId: params.roomId,
|
|
162
|
+
remoteUserId: params.remoteUserId,
|
|
163
|
+
selfUserId: params.selfUserId
|
|
164
|
+
});
|
|
72
165
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
166
|
+
roomId: params.roomId,
|
|
167
|
+
joinedMembers: evidence.joinedMembers,
|
|
168
|
+
strict: evidence.strict && (params.source === "account-data" || evidence.memberStateFlag !== false),
|
|
169
|
+
explicit: evidence.strict && (params.source === "account-data" || evidence.memberStateFlag !== false) && (params.source === "account-data" || evidence.viaMemberState),
|
|
170
|
+
source: params.source
|
|
77
171
|
};
|
|
78
172
|
}
|
|
79
|
-
function
|
|
80
|
-
const
|
|
81
|
-
|
|
173
|
+
function buildNextDirectContent(params) {
|
|
174
|
+
const current = normalizeMappedRoomIds(params.directContent, params.remoteUserId);
|
|
175
|
+
const nextRooms = normalizeRoomIdList([...params.roomIds, ...current]);
|
|
82
176
|
return {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
sender: "",
|
|
86
|
-
senderName: "",
|
|
87
|
-
question: parsed.question,
|
|
88
|
-
answers: parsed.answers.map((answer) => answer.text),
|
|
89
|
-
kind: parsed.kind,
|
|
90
|
-
maxSelections: parsed.maxSelections
|
|
177
|
+
...params.directContent,
|
|
178
|
+
[params.remoteUserId]: nextRooms
|
|
91
179
|
};
|
|
92
180
|
}
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
function resolvePollReferenceEventId(content) {
|
|
102
|
-
if (!content || typeof content !== "object") return null;
|
|
103
|
-
const relates = content["m.relates_to"];
|
|
104
|
-
if (!relates || typeof relates.event_id !== "string") return null;
|
|
105
|
-
const eventId = relates.event_id.trim();
|
|
106
|
-
return eventId.length > 0 ? eventId : null;
|
|
107
|
-
}
|
|
108
|
-
function parsePollResponseAnswerIds(content) {
|
|
109
|
-
if (!content || typeof content !== "object") return null;
|
|
110
|
-
const response = content[M_POLL_RESPONSE] ?? content[ORG_POLL_RESPONSE];
|
|
111
|
-
if (!response || !Array.isArray(response.answers)) return null;
|
|
112
|
-
return response.answers.filter((answer) => typeof answer === "string");
|
|
181
|
+
async function persistMatrixDirectRoomMapping(params) {
|
|
182
|
+
const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
|
|
183
|
+
return (await writeMatrixDirectRoomMappings({
|
|
184
|
+
client: params.client,
|
|
185
|
+
remoteUserId,
|
|
186
|
+
roomIds: [params.roomId]
|
|
187
|
+
})).changed;
|
|
113
188
|
}
|
|
114
|
-
function
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (event.sender !== params.sender) continue;
|
|
122
|
-
const ts = typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts) ? event.origin_server_ts : Number.POSITIVE_INFINITY;
|
|
123
|
-
if (ts < pollClosedAt) pollClosedAt = ts;
|
|
124
|
-
}
|
|
125
|
-
const answerIds = new Set(parsed.answers.map((answer) => answer.id));
|
|
126
|
-
const latestVoteBySender = /* @__PURE__ */ new Map();
|
|
127
|
-
const orderedRelationEvents = [...params.relationEvents].toSorted((left, right) => {
|
|
128
|
-
const leftTs = typeof left.origin_server_ts === "number" && Number.isFinite(left.origin_server_ts) ? left.origin_server_ts : Number.POSITIVE_INFINITY;
|
|
129
|
-
const rightTs = typeof right.origin_server_ts === "number" && Number.isFinite(right.origin_server_ts) ? right.origin_server_ts : Number.POSITIVE_INFINITY;
|
|
130
|
-
if (leftTs !== rightTs) return leftTs - rightTs;
|
|
131
|
-
return (left.event_id ?? "").localeCompare(right.event_id ?? "");
|
|
189
|
+
async function promoteMatrixDirectRoomCandidate(params) {
|
|
190
|
+
const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
|
|
191
|
+
const evidence = await inspectMatrixDirectRoomEvidence({
|
|
192
|
+
client: params.client,
|
|
193
|
+
roomId: params.roomId,
|
|
194
|
+
remoteUserId,
|
|
195
|
+
selfUserId: params.selfUserId
|
|
132
196
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
197
|
+
if (!evidence.strict) return {
|
|
198
|
+
classifyAsDirect: false,
|
|
199
|
+
repaired: false,
|
|
200
|
+
reason: "not-strict"
|
|
201
|
+
};
|
|
202
|
+
if (evidence.memberStateFlag === false) return {
|
|
203
|
+
classifyAsDirect: false,
|
|
204
|
+
repaired: false,
|
|
205
|
+
reason: "local-explicit-false"
|
|
206
|
+
};
|
|
207
|
+
try {
|
|
208
|
+
const repaired = await persistMatrixDirectRoomMapping({
|
|
209
|
+
client: params.client,
|
|
210
|
+
remoteUserId,
|
|
211
|
+
roomId: params.roomId
|
|
146
212
|
});
|
|
213
|
+
return {
|
|
214
|
+
classifyAsDirect: true,
|
|
215
|
+
repaired,
|
|
216
|
+
roomId: params.roomId,
|
|
217
|
+
reason: repaired ? "promoted" : "already-mapped"
|
|
218
|
+
};
|
|
219
|
+
} catch {
|
|
220
|
+
return {
|
|
221
|
+
classifyAsDirect: true,
|
|
222
|
+
repaired: false,
|
|
223
|
+
roomId: params.roomId,
|
|
224
|
+
reason: "repair-failed"
|
|
225
|
+
};
|
|
147
226
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
227
|
+
}
|
|
228
|
+
async function inspectMatrixDirectRooms(params) {
|
|
229
|
+
const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
|
|
230
|
+
const selfUserId = normalizeOptionalString(await params.client.getUserId().catch(() => null)) ?? null;
|
|
231
|
+
const mappedRoomIds = normalizeMappedRoomIds(await readMatrixDirectAccountData(params.client), remoteUserId);
|
|
232
|
+
const mappedRooms = await Promise.all(mappedRoomIds.map(async (roomId) => await classifyDirectRoomCandidate({
|
|
233
|
+
client: params.client,
|
|
234
|
+
roomId,
|
|
235
|
+
remoteUserId,
|
|
236
|
+
selfUserId,
|
|
237
|
+
source: "account-data"
|
|
238
|
+
})));
|
|
239
|
+
const mappedStrict = mappedRooms.find((room) => room.strict);
|
|
240
|
+
let joinedRooms = [];
|
|
241
|
+
if (typeof params.client.getJoinedRooms === "function") try {
|
|
242
|
+
const resolved = await params.client.getJoinedRooms();
|
|
243
|
+
joinedRooms = Array.isArray(resolved) ? resolved : [];
|
|
244
|
+
} catch {
|
|
245
|
+
joinedRooms = [];
|
|
246
|
+
}
|
|
247
|
+
const discoveredStrictRooms = [];
|
|
248
|
+
for (const roomId of normalizeRoomIdList(joinedRooms)) {
|
|
249
|
+
if (mappedRoomIds.includes(roomId)) continue;
|
|
250
|
+
const candidate = await classifyDirectRoomCandidate({
|
|
251
|
+
client: params.client,
|
|
252
|
+
roomId,
|
|
253
|
+
remoteUserId,
|
|
254
|
+
selfUserId,
|
|
255
|
+
source: "joined"
|
|
256
|
+
});
|
|
257
|
+
if (candidate.strict) discoveredStrictRooms.push(candidate);
|
|
154
258
|
}
|
|
259
|
+
const discoveredStrictRoomIds = discoveredStrictRooms.map((room) => room.roomId);
|
|
260
|
+
const discoveredExplicit = discoveredStrictRooms.find((room) => room.explicit);
|
|
155
261
|
return {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
kind: parsed.kind,
|
|
163
|
-
maxSelections: parsed.maxSelections,
|
|
164
|
-
entries: parsed.answers.map((answer) => ({
|
|
165
|
-
id: answer.id,
|
|
166
|
-
text: answer.text,
|
|
167
|
-
votes: voteCounts.get(answer.id) ?? 0
|
|
168
|
-
})),
|
|
169
|
-
totalVotes,
|
|
170
|
-
closed: Number.isFinite(pollClosedAt)
|
|
262
|
+
selfUserId,
|
|
263
|
+
remoteUserId,
|
|
264
|
+
mappedRoomIds,
|
|
265
|
+
mappedRooms,
|
|
266
|
+
discoveredStrictRoomIds,
|
|
267
|
+
activeRoomId: mappedStrict?.roomId ?? discoveredExplicit?.roomId ?? discoveredStrictRoomIds[0] ?? null
|
|
171
268
|
};
|
|
172
269
|
}
|
|
173
|
-
function
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
lines.push("");
|
|
188
|
-
if (!revealResults) lines.push("Responses are hidden until the poll closes.");
|
|
189
|
-
else lines.push(`Total voters: ${summary.totalVotes}`);
|
|
190
|
-
return lines.join("\n");
|
|
191
|
-
}
|
|
192
|
-
function buildTextContent$1(body) {
|
|
270
|
+
async function repairMatrixDirectRooms(params) {
|
|
271
|
+
const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
|
|
272
|
+
const inspected = await inspectMatrixDirectRooms({
|
|
273
|
+
client: params.client,
|
|
274
|
+
remoteUserId
|
|
275
|
+
});
|
|
276
|
+
const activeRoomId = inspected.activeRoomId ?? await params.client.createDirectRoom(remoteUserId, { encrypted: params.encrypted === true });
|
|
277
|
+
const createdRoomId = inspected.activeRoomId ? null : activeRoomId;
|
|
278
|
+
const mappingWrite = await writeMatrixDirectRoomMappings({
|
|
279
|
+
client: params.client,
|
|
280
|
+
remoteUserId,
|
|
281
|
+
roomIds: [activeRoomId, ...inspected.discoveredStrictRoomIds]
|
|
282
|
+
});
|
|
193
283
|
return {
|
|
194
|
-
|
|
195
|
-
|
|
284
|
+
...inspected,
|
|
285
|
+
activeRoomId,
|
|
286
|
+
createdRoomId,
|
|
287
|
+
changed: mappingWrite.changed,
|
|
288
|
+
directContentBefore: mappingWrite.directContentBefore,
|
|
289
|
+
directContentAfter: mappingWrite.directContentAfter
|
|
196
290
|
};
|
|
197
291
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region extensions/matrix/src/matrix/send/targets.ts
|
|
294
|
+
function normalizeTarget(raw) {
|
|
295
|
+
const trimmed = raw.trim();
|
|
296
|
+
if (!trimmed) throw new Error("Matrix target is required (room:<id> or #alias)");
|
|
297
|
+
return trimmed;
|
|
201
298
|
}
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
const answers = normalized.options.map((option, idx) => ({
|
|
205
|
-
id: `answer${idx + 1}`,
|
|
206
|
-
...buildTextContent$1(option)
|
|
207
|
-
}));
|
|
208
|
-
const isMultiple = normalized.maxSelections > 1;
|
|
209
|
-
const fallbackText = buildPollFallbackText(normalized.question, answers.map((answer) => getTextContent(answer)));
|
|
210
|
-
return {
|
|
211
|
-
[M_POLL_START]: {
|
|
212
|
-
question: buildTextContent$1(normalized.question),
|
|
213
|
-
kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed",
|
|
214
|
-
max_selections: normalized.maxSelections,
|
|
215
|
-
answers
|
|
216
|
-
},
|
|
217
|
-
"m.text": fallbackText,
|
|
218
|
-
"org.matrix.msc1767.text": fallbackText
|
|
219
|
-
};
|
|
299
|
+
function normalizeThreadId(raw) {
|
|
300
|
+
return normalizeOptionalStringifiedId(raw) ?? null;
|
|
220
301
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
302
|
+
const MAX_DIRECT_ROOM_CACHE_SIZE = 1024;
|
|
303
|
+
const directRoomCacheByClient = /* @__PURE__ */ new WeakMap();
|
|
304
|
+
function resolveDirectRoomCache(client) {
|
|
305
|
+
const existing = directRoomCacheByClient.get(client);
|
|
306
|
+
if (existing) return existing;
|
|
307
|
+
const created = /* @__PURE__ */ new Map();
|
|
308
|
+
directRoomCacheByClient.set(client, created);
|
|
309
|
+
return created;
|
|
310
|
+
}
|
|
311
|
+
function setDirectRoomCached(client, key, value) {
|
|
312
|
+
const directRoomCache = resolveDirectRoomCache(client);
|
|
313
|
+
directRoomCache.set(key, value);
|
|
314
|
+
if (directRoomCache.size > MAX_DIRECT_ROOM_CACHE_SIZE) {
|
|
315
|
+
const oldest = directRoomCache.keys().next().value;
|
|
316
|
+
if (oldest !== void 0) directRoomCache.delete(oldest);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function resolveDirectRoomId(client, userId) {
|
|
320
|
+
const trimmed = userId.trim();
|
|
321
|
+
if (!isMatrixQualifiedUserId(trimmed)) throw new Error(`Matrix user IDs must be fully qualified (got "${trimmed}")`);
|
|
322
|
+
const selfUserId = (await client.getUserId().catch(() => null))?.trim() || null;
|
|
323
|
+
const directRoomCache = resolveDirectRoomCache(client);
|
|
324
|
+
const cached = directRoomCache.get(trimmed);
|
|
325
|
+
if (cached && await isStrictDirectRoom({
|
|
326
|
+
client,
|
|
327
|
+
roomId: cached,
|
|
328
|
+
remoteUserId: trimmed,
|
|
329
|
+
selfUserId
|
|
330
|
+
})) return cached;
|
|
331
|
+
if (cached) directRoomCache.delete(trimmed);
|
|
332
|
+
const inspection = await inspectMatrixDirectRooms({
|
|
333
|
+
client,
|
|
334
|
+
remoteUserId: trimmed
|
|
335
|
+
});
|
|
336
|
+
if (inspection.activeRoomId) {
|
|
337
|
+
setDirectRoomCached(client, trimmed, inspection.activeRoomId);
|
|
338
|
+
if (inspection.mappedRoomIds[0] !== inspection.activeRoomId) await persistMatrixDirectRoomMapping({
|
|
339
|
+
client,
|
|
340
|
+
remoteUserId: trimmed,
|
|
341
|
+
roomId: inspection.activeRoomId
|
|
342
|
+
}).catch(() => {});
|
|
343
|
+
return inspection.activeRoomId;
|
|
344
|
+
}
|
|
345
|
+
throw new Error(`No direct room found for ${trimmed} (m.direct missing)`);
|
|
346
|
+
}
|
|
347
|
+
async function resolveMatrixRoomId(client, raw) {
|
|
348
|
+
const target = normalizeMatrixResolvableTarget(normalizeTarget(raw));
|
|
349
|
+
if (normalizeLowercaseStringOrEmpty(target).startsWith("user:")) return await resolveDirectRoomId(client, target.slice(5));
|
|
350
|
+
if (isMatrixQualifiedUserId(target)) return await resolveDirectRoomId(client, target);
|
|
351
|
+
if (target.startsWith("#")) {
|
|
352
|
+
const resolved = await client.resolveRoom(target);
|
|
353
|
+
if (!resolved) throw new Error(`Matrix alias ${target} could not be resolved`);
|
|
354
|
+
return resolved;
|
|
355
|
+
}
|
|
356
|
+
return target;
|
|
230
357
|
}
|
|
231
358
|
//#endregion
|
|
232
|
-
//#region extensions/matrix/src/matrix/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
359
|
+
//#region extensions/matrix/src/matrix/delivery-plan.ts
|
|
360
|
+
const DELIVERY_PLAN_VERSION = 1;
|
|
361
|
+
const DELIVERY_PLAN_NAMESPACE = "outbound-delivery-plans";
|
|
362
|
+
const DELIVERY_PLAN_TTL_MS = 1440 * 60 * 1e3;
|
|
363
|
+
var MatrixDeliveryPlanInvariantError = class extends Error {
|
|
364
|
+
constructor(message) {
|
|
365
|
+
super(message);
|
|
366
|
+
this.name = "MatrixDeliveryPlanInvariantError";
|
|
239
367
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
368
|
+
};
|
|
369
|
+
function createDeliveryPlanStore() {
|
|
370
|
+
return getMatrixRuntime().state.openBlobStore({
|
|
371
|
+
namespace: DELIVERY_PLAN_NAMESPACE,
|
|
372
|
+
maxEntries: 1e4,
|
|
373
|
+
maxBytesPerEntry: 8 * 1024 * 1024,
|
|
374
|
+
maxBytesPerNamespace: 256 * 1024 * 1024,
|
|
375
|
+
overflowPolicy: "reject-new",
|
|
376
|
+
defaultTtlMs: DELIVERY_PLAN_TTL_MS
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function requireIndex(value, label) {
|
|
380
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Matrix durable delivery ${label} must be a non-negative integer`);
|
|
381
|
+
return value;
|
|
382
|
+
}
|
|
383
|
+
function requirePartCount(value) {
|
|
384
|
+
if (!Number.isSafeInteger(value) || (value ?? 0) < 1) throw new Error("Matrix durable delivery part count must be a positive integer");
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
function queuePrefix(queueId) {
|
|
388
|
+
const normalized = queueId.trim();
|
|
389
|
+
if (!normalized) throw new Error("Matrix durable delivery requires a queue id");
|
|
390
|
+
return `${createHash("sha256").update(normalized).digest("hex")}.`;
|
|
391
|
+
}
|
|
392
|
+
function planKey(identity) {
|
|
393
|
+
return `${queuePrefix(identity.queueId)}${requireIndex(identity.partIndex, "part index")}`;
|
|
394
|
+
}
|
|
395
|
+
function transactionId(identity, eventIndex) {
|
|
396
|
+
return `oc_${createHash("sha256").update(identity.queueId).update("\0").update(String(requireIndex(identity.partIndex, "part index"))).update("\0").update(String(requireIndex(eventIndex, "event index"))).digest("base64url")}`;
|
|
397
|
+
}
|
|
398
|
+
const RECEIPT_KINDS = /* @__PURE__ */ new Set([
|
|
399
|
+
"text",
|
|
400
|
+
"media",
|
|
401
|
+
"voice",
|
|
402
|
+
"poll",
|
|
403
|
+
"card",
|
|
404
|
+
"preview",
|
|
405
|
+
"unknown"
|
|
406
|
+
]);
|
|
407
|
+
function isPlan(value) {
|
|
408
|
+
if (!value || typeof value !== "object") return false;
|
|
409
|
+
const plan = value;
|
|
410
|
+
return plan.version === DELIVERY_PLAN_VERSION && typeof plan.queueId === "string" && Boolean(plan.queueId.trim()) && typeof plan.accountId === "string" && typeof plan.roomId === "string" && Boolean(plan.roomId.trim()) && (plan.wireEventType === "m.room.message" || plan.wireEventType === "m.room.encrypted") && typeof plan.endpointPrefix === "string" && Boolean(plan.endpointPrefix.trim()) && typeof plan.transactionScopeId === "string" && Boolean(plan.transactionScopeId.trim()) && Number.isSafeInteger(plan.partIndex) && (plan.partIndex ?? -1) >= 0 && Number.isSafeInteger(plan.partCount) && (plan.partCount ?? 0) > 0 && (plan.partIndex ?? -1) < (plan.partCount ?? 0) && Array.isArray(plan.events) && plan.events.length > 0 && plan.events.every((event) => event && typeof event === "object" && typeof event.transactionId === "string" && Boolean(event.transactionId.trim()) && RECEIPT_KINDS.has(event.receiptKind) && Boolean(event.content) && typeof event.content === "object");
|
|
411
|
+
}
|
|
412
|
+
function decodePlan(bytes) {
|
|
413
|
+
let value;
|
|
414
|
+
try {
|
|
415
|
+
value = JSON.parse(new TextDecoder().decode(bytes));
|
|
416
|
+
} catch {
|
|
417
|
+
throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan is invalid JSON");
|
|
418
|
+
}
|
|
419
|
+
if (!isPlan(value)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan is invalid");
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
function assertPlanIdentity(plan, params) {
|
|
423
|
+
if (plan.queueId !== params.identity.queueId || plan.partIndex !== params.identity.partIndex || plan.partCount !== params.identity.partCount || plan.accountId !== (params.accountId ?? "") || plan.roomId !== params.roomId || plan.transactionScopeId !== params.transactionScopeId || plan.wireEventType !== params.wireEventType) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan no longer matches the active delivery target");
|
|
424
|
+
}
|
|
425
|
+
function endpointPrefix(dispatch) {
|
|
426
|
+
const encodedTransactionId = encodeURIComponent(dispatch.transactionId);
|
|
427
|
+
if (!dispatch.requestPath.endsWith(encodedTransactionId)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery transaction does not match its request path");
|
|
428
|
+
return dispatch.requestPath.slice(0, -encodedTransactionId.length);
|
|
429
|
+
}
|
|
430
|
+
function createMatrixPlannedEvents(params) {
|
|
431
|
+
return params.events.map((event, index) => ({
|
|
432
|
+
...structuredClone(event),
|
|
433
|
+
transactionId: transactionId(params.identity, index)
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
function resolveMatrixDurableDeliveryIdentity(params) {
|
|
437
|
+
if (params.queueId === void 0) return null;
|
|
438
|
+
if (params.partIndex === void 0 || params.partCount === void 0) throw new Error("Matrix durable delivery requires stable part topology");
|
|
439
|
+
const partIndex = requireIndex(params.partIndex, "part index");
|
|
440
|
+
const partCount = requirePartCount(params.partCount);
|
|
441
|
+
if (partIndex >= partCount) throw new Error("Matrix durable delivery part index must be below the part count");
|
|
243
442
|
return {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
443
|
+
queueId: params.queueId,
|
|
444
|
+
partIndex,
|
|
445
|
+
partCount
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function loadMatrixDeliveryPlan(params) {
|
|
449
|
+
const entry = await createDeliveryPlanStore().lookup(planKey(params.identity));
|
|
450
|
+
if (!entry) return null;
|
|
451
|
+
const plan = decodePlan(entry.bytes);
|
|
452
|
+
if (planKey(plan) !== planKey(params.identity)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan key is invalid");
|
|
453
|
+
assertPlanIdentity(plan, params);
|
|
454
|
+
return structuredClone(plan);
|
|
455
|
+
}
|
|
456
|
+
async function persistMatrixDeliveryPlan(params) {
|
|
457
|
+
if (params.events.length === 0) throw new Error("Matrix durable delivery plan must contain at least one event");
|
|
458
|
+
if (params.dispatch.roomId !== params.roomId || params.dispatch.eventType !== params.wireEventType || !params.events.some((event) => event.transactionId === params.dispatch.transactionId)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery was dispatched to an unexpected endpoint");
|
|
459
|
+
const partCount = requirePartCount(params.identity.partCount);
|
|
460
|
+
const events = params.events.map((event, index) => {
|
|
461
|
+
if (event.transactionId !== transactionId(params.identity, index)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan has an invalid transaction identifier");
|
|
462
|
+
return structuredClone(event);
|
|
463
|
+
});
|
|
464
|
+
const plan = {
|
|
465
|
+
version: DELIVERY_PLAN_VERSION,
|
|
466
|
+
queueId: params.identity.queueId,
|
|
467
|
+
accountId: params.accountId ?? "",
|
|
468
|
+
roomId: params.roomId,
|
|
469
|
+
wireEventType: params.wireEventType,
|
|
470
|
+
endpointPrefix: endpointPrefix(params.dispatch),
|
|
471
|
+
transactionScopeId: params.transactionScopeId,
|
|
472
|
+
partIndex: requireIndex(params.identity.partIndex, "part index"),
|
|
473
|
+
partCount,
|
|
474
|
+
events
|
|
475
|
+
};
|
|
476
|
+
const store = createDeliveryPlanStore();
|
|
477
|
+
await store.deleteExpired();
|
|
478
|
+
const bytes = new TextEncoder().encode(JSON.stringify(plan));
|
|
479
|
+
if (await store.registerIfAbsent(planKey(params.identity), bytes, {})) return plan;
|
|
480
|
+
const existing = await loadMatrixDeliveryPlan(params);
|
|
481
|
+
if (!existing || JSON.stringify(existing) !== JSON.stringify(plan)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan no longer matches the prepared event batch");
|
|
482
|
+
return existing;
|
|
483
|
+
}
|
|
484
|
+
async function loadQueuePlans(queueId) {
|
|
485
|
+
const store = createDeliveryPlanStore();
|
|
486
|
+
const keys = (await store.entries()).filter((entry) => entry.key.startsWith(queuePrefix(queueId))).map((entry) => entry.key);
|
|
487
|
+
return await Promise.all(keys.map(async (key) => {
|
|
488
|
+
const entry = await store.lookup(key);
|
|
489
|
+
if (!entry) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan disappeared during reconciliation");
|
|
490
|
+
const plan = decodePlan(entry.bytes);
|
|
491
|
+
if (key !== planKey(plan)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan key is invalid");
|
|
492
|
+
return plan;
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
function assertCompletePartTopology(plans) {
|
|
496
|
+
const partCount = plans[0]?.partCount;
|
|
497
|
+
if (!partCount) throw new MatrixDeliveryPlanInvariantError("Matrix ambiguous delivery has no event plan");
|
|
498
|
+
if (plans.some((plan) => plan.partCount !== partCount)) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery plan part topology is inconsistent");
|
|
499
|
+
const storedParts = new Set(plans.map((plan) => plan.partIndex));
|
|
500
|
+
if (storedParts.size !== partCount || Array.from({ length: partCount }, (_, partIndex) => partIndex).some((partIndex) => !storedParts.has(partIndex))) throw new MatrixDeliveryPlanInvariantError("Matrix ambiguous delivery has an incomplete event plan");
|
|
501
|
+
}
|
|
502
|
+
function createReconciledMatrixReceipt(params) {
|
|
503
|
+
const uniqueResults = params.results.filter((result, index, results) => results.findIndex((entry) => entry.eventId === result.eventId) === index);
|
|
504
|
+
const platformMessageIds = uniqueResults.map((result) => result.eventId);
|
|
505
|
+
return {
|
|
506
|
+
...platformMessageIds[0] ? { primaryPlatformMessageId: platformMessageIds[0] } : {},
|
|
507
|
+
platformMessageIds,
|
|
508
|
+
parts: uniqueResults.map((result, index) => {
|
|
509
|
+
const part = {
|
|
510
|
+
platformMessageId: result.eventId,
|
|
511
|
+
kind: result.receiptKind,
|
|
512
|
+
index
|
|
513
|
+
};
|
|
514
|
+
if (params.replyToId) part.replyToId = params.replyToId;
|
|
515
|
+
if (params.threadId) part.threadId = params.threadId;
|
|
516
|
+
return part;
|
|
517
|
+
}),
|
|
518
|
+
...params.replyToId ? { replyToId: params.replyToId } : {},
|
|
519
|
+
...params.threadId ? { threadId: params.threadId } : {},
|
|
520
|
+
sentAt: Date.now()
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function requireTransactionScope(client) {
|
|
524
|
+
const scope = (await client.getTransactionScopeId()).trim();
|
|
525
|
+
if (!scope) throw new MatrixDeliveryPlanInvariantError("Matrix durable delivery requires a stable transaction scope");
|
|
526
|
+
return scope;
|
|
527
|
+
}
|
|
528
|
+
async function reconcileMatrixUnknownSend(ctx) {
|
|
529
|
+
try {
|
|
530
|
+
if (ctx.payloads.length !== 1) throw new MatrixDeliveryPlanInvariantError("Matrix reconciliation requires exactly one prepared payload");
|
|
531
|
+
const plans = await loadQueuePlans(ctx.queueId);
|
|
532
|
+
if (plans.length === 0) throw new MatrixDeliveryPlanInvariantError("Matrix ambiguous delivery has no persisted event plan");
|
|
533
|
+
assertCompletePartTopology(plans);
|
|
534
|
+
return await withResolvedMatrixSendClient({
|
|
535
|
+
cfg: ctx.cfg,
|
|
536
|
+
accountId: ctx.accountId
|
|
537
|
+
}, async (client) => {
|
|
538
|
+
const transactionScopeId = await requireTransactionScope(client);
|
|
539
|
+
const roomId = await resolveMatrixRoomId(client, ctx.to);
|
|
540
|
+
const wireEventType = await client.getMessageWireEventType(roomId);
|
|
541
|
+
const orderedPlans = [...plans].toSorted((left, right) => left.partIndex - right.partIndex);
|
|
542
|
+
const results = [];
|
|
543
|
+
for (const plan of orderedPlans) {
|
|
544
|
+
assertPlanIdentity(plan, {
|
|
545
|
+
identity: plan,
|
|
546
|
+
accountId: ctx.accountId,
|
|
547
|
+
roomId,
|
|
548
|
+
transactionScopeId,
|
|
549
|
+
wireEventType
|
|
550
|
+
});
|
|
551
|
+
for (const event of plan.events) results.push({
|
|
552
|
+
eventId: await client.sendMessage(roomId, event.content, event.transactionId, async (dispatch) => {
|
|
553
|
+
await persistMatrixDeliveryPlan({
|
|
554
|
+
identity: plan,
|
|
555
|
+
accountId: ctx.accountId,
|
|
556
|
+
roomId,
|
|
557
|
+
transactionScopeId,
|
|
558
|
+
wireEventType,
|
|
559
|
+
events: plan.events,
|
|
560
|
+
dispatch
|
|
561
|
+
});
|
|
562
|
+
}),
|
|
563
|
+
receiptKind: event.receiptKind
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const replyToId = ctx.effectiveReplyToId !== void 0 ? ctx.effectiveReplyToId : ctx.replyToMode === "off" ? void 0 : ctx.replyToId;
|
|
567
|
+
const threadId = ctx.threadId == null ? void 0 : String(ctx.threadId);
|
|
568
|
+
const receipt = createReconciledMatrixReceipt({
|
|
569
|
+
results,
|
|
570
|
+
...replyToId ? { replyToId } : {},
|
|
571
|
+
...threadId ? { threadId } : {}
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
status: "sent",
|
|
575
|
+
messageId: receipt.platformMessageIds.at(-1),
|
|
576
|
+
receipt
|
|
577
|
+
};
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
const retryable = !(error instanceof MatrixDeliveryPlanInvariantError);
|
|
581
|
+
let cleanupError;
|
|
582
|
+
if (!retryable) try {
|
|
583
|
+
await cleanupMatrixDeliveryPlans({ queueId: ctx.queueId });
|
|
584
|
+
} catch (cleanupFailure) {
|
|
585
|
+
cleanupError = cleanupFailure;
|
|
586
|
+
}
|
|
587
|
+
const errorMessage = formatErrorMessage(error instanceof Error || typeof error === "string" ? error : "unknown error");
|
|
588
|
+
return {
|
|
589
|
+
status: "unresolved",
|
|
590
|
+
error: cleanupError === void 0 ? errorMessage : `${errorMessage}; Matrix delivery-plan cleanup failed: ${formatErrorMessage(cleanupError instanceof Error || typeof cleanupError === "string" ? cleanupError : "unknown error")}`,
|
|
591
|
+
retryable
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function cleanupMatrixDeliveryPlans(ctx) {
|
|
596
|
+
const store = createDeliveryPlanStore();
|
|
597
|
+
await store.deleteExpired();
|
|
598
|
+
const keys = (await store.entries()).filter((entry) => entry.key.startsWith(queuePrefix(ctx.queueId))).map((entry) => entry.key);
|
|
599
|
+
await Promise.all(keys.map(async (key) => await store.delete(key)));
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region extensions/matrix/src/matrix/poll-types.ts
|
|
603
|
+
/**
|
|
604
|
+
* Matrix Poll Types (MSC3381)
|
|
605
|
+
*
|
|
606
|
+
* Defines types for Matrix poll events:
|
|
607
|
+
* - m.poll.start - Creates a new poll
|
|
608
|
+
* - m.poll.response - Records a vote
|
|
609
|
+
* - m.poll.end - Closes a poll
|
|
610
|
+
*/
|
|
611
|
+
const M_POLL_START = "m.poll.start";
|
|
612
|
+
const M_POLL_RESPONSE = "m.poll.response";
|
|
613
|
+
const M_POLL_END = "m.poll.end";
|
|
614
|
+
const ORG_POLL_START = "org.matrix.msc3381.poll.start";
|
|
615
|
+
const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response";
|
|
616
|
+
const ORG_POLL_END = "org.matrix.msc3381.poll.end";
|
|
617
|
+
const POLL_EVENT_TYPES = [
|
|
618
|
+
M_POLL_START,
|
|
619
|
+
M_POLL_RESPONSE,
|
|
620
|
+
M_POLL_END,
|
|
621
|
+
ORG_POLL_START,
|
|
622
|
+
ORG_POLL_RESPONSE,
|
|
623
|
+
ORG_POLL_END
|
|
624
|
+
];
|
|
625
|
+
const POLL_START_TYPES = [M_POLL_START, ORG_POLL_START];
|
|
626
|
+
const POLL_RESPONSE_TYPES = [M_POLL_RESPONSE, ORG_POLL_RESPONSE];
|
|
627
|
+
const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END];
|
|
628
|
+
function isPollStartType(eventType) {
|
|
629
|
+
return POLL_START_TYPES.includes(eventType);
|
|
630
|
+
}
|
|
631
|
+
function isPollResponseType(eventType) {
|
|
632
|
+
return POLL_RESPONSE_TYPES.includes(eventType);
|
|
633
|
+
}
|
|
634
|
+
function isPollEndType(eventType) {
|
|
635
|
+
return POLL_END_TYPES.includes(eventType);
|
|
636
|
+
}
|
|
637
|
+
function isPollEventType(eventType) {
|
|
638
|
+
return POLL_EVENT_TYPES.includes(eventType);
|
|
639
|
+
}
|
|
640
|
+
function getTextContent(text) {
|
|
641
|
+
if (!isRecord(text)) return "";
|
|
642
|
+
return normalizeOptionalString(text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body) ?? "";
|
|
643
|
+
}
|
|
644
|
+
function parsePollStart(content) {
|
|
645
|
+
const poll = content["m.poll.start"] ?? content[ORG_POLL_START] ?? content["m.poll"];
|
|
646
|
+
if (!poll) return null;
|
|
647
|
+
const question = getTextContent(poll.question);
|
|
648
|
+
if (!question) return null;
|
|
649
|
+
const rawAnswers = poll.answers;
|
|
650
|
+
const answers = (Array.isArray(rawAnswers) ? rawAnswers : []).map((answer) => ({
|
|
651
|
+
id: isRecord(answer) && typeof answer.id === "string" ? answer.id : "",
|
|
652
|
+
text: getTextContent(answer)
|
|
653
|
+
})).filter((answer) => answer.id.trim().length > 0 && answer.text.length > 0);
|
|
654
|
+
if (answers.length === 0) return null;
|
|
655
|
+
const maxSelectionsRaw = poll.max_selections;
|
|
656
|
+
const maxSelections = typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw) ? Math.floor(maxSelectionsRaw) : 1;
|
|
657
|
+
return {
|
|
658
|
+
question,
|
|
659
|
+
answers,
|
|
660
|
+
kind: poll.kind ?? "m.poll.disclosed",
|
|
661
|
+
maxSelections: Math.min(Math.max(maxSelections, 1), answers.length)
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function parsePollStartContent(content) {
|
|
665
|
+
const parsed = parsePollStart(content);
|
|
666
|
+
if (!parsed) return null;
|
|
667
|
+
return {
|
|
668
|
+
eventId: "",
|
|
669
|
+
roomId: "",
|
|
670
|
+
sender: "",
|
|
671
|
+
senderName: "",
|
|
672
|
+
question: parsed.question,
|
|
673
|
+
answers: parsed.answers.map((answer) => answer.text),
|
|
674
|
+
kind: parsed.kind,
|
|
675
|
+
maxSelections: parsed.maxSelections
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function formatPollAsText(summary) {
|
|
679
|
+
return [
|
|
680
|
+
"[Poll]",
|
|
681
|
+
summary.question,
|
|
682
|
+
"",
|
|
683
|
+
...summary.answers.map((answer, idx) => `${idx + 1}. ${answer}`)
|
|
684
|
+
].join("\n");
|
|
685
|
+
}
|
|
686
|
+
function resolvePollReferenceEventId(content) {
|
|
687
|
+
if (!content || typeof content !== "object") return null;
|
|
688
|
+
const relates = content["m.relates_to"];
|
|
689
|
+
if (!relates || typeof relates.event_id !== "string") return null;
|
|
690
|
+
const eventId = relates.event_id.trim();
|
|
691
|
+
return eventId.length > 0 ? eventId : null;
|
|
692
|
+
}
|
|
693
|
+
function parsePollResponseAnswerIds(content) {
|
|
694
|
+
if (!content || typeof content !== "object") return null;
|
|
695
|
+
const response = content[M_POLL_RESPONSE] ?? content[ORG_POLL_RESPONSE];
|
|
696
|
+
if (!response || !Array.isArray(response.answers)) return null;
|
|
697
|
+
return response.answers.filter((answer) => typeof answer === "string");
|
|
698
|
+
}
|
|
699
|
+
function buildPollResultsSummary(params) {
|
|
700
|
+
const parsed = parsePollStart(params.content);
|
|
701
|
+
if (!parsed) return null;
|
|
702
|
+
let pollClosedAt = Number.POSITIVE_INFINITY;
|
|
703
|
+
for (const event of params.relationEvents) {
|
|
704
|
+
if (event.unsigned?.redacted_because) continue;
|
|
705
|
+
if (!isPollEndType(typeof event.type === "string" ? event.type : "")) continue;
|
|
706
|
+
if (event.sender !== params.sender) continue;
|
|
707
|
+
const ts = typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts) ? event.origin_server_ts : Number.POSITIVE_INFINITY;
|
|
708
|
+
if (ts < pollClosedAt) pollClosedAt = ts;
|
|
709
|
+
}
|
|
710
|
+
const answerIds = new Set(parsed.answers.map((answer) => answer.id));
|
|
711
|
+
const latestVoteBySender = /* @__PURE__ */ new Map();
|
|
712
|
+
const orderedRelationEvents = [...params.relationEvents].toSorted((left, right) => {
|
|
713
|
+
const leftTs = typeof left.origin_server_ts === "number" && Number.isFinite(left.origin_server_ts) ? left.origin_server_ts : Number.POSITIVE_INFINITY;
|
|
714
|
+
const rightTs = typeof right.origin_server_ts === "number" && Number.isFinite(right.origin_server_ts) ? right.origin_server_ts : Number.POSITIVE_INFINITY;
|
|
715
|
+
if (leftTs !== rightTs) return leftTs - rightTs;
|
|
716
|
+
return (left.event_id ?? "").localeCompare(right.event_id ?? "");
|
|
717
|
+
});
|
|
718
|
+
for (const event of orderedRelationEvents) {
|
|
719
|
+
if (event.unsigned?.redacted_because) continue;
|
|
720
|
+
if (!isPollResponseType(typeof event.type === "string" ? event.type : "")) continue;
|
|
721
|
+
const senderId = normalizeOptionalString(event.sender) ?? "";
|
|
722
|
+
if (!senderId) continue;
|
|
723
|
+
const eventTs = typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts) ? event.origin_server_ts : Number.POSITIVE_INFINITY;
|
|
724
|
+
if (eventTs > pollClosedAt) continue;
|
|
725
|
+
const rawAnswers = parsePollResponseAnswerIds(event.content) ?? [];
|
|
726
|
+
const normalizedAnswers = Array.from(new Set(rawAnswers.map((answerId) => normalizeOptionalString(answerId) ?? "").filter((answerId) => answerIds.has(answerId)).slice(0, parsed.maxSelections)));
|
|
727
|
+
latestVoteBySender.set(senderId, {
|
|
728
|
+
ts: eventTs,
|
|
729
|
+
eventId: typeof event.event_id === "string" ? event.event_id : "",
|
|
730
|
+
answerIds: normalizedAnswers
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const voteCounts = new Map(parsed.answers.map((answer) => [answer.id, 0]));
|
|
734
|
+
let totalVotes = 0;
|
|
735
|
+
for (const latestVote of latestVoteBySender.values()) {
|
|
736
|
+
if (latestVote.answerIds.length === 0) continue;
|
|
737
|
+
totalVotes += 1;
|
|
738
|
+
for (const answerId of latestVote.answerIds) voteCounts.set(answerId, (voteCounts.get(answerId) ?? 0) + 1);
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
eventId: params.pollEventId,
|
|
742
|
+
roomId: params.roomId,
|
|
743
|
+
sender: params.sender,
|
|
744
|
+
senderName: params.senderName,
|
|
745
|
+
question: parsed.question,
|
|
746
|
+
answers: parsed.answers.map((answer) => answer.text),
|
|
747
|
+
kind: parsed.kind,
|
|
748
|
+
maxSelections: parsed.maxSelections,
|
|
749
|
+
entries: parsed.answers.map((answer) => ({
|
|
750
|
+
id: answer.id,
|
|
751
|
+
text: answer.text,
|
|
752
|
+
votes: voteCounts.get(answer.id) ?? 0
|
|
753
|
+
})),
|
|
754
|
+
totalVotes,
|
|
755
|
+
closed: Number.isFinite(pollClosedAt)
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function formatPollResultsAsText(summary) {
|
|
759
|
+
const lines = [
|
|
760
|
+
summary.closed ? "[Poll closed]" : "[Poll]",
|
|
761
|
+
summary.question,
|
|
762
|
+
""
|
|
763
|
+
];
|
|
764
|
+
const revealResults = summary.kind === "m.poll.disclosed" || summary.closed;
|
|
765
|
+
for (const [index, entry] of summary.entries.entries()) {
|
|
766
|
+
if (!revealResults) {
|
|
767
|
+
lines.push(`${index + 1}. ${entry.text}`);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
lines.push(`${index + 1}. ${entry.text} (${entry.votes} vote${entry.votes === 1 ? "" : "s"})`);
|
|
771
|
+
}
|
|
772
|
+
lines.push("");
|
|
773
|
+
if (!revealResults) lines.push("Responses are hidden until the poll closes.");
|
|
774
|
+
else lines.push(`Total voters: ${summary.totalVotes}`);
|
|
775
|
+
return lines.join("\n");
|
|
776
|
+
}
|
|
777
|
+
function buildTextContent$1(body) {
|
|
778
|
+
return {
|
|
779
|
+
"m.text": body,
|
|
780
|
+
"org.matrix.msc1767.text": body
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function buildPollFallbackText(question, answers) {
|
|
784
|
+
if (answers.length === 0) return question;
|
|
785
|
+
return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
|
|
786
|
+
}
|
|
787
|
+
function buildPollStartContent(poll) {
|
|
788
|
+
const normalized = normalizePollInput(poll);
|
|
789
|
+
const answers = normalized.options.map((option, idx) => ({
|
|
790
|
+
id: `answer${idx + 1}`,
|
|
791
|
+
...buildTextContent$1(option)
|
|
792
|
+
}));
|
|
793
|
+
const isMultiple = normalized.maxSelections > 1;
|
|
794
|
+
const fallbackText = buildPollFallbackText(normalized.question, answers.map((answer) => getTextContent(answer)));
|
|
795
|
+
return {
|
|
796
|
+
[M_POLL_START]: {
|
|
797
|
+
question: buildTextContent$1(normalized.question),
|
|
798
|
+
kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed",
|
|
799
|
+
max_selections: normalized.maxSelections,
|
|
800
|
+
answers
|
|
801
|
+
},
|
|
802
|
+
"m.text": fallbackText,
|
|
803
|
+
"org.matrix.msc1767.text": fallbackText
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function buildPollResponseContent(pollEventId, answerIds) {
|
|
807
|
+
return {
|
|
808
|
+
[M_POLL_RESPONSE]: { answers: answerIds },
|
|
809
|
+
[ORG_POLL_RESPONSE]: { answers: answerIds },
|
|
810
|
+
"m.relates_to": {
|
|
811
|
+
rel_type: "m.reference",
|
|
812
|
+
event_id: pollEventId
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region extensions/matrix/src/matrix/format-profile.ts
|
|
818
|
+
function createMatrixPrivateMarkers(markdown, exhaustedMessage) {
|
|
819
|
+
const used = new Set(Array.from(markdown, (character) => character.charCodeAt(0)));
|
|
820
|
+
for (const match of markdown.matchAll(/&#(?:x([0-9a-f]+)|(\d+));/giu)) {
|
|
821
|
+
const radix = match[1] ? 16 : 10;
|
|
822
|
+
const value = Number.parseInt(match[1] ?? match[2] ?? "", radix);
|
|
823
|
+
if (Number.isFinite(value) && value <= 65535) used.add(value);
|
|
824
|
+
}
|
|
825
|
+
const markers = [];
|
|
826
|
+
for (let code = 57344; code <= 63743 && markers.length < 3; code += 1) if (!used.has(code)) markers.push(String.fromCharCode(code));
|
|
827
|
+
if (markers.length < 3) throw new Error(exhaustedMessage);
|
|
828
|
+
return {
|
|
829
|
+
open: markers[0] ?? "",
|
|
830
|
+
close: markers[1] ?? "",
|
|
831
|
+
padding: markers[2] ?? ""
|
|
247
832
|
};
|
|
248
833
|
}
|
|
249
834
|
const MATRIX_FORMAT_PROFILE = FormatCapabilityProfile.define({
|
|
@@ -759,937 +1344,559 @@ function mutateInlineTokensWithMentions(params) {
|
|
|
759
1344
|
roomMentioned
|
|
760
1345
|
};
|
|
761
1346
|
}
|
|
762
|
-
function compactLooseListTokens(tokens) {
|
|
763
|
-
const listItemStack = [];
|
|
764
|
-
for (const [index, token] of tokens.entries()) {
|
|
765
|
-
if (token.type === "list_item_open") {
|
|
766
|
-
listItemStack.push({
|
|
767
|
-
level: token.level,
|
|
768
|
-
immediateParagraphOpenIndexes: [],
|
|
769
|
-
immediateParagraphCloseIndexes: []
|
|
770
|
-
});
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
if (token.type === "list_item_close") {
|
|
774
|
-
const item = listItemStack.pop();
|
|
775
|
-
if (item && item.immediateParagraphOpenIndexes.length === 1 && item.immediateParagraphCloseIndexes.length === 1) {
|
|
776
|
-
const openIndex = item.immediateParagraphOpenIndexes[0];
|
|
777
|
-
const closeIndex = item.immediateParagraphCloseIndexes[0];
|
|
778
|
-
const openToken = openIndex === void 0 ? void 0 : tokens[openIndex];
|
|
779
|
-
const closeToken = closeIndex === void 0 ? void 0 : tokens[closeIndex];
|
|
780
|
-
if (openToken && closeToken) {
|
|
781
|
-
openToken.hidden = true;
|
|
782
|
-
closeToken.hidden = true;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
const currentItem = listItemStack.at(-1);
|
|
788
|
-
if (!currentItem || token.level !== currentItem.level + 1) continue;
|
|
789
|
-
if (token.type === "paragraph_open") currentItem.immediateParagraphOpenIndexes.push(index);
|
|
790
|
-
else if (token.type === "paragraph_close") currentItem.immediateParagraphCloseIndexes.push(index);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
function markdownToMatrixHtml(markdown, options = {}) {
|
|
794
|
-
if (hasMatrixSpoilerMetadataCollision(markdown)) return renderMatrixFallbackHtml(markdown);
|
|
795
|
-
const tokens = parseMatrixMarkdown(projectMatrixMarkdown(markdown), options.tableMode);
|
|
796
|
-
compactLooseListTokens(tokens);
|
|
797
|
-
return md.renderer.render(tokens, md.options, {}).trimEnd();
|
|
798
|
-
}
|
|
799
|
-
function protectMatrixSpoilerDelimiters(markdown) {
|
|
800
|
-
const offsets = findMatrixSpoilerDelimiterOffsets(markdown);
|
|
801
|
-
if (offsets.length === 0) return { markdown };
|
|
802
|
-
const markers = createMatrixPrivateMarkers(markdown, "Matrix spoiler formatting exhausted its private marker pool");
|
|
803
|
-
let protectedMarkdown = "";
|
|
804
|
-
let cursor = 0;
|
|
805
|
-
for (const [index, offset] of offsets.entries()) {
|
|
806
|
-
const marker = index % 2 === 0 ? markers.open : markers.close;
|
|
807
|
-
protectedMarkdown += `${markdown.slice(cursor, offset)}${marker}${markers.padding}`;
|
|
808
|
-
cursor = offset + 2;
|
|
809
|
-
}
|
|
810
|
-
protectedMarkdown += markdown.slice(cursor);
|
|
811
|
-
return {
|
|
812
|
-
markdown: protectedMarkdown,
|
|
813
|
-
markers
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
function parseMatrixMarkdown(markdown, tableMode) {
|
|
817
|
-
const protectedSpoilers = protectMatrixSpoilerDelimiters(markdown);
|
|
818
|
-
if (tableMode === "off") md.disable("table");
|
|
819
|
-
try {
|
|
820
|
-
return md.parse(protectedSpoilers.markdown, { matrixSpoilerMarkers: protectedSpoilers.markers });
|
|
821
|
-
} finally {
|
|
822
|
-
if (tableMode === "off") md.enable("table");
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
function markdownToMatrixBody(markdown) {
|
|
826
|
-
const projected = projectMatrixMarkdown(markdown);
|
|
827
|
-
const offsets = findMatrixSpoilerDelimiterOffsets(projected);
|
|
828
|
-
const metadataCollision = hasMatrixSpoilerMetadataCollision(projected);
|
|
829
|
-
if (offsets.length === 0 && !metadataCollision) return projected;
|
|
830
|
-
let body = projected;
|
|
831
|
-
if (metadataCollision) body = "[Spoiler]";
|
|
832
|
-
else for (let index = offsets.length - 2; index >= 0; index -= 2) {
|
|
833
|
-
const open = offsets[index];
|
|
834
|
-
const close = offsets[index + 1];
|
|
835
|
-
if (open !== void 0 && close !== void 0) body = `${body.slice(0, open)}[Spoiler]${body.slice(close + 2)}`;
|
|
836
|
-
}
|
|
837
|
-
return renderMarkdownWithMarkers(markdownToIR(body, {
|
|
838
|
-
enableHtmlUnderline: true,
|
|
839
|
-
headingStyle: "rich",
|
|
840
|
-
linkify: true
|
|
841
|
-
}), {
|
|
842
|
-
styleMarkers: {},
|
|
843
|
-
escapeText: (text) => text
|
|
844
|
-
}, MATRIX_FORMAT_PROFILE);
|
|
845
|
-
}
|
|
846
|
-
function renderMatrixFallbackHtml(markdown) {
|
|
847
|
-
return `<p>${escapeHtml(markdownToMatrixBody(markdown)).replaceAll("\n", "<br>\n")}</p>`;
|
|
848
|
-
}
|
|
849
|
-
async function resolveMarkdownMentionState(params) {
|
|
850
|
-
const tokens = parseMatrixMarkdown(maskEscapedMentions(projectMatrixMarkdown(params.markdown)), params.tableMode);
|
|
851
|
-
restoreEscapedMentionsInBlockTokens(tokens);
|
|
852
|
-
const selfUserId = await resolveMatrixSelfUserId(params.client);
|
|
853
|
-
const userIds = [];
|
|
854
|
-
const seenUserIds = /* @__PURE__ */ new Set();
|
|
855
|
-
let roomMentioned = false;
|
|
856
|
-
for (const token of tokens) {
|
|
857
|
-
if (!token.children?.length) continue;
|
|
858
|
-
const mutated = mutateInlineTokensWithMentions({
|
|
859
|
-
children: token.children,
|
|
860
|
-
userIds,
|
|
861
|
-
seenUserIds,
|
|
862
|
-
selfUserId
|
|
863
|
-
});
|
|
864
|
-
token.children = mutated.children;
|
|
865
|
-
roomMentioned ||= mutated.roomMentioned;
|
|
866
|
-
}
|
|
867
|
-
const mentions = {};
|
|
868
|
-
if (userIds.length > 0) mentions.user_ids = userIds;
|
|
869
|
-
if (roomMentioned) mentions.room = true;
|
|
870
|
-
return {
|
|
871
|
-
tokens,
|
|
872
|
-
mentions
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
async function resolveMatrixMentionsInMarkdown(params) {
|
|
876
|
-
return (await resolveMarkdownMentionState(params)).mentions;
|
|
877
|
-
}
|
|
878
|
-
async function renderMarkdownToMatrixHtmlWithMentions(params) {
|
|
879
|
-
const state = await resolveMarkdownMentionState(params);
|
|
880
|
-
if (hasMatrixSpoilerMetadataCollision(params.markdown)) {
|
|
881
|
-
const redacted = markdownToMatrixBody(params.markdown);
|
|
882
|
-
const redactedState = await resolveMarkdownMentionState({
|
|
883
|
-
...params,
|
|
884
|
-
markdown: redacted
|
|
885
|
-
});
|
|
886
|
-
return {
|
|
887
|
-
html: renderMatrixFallbackHtml(params.markdown),
|
|
888
|
-
mentions: redactedState.mentions
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
compactLooseListTokens(state.tokens);
|
|
892
|
-
return {
|
|
893
|
-
html: md.renderer.render(state.tokens, md.options, {}).trimEnd() || void 0,
|
|
894
|
-
mentions: state.mentions
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
//#endregion
|
|
898
|
-
//#region extensions/matrix/src/matrix/send/chunking.ts
|
|
899
|
-
const getCore$2 = () => getMatrixRuntime();
|
|
900
|
-
function protectMatrixUnderlineTags(markdown) {
|
|
901
|
-
const codeRegions = findCodeRegions(markdown);
|
|
902
|
-
const metadataRanges = findMatrixMarkdownMetadataRanges(markdown);
|
|
903
|
-
const tags = [...tokenizeHtmlTags(markdown)].filter((tag) => (tag.name === "u" || tag.name === "ins") && !tag.selfClosing && !isInsideCode(tag.start, codeRegions) && !isMarkdownEscaped(markdown, tag.start) && !metadataRanges.some((range) => tag.start >= range.start && tag.start < range.end));
|
|
904
|
-
if (tags.length === 0) return { markdown };
|
|
905
|
-
const markers = createMatrixPrivateMarkers(markdown, "Matrix underline chunking exhausted its private marker pool");
|
|
906
|
-
let depth = 0;
|
|
907
|
-
const replacements = tags.flatMap((tag) => {
|
|
908
|
-
if (!tag.closing) {
|
|
909
|
-
depth += 1;
|
|
910
|
-
return [{
|
|
911
|
-
tag,
|
|
912
|
-
marker: depth === 1 ? markers.open : ""
|
|
913
|
-
}];
|
|
914
|
-
}
|
|
915
|
-
if (depth === 0) return [];
|
|
916
|
-
depth -= 1;
|
|
917
|
-
return [{
|
|
918
|
-
tag,
|
|
919
|
-
marker: depth === 0 ? markers.close : ""
|
|
920
|
-
}];
|
|
921
|
-
});
|
|
922
|
-
let protectedMarkdown = markdown;
|
|
923
|
-
for (const { tag, marker } of replacements.toReversed()) protectedMarkdown = `${protectedMarkdown.slice(0, tag.start)}${marker}${markers.padding.repeat(tag.raw.length - marker.length)}${protectedMarkdown.slice(tag.end)}`;
|
|
924
|
-
return {
|
|
925
|
-
markdown: protectedMarkdown,
|
|
926
|
-
markers
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
function restoreMatrixStyleChunks(chunks, spoiler, underline) {
|
|
930
|
-
const stack = [];
|
|
931
|
-
const syntax = {
|
|
932
|
-
spoiler: {
|
|
933
|
-
open: "||",
|
|
934
|
-
close: "||",
|
|
935
|
-
markers: spoiler
|
|
936
|
-
},
|
|
937
|
-
underline: {
|
|
938
|
-
open: "<u>",
|
|
939
|
-
close: "</u>",
|
|
940
|
-
markers: underline
|
|
941
|
-
}
|
|
942
|
-
};
|
|
943
|
-
return chunks.map((chunk) => {
|
|
944
|
-
let restored = stack.map((style) => syntax[style].open).join("");
|
|
945
|
-
for (const character of chunk) {
|
|
946
|
-
const opening = Object.keys(syntax).find((style) => character === syntax[style].markers?.open);
|
|
947
|
-
const closing = Object.keys(syntax).find((style) => character === syntax[style].markers?.close);
|
|
948
|
-
if (opening) {
|
|
949
|
-
stack.push(opening);
|
|
950
|
-
restored += syntax[opening].open;
|
|
951
|
-
} else if (closing) {
|
|
952
|
-
const stackIndex = stack.lastIndexOf(closing);
|
|
953
|
-
if (stackIndex >= 0) {
|
|
954
|
-
const above = stack.slice(stackIndex + 1);
|
|
955
|
-
restored += above.toReversed().map((style) => syntax[style].close).join("");
|
|
956
|
-
restored += syntax[closing].close;
|
|
957
|
-
stack.splice(stackIndex, 1);
|
|
958
|
-
restored += above.map((style) => syntax[style].open).join("");
|
|
959
|
-
}
|
|
960
|
-
} else if (character !== spoiler?.padding && character !== underline?.padding) restored += character;
|
|
961
|
-
}
|
|
962
|
-
return restored + stack.toReversed().map((style) => syntax[style].close).join("");
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
function splitMatrixTableSegments(markdown) {
|
|
966
|
-
const segments = [];
|
|
967
|
-
let cursor = 0;
|
|
968
|
-
for (const range of findMatrixTableSourceRanges(markdown)) {
|
|
969
|
-
const plain = markdown.slice(cursor, range.start).replace(/(?:[ \t]*\n)+$/u, "");
|
|
970
|
-
if (plain.trim()) segments.push({
|
|
971
|
-
table: false,
|
|
972
|
-
text: plain
|
|
973
|
-
});
|
|
974
|
-
const rawTable = markdown.slice(range.start, range.end).trimEnd();
|
|
975
|
-
const indent = /^ +/u.exec(rawTable)?.[0] ?? "";
|
|
976
|
-
const table = indent ? rawTable.split("\n").map((line) => line.startsWith(indent) ? line.slice(indent.length) : line).join("\n") : rawTable;
|
|
977
|
-
segments.push({
|
|
978
|
-
table: true,
|
|
979
|
-
text: table
|
|
980
|
-
});
|
|
981
|
-
cursor = range.end;
|
|
982
|
-
}
|
|
983
|
-
const tail = markdown.slice(cursor).replace(/^(?:[ \t]*\n)+/u, "");
|
|
984
|
-
if (tail.trim()) segments.push({
|
|
985
|
-
table: false,
|
|
986
|
-
text: tail
|
|
987
|
-
});
|
|
988
|
-
return segments;
|
|
989
|
-
}
|
|
990
|
-
function prepareMatrixSingleText(text, opts) {
|
|
991
|
-
const normalizedText = text.replace(/\r\n?/gu, "\n");
|
|
992
|
-
const trimmedText = opts.preserveWhitespace ? normalizedText : normalizedText.trim();
|
|
993
|
-
const cfg = requireRuntimeConfig(opts.cfg, "Matrix text preparation");
|
|
994
|
-
const tableMode = opts.tableMode ?? getCore$2().channel.text.resolveMarkdownTableMode({
|
|
995
|
-
cfg,
|
|
996
|
-
channel: "matrix",
|
|
997
|
-
accountId: opts.accountId,
|
|
998
|
-
supportsBlockTables: MATRIX_FORMAT_PROFILE.constructs.table === "native"
|
|
999
|
-
});
|
|
1000
|
-
const singleEventLimit = Math.min(getCore$2().channel.text.resolveTextChunkLimit(cfg, "matrix", opts.accountId), MATRIX_FORMAT_PROFILE.chunk.limit);
|
|
1001
|
-
const convertedText = renderMatrixMarkdownTables(trimmedText, tableMode);
|
|
1002
|
-
const eventTextLength = Math.max(convertedText.length, markdownToMatrixBody(convertedText).length);
|
|
1003
|
-
return {
|
|
1004
|
-
trimmedText,
|
|
1005
|
-
convertedText,
|
|
1006
|
-
singleEventLimit,
|
|
1007
|
-
eventTextLength,
|
|
1008
|
-
fitsInSingleEvent: eventTextLength <= singleEventLimit,
|
|
1009
|
-
tableMode
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
function chunkMatrixText(text, opts) {
|
|
1013
|
-
const preparedText = prepareMatrixSingleText(text, opts);
|
|
1014
|
-
if (preparedText.fitsInSingleEvent) return {
|
|
1015
|
-
...preparedText,
|
|
1016
|
-
chunks: preparedText.convertedText ? [preparedText.convertedText] : []
|
|
1017
|
-
};
|
|
1018
|
-
const cfg = requireRuntimeConfig(opts.cfg, "Matrix text chunking");
|
|
1019
|
-
const chunkMode = getCore$2().channel.text.resolveChunkMode(cfg, "matrix", opts.accountId);
|
|
1020
|
-
const collisionRedacted = hasMatrixSpoilerMetadataCollision(preparedText.convertedText) ? markdownToMatrixBody(preparedText.convertedText) : void 0;
|
|
1021
|
-
const chunkSegment = (segmentText) => {
|
|
1022
|
-
const protectedUnderline = protectMatrixUnderlineTags(hasMatrixSpoilerMetadataCollision(segmentText) ? markdownToMatrixBody(segmentText) : segmentText);
|
|
1023
|
-
const protectedSpoilers = protectMatrixSpoilerDelimiters(protectedUnderline.markdown);
|
|
1024
|
-
const wrapperReserve = (protectedSpoilers.markers ? 4 : 0) + (protectedUnderline.markers ? 7 : 0);
|
|
1025
|
-
const privateMarkers = [protectedSpoilers.markers, protectedUnderline.markers].flatMap((markers) => markers ? [
|
|
1026
|
-
markers.open,
|
|
1027
|
-
markers.close,
|
|
1028
|
-
markers.padding
|
|
1029
|
-
] : []);
|
|
1030
|
-
let reserve = wrapperReserve;
|
|
1031
|
-
while (reserve < preparedText.singleEventLimit) {
|
|
1032
|
-
const protectedChunks = getCore$2().channel.text.chunkMarkdownTextWithMode(protectedSpoilers.markdown, preparedText.singleEventLimit - reserve, chunkMode);
|
|
1033
|
-
const restored = restoreMatrixStyleChunks(protectedChunks, protectedSpoilers.markers, protectedUnderline.markers).filter((_, index) => {
|
|
1034
|
-
return privateMarkers.reduce((value, marker) => value.replaceAll(marker, ""), protectedChunks[index] ?? "").length > 0;
|
|
1035
|
-
});
|
|
1036
|
-
const overflow = Math.max(0, ...restored.map((chunk) => Math.max(chunk.length, markdownToMatrixBody(chunk).length) - preparedText.singleEventLimit));
|
|
1037
|
-
if (overflow === 0) return restored;
|
|
1038
|
-
reserve += overflow;
|
|
1039
|
-
}
|
|
1040
|
-
throw new Error("Matrix text chunk limit is too small for formatted content");
|
|
1041
|
-
};
|
|
1042
|
-
const chunks = collisionRedacted !== void 0 ? chunkSegment(collisionRedacted) : preparedText.tableMode === "block" ? splitMatrixTableSegments(preparedText.convertedText).flatMap((segment) => {
|
|
1043
|
-
if (!segment.table) return chunkSegment(segment.text);
|
|
1044
|
-
return segment.text.length <= preparedText.singleEventLimit ? [segment.text] : chunkSegment(renderMatrixMarkdownTables(segment.text, "bullets"));
|
|
1045
|
-
}) : chunkSegment(preparedText.convertedText);
|
|
1046
|
-
return {
|
|
1047
|
-
...preparedText,
|
|
1048
|
-
chunks
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
//#endregion
|
|
1052
|
-
//#region extensions/matrix/src/matrix/send/client.ts
|
|
1053
|
-
const loadMatrixSendClientRuntime = createLazyRuntimeModule(() => import("./client-bootstrap-XsIkHNt0.js").then((n) => n.t));
|
|
1054
|
-
function resolveMediaMaxBytes(accountId, cfg) {
|
|
1055
|
-
if (!cfg) throw new Error("Matrix media limits requires a resolved runtime config. Load and resolve config at the command or gateway boundary, then pass cfg through the runtime path.");
|
|
1056
|
-
const matrixCfg = resolveMatrixAccountConfig({
|
|
1057
|
-
cfg: requireRuntimeConfig(cfg, "Matrix media limits"),
|
|
1058
|
-
accountId
|
|
1059
|
-
});
|
|
1060
|
-
const mediaMaxMb = typeof matrixCfg.mediaMaxMb === "number" ? matrixCfg.mediaMaxMb : void 0;
|
|
1061
|
-
if (typeof mediaMaxMb === "number") return mediaMaxMb * 1024 * 1024;
|
|
1062
|
-
}
|
|
1063
|
-
async function withResolvedMatrixSendClient(opts, run) {
|
|
1064
|
-
return await withResolvedMatrixClient({
|
|
1065
|
-
...opts,
|
|
1066
|
-
readiness: "started"
|
|
1067
|
-
}, run, "persist");
|
|
1068
|
-
}
|
|
1069
|
-
async function withResolvedMatrixControlClient(opts, run) {
|
|
1070
|
-
return await withResolvedMatrixClient({
|
|
1071
|
-
...opts,
|
|
1072
|
-
readiness: "none"
|
|
1073
|
-
}, run);
|
|
1074
|
-
}
|
|
1075
|
-
async function withResolvedMatrixClient(opts, run, shutdownBehavior) {
|
|
1076
|
-
if (opts.client) return await run(opts.client);
|
|
1077
|
-
const { withResolvedRuntimeMatrixClient } = await loadMatrixSendClientRuntime();
|
|
1078
|
-
return await withResolvedRuntimeMatrixClient(opts, run, shutdownBehavior);
|
|
1079
|
-
}
|
|
1080
|
-
//#endregion
|
|
1081
|
-
//#region extensions/matrix/src/matrix/send/types.ts
|
|
1082
|
-
const MsgType = {
|
|
1083
|
-
Text: "m.text",
|
|
1084
|
-
Image: "m.image",
|
|
1085
|
-
Audio: "m.audio",
|
|
1086
|
-
Video: "m.video",
|
|
1087
|
-
File: "m.file",
|
|
1088
|
-
Notice: "m.notice"
|
|
1089
|
-
};
|
|
1090
|
-
const RelationType = {
|
|
1091
|
-
Annotation: MATRIX_ANNOTATION_RELATION_TYPE,
|
|
1092
|
-
Replace: "m.replace",
|
|
1093
|
-
Thread: "m.thread"
|
|
1094
|
-
};
|
|
1095
|
-
const EventType = {
|
|
1096
|
-
Direct: "m.direct",
|
|
1097
|
-
Reaction: MATRIX_REACTION_EVENT_TYPE,
|
|
1098
|
-
RoomMessage: "m.room.message"
|
|
1099
|
-
};
|
|
1100
|
-
const MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY = "com.openclaw.finalized_preview";
|
|
1101
|
-
/**
|
|
1102
|
-
* MSC4357 live marker key.
|
|
1103
|
-
* When present on event content, signals that the message is still being
|
|
1104
|
-
* streamed (e.g. an LLM generating a response). Supporting clients render
|
|
1105
|
-
* the message with a streaming animation until an edit without this marker
|
|
1106
|
-
* arrives, indicating the stream is complete.
|
|
1107
|
-
* @see https://github.com/matrix-org/matrix-spec-proposals/pull/4357
|
|
1108
|
-
*/
|
|
1109
|
-
const MSC4357_LIVE_KEY = "org.matrix.msc4357.live";
|
|
1110
|
-
//#endregion
|
|
1111
|
-
//#region extensions/matrix/src/matrix/send/formatting.ts
|
|
1112
|
-
const getCore$1 = () => getMatrixRuntime();
|
|
1113
|
-
async function renderMatrixFormattedContent(params) {
|
|
1114
|
-
const markdown = params.markdown ?? "";
|
|
1115
|
-
const body = markdownToMatrixBody(markdown);
|
|
1116
|
-
if (params.includeMentions === false) return {
|
|
1117
|
-
body,
|
|
1118
|
-
html: markdownToMatrixHtml(markdown, { tableMode: params.tableMode }).trimEnd() || void 0
|
|
1119
|
-
};
|
|
1120
|
-
const { html, mentions } = await renderMarkdownToMatrixHtmlWithMentions({
|
|
1121
|
-
markdown,
|
|
1122
|
-
client: params.client,
|
|
1123
|
-
tableMode: params.tableMode
|
|
1124
|
-
});
|
|
1125
|
-
return {
|
|
1126
|
-
body,
|
|
1127
|
-
html,
|
|
1128
|
-
mentions
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
function buildTextContent(body, relation, opts = {}) {
|
|
1132
|
-
const msgtype = opts.msgtype ?? MsgType.Text;
|
|
1133
|
-
return relation ? {
|
|
1134
|
-
msgtype,
|
|
1135
|
-
body,
|
|
1136
|
-
"m.relates_to": relation
|
|
1137
|
-
} : {
|
|
1138
|
-
msgtype,
|
|
1139
|
-
body
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
async function enrichMatrixFormattedContent(params) {
|
|
1143
|
-
const { body, html, mentions } = await renderMatrixFormattedContent({
|
|
1144
|
-
client: params.client,
|
|
1145
|
-
markdown: params.markdown,
|
|
1146
|
-
includeMentions: params.includeMentions,
|
|
1147
|
-
tableMode: params.tableMode
|
|
1148
|
-
});
|
|
1149
|
-
params.content.body = body || params.content.body;
|
|
1150
|
-
if (mentions) params.content["m.mentions"] = mentions;
|
|
1151
|
-
else delete params.content["m.mentions"];
|
|
1152
|
-
if (!html) {
|
|
1153
|
-
delete params.content.format;
|
|
1154
|
-
delete params.content.formatted_body;
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
params.content.format = "org.matrix.custom.html";
|
|
1158
|
-
params.content.formatted_body = html;
|
|
1159
|
-
}
|
|
1160
|
-
async function resolveMatrixMentionsForBody(params) {
|
|
1161
|
-
return await resolveMatrixMentionsInMarkdown({
|
|
1162
|
-
markdown: params.body ?? "",
|
|
1163
|
-
client: params.client
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
function normalizeMentionUserIds(value) {
|
|
1167
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
|
|
1168
|
-
}
|
|
1169
|
-
function extractMatrixMentions(content) {
|
|
1170
|
-
const rawMentions = content?.["m.mentions"];
|
|
1171
|
-
if (!rawMentions || typeof rawMentions !== "object") return {};
|
|
1172
|
-
const mentions = rawMentions;
|
|
1173
|
-
const normalized = {};
|
|
1174
|
-
const userIds = normalizeMentionUserIds(mentions.user_ids);
|
|
1175
|
-
if (userIds.length > 0) normalized.user_ids = userIds;
|
|
1176
|
-
if (mentions.room === true) normalized.room = true;
|
|
1177
|
-
return normalized;
|
|
1178
|
-
}
|
|
1179
|
-
function diffMatrixMentions(current, previous) {
|
|
1180
|
-
const previousUserIds = new Set(previous.user_ids ?? []);
|
|
1181
|
-
const newUserIds = (current.user_ids ?? []).filter((userId) => !previousUserIds.has(userId));
|
|
1182
|
-
const delta = {};
|
|
1183
|
-
if (newUserIds.length > 0) delta.user_ids = newUserIds;
|
|
1184
|
-
if (current.room && !previous.room) delta.room = true;
|
|
1185
|
-
return delta;
|
|
1186
|
-
}
|
|
1187
|
-
function buildReplyRelation(replyToId) {
|
|
1188
|
-
const trimmed = replyToId?.trim();
|
|
1189
|
-
if (!trimmed) return;
|
|
1190
|
-
return { "m.in_reply_to": { event_id: trimmed } };
|
|
1191
|
-
}
|
|
1192
|
-
function buildThreadRelation(threadId, replyToId) {
|
|
1193
|
-
const trimmed = threadId.trim();
|
|
1194
|
-
const relation = {
|
|
1195
|
-
rel_type: RelationType.Thread,
|
|
1196
|
-
event_id: trimmed
|
|
1197
|
-
};
|
|
1198
|
-
const fallbackReplyToId = replyToId?.trim();
|
|
1199
|
-
if (fallbackReplyToId) {
|
|
1200
|
-
relation.is_falling_back = true;
|
|
1201
|
-
relation["m.in_reply_to"] = { event_id: fallbackReplyToId };
|
|
1202
|
-
}
|
|
1203
|
-
return relation;
|
|
1204
|
-
}
|
|
1205
|
-
function resolveMatrixMsgType(contentType, _fileName) {
|
|
1206
|
-
switch (getCore$1().media.mediaKindFromMime(contentType ?? "")) {
|
|
1207
|
-
case "image": return MsgType.Image;
|
|
1208
|
-
case "audio": return MsgType.Audio;
|
|
1209
|
-
case "video": return MsgType.Video;
|
|
1210
|
-
default: return MsgType.File;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
function resolveMatrixVoiceDecision(opts) {
|
|
1214
|
-
if (!opts.wantsVoice) return { useVoice: false };
|
|
1215
|
-
if (isMatrixVoiceCompatibleAudio(opts)) return { useVoice: true };
|
|
1216
|
-
return { useVoice: false };
|
|
1217
|
-
}
|
|
1218
|
-
function isMatrixVoiceCompatibleAudio(opts) {
|
|
1219
|
-
return isVoiceMessageCompatibleAudio({
|
|
1220
|
-
contentType: opts.contentType,
|
|
1221
|
-
fileName: opts.fileName
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
//#endregion
|
|
1225
|
-
//#region extensions/matrix/src/matrix/send/media.ts
|
|
1226
|
-
const getCore = () => getMatrixRuntime();
|
|
1227
|
-
function buildMatrixMediaInfo(params) {
|
|
1228
|
-
const base = {};
|
|
1229
|
-
if (Number.isFinite(params.size)) base.size = params.size;
|
|
1230
|
-
if (params.mimetype) base.mimetype = params.mimetype;
|
|
1231
|
-
if (params.imageInfo) {
|
|
1232
|
-
const dimensional = {
|
|
1233
|
-
...base,
|
|
1234
|
-
...params.imageInfo
|
|
1235
|
-
};
|
|
1236
|
-
if (typeof params.durationMs === "number") return {
|
|
1237
|
-
...dimensional,
|
|
1238
|
-
duration: params.durationMs
|
|
1239
|
-
};
|
|
1240
|
-
return dimensional;
|
|
1241
|
-
}
|
|
1242
|
-
if (typeof params.durationMs === "number") return {
|
|
1243
|
-
...base,
|
|
1244
|
-
duration: params.durationMs
|
|
1245
|
-
};
|
|
1246
|
-
if (Object.keys(base).length === 0) return;
|
|
1247
|
-
return base;
|
|
1248
|
-
}
|
|
1249
|
-
function buildMediaContent(params) {
|
|
1250
|
-
const info = buildMatrixMediaInfo({
|
|
1251
|
-
size: params.size,
|
|
1252
|
-
mimetype: params.mimetype,
|
|
1253
|
-
durationMs: params.durationMs,
|
|
1254
|
-
imageInfo: params.imageInfo
|
|
1255
|
-
});
|
|
1256
|
-
const base = {
|
|
1257
|
-
msgtype: params.msgtype,
|
|
1258
|
-
body: params.body,
|
|
1259
|
-
filename: params.filename,
|
|
1260
|
-
info: info ?? void 0
|
|
1261
|
-
};
|
|
1262
|
-
if (!params.file && params.url) base.url = params.url;
|
|
1263
|
-
if (params.file) base.file = params.file;
|
|
1264
|
-
if (params.isVoice) {
|
|
1265
|
-
base["org.matrix.msc3245.voice"] = {};
|
|
1266
|
-
if (typeof params.durationMs === "number") base["org.matrix.msc1767.audio"] = { duration: params.durationMs };
|
|
1267
|
-
}
|
|
1268
|
-
if (params.relation) base["m.relates_to"] = params.relation;
|
|
1269
|
-
return base;
|
|
1270
|
-
}
|
|
1271
|
-
const THUMBNAIL_MAX_SIDE = 800;
|
|
1272
|
-
const THUMBNAIL_QUALITY = 80;
|
|
1273
|
-
async function prepareImageInfo(params) {
|
|
1274
|
-
const meta = await getCore().media.getImageMetadata(params.buffer).catch(() => null);
|
|
1275
|
-
if (!meta) return;
|
|
1276
|
-
const imageInfo = {
|
|
1277
|
-
w: meta.width,
|
|
1278
|
-
h: meta.height
|
|
1279
|
-
};
|
|
1280
|
-
if (Math.max(meta.width, meta.height) > THUMBNAIL_MAX_SIDE) try {
|
|
1281
|
-
const thumbBuffer = await getCore().media.resizeToJpeg({
|
|
1282
|
-
buffer: params.buffer,
|
|
1283
|
-
maxSide: THUMBNAIL_MAX_SIDE,
|
|
1284
|
-
quality: THUMBNAIL_QUALITY,
|
|
1285
|
-
withoutEnlargement: true
|
|
1286
|
-
});
|
|
1287
|
-
const thumbMeta = await getCore().media.getImageMetadata(thumbBuffer).catch(() => null);
|
|
1288
|
-
const result = await uploadMediaWithEncryption(params.client, thumbBuffer, {
|
|
1289
|
-
contentType: "image/jpeg",
|
|
1290
|
-
filename: "thumbnail.jpg",
|
|
1291
|
-
encrypted: params.encrypted === true
|
|
1292
|
-
});
|
|
1293
|
-
if (result.file) imageInfo.thumbnail_file = result.file;
|
|
1294
|
-
else imageInfo.thumbnail_url = result.url;
|
|
1295
|
-
if (thumbMeta) imageInfo.thumbnail_info = {
|
|
1296
|
-
w: thumbMeta.width,
|
|
1297
|
-
h: thumbMeta.height,
|
|
1298
|
-
mimetype: "image/jpeg",
|
|
1299
|
-
size: thumbBuffer.byteLength
|
|
1300
|
-
};
|
|
1301
|
-
} catch {}
|
|
1302
|
-
return imageInfo;
|
|
1303
|
-
}
|
|
1304
|
-
async function resolveMediaDurationMs(params) {
|
|
1305
|
-
if (params.kind !== "audio" && params.kind !== "video") return;
|
|
1306
|
-
try {
|
|
1307
|
-
const fileInfo = params.contentType || params.fileName ? {
|
|
1308
|
-
mimeType: params.contentType,
|
|
1309
|
-
size: params.buffer.byteLength,
|
|
1310
|
-
path: params.fileName
|
|
1311
|
-
} : void 0;
|
|
1312
|
-
const durationSeconds = (await parseBuffer(params.buffer, fileInfo, {
|
|
1313
|
-
duration: true,
|
|
1314
|
-
skipCovers: true
|
|
1315
|
-
})).format.duration;
|
|
1316
|
-
if (typeof durationSeconds === "number" && Number.isFinite(durationSeconds)) return Math.max(0, Math.round(durationSeconds * 1e3));
|
|
1317
|
-
} catch {}
|
|
1318
|
-
}
|
|
1319
|
-
async function uploadFile(client, file, params) {
|
|
1320
|
-
return await client.uploadContent(file, params.contentType, params.filename);
|
|
1321
|
-
}
|
|
1322
|
-
async function uploadMediaWithEncryption(client, buffer, params) {
|
|
1323
|
-
if (params.encrypted && client.crypto) {
|
|
1324
|
-
const encrypted = await client.crypto.encryptMedia(buffer);
|
|
1325
|
-
const mxc = await client.uploadContent(encrypted.buffer, params.contentType, params.filename);
|
|
1326
|
-
return {
|
|
1327
|
-
url: mxc,
|
|
1328
|
-
file: {
|
|
1329
|
-
url: mxc,
|
|
1330
|
-
...encrypted.file
|
|
1347
|
+
function compactLooseListTokens(tokens) {
|
|
1348
|
+
const listItemStack = [];
|
|
1349
|
+
for (const [index, token] of tokens.entries()) {
|
|
1350
|
+
if (token.type === "list_item_open") {
|
|
1351
|
+
listItemStack.push({
|
|
1352
|
+
level: token.level,
|
|
1353
|
+
immediateParagraphOpenIndexes: [],
|
|
1354
|
+
immediateParagraphCloseIndexes: []
|
|
1355
|
+
});
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (token.type === "list_item_close") {
|
|
1359
|
+
const item = listItemStack.pop();
|
|
1360
|
+
if (item && item.immediateParagraphOpenIndexes.length === 1 && item.immediateParagraphCloseIndexes.length === 1) {
|
|
1361
|
+
const openIndex = item.immediateParagraphOpenIndexes[0];
|
|
1362
|
+
const closeIndex = item.immediateParagraphCloseIndexes[0];
|
|
1363
|
+
const openToken = openIndex === void 0 ? void 0 : tokens[openIndex];
|
|
1364
|
+
const closeToken = closeIndex === void 0 ? void 0 : tokens[closeIndex];
|
|
1365
|
+
if (openToken && closeToken) {
|
|
1366
|
+
openToken.hidden = true;
|
|
1367
|
+
closeToken.hidden = true;
|
|
1368
|
+
}
|
|
1331
1369
|
}
|
|
1332
|
-
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const currentItem = listItemStack.at(-1);
|
|
1373
|
+
if (!currentItem || token.level !== currentItem.level + 1) continue;
|
|
1374
|
+
if (token.type === "paragraph_open") currentItem.immediateParagraphOpenIndexes.push(index);
|
|
1375
|
+
else if (token.type === "paragraph_close") currentItem.immediateParagraphCloseIndexes.push(index);
|
|
1333
1376
|
}
|
|
1334
|
-
return { url: await uploadFile(client, buffer, params) };
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Upload media with optional encryption for E2EE rooms.
|
|
1338
|
-
*/
|
|
1339
|
-
async function uploadMediaMaybeEncrypted(client, roomId, buffer, params) {
|
|
1340
|
-
const isEncrypted = Boolean(client.crypto && await client.crypto.isRoomEncrypted(roomId));
|
|
1341
|
-
return await uploadMediaWithEncryption(client, buffer, {
|
|
1342
|
-
...params,
|
|
1343
|
-
encrypted: isEncrypted
|
|
1344
|
-
});
|
|
1345
1377
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
isStrictDirectMembership: () => isStrictDirectMembership,
|
|
1352
|
-
isStrictDirectRoom: () => isStrictDirectRoom,
|
|
1353
|
-
readJoinedMatrixMembers: () => readJoinedMatrixMembers
|
|
1354
|
-
});
|
|
1355
|
-
function normalizeJoinedMatrixMembers(joinedMembers) {
|
|
1356
|
-
if (!Array.isArray(joinedMembers)) return [];
|
|
1357
|
-
return joinedMembers.map((entry) => normalizeNullableString(entry)).filter((entry) => Boolean(entry));
|
|
1378
|
+
function markdownToMatrixHtml(markdown, options = {}) {
|
|
1379
|
+
if (hasMatrixSpoilerMetadataCollision(markdown)) return renderMatrixFallbackHtml(markdown);
|
|
1380
|
+
const tokens = parseMatrixMarkdown(projectMatrixMarkdown(markdown), options.tableMode);
|
|
1381
|
+
compactLooseListTokens(tokens);
|
|
1382
|
+
return md.renderer.render(tokens, md.options, {}).trimEnd();
|
|
1358
1383
|
}
|
|
1359
|
-
function
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1384
|
+
function protectMatrixSpoilerDelimiters(markdown) {
|
|
1385
|
+
const offsets = findMatrixSpoilerDelimiterOffsets(markdown);
|
|
1386
|
+
if (offsets.length === 0) return { markdown };
|
|
1387
|
+
const markers = createMatrixPrivateMarkers(markdown, "Matrix spoiler formatting exhausted its private marker pool");
|
|
1388
|
+
let protectedMarkdown = "";
|
|
1389
|
+
let cursor = 0;
|
|
1390
|
+
for (const [index, offset] of offsets.entries()) {
|
|
1391
|
+
const marker = index % 2 === 0 ? markers.open : markers.close;
|
|
1392
|
+
protectedMarkdown += `${markdown.slice(cursor, offset)}${marker}${markers.padding}`;
|
|
1393
|
+
cursor = offset + 2;
|
|
1394
|
+
}
|
|
1395
|
+
protectedMarkdown += markdown.slice(cursor);
|
|
1396
|
+
return {
|
|
1397
|
+
markdown: protectedMarkdown,
|
|
1398
|
+
markers
|
|
1399
|
+
};
|
|
1364
1400
|
}
|
|
1365
|
-
|
|
1401
|
+
function parseMatrixMarkdown(markdown, tableMode) {
|
|
1402
|
+
const protectedSpoilers = protectMatrixSpoilerDelimiters(markdown);
|
|
1403
|
+
if (tableMode === "off") md.disable("table");
|
|
1366
1404
|
try {
|
|
1367
|
-
return
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1405
|
+
return md.parse(protectedSpoilers.markdown, { matrixSpoilerMarkers: protectedSpoilers.markers });
|
|
1406
|
+
} finally {
|
|
1407
|
+
if (tableMode === "off") md.enable("table");
|
|
1370
1408
|
}
|
|
1371
1409
|
}
|
|
1372
|
-
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1410
|
+
function markdownToMatrixBody(markdown) {
|
|
1411
|
+
const projected = projectMatrixMarkdown(markdown);
|
|
1412
|
+
const offsets = findMatrixSpoilerDelimiterOffsets(projected);
|
|
1413
|
+
const metadataCollision = hasMatrixSpoilerMetadataCollision(projected);
|
|
1414
|
+
if (offsets.length === 0 && !metadataCollision) return projected;
|
|
1415
|
+
let body = projected;
|
|
1416
|
+
if (metadataCollision) body = "[Spoiler]";
|
|
1417
|
+
else for (let index = offsets.length - 2; index >= 0; index -= 2) {
|
|
1418
|
+
const open = offsets[index];
|
|
1419
|
+
const close = offsets[index + 1];
|
|
1420
|
+
if (open !== void 0 && close !== void 0) body = `${body.slice(0, open)}[Spoiler]${body.slice(close + 2)}`;
|
|
1382
1421
|
}
|
|
1422
|
+
return renderMarkdownWithMarkers(markdownToIR(body, {
|
|
1423
|
+
enableHtmlUnderline: true,
|
|
1424
|
+
headingStyle: "rich",
|
|
1425
|
+
linkify: true
|
|
1426
|
+
}), {
|
|
1427
|
+
styleMarkers: {},
|
|
1428
|
+
escapeText: (text) => text
|
|
1429
|
+
}, MATRIX_FORMAT_PROFILE);
|
|
1383
1430
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1431
|
+
function renderMatrixFallbackHtml(markdown) {
|
|
1432
|
+
return `<p>${escapeHtml(markdownToMatrixBody(markdown)).replaceAll("\n", "<br>\n")}</p>`;
|
|
1433
|
+
}
|
|
1434
|
+
async function resolveMarkdownMentionState(params) {
|
|
1435
|
+
const tokens = parseMatrixMarkdown(maskEscapedMentions(projectMatrixMarkdown(params.markdown)), params.tableMode);
|
|
1436
|
+
restoreEscapedMentionsInBlockTokens(tokens);
|
|
1437
|
+
const selfUserId = await resolveMatrixSelfUserId(params.client);
|
|
1438
|
+
const userIds = [];
|
|
1439
|
+
const seenUserIds = /* @__PURE__ */ new Set();
|
|
1440
|
+
let roomMentioned = false;
|
|
1441
|
+
for (const token of tokens) {
|
|
1442
|
+
if (!token.children?.length) continue;
|
|
1443
|
+
const mutated = mutateInlineTokensWithMentions({
|
|
1444
|
+
children: token.children,
|
|
1445
|
+
userIds,
|
|
1446
|
+
seenUserIds,
|
|
1447
|
+
selfUserId
|
|
1448
|
+
});
|
|
1449
|
+
token.children = mutated.children;
|
|
1450
|
+
roomMentioned ||= mutated.roomMentioned;
|
|
1451
|
+
}
|
|
1452
|
+
const mentions = {};
|
|
1453
|
+
if (userIds.length > 0) mentions.user_ids = userIds;
|
|
1454
|
+
if (roomMentioned) mentions.room = true;
|
|
1399
1455
|
return {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
viaMemberState: memberStateFlag === true,
|
|
1403
|
-
memberStateFlag
|
|
1456
|
+
tokens,
|
|
1457
|
+
mentions
|
|
1404
1458
|
};
|
|
1405
1459
|
}
|
|
1406
|
-
async function
|
|
1407
|
-
return (await
|
|
1408
|
-
client: params.client,
|
|
1409
|
-
roomId: params.roomId,
|
|
1410
|
-
remoteUserId: params.remoteUserId,
|
|
1411
|
-
selfUserId: params.selfUserId
|
|
1412
|
-
})).strict;
|
|
1460
|
+
async function resolveMatrixMentionsInMarkdown(params) {
|
|
1461
|
+
return (await resolveMarkdownMentionState(params)).mentions;
|
|
1413
1462
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
});
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const direct = await client.getAccountData(EventType.Direct);
|
|
1427
|
-
return direct && typeof direct === "object" && !Array.isArray(direct) ? direct : {};
|
|
1428
|
-
} catch {
|
|
1429
|
-
return {};
|
|
1463
|
+
async function renderMarkdownToMatrixHtmlWithMentions(params) {
|
|
1464
|
+
const state = await resolveMarkdownMentionState(params);
|
|
1465
|
+
if (hasMatrixSpoilerMetadataCollision(params.markdown)) {
|
|
1466
|
+
const redacted = markdownToMatrixBody(params.markdown);
|
|
1467
|
+
const redactedState = await resolveMarkdownMentionState({
|
|
1468
|
+
...params,
|
|
1469
|
+
markdown: redacted
|
|
1470
|
+
});
|
|
1471
|
+
return {
|
|
1472
|
+
html: renderMatrixFallbackHtml(params.markdown),
|
|
1473
|
+
mentions: redactedState.mentions
|
|
1474
|
+
};
|
|
1430
1475
|
}
|
|
1476
|
+
compactLooseListTokens(state.tokens);
|
|
1477
|
+
return {
|
|
1478
|
+
html: md.renderer.render(state.tokens, md.options, {}).trimEnd() || void 0,
|
|
1479
|
+
mentions: state.mentions
|
|
1480
|
+
};
|
|
1431
1481
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
const current = direct[remoteUserId];
|
|
1439
|
-
if (!Array.isArray(current)) return [];
|
|
1440
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1441
|
-
const normalized = [];
|
|
1442
|
-
for (const value of current) {
|
|
1443
|
-
const roomId = normalizeOptionalString(value) ?? "";
|
|
1444
|
-
if (!roomId || seen.has(roomId)) continue;
|
|
1445
|
-
seen.add(roomId);
|
|
1446
|
-
normalized.push(roomId);
|
|
1447
|
-
}
|
|
1448
|
-
return normalized;
|
|
1482
|
+
//#endregion
|
|
1483
|
+
//#region extensions/matrix/src/matrix/send/chunking.ts
|
|
1484
|
+
const getCore$2 = () => getMatrixRuntime();
|
|
1485
|
+
function normalizeMatrixEventLimit(limit) {
|
|
1486
|
+
if (!Number.isFinite(limit) || limit <= 0) return limit;
|
|
1487
|
+
return Math.max(1, Math.floor(limit));
|
|
1449
1488
|
}
|
|
1450
|
-
function
|
|
1451
|
-
const
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
if (!roomId || seen.has(roomId)) continue;
|
|
1456
|
-
seen.add(roomId);
|
|
1457
|
-
normalized.push(roomId);
|
|
1458
|
-
}
|
|
1459
|
-
return normalized;
|
|
1489
|
+
function resolveMatrixChunkOverflow(chunk, limit) {
|
|
1490
|
+
const body = markdownToMatrixBody(chunk);
|
|
1491
|
+
const renderedLength = Math.max(chunk.length, body.length);
|
|
1492
|
+
if (limit === 1 && Array.from(chunk).length === 1 && Array.from(body).length === 1) return 0;
|
|
1493
|
+
return Math.max(0, renderedLength - limit);
|
|
1460
1494
|
}
|
|
1461
|
-
function
|
|
1462
|
-
const
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1495
|
+
function protectMatrixUnderlineTags(markdown) {
|
|
1496
|
+
const codeRegions = findCodeRegions(markdown);
|
|
1497
|
+
const metadataRanges = findMatrixMarkdownMetadataRanges(markdown);
|
|
1498
|
+
const tags = [...tokenizeHtmlTags(markdown)].filter((tag) => (tag.name === "u" || tag.name === "ins") && !tag.selfClosing && !isInsideCode(tag.start, codeRegions) && !isMarkdownEscaped(markdown, tag.start) && !metadataRanges.some((range) => tag.start >= range.start && tag.start < range.end));
|
|
1499
|
+
if (tags.length === 0) return { markdown };
|
|
1500
|
+
const markers = createMatrixPrivateMarkers(markdown, "Matrix underline chunking exhausted its private marker pool");
|
|
1501
|
+
let depth = 0;
|
|
1502
|
+
const replacements = tags.flatMap((tag) => {
|
|
1503
|
+
if (!tag.closing) {
|
|
1504
|
+
depth += 1;
|
|
1505
|
+
return [{
|
|
1506
|
+
tag,
|
|
1507
|
+
marker: depth === 1 ? markers.open : ""
|
|
1508
|
+
}];
|
|
1509
|
+
}
|
|
1510
|
+
if (depth === 0) return [];
|
|
1511
|
+
depth -= 1;
|
|
1512
|
+
return [{
|
|
1513
|
+
tag,
|
|
1514
|
+
marker: depth === 0 ? markers.close : ""
|
|
1515
|
+
}];
|
|
1516
|
+
});
|
|
1517
|
+
let protectedMarkdown = markdown;
|
|
1518
|
+
for (const { tag, marker } of replacements.toReversed()) protectedMarkdown = `${protectedMarkdown.slice(0, tag.start)}${marker}${markers.padding.repeat(tag.raw.length - marker.length)}${protectedMarkdown.slice(tag.end)}`;
|
|
1519
|
+
return {
|
|
1520
|
+
markdown: protectedMarkdown,
|
|
1521
|
+
markers
|
|
1522
|
+
};
|
|
1465
1523
|
}
|
|
1466
|
-
function
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1524
|
+
function restoreMatrixStyleChunks(chunks, spoiler, underline) {
|
|
1525
|
+
const stack = [];
|
|
1526
|
+
const syntax = {
|
|
1527
|
+
spoiler: {
|
|
1528
|
+
open: "||",
|
|
1529
|
+
close: "||",
|
|
1530
|
+
markers: spoiler
|
|
1531
|
+
},
|
|
1532
|
+
underline: {
|
|
1533
|
+
open: "<u>",
|
|
1534
|
+
close: "</u>",
|
|
1535
|
+
markers: underline
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
return chunks.map((chunk) => {
|
|
1539
|
+
let restored = stack.map((style) => syntax[style].open).join("");
|
|
1540
|
+
for (const character of chunk) {
|
|
1541
|
+
const opening = Object.keys(syntax).find((style) => character === syntax[style].markers?.open);
|
|
1542
|
+
const closing = Object.keys(syntax).find((style) => character === syntax[style].markers?.close);
|
|
1543
|
+
if (opening) {
|
|
1544
|
+
stack.push(opening);
|
|
1545
|
+
restored += syntax[opening].open;
|
|
1546
|
+
} else if (closing) {
|
|
1547
|
+
const stackIndex = stack.lastIndexOf(closing);
|
|
1548
|
+
if (stackIndex >= 0) {
|
|
1549
|
+
const above = stack.slice(stackIndex + 1);
|
|
1550
|
+
restored += above.toReversed().map((style) => syntax[style].close).join("");
|
|
1551
|
+
restored += syntax[closing].close;
|
|
1552
|
+
stack.splice(stackIndex, 1);
|
|
1553
|
+
restored += above.map((style) => syntax[style].open).join("");
|
|
1554
|
+
}
|
|
1555
|
+
} else if (character !== spoiler?.padding && character !== underline?.padding) restored += character;
|
|
1556
|
+
}
|
|
1557
|
+
return restored + stack.toReversed().map((style) => syntax[style].close).join("");
|
|
1558
|
+
});
|
|
1472
1559
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1560
|
+
function splitMatrixTableSegments(markdown) {
|
|
1561
|
+
const segments = [];
|
|
1562
|
+
let cursor = 0;
|
|
1563
|
+
for (const range of findMatrixTableSourceRanges(markdown)) {
|
|
1564
|
+
const plain = markdown.slice(cursor, range.start).replace(/(?:[ \t]*\n)+$/u, "");
|
|
1565
|
+
if (plain.trim()) segments.push({
|
|
1566
|
+
table: false,
|
|
1567
|
+
text: plain
|
|
1480
1568
|
});
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1569
|
+
const rawTable = markdown.slice(range.start, range.end).trimEnd();
|
|
1570
|
+
const indent = /^ +/u.exec(rawTable)?.[0] ?? "";
|
|
1571
|
+
const table = indent ? rawTable.split("\n").map((line) => line.startsWith(indent) ? line.slice(indent.length) : line).join("\n") : rawTable;
|
|
1572
|
+
segments.push({
|
|
1573
|
+
table: true,
|
|
1574
|
+
text: table
|
|
1485
1575
|
});
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1576
|
+
cursor = range.end;
|
|
1577
|
+
}
|
|
1578
|
+
const tail = markdown.slice(cursor).replace(/^(?:[ \t]*\n)+/u, "");
|
|
1579
|
+
if (tail.trim()) segments.push({
|
|
1580
|
+
table: false,
|
|
1581
|
+
text: tail
|
|
1492
1582
|
});
|
|
1583
|
+
return segments;
|
|
1493
1584
|
}
|
|
1494
|
-
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1585
|
+
function prepareMatrixSingleText(text, opts) {
|
|
1586
|
+
const normalizedText = text.replace(/\r\n?/gu, "\n");
|
|
1587
|
+
const trimmedText = opts.preserveWhitespace ? normalizedText : normalizedText.trim();
|
|
1588
|
+
const cfg = requireRuntimeConfig(opts.cfg, "Matrix text preparation");
|
|
1589
|
+
const tableMode = opts.tableMode ?? getCore$2().channel.text.resolveMarkdownTableMode({
|
|
1590
|
+
cfg,
|
|
1591
|
+
channel: "matrix",
|
|
1592
|
+
accountId: opts.accountId,
|
|
1593
|
+
supportsBlockTables: MATRIX_FORMAT_PROFILE.constructs.table === "native"
|
|
1500
1594
|
});
|
|
1595
|
+
const convertedText = renderMatrixMarkdownTables(trimmedText, tableMode);
|
|
1596
|
+
const singleEventLimit = normalizeMatrixEventLimit(Math.min(getCore$2().channel.text.resolveTextChunkLimit(cfg, "matrix", opts.accountId), MATRIX_FORMAT_PROFILE.chunk.limit));
|
|
1597
|
+
const eventTextLength = Math.max(convertedText.length, markdownToMatrixBody(convertedText).length);
|
|
1501
1598
|
return {
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1599
|
+
trimmedText,
|
|
1600
|
+
convertedText,
|
|
1601
|
+
singleEventLimit,
|
|
1602
|
+
eventTextLength,
|
|
1603
|
+
fitsInSingleEvent: eventTextLength <= singleEventLimit,
|
|
1604
|
+
tableMode
|
|
1507
1605
|
};
|
|
1508
1606
|
}
|
|
1509
|
-
function
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1607
|
+
function chunkMatrixText(text, opts) {
|
|
1608
|
+
const preparedText = prepareMatrixSingleText(text, opts);
|
|
1609
|
+
if (preparedText.fitsInSingleEvent) return {
|
|
1610
|
+
...preparedText,
|
|
1611
|
+
chunks: preparedText.convertedText ? [preparedText.convertedText] : []
|
|
1612
|
+
};
|
|
1613
|
+
const cfg = requireRuntimeConfig(opts.cfg, "Matrix text chunking");
|
|
1614
|
+
const chunkMode = getCore$2().channel.text.resolveChunkMode(cfg, "matrix", opts.accountId);
|
|
1615
|
+
const collisionRedacted = hasMatrixSpoilerMetadataCollision(preparedText.convertedText) ? markdownToMatrixBody(preparedText.convertedText) : void 0;
|
|
1616
|
+
const chunkSegment = (segmentText) => {
|
|
1617
|
+
const protectedUnderline = protectMatrixUnderlineTags(hasMatrixSpoilerMetadataCollision(segmentText) ? markdownToMatrixBody(segmentText) : segmentText);
|
|
1618
|
+
const protectedSpoilers = protectMatrixSpoilerDelimiters(protectedUnderline.markdown);
|
|
1619
|
+
const wrapperReserve = (protectedSpoilers.markers ? 4 : 0) + (protectedUnderline.markers ? 7 : 0);
|
|
1620
|
+
const privateMarkers = [protectedSpoilers.markers, protectedUnderline.markers].flatMap((markers) => markers ? [
|
|
1621
|
+
markers.open,
|
|
1622
|
+
markers.close,
|
|
1623
|
+
markers.padding
|
|
1624
|
+
] : []);
|
|
1625
|
+
let reserve = wrapperReserve;
|
|
1626
|
+
while (reserve < preparedText.singleEventLimit) {
|
|
1627
|
+
const protectedChunks = getCore$2().channel.text.chunkMarkdownTextWithMode(protectedSpoilers.markdown, preparedText.singleEventLimit - reserve, chunkMode);
|
|
1628
|
+
const restored = restoreMatrixStyleChunks(protectedChunks, protectedSpoilers.markers, protectedUnderline.markers).filter((_, index) => {
|
|
1629
|
+
return privateMarkers.reduce((value, marker) => value.replaceAll(marker, ""), protectedChunks[index] ?? "").length > 0;
|
|
1630
|
+
});
|
|
1631
|
+
const overflow = Math.max(0, ...restored.map((chunk) => resolveMatrixChunkOverflow(chunk, preparedText.singleEventLimit)));
|
|
1632
|
+
if (overflow === 0) return restored;
|
|
1633
|
+
reserve += overflow;
|
|
1634
|
+
}
|
|
1635
|
+
throw new Error("Matrix text chunk limit is too small for formatted content");
|
|
1636
|
+
};
|
|
1637
|
+
const chunks = collisionRedacted !== void 0 ? chunkSegment(collisionRedacted) : preparedText.tableMode === "block" ? splitMatrixTableSegments(preparedText.convertedText).flatMap((segment) => {
|
|
1638
|
+
if (!segment.table) return chunkSegment(segment.text);
|
|
1639
|
+
return segment.text.length <= preparedText.singleEventLimit ? [segment.text] : chunkSegment(renderMatrixMarkdownTables(segment.text, "bullets"));
|
|
1640
|
+
}) : chunkSegment(preparedText.convertedText);
|
|
1512
1641
|
return {
|
|
1513
|
-
...
|
|
1514
|
-
|
|
1642
|
+
...preparedText,
|
|
1643
|
+
chunks
|
|
1515
1644
|
};
|
|
1516
1645
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region extensions/matrix/src/matrix/send/formatting.ts
|
|
1648
|
+
const getCore$1 = () => getMatrixRuntime();
|
|
1649
|
+
async function renderMatrixFormattedContent(params) {
|
|
1650
|
+
const markdown = params.markdown ?? "";
|
|
1651
|
+
const body = markdownToMatrixBody(markdown);
|
|
1652
|
+
if (params.includeMentions === false) return {
|
|
1653
|
+
body,
|
|
1654
|
+
html: markdownToMatrixHtml(markdown, { tableMode: params.tableMode }).trimEnd() || void 0
|
|
1655
|
+
};
|
|
1656
|
+
const { html, mentions } = await renderMarkdownToMatrixHtmlWithMentions({
|
|
1657
|
+
markdown,
|
|
1528
1658
|
client: params.client,
|
|
1529
|
-
|
|
1530
|
-
remoteUserId,
|
|
1531
|
-
selfUserId: params.selfUserId
|
|
1659
|
+
tableMode: params.tableMode
|
|
1532
1660
|
});
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1661
|
+
return {
|
|
1662
|
+
body,
|
|
1663
|
+
html,
|
|
1664
|
+
mentions
|
|
1537
1665
|
};
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1666
|
+
}
|
|
1667
|
+
function buildTextContent(body, relation, opts = {}) {
|
|
1668
|
+
const msgtype = opts.msgtype ?? MsgType.Text;
|
|
1669
|
+
return relation ? {
|
|
1670
|
+
msgtype,
|
|
1671
|
+
body,
|
|
1672
|
+
"m.relates_to": relation
|
|
1673
|
+
} : {
|
|
1674
|
+
msgtype,
|
|
1675
|
+
body
|
|
1542
1676
|
};
|
|
1543
|
-
try {
|
|
1544
|
-
const repaired = await persistMatrixDirectRoomMapping({
|
|
1545
|
-
client: params.client,
|
|
1546
|
-
remoteUserId,
|
|
1547
|
-
roomId: params.roomId
|
|
1548
|
-
});
|
|
1549
|
-
return {
|
|
1550
|
-
classifyAsDirect: true,
|
|
1551
|
-
repaired,
|
|
1552
|
-
roomId: params.roomId,
|
|
1553
|
-
reason: repaired ? "promoted" : "already-mapped"
|
|
1554
|
-
};
|
|
1555
|
-
} catch {
|
|
1556
|
-
return {
|
|
1557
|
-
classifyAsDirect: true,
|
|
1558
|
-
repaired: false,
|
|
1559
|
-
roomId: params.roomId,
|
|
1560
|
-
reason: "repair-failed"
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
1677
|
}
|
|
1564
|
-
async function
|
|
1565
|
-
const
|
|
1566
|
-
const selfUserId = normalizeOptionalString(await params.client.getUserId().catch(() => null)) ?? null;
|
|
1567
|
-
const mappedRoomIds = normalizeMappedRoomIds(await readMatrixDirectAccountData(params.client), remoteUserId);
|
|
1568
|
-
const mappedRooms = await Promise.all(mappedRoomIds.map(async (roomId) => await classifyDirectRoomCandidate({
|
|
1678
|
+
async function enrichMatrixFormattedContent(params) {
|
|
1679
|
+
const { body, html, mentions } = await renderMatrixFormattedContent({
|
|
1569
1680
|
client: params.client,
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
if (
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
joinedRooms = [];
|
|
1582
|
-
}
|
|
1583
|
-
const discoveredStrictRooms = [];
|
|
1584
|
-
for (const roomId of normalizeRoomIdList(joinedRooms)) {
|
|
1585
|
-
if (mappedRoomIds.includes(roomId)) continue;
|
|
1586
|
-
const candidate = await classifyDirectRoomCandidate({
|
|
1587
|
-
client: params.client,
|
|
1588
|
-
roomId,
|
|
1589
|
-
remoteUserId,
|
|
1590
|
-
selfUserId,
|
|
1591
|
-
source: "joined"
|
|
1592
|
-
});
|
|
1593
|
-
if (candidate.strict) discoveredStrictRooms.push(candidate);
|
|
1681
|
+
markdown: params.markdown,
|
|
1682
|
+
includeMentions: params.includeMentions,
|
|
1683
|
+
tableMode: params.tableMode
|
|
1684
|
+
});
|
|
1685
|
+
params.content.body = body || params.content.body;
|
|
1686
|
+
if (mentions) params.content["m.mentions"] = mentions;
|
|
1687
|
+
else delete params.content["m.mentions"];
|
|
1688
|
+
if (!html) {
|
|
1689
|
+
delete params.content.format;
|
|
1690
|
+
delete params.content.formatted_body;
|
|
1691
|
+
return;
|
|
1594
1692
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1693
|
+
params.content.format = "org.matrix.custom.html";
|
|
1694
|
+
params.content.formatted_body = html;
|
|
1695
|
+
}
|
|
1696
|
+
async function resolveMatrixMentionsForBody(params) {
|
|
1697
|
+
return await resolveMatrixMentionsInMarkdown({
|
|
1698
|
+
markdown: params.body ?? "",
|
|
1699
|
+
client: params.client
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
function normalizeMentionUserIds(value) {
|
|
1703
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
|
|
1704
|
+
}
|
|
1705
|
+
function extractMatrixMentions(content) {
|
|
1706
|
+
const rawMentions = content?.["m.mentions"];
|
|
1707
|
+
if (!rawMentions || typeof rawMentions !== "object") return {};
|
|
1708
|
+
const mentions = rawMentions;
|
|
1709
|
+
const normalized = {};
|
|
1710
|
+
const userIds = normalizeMentionUserIds(mentions.user_ids);
|
|
1711
|
+
if (userIds.length > 0) normalized.user_ids = userIds;
|
|
1712
|
+
if (mentions.room === true) normalized.room = true;
|
|
1713
|
+
return normalized;
|
|
1714
|
+
}
|
|
1715
|
+
function diffMatrixMentions(current, previous) {
|
|
1716
|
+
const previousUserIds = new Set(previous.user_ids ?? []);
|
|
1717
|
+
const newUserIds = (current.user_ids ?? []).filter((userId) => !previousUserIds.has(userId));
|
|
1718
|
+
const delta = {};
|
|
1719
|
+
if (newUserIds.length > 0) delta.user_ids = newUserIds;
|
|
1720
|
+
if (current.room && !previous.room) delta.room = true;
|
|
1721
|
+
return delta;
|
|
1605
1722
|
}
|
|
1606
|
-
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
const
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
remoteUserId,
|
|
1617
|
-
roomIds: [activeRoomId, ...inspected.discoveredStrictRoomIds]
|
|
1618
|
-
});
|
|
1619
|
-
return {
|
|
1620
|
-
...inspected,
|
|
1621
|
-
activeRoomId,
|
|
1622
|
-
createdRoomId,
|
|
1623
|
-
changed: mappingWrite.changed,
|
|
1624
|
-
directContentBefore: mappingWrite.directContentBefore,
|
|
1625
|
-
directContentAfter: mappingWrite.directContentAfter
|
|
1723
|
+
function buildReplyRelation(replyToId) {
|
|
1724
|
+
const trimmed = replyToId?.trim();
|
|
1725
|
+
if (!trimmed) return;
|
|
1726
|
+
return { "m.in_reply_to": { event_id: trimmed } };
|
|
1727
|
+
}
|
|
1728
|
+
function buildThreadRelation(threadId, replyToId) {
|
|
1729
|
+
const trimmed = threadId.trim();
|
|
1730
|
+
const relation = {
|
|
1731
|
+
rel_type: RelationType.Thread,
|
|
1732
|
+
event_id: trimmed
|
|
1626
1733
|
};
|
|
1734
|
+
const fallbackReplyToId = replyToId?.trim();
|
|
1735
|
+
if (fallbackReplyToId) {
|
|
1736
|
+
relation.is_falling_back = true;
|
|
1737
|
+
relation["m.in_reply_to"] = { event_id: fallbackReplyToId };
|
|
1738
|
+
}
|
|
1739
|
+
return relation;
|
|
1627
1740
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1741
|
+
function resolveMatrixMsgType(contentType, _fileName) {
|
|
1742
|
+
switch (getCore$1().media.mediaKindFromMime(contentType ?? "")) {
|
|
1743
|
+
case "image": return MsgType.Image;
|
|
1744
|
+
case "audio": return MsgType.Audio;
|
|
1745
|
+
case "video": return MsgType.Video;
|
|
1746
|
+
default: return MsgType.File;
|
|
1747
|
+
}
|
|
1634
1748
|
}
|
|
1635
|
-
function
|
|
1636
|
-
|
|
1749
|
+
function resolveMatrixVoiceDecision(opts) {
|
|
1750
|
+
if (!opts.wantsVoice) return { useVoice: false };
|
|
1751
|
+
if (isMatrixVoiceCompatibleAudio(opts)) return { useVoice: true };
|
|
1752
|
+
return { useVoice: false };
|
|
1637
1753
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
const created = /* @__PURE__ */ new Map();
|
|
1644
|
-
directRoomCacheByClient.set(client, created);
|
|
1645
|
-
return created;
|
|
1754
|
+
function isMatrixVoiceCompatibleAudio(opts) {
|
|
1755
|
+
return isVoiceMessageCompatibleAudio({
|
|
1756
|
+
contentType: opts.contentType,
|
|
1757
|
+
fileName: opts.fileName
|
|
1758
|
+
});
|
|
1646
1759
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1760
|
+
//#endregion
|
|
1761
|
+
//#region extensions/matrix/src/matrix/send/media.ts
|
|
1762
|
+
const getCore = () => getMatrixRuntime();
|
|
1763
|
+
function buildMatrixMediaInfo(params) {
|
|
1764
|
+
const base = {};
|
|
1765
|
+
if (Number.isFinite(params.size)) base.size = params.size;
|
|
1766
|
+
if (params.mimetype) base.mimetype = params.mimetype;
|
|
1767
|
+
if (params.imageInfo) {
|
|
1768
|
+
const dimensional = {
|
|
1769
|
+
...base,
|
|
1770
|
+
...params.imageInfo
|
|
1771
|
+
};
|
|
1772
|
+
if (typeof params.durationMs === "number") return {
|
|
1773
|
+
...dimensional,
|
|
1774
|
+
duration: params.durationMs
|
|
1775
|
+
};
|
|
1776
|
+
return dimensional;
|
|
1653
1777
|
}
|
|
1778
|
+
if (typeof params.durationMs === "number") return {
|
|
1779
|
+
...base,
|
|
1780
|
+
duration: params.durationMs
|
|
1781
|
+
};
|
|
1782
|
+
if (Object.keys(base).length === 0) return;
|
|
1783
|
+
return base;
|
|
1654
1784
|
}
|
|
1655
|
-
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
if (cached && await isStrictDirectRoom({
|
|
1662
|
-
client,
|
|
1663
|
-
roomId: cached,
|
|
1664
|
-
remoteUserId: trimmed,
|
|
1665
|
-
selfUserId
|
|
1666
|
-
})) return cached;
|
|
1667
|
-
if (cached) directRoomCache.delete(trimmed);
|
|
1668
|
-
const inspection = await inspectMatrixDirectRooms({
|
|
1669
|
-
client,
|
|
1670
|
-
remoteUserId: trimmed
|
|
1785
|
+
function buildMediaContent(params) {
|
|
1786
|
+
const info = buildMatrixMediaInfo({
|
|
1787
|
+
size: params.size,
|
|
1788
|
+
mimetype: params.mimetype,
|
|
1789
|
+
durationMs: params.durationMs,
|
|
1790
|
+
imageInfo: params.imageInfo
|
|
1671
1791
|
});
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1792
|
+
const base = {
|
|
1793
|
+
msgtype: params.msgtype,
|
|
1794
|
+
body: params.body,
|
|
1795
|
+
filename: params.filename,
|
|
1796
|
+
info: info ?? void 0
|
|
1797
|
+
};
|
|
1798
|
+
if (!params.file && params.url) base.url = params.url;
|
|
1799
|
+
if (params.file) base.file = params.file;
|
|
1800
|
+
if (params.isVoice) {
|
|
1801
|
+
base["org.matrix.msc3245.voice"] = {};
|
|
1802
|
+
if (typeof params.durationMs === "number") base["org.matrix.msc1767.audio"] = { duration: params.durationMs };
|
|
1680
1803
|
}
|
|
1681
|
-
|
|
1804
|
+
if (params.relation) base["m.relates_to"] = params.relation;
|
|
1805
|
+
return base;
|
|
1682
1806
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1807
|
+
const THUMBNAIL_MAX_SIDE = 800;
|
|
1808
|
+
const THUMBNAIL_QUALITY = 80;
|
|
1809
|
+
const AIFC_IMA4_BYTES_PER_CHANNEL_PACKET = 34;
|
|
1810
|
+
const AIFC_IMA4_FRAMES_PER_PACKET = 64;
|
|
1811
|
+
function resolveAifcIma4DurationSeconds(buffer, sampleRate) {
|
|
1812
|
+
if (!sampleRate || !Number.isFinite(sampleRate) || buffer.length < 12 || buffer.toString("ascii", 0, 4) !== "FORM" || buffer.toString("ascii", 8, 12) !== "AIFC") return;
|
|
1813
|
+
let channels;
|
|
1814
|
+
let declaredFrameCount;
|
|
1815
|
+
let soundDataBytes;
|
|
1816
|
+
for (let offset = 12; offset + 8 <= buffer.length;) {
|
|
1817
|
+
const chunkType = buffer.toString("ascii", offset, offset + 4);
|
|
1818
|
+
const chunkSize = buffer.readUInt32BE(offset + 4);
|
|
1819
|
+
const chunkStart = offset + 8;
|
|
1820
|
+
if (chunkSize > buffer.length - chunkStart) return;
|
|
1821
|
+
if (chunkType === "COMM") {
|
|
1822
|
+
if (chunkSize < 22 || buffer.toString("ascii", chunkStart + 18, chunkStart + 22) !== "ima4") return;
|
|
1823
|
+
channels = buffer.readUInt16BE(chunkStart);
|
|
1824
|
+
declaredFrameCount = buffer.readUInt32BE(chunkStart + 2);
|
|
1825
|
+
} else if (chunkType === "SSND") {
|
|
1826
|
+
if (chunkSize < 8) return;
|
|
1827
|
+
const soundDataOffset = buffer.readUInt32BE(chunkStart);
|
|
1828
|
+
if (soundDataOffset > chunkSize - 8) return;
|
|
1829
|
+
soundDataBytes = chunkSize - 8 - soundDataOffset;
|
|
1830
|
+
}
|
|
1831
|
+
offset = chunkStart + chunkSize + (chunkSize & 1);
|
|
1691
1832
|
}
|
|
1692
|
-
return
|
|
1833
|
+
if (!channels || !declaredFrameCount || !soundDataBytes) return;
|
|
1834
|
+
const packetSize = channels * AIFC_IMA4_BYTES_PER_CHANNEL_PACKET;
|
|
1835
|
+
if (soundDataBytes % packetSize !== 0) return;
|
|
1836
|
+
const packetCount = soundDataBytes / packetSize;
|
|
1837
|
+
if (packetCount !== declaredFrameCount) return;
|
|
1838
|
+
return packetCount * AIFC_IMA4_FRAMES_PER_PACKET / sampleRate;
|
|
1839
|
+
}
|
|
1840
|
+
async function prepareImageInfo(params) {
|
|
1841
|
+
const meta = await getCore().media.getImageMetadata(params.buffer).catch(() => null);
|
|
1842
|
+
if (!meta) return;
|
|
1843
|
+
const imageInfo = {
|
|
1844
|
+
w: meta.width,
|
|
1845
|
+
h: meta.height
|
|
1846
|
+
};
|
|
1847
|
+
if (Math.max(meta.width, meta.height) > THUMBNAIL_MAX_SIDE) try {
|
|
1848
|
+
const thumbBuffer = await getCore().media.resizeToJpeg({
|
|
1849
|
+
buffer: params.buffer,
|
|
1850
|
+
maxSide: THUMBNAIL_MAX_SIDE,
|
|
1851
|
+
quality: THUMBNAIL_QUALITY,
|
|
1852
|
+
withoutEnlargement: true
|
|
1853
|
+
});
|
|
1854
|
+
const thumbMeta = await getCore().media.getImageMetadata(thumbBuffer).catch(() => null);
|
|
1855
|
+
const result = await uploadMediaWithEncryption(params.client, params.roomId, thumbBuffer, {
|
|
1856
|
+
contentType: "image/jpeg",
|
|
1857
|
+
filename: "thumbnail.jpg"
|
|
1858
|
+
});
|
|
1859
|
+
if (result.file) imageInfo.thumbnail_file = result.file;
|
|
1860
|
+
else imageInfo.thumbnail_url = result.url;
|
|
1861
|
+
if (thumbMeta) imageInfo.thumbnail_info = {
|
|
1862
|
+
w: thumbMeta.width,
|
|
1863
|
+
h: thumbMeta.height,
|
|
1864
|
+
mimetype: "image/jpeg",
|
|
1865
|
+
size: thumbBuffer.byteLength
|
|
1866
|
+
};
|
|
1867
|
+
} catch {}
|
|
1868
|
+
return imageInfo;
|
|
1869
|
+
}
|
|
1870
|
+
async function resolveMediaDurationMs(params) {
|
|
1871
|
+
if (params.kind !== "audio" && params.kind !== "video") return;
|
|
1872
|
+
try {
|
|
1873
|
+
const fileInfo = params.contentType || params.fileName ? {
|
|
1874
|
+
mimeType: params.contentType,
|
|
1875
|
+
size: params.buffer.byteLength,
|
|
1876
|
+
path: params.fileName
|
|
1877
|
+
} : void 0;
|
|
1878
|
+
const metadata = await parseBuffer(params.buffer, fileInfo, {
|
|
1879
|
+
duration: true,
|
|
1880
|
+
skipCovers: true
|
|
1881
|
+
});
|
|
1882
|
+
const durationSeconds = resolveAifcIma4DurationSeconds(params.buffer, metadata.format.sampleRate) ?? metadata.format.duration;
|
|
1883
|
+
if (typeof durationSeconds === "number" && Number.isFinite(durationSeconds)) return Math.max(0, Math.round(durationSeconds * 1e3));
|
|
1884
|
+
} catch {}
|
|
1885
|
+
}
|
|
1886
|
+
async function uploadMediaWithEncryption(client, roomId, buffer, params) {
|
|
1887
|
+
if (await client.prepareRoomForMessageSend(roomId) === "m.room.encrypted") {
|
|
1888
|
+
if (!client.crypto) throw new Error("Encrypted Matrix room: enable encryption before uploading media");
|
|
1889
|
+
const encrypted = await client.crypto.encryptMedia(buffer);
|
|
1890
|
+
const mxc = await client.uploadContent(encrypted.buffer, "application/octet-stream");
|
|
1891
|
+
return {
|
|
1892
|
+
url: mxc,
|
|
1893
|
+
file: {
|
|
1894
|
+
url: mxc,
|
|
1895
|
+
...encrypted.file
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
return { url: await client.uploadContent(buffer, params.contentType, params.filename) };
|
|
1693
1900
|
}
|
|
1694
1901
|
//#endregion
|
|
1695
1902
|
//#region extensions/matrix/src/matrix/send.ts
|
|
@@ -1707,16 +1914,28 @@ var send_exports = /* @__PURE__ */ __exportAll({
|
|
|
1707
1914
|
sendTypingMatrix: () => sendTypingMatrix
|
|
1708
1915
|
});
|
|
1709
1916
|
function createMatrixSendReceipt(params) {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1917
|
+
const firstEvent = params.events[0];
|
|
1918
|
+
const receipt = createMessageReceiptFromOutboundResults({
|
|
1919
|
+
kind: firstEvent?.kind ?? "text",
|
|
1920
|
+
...firstEvent?.replyToId ? { replyToId: firstEvent.replyToId } : {},
|
|
1713
1921
|
...params.threadId ? { threadId: params.threadId } : {},
|
|
1714
|
-
results: params.
|
|
1922
|
+
results: params.events.map(({ messageId }) => ({
|
|
1715
1923
|
channel: "matrix",
|
|
1716
1924
|
messageId,
|
|
1717
1925
|
roomId: params.roomId
|
|
1718
1926
|
}))
|
|
1719
1927
|
});
|
|
1928
|
+
receipt.parts = receipt.parts.map((part, index) => {
|
|
1929
|
+
const event = params.events[index];
|
|
1930
|
+
const actualPart = {
|
|
1931
|
+
...part,
|
|
1932
|
+
kind: event.kind
|
|
1933
|
+
};
|
|
1934
|
+
if (event.replyToId) actualPart.replyToId = event.replyToId;
|
|
1935
|
+
else delete actualPart.replyToId;
|
|
1936
|
+
return actualPart;
|
|
1937
|
+
});
|
|
1938
|
+
return receipt;
|
|
1720
1939
|
}
|
|
1721
1940
|
function isMatrixClient(value) {
|
|
1722
1941
|
return typeof value.sendEvent === "function";
|
|
@@ -1769,8 +1988,13 @@ async function resolvePreviousEditMentions(params) {
|
|
|
1769
1988
|
});
|
|
1770
1989
|
}
|
|
1771
1990
|
async function sendMessageMatrix(to, message, opts) {
|
|
1772
|
-
const
|
|
1773
|
-
if (!
|
|
1991
|
+
const messageText = message?.trimEnd() ?? "";
|
|
1992
|
+
if (!messageText.trim() && !opts.mediaUrl) throw new Error("Matrix send requires text or media");
|
|
1993
|
+
const durableIdentity = resolveMatrixDurableDeliveryIdentity({
|
|
1994
|
+
queueId: opts.deliveryQueueId,
|
|
1995
|
+
partIndex: opts.deliveryPartIndex,
|
|
1996
|
+
partCount: opts.deliveryPartCount
|
|
1997
|
+
});
|
|
1774
1998
|
return await withResolvedMatrixSendClient({
|
|
1775
1999
|
client: opts.client,
|
|
1776
2000
|
cfg: opts.cfg,
|
|
@@ -1778,135 +2002,174 @@ async function sendMessageMatrix(to, message, opts) {
|
|
|
1778
2002
|
accountId: opts.accountId
|
|
1779
2003
|
}, async (client) => {
|
|
1780
2004
|
const roomId = await resolveMatrixRoomId(client, to);
|
|
2005
|
+
const wireEventType = await client.prepareRoomForMessageSend(roomId);
|
|
1781
2006
|
const cfg = requireRuntimeConfig(opts.cfg, "Matrix send");
|
|
1782
|
-
const { chunks, tableMode } = chunkMatrixText(trimmedMessage, {
|
|
1783
|
-
cfg,
|
|
1784
|
-
accountId: opts.accountId
|
|
1785
|
-
});
|
|
1786
2007
|
const threadId = normalizeThreadId(opts.threadId);
|
|
1787
|
-
const
|
|
1788
|
-
let
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
receipt: createMatrixSendReceipt({
|
|
1801
|
-
roomId,
|
|
1802
|
-
platformMessageIds: [eventId],
|
|
1803
|
-
kind,
|
|
1804
|
-
replyToId: opts.replyToId,
|
|
1805
|
-
threadId
|
|
1806
|
-
}),
|
|
1807
|
-
content: visibleContent
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
return eventId;
|
|
1811
|
-
};
|
|
1812
|
-
const platformMessageIds = [];
|
|
1813
|
-
const acceptedContents = [];
|
|
1814
|
-
let lastMessageId = "";
|
|
1815
|
-
let receiptKind = "text";
|
|
1816
|
-
if (opts.mediaUrl) {
|
|
1817
|
-
const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg);
|
|
1818
|
-
const media = await loadOutboundMediaFromUrl(opts.mediaUrl, {
|
|
1819
|
-
maxBytes,
|
|
1820
|
-
mediaAccess: opts.mediaAccess,
|
|
1821
|
-
mediaLocalRoots: opts.mediaLocalRoots,
|
|
1822
|
-
mediaReadFile: opts.mediaReadFile
|
|
1823
|
-
});
|
|
1824
|
-
const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, {
|
|
1825
|
-
contentType: media.contentType,
|
|
1826
|
-
filename: media.fileName
|
|
1827
|
-
});
|
|
1828
|
-
const durationMs = await resolveMediaDurationMs({
|
|
1829
|
-
buffer: media.buffer,
|
|
1830
|
-
contentType: media.contentType,
|
|
1831
|
-
fileName: media.fileName,
|
|
1832
|
-
kind: media.kind === "sticker" ? "unknown" : media.kind ?? "unknown"
|
|
1833
|
-
});
|
|
1834
|
-
const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName);
|
|
1835
|
-
const { useVoice } = resolveMatrixVoiceDecision({
|
|
1836
|
-
wantsVoice: opts.audioAsVoice === true,
|
|
1837
|
-
contentType: media.contentType,
|
|
1838
|
-
fileName: media.fileName
|
|
1839
|
-
});
|
|
1840
|
-
const msgtype = useVoice ? MsgType.Audio : baseMsgType;
|
|
1841
|
-
receiptKind = useVoice ? "voice" : "media";
|
|
1842
|
-
const imageInfo = msgtype === MsgType.Image ? await prepareImageInfo({
|
|
1843
|
-
buffer: media.buffer,
|
|
1844
|
-
client,
|
|
1845
|
-
encrypted: Boolean(uploaded.file)
|
|
1846
|
-
}) : void 0;
|
|
1847
|
-
const [firstChunk, ...rest] = chunks;
|
|
1848
|
-
const captionMarkdown = useVoice ? "" : firstChunk ?? "";
|
|
1849
|
-
const content = buildMediaContent({
|
|
1850
|
-
msgtype,
|
|
1851
|
-
body: useVoice ? "Voice message" : captionMarkdown || media.fileName || "(file)",
|
|
1852
|
-
url: uploaded.url,
|
|
1853
|
-
file: uploaded.file,
|
|
1854
|
-
filename: media.fileName,
|
|
1855
|
-
mimetype: media.contentType,
|
|
1856
|
-
size: media.buffer.byteLength,
|
|
1857
|
-
durationMs,
|
|
1858
|
-
relation,
|
|
1859
|
-
isVoice: useVoice,
|
|
1860
|
-
imageInfo
|
|
1861
|
-
});
|
|
1862
|
-
await enrichMatrixFormattedContent({
|
|
1863
|
-
client,
|
|
1864
|
-
content,
|
|
1865
|
-
markdown: captionMarkdown,
|
|
1866
|
-
tableMode
|
|
2008
|
+
const transactionScopeId = durableIdentity ? await client.getTransactionScopeId() : void 0;
|
|
2009
|
+
let plannedEvents = (durableIdentity ? await loadMatrixDeliveryPlan({
|
|
2010
|
+
identity: durableIdentity,
|
|
2011
|
+
accountId: opts.accountId,
|
|
2012
|
+
roomId,
|
|
2013
|
+
transactionScopeId,
|
|
2014
|
+
wireEventType
|
|
2015
|
+
}) : null)?.events;
|
|
2016
|
+
if (!plannedEvents) {
|
|
2017
|
+
const { chunks, tableMode } = chunkMatrixText(messageText, {
|
|
2018
|
+
cfg,
|
|
2019
|
+
accountId: opts.accountId,
|
|
2020
|
+
preserveWhitespace: true
|
|
1867
2021
|
});
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2022
|
+
const relation = threadId ? buildThreadRelation(threadId, opts.replyToId) : buildReplyRelation(opts.replyToId);
|
|
2023
|
+
let pendingExtraContent = opts.extraContent;
|
|
2024
|
+
const events = [];
|
|
2025
|
+
const prepareContent = (content, receiptKind) => {
|
|
2026
|
+
events.push({
|
|
2027
|
+
content: withMatrixExtraContentFields(content, pendingExtraContent),
|
|
2028
|
+
receiptKind
|
|
2029
|
+
});
|
|
2030
|
+
pendingExtraContent = void 0;
|
|
2031
|
+
};
|
|
2032
|
+
if (opts.mediaUrl) {
|
|
2033
|
+
const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg);
|
|
2034
|
+
const media = await loadOutboundMediaFromUrl(opts.mediaUrl, {
|
|
2035
|
+
maxBytes,
|
|
2036
|
+
mediaAccess: opts.mediaAccess,
|
|
2037
|
+
mediaLocalRoots: opts.mediaLocalRoots,
|
|
2038
|
+
mediaReadFile: opts.mediaReadFile
|
|
2039
|
+
});
|
|
2040
|
+
const uploaded = await uploadMediaWithEncryption(client, roomId, media.buffer, {
|
|
2041
|
+
contentType: media.contentType,
|
|
2042
|
+
filename: media.fileName
|
|
2043
|
+
});
|
|
2044
|
+
const durationMs = await resolveMediaDurationMs({
|
|
2045
|
+
buffer: media.buffer,
|
|
2046
|
+
contentType: media.contentType,
|
|
2047
|
+
fileName: media.fileName,
|
|
2048
|
+
kind: media.kind === "sticker" ? "unknown" : media.kind ?? "unknown"
|
|
2049
|
+
});
|
|
2050
|
+
const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName);
|
|
2051
|
+
const { useVoice } = resolveMatrixVoiceDecision({
|
|
2052
|
+
wantsVoice: opts.audioAsVoice === true,
|
|
2053
|
+
contentType: media.contentType,
|
|
2054
|
+
fileName: media.fileName
|
|
2055
|
+
});
|
|
2056
|
+
const msgtype = useVoice ? MsgType.Audio : baseMsgType;
|
|
2057
|
+
const receiptKind = useVoice ? "voice" : "media";
|
|
2058
|
+
const imageInfo = msgtype === MsgType.Image ? await prepareImageInfo({
|
|
2059
|
+
buffer: media.buffer,
|
|
2060
|
+
client,
|
|
2061
|
+
roomId
|
|
2062
|
+
}) : void 0;
|
|
2063
|
+
const [firstChunk, ...rest] = chunks;
|
|
2064
|
+
const captionMarkdown = useVoice ? "" : firstChunk ?? "";
|
|
2065
|
+
const content = buildMediaContent({
|
|
2066
|
+
msgtype,
|
|
2067
|
+
body: useVoice ? "Voice message" : captionMarkdown || media.fileName || "(file)",
|
|
2068
|
+
url: uploaded.url,
|
|
2069
|
+
file: uploaded.file,
|
|
2070
|
+
filename: media.fileName,
|
|
2071
|
+
mimetype: media.contentType,
|
|
2072
|
+
size: media.buffer.byteLength,
|
|
2073
|
+
durationMs,
|
|
2074
|
+
relation,
|
|
2075
|
+
isVoice: useVoice,
|
|
2076
|
+
imageInfo
|
|
2077
|
+
});
|
|
2078
|
+
await enrichMatrixFormattedContent({
|
|
2079
|
+
client,
|
|
2080
|
+
content,
|
|
2081
|
+
markdown: captionMarkdown,
|
|
2082
|
+
tableMode
|
|
2083
|
+
});
|
|
2084
|
+
prepareContent(content, receiptKind);
|
|
2085
|
+
const textChunks = useVoice ? chunks : rest;
|
|
2086
|
+
const followupRelation = useVoice || threadId ? relation : void 0;
|
|
2087
|
+
for (const chunk of textChunks) {
|
|
2088
|
+
if (!chunk.trim()) continue;
|
|
2089
|
+
const followup = buildTextContent(chunk, followupRelation);
|
|
2090
|
+
await enrichMatrixFormattedContent({
|
|
2091
|
+
client,
|
|
2092
|
+
content: followup,
|
|
2093
|
+
markdown: chunk,
|
|
2094
|
+
tableMode
|
|
2095
|
+
});
|
|
2096
|
+
prepareContent(followup, "text");
|
|
2097
|
+
}
|
|
2098
|
+
} else for (const chunk of chunks.length ? chunks : [""]) {
|
|
2099
|
+
if (!chunk.trim()) continue;
|
|
2100
|
+
const content = buildTextContent(chunk, relation);
|
|
1877
2101
|
await enrichMatrixFormattedContent({
|
|
1878
2102
|
client,
|
|
1879
|
-
content
|
|
1880
|
-
markdown:
|
|
2103
|
+
content,
|
|
2104
|
+
markdown: chunk,
|
|
1881
2105
|
tableMode
|
|
1882
2106
|
});
|
|
1883
|
-
|
|
1884
|
-
lastMessageId = followupEventId ?? lastMessageId;
|
|
1885
|
-
if (followupEventId) platformMessageIds.push(followupEventId);
|
|
2107
|
+
prepareContent(content, "text");
|
|
1886
2108
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
2109
|
+
plannedEvents = durableIdentity ? createMatrixPlannedEvents({
|
|
2110
|
+
identity: durableIdentity,
|
|
2111
|
+
events
|
|
2112
|
+
}) : events.map((event) => ({
|
|
2113
|
+
content: event.content,
|
|
2114
|
+
receiptKind: event.receiptKind,
|
|
2115
|
+
transactionId: ""
|
|
2116
|
+
}));
|
|
2117
|
+
}
|
|
2118
|
+
if (opts.mediaUrl) await client.prepareRoomForMessageSend(roomId, plannedEvents[0]?.content);
|
|
2119
|
+
let platformDispatchStarted = false;
|
|
2120
|
+
if (!durableIdentity) {
|
|
2121
|
+
await opts.onPlatformSendDispatch?.();
|
|
2122
|
+
platformDispatchStarted = true;
|
|
2123
|
+
}
|
|
2124
|
+
const acceptedEvents = [];
|
|
2125
|
+
const acceptedContents = [];
|
|
2126
|
+
let lastMessageId = "";
|
|
2127
|
+
for (const planned of plannedEvents) {
|
|
2128
|
+
const eventId = await client.sendMessage(roomId, planned.content, planned.transactionId || void 0, durableIdentity ? async (dispatch) => {
|
|
2129
|
+
await persistMatrixDeliveryPlan({
|
|
2130
|
+
identity: durableIdentity,
|
|
2131
|
+
accountId: opts.accountId,
|
|
2132
|
+
roomId,
|
|
2133
|
+
transactionScopeId,
|
|
2134
|
+
wireEventType: dispatch.eventType,
|
|
2135
|
+
events: plannedEvents,
|
|
2136
|
+
dispatch
|
|
2137
|
+
});
|
|
2138
|
+
if (!platformDispatchStarted) {
|
|
2139
|
+
await opts.onPlatformSendDispatch?.();
|
|
2140
|
+
platformDispatchStarted = true;
|
|
2141
|
+
}
|
|
2142
|
+
} : void 0);
|
|
2143
|
+
lastMessageId = eventId || lastMessageId;
|
|
2144
|
+
if (!eventId) continue;
|
|
2145
|
+
const eventReplyToId = planned.content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
|
2146
|
+
const acceptedEvent = {
|
|
2147
|
+
messageId: eventId,
|
|
2148
|
+
kind: planned.receiptKind,
|
|
2149
|
+
...eventReplyToId ? { replyToId: eventReplyToId } : {}
|
|
2150
|
+
};
|
|
2151
|
+
acceptedEvents.push(acceptedEvent);
|
|
2152
|
+
const visibleContent = planned.content.body ?? "";
|
|
2153
|
+
acceptedContents.push(visibleContent);
|
|
2154
|
+
await opts.onDeliveryResult?.({
|
|
2155
|
+
messageId: eventId,
|
|
2156
|
+
roomId,
|
|
2157
|
+
primaryMessageId: eventId,
|
|
2158
|
+
receipt: createMatrixSendReceipt({
|
|
2159
|
+
roomId,
|
|
2160
|
+
events: [acceptedEvent],
|
|
2161
|
+
threadId
|
|
2162
|
+
}),
|
|
2163
|
+
content: visibleContent
|
|
1896
2164
|
});
|
|
1897
|
-
const eventId = await sendContent(content, "text");
|
|
1898
|
-
lastMessageId = eventId ?? lastMessageId;
|
|
1899
|
-
if (eventId) platformMessageIds.push(eventId);
|
|
1900
2165
|
}
|
|
1901
2166
|
return {
|
|
1902
2167
|
messageId: lastMessageId || "unknown",
|
|
1903
2168
|
roomId,
|
|
1904
|
-
primaryMessageId:
|
|
2169
|
+
primaryMessageId: acceptedEvents[0]?.messageId ?? (lastMessageId || "unknown"),
|
|
1905
2170
|
receipt: createMatrixSendReceipt({
|
|
1906
2171
|
roomId,
|
|
1907
|
-
|
|
1908
|
-
kind: receiptKind,
|
|
1909
|
-
replyToId: opts.replyToId,
|
|
2172
|
+
events: acceptedEvents,
|
|
1910
2173
|
threadId
|
|
1911
2174
|
}),
|
|
1912
2175
|
content: acceptedContents.join("\n")
|
|
@@ -1967,11 +2230,12 @@ async function sendReadReceiptMatrix(roomId, eventId, client) {
|
|
|
1967
2230
|
});
|
|
1968
2231
|
}
|
|
1969
2232
|
async function sendSingleTextMessageMatrix(roomId, text, opts) {
|
|
1970
|
-
const { trimmedText, convertedText, singleEventLimit, eventTextLength, fitsInSingleEvent, tableMode } = prepareMatrixSingleText(text, {
|
|
2233
|
+
const { trimmedText, convertedText, singleEventLimit, eventTextLength, fitsInSingleEvent, tableMode } = prepareMatrixSingleText(text.trimEnd(), {
|
|
1971
2234
|
cfg: opts.cfg,
|
|
1972
|
-
accountId: opts.accountId
|
|
2235
|
+
accountId: opts.accountId,
|
|
2236
|
+
preserveWhitespace: true
|
|
1973
2237
|
});
|
|
1974
|
-
if (!trimmedText) throw new Error("Matrix single-message send requires text");
|
|
2238
|
+
if (!trimmedText.trim()) throw new Error("Matrix single-message send requires text");
|
|
1975
2239
|
if (!fitsInSingleEvent) throw new Error(`Matrix single-message text exceeds limit (${eventTextLength} > ${singleEventLimit})`);
|
|
1976
2240
|
return await withResolvedMatrixSendClient({
|
|
1977
2241
|
client: opts.client,
|
|
@@ -1991,15 +2255,18 @@ async function sendSingleTextMessageMatrix(roomId, text, opts) {
|
|
|
1991
2255
|
});
|
|
1992
2256
|
if (opts.live) content[MSC4357_LIVE_KEY] = {};
|
|
1993
2257
|
const eventId = await client.sendMessage(resolvedRoom, content);
|
|
2258
|
+
const replyToId = content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
|
1994
2259
|
return {
|
|
1995
2260
|
messageId: eventId ?? "unknown",
|
|
1996
2261
|
roomId: resolvedRoom,
|
|
1997
2262
|
primaryMessageId: eventId ?? "unknown",
|
|
1998
2263
|
receipt: createMatrixSendReceipt({
|
|
1999
2264
|
roomId: resolvedRoom,
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2265
|
+
events: eventId ? [{
|
|
2266
|
+
messageId: eventId,
|
|
2267
|
+
kind: "text",
|
|
2268
|
+
...replyToId ? { replyToId } : {}
|
|
2269
|
+
}] : [],
|
|
2003
2270
|
threadId: normalizedThreadId
|
|
2004
2271
|
}),
|
|
2005
2272
|
content: content.body
|
|
@@ -2074,4 +2341,4 @@ async function reactMatrixMessage(roomId, messageId, emoji, opts) {
|
|
|
2074
2341
|
});
|
|
2075
2342
|
}
|
|
2076
2343
|
//#endregion
|
|
2077
|
-
export {
|
|
2344
|
+
export { promoteMatrixDirectRoomCandidate as C, MsgType as E, direct_management_exports as S, MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY as T, parsePollStartContent as _, sendSingleTextMessageMatrix as a, reconcileMatrixUnknownSend as b, chunkMatrixText as c, buildPollResultsSummary as d, formatPollAsText as f, parsePollStart as g, isPollStartType as h, sendPollMatrix as i, prepareMatrixSingleText as l, isPollEventType as m, reactMatrixMessage as n, sendTypingMatrix as o, formatPollResultsAsText as p, sendMessageMatrix as r, send_exports as s, editMessageMatrix as t, buildPollResponseContent as u, resolvePollReferenceEventId as v, repairMatrixDirectRooms as w, resolveMatrixRoomId as x, cleanupMatrixDeliveryPlans as y };
|