@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.
Files changed (92) hide show
  1. package/dist/{account-selection-kqquRzCf.js → account-selection-CWuhuFn1.js} +7 -8
  2. package/dist/{accounts-DfngqebB.js → accounts-BCzyubQt.js} +3 -3
  3. package/dist/api.js +8 -11
  4. package/dist/{approval-handler.runtime-DSeaIsGa.js → approval-handler.runtime-CSvueU9P.js} +7 -6
  5. package/dist/{approval-ids-BdJGPAJT.js → approval-ids-CEcf-EiM.js} +1 -1
  6. package/dist/{approval-reaction-auth-BjOpELxu.js → approval-reaction-auth-CziKXatm.js} +2 -2
  7. package/dist/{approval-reactions-D-BxBK_L.js → approval-reactions-tXh9KYrW.js} +2 -8
  8. package/dist/auth-presence.js +2 -2
  9. package/dist/{channel-vj788dNZ.js → channel-CzQw5OZN.js} +376 -148
  10. package/dist/channel-plugin-api.js +1 -1
  11. package/dist/{channel.runtime-CLoF2S9T.js → channel.runtime-I9pBRQb3.js} +79 -55
  12. package/dist/{config-schema-Djxw3C6b.js → channel.setup-aNaa3Uyn.js} +51 -10
  13. package/dist/{cli-DMqfDE-q.js → cli-iR6QJ5U0.js} +16 -16
  14. package/dist/{cli-metadata-C7D53SND.js → cli-metadata-BmUYWjuM.js} +1 -1
  15. package/dist/cli-metadata.js +1 -1
  16. package/dist/client-BBQdBG7t.js +799 -0
  17. package/dist/{client-6eRENr7k.js → client-D8XghqmX.js} +4 -4
  18. package/dist/{client-bootstrap-XsIkHNt0.js → client-bootstrap-Co3RlItb.js} +25 -61
  19. package/dist/client-support-DLj4vDeu.js +93 -0
  20. package/dist/config-update-Bor_Pjsv.js +150 -0
  21. package/dist/contract-api.js +2 -2
  22. package/dist/{credentials-W2EmwRhu.js → credentials-BdAaWuiR.js} +2 -1
  23. package/dist/credentials-read-B37cQknZ.js +51 -0
  24. package/dist/credentials-state-B3cuUFwZ.js +27 -0
  25. package/dist/{credentials-write.runtime-Be9zNSUw.js → credentials-write.runtime-nAV2-5uR.js} +1 -1
  26. package/dist/{crypto-runtime-D9_6J5tZ.js → crypto-runtime-Bnz3zEcS.js} +55 -32
  27. package/dist/direct-room-RinHjtqX.js +71 -0
  28. package/dist/{directory-live-C8dVBiCc.js → directory-live-DLQzel12.js} +2 -3
  29. package/dist/{doctor-CM8ijNpW.js → doctor-EWf9CDso.js} +2 -2
  30. package/dist/{doctor-contract-BMSJkkAE.js → doctor-contract-DEt4O4Zt.js} +2 -3
  31. package/dist/doctor-contract-api.js +209 -91
  32. package/dist/{draft-stream-CtvOXq0Z.js → draft-stream-DQ7QNC-l.js} +20 -23
  33. package/dist/{encryption-guidance-CbEFe38F.js → encryption-guidance-DmLY8NiO.js} +1 -1
  34. package/dist/{env-auth-CQsYbz-p.js → env-auth-MVS-Vv41.js} +4 -3
  35. package/dist/{env-vars-B0NxQnPk.js → env-vars-dGqak9dN.js} +1 -1
  36. package/dist/{errors-C47hvAF8.js → errors-DKVS3_DO.js} +2 -5
  37. package/dist/{file-sync-store-DiiFsayX.js → file-sync-store-DjskBX2o.js} +24 -3
  38. package/dist/helper-api.js +2 -2
  39. package/dist/{idb-persistence-B6x7dMsF.js → idb-persistence-BM0P27Zg.js} +3 -2
  40. package/dist/{inbound-dedupe-CaBLblef.js → inbound-dedupe-RvSLqdsz.js} +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/{logger-D0GCSDQq.js → logger-B_71hN6W.js} +1 -1
  43. package/dist/{logging-DV-p52N0.js → logging-DRjvUOIh.js} +1 -1
  44. package/dist/{media-text-BTwzADS_.js → media-text-Dh-DpTc3.js} +4 -1
  45. package/dist/{messages-DxQJkhxs.js → messages-DEqg7HVy.js} +31 -11
  46. package/dist/{monitor-VyWlSLwo.js → monitor-FNrfkuWL.js} +413 -405
  47. package/dist/plugin-entry.handlers.runtime.js +3 -3
  48. package/dist/probe.runtime-NIJZYqc2.js +2 -0
  49. package/dist/profile-D-Wubo0A.js +108 -0
  50. package/dist/{profile-update-CffmyW_4.js → profile-update-BQlE5s2J.js} +3 -2
  51. package/dist/{reaction-events-B00WSi98.js → reaction-events-BKOLlnCo.js} +10 -7
  52. package/dist/{recovery-key-store-BNnHvXdk.js → recovery-key-store-CognuDys.js} +5 -4
  53. package/dist/{resolve-targets-nm2L48sh.js → resolve-targets-e7AI0P75.js} +2 -2
  54. package/dist/{resolver.runtime-DS6uPaEe.js → resolver.runtime-V8u0pHEb.js} +1 -1
  55. package/dist/{room-info-CAFZ3Z9J.js → room-info-BXQA7y10.js} +12 -3
  56. package/dist/runtime-api.js +7 -4
  57. package/dist/{sdk-bUytQHx7.js → sdk-NxK3vzMa.js} +242 -119
  58. package/dist/{secret-contract-M3hRmGO-.js → secret-contract-CufozuyJ.js} +1 -1
  59. package/dist/secret-contract-api.js +1 -1
  60. package/dist/{send-BldGYG8g.js → send-ClHgDZLT.js} +1477 -1210
  61. package/dist/session-binding-contract-api.js +2 -3
  62. package/dist/{setup-bootstrap-Deleiibz.js → setup-bootstrap-BGPjbA5N.js} +4 -4
  63. package/dist/{setup-core-BVfBv2ai.js → setup-core-DigrLebY.js} +17 -266
  64. package/dist/setup-plugin-api.js +1 -44
  65. package/dist/{setup-surface-BBq3WeqX.js → setup-surface-Dtn8sAWs.js} +15 -16
  66. package/dist/{startup-verification-wm3_i88s.js → startup-verification-Cs08ca8u.js} +3 -3
  67. package/dist/{storage-BvAkgzvT.js → storage-BsGuHngK.js} +3 -3
  68. package/dist/subagent-hooks-CMoZaARG.js +48 -0
  69. package/dist/subagent-hooks-api.js +1 -1
  70. package/dist/{target-ids-B-5aQxwn.js → target-ids-Nwx7cMVE.js} +11 -1
  71. package/dist/{thread-binding-api-DRbBZlhS.js → thread-binding-api-CzkeLZCU.js} +1 -1
  72. package/dist/thread-binding-api.js +1 -1
  73. package/dist/{thread-bindings-DGbl3vDW.js → thread-bindings-BYEGfw3h.js} +17 -10
  74. package/dist/thread-bindings-runtime.js +1 -1
  75. package/dist/{thread-bindings-shared-CKnY4LSd.js → thread-bindings-shared-oNPiyMWY.js} +1 -6
  76. package/dist/{tool-actions.runtime-uu9blSqO.js → tool-actions.runtime-ck6IJyP0.js} +36 -19
  77. package/dist/{verification-DYMQ1QpI.js → verification-CabtBTtB.js} +2 -2
  78. package/openclaw.plugin.json +61 -1
  79. package/package.json +4 -4
  80. package/dist/active-client-DXvz2gCo.js +0 -20
  81. package/dist/client-Dbso3-0V.js +0 -30
  82. package/dist/create-client-BmSU-q-D.js +0 -61
  83. package/dist/credentials-read-DVmFCm7X.js +0 -77
  84. package/dist/exec-approval-resolver-BCgqRtZS.js +0 -16
  85. package/dist/preflight-audio.runtime-BZxxQsxY.js +0 -11
  86. package/dist/probe.runtime-BuieqmS3.js +0 -3
  87. package/dist/record-shared-CWWx6fvI.js +0 -2
  88. package/dist/route-DY6at4qJ.js +0 -161
  89. package/dist/shared-CUMWxcMz.js +0 -673
  90. package/dist/subagent-hooks-dkPAF6Et.js +0 -143
  91. package/dist/sync-state-CWbp0QSY.js +0 -12
  92. 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 { r as normalizeMatrixResolvableTarget, t as isMatrixQualifiedUserId } from "./target-ids-B-5aQxwn.js";
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 { normalizeLowercaseStringOrEmpty, normalizeNullableString, normalizeOptionalString, normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/string-coerce-runtime";
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
- import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
18
- //#region extensions/matrix/src/matrix/poll-types.ts
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
- * Matrix Poll Types (MSC3381)
21
- *
22
- * Defines types for Matrix poll events:
23
- * - m.poll.start - Creates a new poll
24
- * - m.poll.response - Records a vote
25
- * - m.poll.end - Closes a poll
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 M_POLL_START = "m.poll.start";
28
- const M_POLL_RESPONSE = "m.poll.response";
29
- const M_POLL_END = "m.poll.end";
30
- const ORG_POLL_START = "org.matrix.msc3381.poll.start";
31
- const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response";
32
- const ORG_POLL_END = "org.matrix.msc3381.poll.end";
33
- const POLL_EVENT_TYPES = [
34
- M_POLL_START,
35
- M_POLL_RESPONSE,
36
- M_POLL_END,
37
- ORG_POLL_START,
38
- ORG_POLL_RESPONSE,
39
- ORG_POLL_END
40
- ];
41
- const POLL_START_TYPES = [M_POLL_START, ORG_POLL_START];
42
- const POLL_RESPONSE_TYPES = [M_POLL_RESPONSE, ORG_POLL_RESPONSE];
43
- const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END];
44
- function isPollStartType(eventType) {
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 isPollResponseType(eventType) {
48
- return POLL_RESPONSE_TYPES.includes(eventType);
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 isPollEndType(eventType) {
51
- return POLL_END_TYPES.includes(eventType);
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 isPollEventType(eventType) {
54
- return POLL_EVENT_TYPES.includes(eventType);
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 getTextContent(text) {
57
- if (!text) return "";
58
- return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? "";
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 parsePollStart(content) {
61
- const poll = content["m.poll.start"] ?? content[ORG_POLL_START] ?? content["m.poll"];
62
- if (!poll) return null;
63
- const question = getTextContent(poll.question).trim();
64
- if (!question) return null;
65
- const answers = poll.answers.map((answer) => ({
66
- id: answer.id,
67
- text: getTextContent(answer).trim()
68
- })).filter((answer) => answer.id.trim().length > 0 && answer.text.length > 0);
69
- if (answers.length === 0) return null;
70
- const maxSelectionsRaw = poll.max_selections;
71
- const maxSelections = typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw) ? Math.floor(maxSelectionsRaw) : 1;
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
- question,
74
- answers,
75
- kind: poll.kind ?? "m.poll.disclosed",
76
- maxSelections: Math.min(Math.max(maxSelections, 1), answers.length)
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 parsePollStartContent(content) {
80
- const parsed = parsePollStart(content);
81
- if (!parsed) return null;
173
+ function buildNextDirectContent(params) {
174
+ const current = normalizeMappedRoomIds(params.directContent, params.remoteUserId);
175
+ const nextRooms = normalizeRoomIdList([...params.roomIds, ...current]);
82
176
  return {
83
- eventId: "",
84
- roomId: "",
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 formatPollAsText(summary) {
94
- return [
95
- "[Poll]",
96
- summary.question,
97
- "",
98
- ...summary.answers.map((answer, idx) => `${idx + 1}. ${answer}`)
99
- ].join("\n");
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 buildPollResultsSummary(params) {
115
- const parsed = parsePollStart(params.content);
116
- if (!parsed) return null;
117
- let pollClosedAt = Number.POSITIVE_INFINITY;
118
- for (const event of params.relationEvents) {
119
- if (event.unsigned?.redacted_because) continue;
120
- if (!isPollEndType(typeof event.type === "string" ? event.type : "")) continue;
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
- for (const event of orderedRelationEvents) {
134
- if (event.unsigned?.redacted_because) continue;
135
- if (!isPollResponseType(typeof event.type === "string" ? event.type : "")) continue;
136
- const senderId = normalizeOptionalString(event.sender) ?? "";
137
- if (!senderId) continue;
138
- const eventTs = typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts) ? event.origin_server_ts : Number.POSITIVE_INFINITY;
139
- if (eventTs > pollClosedAt) continue;
140
- const rawAnswers = parsePollResponseAnswerIds(event.content) ?? [];
141
- const normalizedAnswers = Array.from(new Set(rawAnswers.map((answerId) => normalizeOptionalString(answerId) ?? "").filter((answerId) => answerIds.has(answerId)).slice(0, parsed.maxSelections)));
142
- latestVoteBySender.set(senderId, {
143
- ts: eventTs,
144
- eventId: typeof event.event_id === "string" ? event.event_id : "",
145
- answerIds: normalizedAnswers
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
- const voteCounts = new Map(parsed.answers.map((answer) => [answer.id, 0]));
149
- let totalVotes = 0;
150
- for (const latestVote of latestVoteBySender.values()) {
151
- if (latestVote.answerIds.length === 0) continue;
152
- totalVotes += 1;
153
- for (const answerId of latestVote.answerIds) voteCounts.set(answerId, (voteCounts.get(answerId) ?? 0) + 1);
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
- eventId: params.pollEventId,
157
- roomId: params.roomId,
158
- sender: params.sender,
159
- senderName: params.senderName,
160
- question: parsed.question,
161
- answers: parsed.answers.map((answer) => answer.text),
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 formatPollResultsAsText(summary) {
174
- const lines = [
175
- summary.closed ? "[Poll closed]" : "[Poll]",
176
- summary.question,
177
- ""
178
- ];
179
- const revealResults = summary.kind === "m.poll.disclosed" || summary.closed;
180
- for (const [index, entry] of summary.entries.entries()) {
181
- if (!revealResults) {
182
- lines.push(`${index + 1}. ${entry.text}`);
183
- continue;
184
- }
185
- lines.push(`${index + 1}. ${entry.text} (${entry.votes} vote${entry.votes === 1 ? "" : "s"})`);
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
- "m.text": body,
195
- "org.matrix.msc1767.text": body
284
+ ...inspected,
285
+ activeRoomId,
286
+ createdRoomId,
287
+ changed: mappingWrite.changed,
288
+ directContentBefore: mappingWrite.directContentBefore,
289
+ directContentAfter: mappingWrite.directContentAfter
196
290
  };
197
291
  }
198
- function buildPollFallbackText(question, answers) {
199
- if (answers.length === 0) return question;
200
- return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
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 buildPollStartContent(poll) {
203
- const normalized = normalizePollInput(poll);
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
- function buildPollResponseContent(pollEventId, answerIds) {
222
- return {
223
- [M_POLL_RESPONSE]: { answers: answerIds },
224
- [ORG_POLL_RESPONSE]: { answers: answerIds },
225
- "m.relates_to": {
226
- rel_type: "m.reference",
227
- event_id: pollEventId
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/format-profile.ts
233
- function createMatrixPrivateMarkers(markdown, exhaustedMessage) {
234
- const used = new Set(Array.from(markdown, (character) => character.charCodeAt(0)));
235
- for (const match of markdown.matchAll(/&#(?:x([0-9a-f]+)|(\d+));/giu)) {
236
- const radix = match[1] ? 16 : 10;
237
- const value = Number.parseInt(match[1] ?? match[2] ?? "", radix);
238
- if (Number.isFinite(value) && value <= 65535) used.add(value);
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
- const markers = [];
241
- for (let code = 57344; code <= 63743 && markers.length < 3; code += 1) if (!used.has(code)) markers.push(String.fromCharCode(code));
242
- if (markers.length < 3) throw new Error(exhaustedMessage);
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
- open: markers[0] ?? "",
245
- close: markers[1] ?? "",
246
- padding: markers[2] ?? ""
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
- //#endregion
1347
- //#region extensions/matrix/src/matrix/direct-room.ts
1348
- var direct_room_exports = /* @__PURE__ */ __exportAll({
1349
- hasDirectMatrixMemberFlag: () => hasDirectMatrixMemberFlag,
1350
- inspectMatrixDirectRoomEvidence: () => inspectMatrixDirectRoomEvidence,
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 isStrictDirectMembership(params) {
1360
- const selfUserId = normalizeNullableString(params.selfUserId);
1361
- const remoteUserId = normalizeNullableString(params.remoteUserId);
1362
- const joinedMembers = params.joinedMembers ?? [];
1363
- return Boolean(selfUserId && remoteUserId && joinedMembers.length === 2 && joinedMembers.includes(selfUserId) && joinedMembers.includes(remoteUserId));
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
- async function readJoinedMatrixMembers(client, roomId) {
1401
+ function parseMatrixMarkdown(markdown, tableMode) {
1402
+ const protectedSpoilers = protectMatrixSpoilerDelimiters(markdown);
1403
+ if (tableMode === "off") md.disable("table");
1366
1404
  try {
1367
- return normalizeJoinedMatrixMembers(await client.getJoinedRoomMembers(roomId));
1368
- } catch {
1369
- return null;
1405
+ return md.parse(protectedSpoilers.markdown, { matrixSpoilerMarkers: protectedSpoilers.markers });
1406
+ } finally {
1407
+ if (tableMode === "off") md.enable("table");
1370
1408
  }
1371
1409
  }
1372
- async function hasDirectMatrixMemberFlag(client, roomId, userId) {
1373
- const normalizedUserId = normalizeNullableString(userId);
1374
- if (!normalizedUserId) return null;
1375
- try {
1376
- const state = await client.getRoomStateEvent(roomId, "m.room.member", normalizedUserId);
1377
- if (state?.is_direct === true) return true;
1378
- if (state?.is_direct === false) return false;
1379
- return null;
1380
- } catch {
1381
- return null;
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
- async function inspectMatrixDirectRoomEvidence(params) {
1385
- const selfUserId = params.selfUserId !== void 0 ? normalizeNullableString(params.selfUserId) : normalizeNullableString(await params.client.getUserId().catch(() => null));
1386
- const joinedMembers = await readJoinedMatrixMembers(params.client, params.roomId);
1387
- const strict = isStrictDirectMembership({
1388
- selfUserId,
1389
- remoteUserId: params.remoteUserId,
1390
- joinedMembers
1391
- });
1392
- if (!strict) return {
1393
- joinedMembers,
1394
- strict: false,
1395
- viaMemberState: false,
1396
- memberStateFlag: null
1397
- };
1398
- const memberStateFlag = await hasDirectMatrixMemberFlag(params.client, params.roomId, selfUserId);
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
- joinedMembers,
1401
- strict,
1402
- viaMemberState: memberStateFlag === true,
1403
- memberStateFlag
1456
+ tokens,
1457
+ mentions
1404
1458
  };
1405
1459
  }
1406
- async function isStrictDirectRoom(params) {
1407
- return (await inspectMatrixDirectRoomEvidence({
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
- //#endregion
1415
- //#region extensions/matrix/src/matrix/direct-management.ts
1416
- var direct_management_exports = /* @__PURE__ */ __exportAll({
1417
- inspectMatrixDirectRooms: () => inspectMatrixDirectRooms,
1418
- persistMatrixDirectRoomMapping: () => persistMatrixDirectRoomMapping,
1419
- promoteMatrixDirectRoomCandidate: () => promoteMatrixDirectRoomCandidate,
1420
- repairMatrixDirectRooms: () => repairMatrixDirectRooms
1421
- });
1422
- const DIRECT_ACCOUNT_DATA_QUEUE_KEY = EventType.Direct;
1423
- const directAccountDataWriteQueues = /* @__PURE__ */ new WeakMap();
1424
- async function readMatrixDirectAccountData(client) {
1425
- try {
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
- function normalizeRemoteUserId(remoteUserId) {
1433
- const normalized = normalizeOptionalString(remoteUserId) ?? "";
1434
- if (!isMatrixQualifiedUserId(normalized)) throw new Error(`Matrix user IDs must be fully qualified (got "${remoteUserId}")`);
1435
- return normalized;
1436
- }
1437
- function normalizeMappedRoomIds(direct, remoteUserId) {
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 normalizeRoomIdList(values) {
1451
- const seen = /* @__PURE__ */ new Set();
1452
- const normalized = [];
1453
- for (const value of values) {
1454
- const roomId = value.trim();
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 hasMatrixDirectRoomMappings(params) {
1462
- const current = normalizeMappedRoomIds(params.directContent, params.remoteUserId);
1463
- const next = normalizeRoomIdList([...params.roomIds, ...current]);
1464
- return current.length === next.length && current.every((roomId, index) => roomId === next[index]);
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 resolveDirectAccountDataWriteQueue(client) {
1467
- const existing = directAccountDataWriteQueues.get(client);
1468
- if (existing) return existing;
1469
- const created = new KeyedAsyncQueue();
1470
- directAccountDataWriteQueues.set(client, created);
1471
- return created;
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
- async function writeMatrixDirectRoomMappings(params) {
1474
- return await resolveDirectAccountDataWriteQueue(params.client).enqueue(DIRECT_ACCOUNT_DATA_QUEUE_KEY, async () => {
1475
- const directContentBefore = await readMatrixDirectAccountData(params.client);
1476
- const directContentAfter = buildNextDirectContent({
1477
- directContent: directContentBefore,
1478
- remoteUserId: params.remoteUserId,
1479
- roomIds: params.roomIds
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 changed = !hasMatrixDirectRoomMappings({
1482
- directContent: directContentBefore,
1483
- remoteUserId: params.remoteUserId,
1484
- roomIds: params.roomIds
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
- if (changed) await params.client.setAccountData(EventType.Direct, directContentAfter);
1487
- return {
1488
- changed,
1489
- directContentBefore,
1490
- directContentAfter
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
- async function classifyDirectRoomCandidate(params) {
1495
- const evidence = await inspectMatrixDirectRoomEvidence({
1496
- client: params.client,
1497
- roomId: params.roomId,
1498
- remoteUserId: params.remoteUserId,
1499
- selfUserId: params.selfUserId
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
- roomId: params.roomId,
1503
- joinedMembers: evidence.joinedMembers,
1504
- strict: evidence.strict && (params.source === "account-data" || evidence.memberStateFlag !== false),
1505
- explicit: evidence.strict && (params.source === "account-data" || evidence.memberStateFlag !== false) && (params.source === "account-data" || evidence.viaMemberState),
1506
- source: params.source
1599
+ trimmedText,
1600
+ convertedText,
1601
+ singleEventLimit,
1602
+ eventTextLength,
1603
+ fitsInSingleEvent: eventTextLength <= singleEventLimit,
1604
+ tableMode
1507
1605
  };
1508
1606
  }
1509
- function buildNextDirectContent(params) {
1510
- const current = normalizeMappedRoomIds(params.directContent, params.remoteUserId);
1511
- const nextRooms = normalizeRoomIdList([...params.roomIds, ...current]);
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
- ...params.directContent,
1514
- [params.remoteUserId]: nextRooms
1642
+ ...preparedText,
1643
+ chunks
1515
1644
  };
1516
1645
  }
1517
- async function persistMatrixDirectRoomMapping(params) {
1518
- const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
1519
- return (await writeMatrixDirectRoomMappings({
1520
- client: params.client,
1521
- remoteUserId,
1522
- roomIds: [params.roomId]
1523
- })).changed;
1524
- }
1525
- async function promoteMatrixDirectRoomCandidate(params) {
1526
- const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
1527
- const evidence = await inspectMatrixDirectRoomEvidence({
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
- roomId: params.roomId,
1530
- remoteUserId,
1531
- selfUserId: params.selfUserId
1659
+ tableMode: params.tableMode
1532
1660
  });
1533
- if (!evidence.strict) return {
1534
- classifyAsDirect: false,
1535
- repaired: false,
1536
- reason: "not-strict"
1661
+ return {
1662
+ body,
1663
+ html,
1664
+ mentions
1537
1665
  };
1538
- if (evidence.memberStateFlag === false) return {
1539
- classifyAsDirect: false,
1540
- repaired: false,
1541
- reason: "local-explicit-false"
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 inspectMatrixDirectRooms(params) {
1565
- const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
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
- roomId,
1571
- remoteUserId,
1572
- selfUserId,
1573
- source: "account-data"
1574
- })));
1575
- const mappedStrict = mappedRooms.find((room) => room.strict);
1576
- let joinedRooms = [];
1577
- if (typeof params.client.getJoinedRooms === "function") try {
1578
- const resolved = await params.client.getJoinedRooms();
1579
- joinedRooms = Array.isArray(resolved) ? resolved : [];
1580
- } catch {
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
- const discoveredStrictRoomIds = discoveredStrictRooms.map((room) => room.roomId);
1596
- const discoveredExplicit = discoveredStrictRooms.find((room) => room.explicit);
1597
- return {
1598
- selfUserId,
1599
- remoteUserId,
1600
- mappedRoomIds,
1601
- mappedRooms,
1602
- discoveredStrictRoomIds,
1603
- activeRoomId: mappedStrict?.roomId ?? discoveredExplicit?.roomId ?? discoveredStrictRoomIds[0] ?? null
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
- async function repairMatrixDirectRooms(params) {
1607
- const remoteUserId = normalizeRemoteUserId(params.remoteUserId);
1608
- const inspected = await inspectMatrixDirectRooms({
1609
- client: params.client,
1610
- remoteUserId
1611
- });
1612
- const activeRoomId = inspected.activeRoomId ?? await params.client.createDirectRoom(remoteUserId, { encrypted: params.encrypted === true });
1613
- const createdRoomId = inspected.activeRoomId ? null : activeRoomId;
1614
- const mappingWrite = await writeMatrixDirectRoomMappings({
1615
- client: params.client,
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
- //#endregion
1629
- //#region extensions/matrix/src/matrix/send/targets.ts
1630
- function normalizeTarget(raw) {
1631
- const trimmed = raw.trim();
1632
- if (!trimmed) throw new Error("Matrix target is required (room:<id> or #alias)");
1633
- return trimmed;
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 normalizeThreadId(raw) {
1636
- return normalizeOptionalStringifiedId(raw) ?? null;
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
- const MAX_DIRECT_ROOM_CACHE_SIZE = 1024;
1639
- const directRoomCacheByClient = /* @__PURE__ */ new WeakMap();
1640
- function resolveDirectRoomCache(client) {
1641
- const existing = directRoomCacheByClient.get(client);
1642
- if (existing) return existing;
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
- function setDirectRoomCached(client, key, value) {
1648
- const directRoomCache = resolveDirectRoomCache(client);
1649
- directRoomCache.set(key, value);
1650
- if (directRoomCache.size > MAX_DIRECT_ROOM_CACHE_SIZE) {
1651
- const oldest = directRoomCache.keys().next().value;
1652
- if (oldest !== void 0) directRoomCache.delete(oldest);
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
- async function resolveDirectRoomId(client, userId) {
1656
- const trimmed = userId.trim();
1657
- if (!isMatrixQualifiedUserId(trimmed)) throw new Error(`Matrix user IDs must be fully qualified (got "${trimmed}")`);
1658
- const selfUserId = (await client.getUserId().catch(() => null))?.trim() || null;
1659
- const directRoomCache = resolveDirectRoomCache(client);
1660
- const cached = directRoomCache.get(trimmed);
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
- if (inspection.activeRoomId) {
1673
- setDirectRoomCached(client, trimmed, inspection.activeRoomId);
1674
- if (inspection.mappedRoomIds[0] !== inspection.activeRoomId) await persistMatrixDirectRoomMapping({
1675
- client,
1676
- remoteUserId: trimmed,
1677
- roomId: inspection.activeRoomId
1678
- }).catch(() => {});
1679
- return inspection.activeRoomId;
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
- throw new Error(`No direct room found for ${trimmed} (m.direct missing)`);
1804
+ if (params.relation) base["m.relates_to"] = params.relation;
1805
+ return base;
1682
1806
  }
1683
- async function resolveMatrixRoomId(client, raw) {
1684
- const target = normalizeMatrixResolvableTarget(normalizeTarget(raw));
1685
- if (normalizeLowercaseStringOrEmpty(target).startsWith("user:")) return await resolveDirectRoomId(client, target.slice(5));
1686
- if (isMatrixQualifiedUserId(target)) return await resolveDirectRoomId(client, target);
1687
- if (target.startsWith("#")) {
1688
- const resolved = await client.resolveRoom(target);
1689
- if (!resolved) throw new Error(`Matrix alias ${target} could not be resolved`);
1690
- return resolved;
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 target;
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
- return createMessageReceiptFromOutboundResults({
1711
- kind: params.kind,
1712
- ...params.replyToId ? { replyToId: params.replyToId } : {},
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.platformMessageIds.map((messageId) => ({
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 trimmedMessage = message?.trim() ?? "";
1773
- if (!trimmedMessage && !opts.mediaUrl) throw new Error("Matrix send requires text or media");
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 relation = threadId ? buildThreadRelation(threadId, opts.replyToId) : buildReplyRelation(opts.replyToId);
1788
- let pendingExtraContent = opts.extraContent;
1789
- const sendContent = async (content, kind) => {
1790
- const contentWithExtra = withMatrixExtraContentFields(content, pendingExtraContent);
1791
- pendingExtraContent = void 0;
1792
- const eventId = await client.sendMessage(roomId, contentWithExtra);
1793
- const visibleContent = contentWithExtra.body ?? "";
1794
- if (eventId) {
1795
- acceptedContents.push(visibleContent);
1796
- await opts.onDeliveryResult?.({
1797
- messageId: eventId,
1798
- roomId,
1799
- primaryMessageId: eventId,
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 eventId = await sendContent(content, receiptKind);
1869
- lastMessageId = eventId ?? lastMessageId;
1870
- if (eventId) platformMessageIds.push(eventId);
1871
- const textChunks = useVoice ? chunks : rest;
1872
- const followupRelation = useVoice || threadId ? relation : void 0;
1873
- for (const chunk of textChunks) {
1874
- const text = chunk;
1875
- if (!text.trim()) continue;
1876
- const followup = buildTextContent(text, followupRelation);
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: followup,
1880
- markdown: text,
2103
+ content,
2104
+ markdown: chunk,
1881
2105
  tableMode
1882
2106
  });
1883
- const followupEventId = await sendContent(followup, "text");
1884
- lastMessageId = followupEventId ?? lastMessageId;
1885
- if (followupEventId) platformMessageIds.push(followupEventId);
2107
+ prepareContent(content, "text");
1886
2108
  }
1887
- } else for (const chunk of chunks.length ? chunks : [""]) {
1888
- const text = chunk;
1889
- if (!text.trim()) continue;
1890
- const content = buildTextContent(text, relation);
1891
- await enrichMatrixFormattedContent({
1892
- client,
1893
- content,
1894
- markdown: text,
1895
- tableMode
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: platformMessageIds[0] ?? (lastMessageId || "unknown"),
2169
+ primaryMessageId: acceptedEvents[0]?.messageId ?? (lastMessageId || "unknown"),
1905
2170
  receipt: createMatrixSendReceipt({
1906
2171
  roomId,
1907
- platformMessageIds,
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
- platformMessageIds: eventId ? [eventId] : [],
2001
- kind: "text",
2002
- replyToId: opts.replyToId,
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 { formatPollResultsAsText as C, parsePollStartContent as D, parsePollStart as E, resolvePollReferenceEventId as O, formatPollAsText as S, isPollStartType as T, MsgType as _, sendSingleTextMessageMatrix as a, buildPollResponseContent as b, resolveMatrixRoomId as c, repairMatrixDirectRooms as d, direct_room_exports as f, MATRIX_OPENCLAW_FINALIZED_PREVIEW_KEY as g, readJoinedMatrixMembers as h, sendPollMatrix as i, direct_management_exports as l, isStrictDirectMembership as m, reactMatrixMessage as n, sendTypingMatrix as o, hasDirectMatrixMemberFlag as p, sendMessageMatrix as r, send_exports as s, editMessageMatrix as t, promoteMatrixDirectRoomCandidate as u, chunkMatrixText as v, isPollEventType as w, buildPollResultsSummary as x, prepareMatrixSingleText as y };
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 };