@openclaw/matrix 2026.7.2-beta.7 → 2026.8.1-beta.2

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 (83) 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 +7 -10
  4. package/dist/{approval-handler.runtime-DSeaIsGa.js → approval-handler.runtime-CJXCUdRg.js} +5 -5
  5. package/dist/{approval-ids-BdJGPAJT.js → approval-ids-7rf7etWE.js} +1 -1
  6. package/dist/{approval-reaction-auth-BjOpELxu.js → approval-reaction-auth-wBwbEDpZ.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-VSrHgTON.js} +156 -136
  10. package/dist/channel-plugin-api.js +1 -1
  11. package/dist/{channel.runtime-CLoF2S9T.js → channel.runtime-pOOL0uT_.js} +70 -56
  12. package/dist/{config-schema-Djxw3C6b.js → channel.setup-PpxRP9Du.js} +50 -3
  13. package/dist/{cli-DMqfDE-q.js → cli-CkTfKC_c.js} +16 -16
  14. package/dist/{cli-metadata-C7D53SND.js → cli-metadata-B1d1aGM5.js} +1 -1
  15. package/dist/cli-metadata.js +1 -1
  16. package/dist/client-CeGw0G5S.js +784 -0
  17. package/dist/{client-bootstrap-XsIkHNt0.js → client-bootstrap-BFwT_2VT.js} +25 -61
  18. package/dist/{client-6eRENr7k.js → client-sJesKQmp.js} +4 -4
  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-Bvg2wAnR.js} +1 -2
  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 +202 -90
  32. package/dist/{draft-stream-CtvOXq0Z.js → draft-stream-BRi7PRRS.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-JudlYOSc.js} +1 -1
  45. package/dist/{messages-DxQJkhxs.js → messages-BW0s-6Y9.js} +7 -7
  46. package/dist/{monitor-VyWlSLwo.js → monitor-Ddfc-vjX.js} +394 -384
  47. package/dist/plugin-entry.handlers.runtime.js +3 -3
  48. package/dist/probe.runtime-Dr91B_X3.js +2 -0
  49. package/dist/profile-D-Wubo0A.js +108 -0
  50. package/dist/{profile-update-CffmyW_4.js → profile-update-BkgWYDIp.js} +3 -2
  51. package/dist/{reaction-events-B00WSi98.js → reaction-events-nfUbFLdV.js} +9 -6
  52. package/dist/{recovery-key-store-BNnHvXdk.js → recovery-key-store-CognuDys.js} +5 -4
  53. package/dist/{resolve-targets-nm2L48sh.js → resolve-targets-BJAPY4Vb.js} +1 -1
  54. package/dist/{resolver.runtime-DS6uPaEe.js → resolver.runtime-LWJELYq5.js} +1 -1
  55. package/dist/{room-info-CAFZ3Z9J.js → room-info-BXQA7y10.js} +12 -3
  56. package/dist/runtime-api.js +6 -3
  57. package/dist/{sdk-bUytQHx7.js → sdk-DjaQFCHc.js} +229 -120
  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-ZDLU4JTk.js} +1462 -1196
  61. package/dist/session-binding-contract-api.js +1 -1
  62. package/dist/{setup-bootstrap-Deleiibz.js → setup-bootstrap-BaXkhgJg.js} +4 -4
  63. package/dist/{setup-core-BVfBv2ai.js → setup-core-DQn6b5vi.js} +17 -266
  64. package/dist/setup-plugin-api.js +1 -44
  65. package/dist/{setup-surface-BBq3WeqX.js → setup-surface-DlhJdrNI.js} +9 -11
  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/{thread-bindings-DGbl3vDW.js → thread-bindings-CcHZP3w2.js} +7 -4
  69. package/dist/{tool-actions.runtime-uu9blSqO.js → tool-actions.runtime-DmdRL0bN.js} +35 -18
  70. package/dist/{verification-DYMQ1QpI.js → verification-C7ig-bQE.js} +2 -2
  71. package/openclaw.plugin.json +54 -1
  72. package/package.json +4 -4
  73. package/dist/active-client-DXvz2gCo.js +0 -20
  74. package/dist/client-Dbso3-0V.js +0 -30
  75. package/dist/create-client-BmSU-q-D.js +0 -61
  76. package/dist/credentials-read-DVmFCm7X.js +0 -77
  77. package/dist/exec-approval-resolver-BCgqRtZS.js +0 -16
  78. package/dist/preflight-audio.runtime-BZxxQsxY.js +0 -11
  79. package/dist/probe.runtime-BuieqmS3.js +0 -3
  80. package/dist/record-shared-CWWx6fvI.js +0 -2
  81. package/dist/shared-CUMWxcMz.js +0 -673
  82. package/dist/sync-state-CWbp0QSY.js +0 -12
  83. package/dist/{runtime-api-D1xI_cvh.js → runtime-api-BGO16Fu8.js} +3 -3
@@ -2,235 +2,819 @@ 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
4
  import { r as normalizeMatrixResolvableTarget, t as isMatrixQualifiedUserId } from "./target-ids-B-5aQxwn.js";
5
+ import { a as isStrictDirectRoom, r as inspectMatrixDirectRoomEvidence } from "./direct-room-RinHjtqX.js";
5
6
  import { n as MATRIX_REACTION_EVENT_TYPE, r as buildMatrixReactionContent, t as MATRIX_ANNOTATION_RELATION_TYPE } from "./reaction-common-DkrQdBSZ.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 { 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-BFwT_2VT.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 ?? "");
132
- });
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
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
196
+ });
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)
299
+ function normalizeThreadId(raw) {
300
+ return normalizeOptionalStringifiedId(raw) ?? null;
301
+ }
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;
357
+ }
358
+ //#endregion
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";
367
+ }
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)
207
434
  }));
208
- const isMultiple = normalized.maxSelections > 1;
209
- const fallbackText = buildPollFallbackText(normalized.question, answers.map((answer) => getTextContent(answer)));
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");
210
442
  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
443
+ queueId: params.queueId,
444
+ partIndex,
445
+ partCount
219
446
  };
220
447
  }
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
- }
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
229
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
+ }));
230
494
  }
231
- //#endregion
232
- //#region extensions/matrix/src/matrix/format-profile.ts
233
- function createMatrixPrivateMarkers(markdown, exhaustedMessage) {
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 (!text) return "";
642
+ return 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).trim();
648
+ if (!question) return null;
649
+ const answers = poll.answers.map((answer) => ({
650
+ id: answer.id,
651
+ text: getTextContent(answer).trim()
652
+ })).filter((answer) => answer.id.trim().length > 0 && answer.text.length > 0);
653
+ if (answers.length === 0) return null;
654
+ const maxSelectionsRaw = poll.max_selections;
655
+ const maxSelections = typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw) ? Math.floor(maxSelectionsRaw) : 1;
656
+ return {
657
+ question,
658
+ answers,
659
+ kind: poll.kind ?? "m.poll.disclosed",
660
+ maxSelections: Math.min(Math.max(maxSelections, 1), answers.length)
661
+ };
662
+ }
663
+ function parsePollStartContent(content) {
664
+ const parsed = parsePollStart(content);
665
+ if (!parsed) return null;
666
+ return {
667
+ eventId: "",
668
+ roomId: "",
669
+ sender: "",
670
+ senderName: "",
671
+ question: parsed.question,
672
+ answers: parsed.answers.map((answer) => answer.text),
673
+ kind: parsed.kind,
674
+ maxSelections: parsed.maxSelections
675
+ };
676
+ }
677
+ function formatPollAsText(summary) {
678
+ return [
679
+ "[Poll]",
680
+ summary.question,
681
+ "",
682
+ ...summary.answers.map((answer, idx) => `${idx + 1}. ${answer}`)
683
+ ].join("\n");
684
+ }
685
+ function resolvePollReferenceEventId(content) {
686
+ if (!content || typeof content !== "object") return null;
687
+ const relates = content["m.relates_to"];
688
+ if (!relates || typeof relates.event_id !== "string") return null;
689
+ const eventId = relates.event_id.trim();
690
+ return eventId.length > 0 ? eventId : null;
691
+ }
692
+ function parsePollResponseAnswerIds(content) {
693
+ if (!content || typeof content !== "object") return null;
694
+ const response = content[M_POLL_RESPONSE] ?? content[ORG_POLL_RESPONSE];
695
+ if (!response || !Array.isArray(response.answers)) return null;
696
+ return response.answers.filter((answer) => typeof answer === "string");
697
+ }
698
+ function buildPollResultsSummary(params) {
699
+ const parsed = parsePollStart(params.content);
700
+ if (!parsed) return null;
701
+ let pollClosedAt = Number.POSITIVE_INFINITY;
702
+ for (const event of params.relationEvents) {
703
+ if (event.unsigned?.redacted_because) continue;
704
+ if (!isPollEndType(typeof event.type === "string" ? event.type : "")) continue;
705
+ if (event.sender !== params.sender) continue;
706
+ const ts = typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts) ? event.origin_server_ts : Number.POSITIVE_INFINITY;
707
+ if (ts < pollClosedAt) pollClosedAt = ts;
708
+ }
709
+ const answerIds = new Set(parsed.answers.map((answer) => answer.id));
710
+ const latestVoteBySender = /* @__PURE__ */ new Map();
711
+ const orderedRelationEvents = [...params.relationEvents].toSorted((left, right) => {
712
+ const leftTs = typeof left.origin_server_ts === "number" && Number.isFinite(left.origin_server_ts) ? left.origin_server_ts : Number.POSITIVE_INFINITY;
713
+ const rightTs = typeof right.origin_server_ts === "number" && Number.isFinite(right.origin_server_ts) ? right.origin_server_ts : Number.POSITIVE_INFINITY;
714
+ if (leftTs !== rightTs) return leftTs - rightTs;
715
+ return (left.event_id ?? "").localeCompare(right.event_id ?? "");
716
+ });
717
+ for (const event of orderedRelationEvents) {
718
+ if (event.unsigned?.redacted_because) continue;
719
+ if (!isPollResponseType(typeof event.type === "string" ? event.type : "")) continue;
720
+ const senderId = normalizeOptionalString(event.sender) ?? "";
721
+ if (!senderId) continue;
722
+ const eventTs = typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts) ? event.origin_server_ts : Number.POSITIVE_INFINITY;
723
+ if (eventTs > pollClosedAt) continue;
724
+ const rawAnswers = parsePollResponseAnswerIds(event.content) ?? [];
725
+ const normalizedAnswers = Array.from(new Set(rawAnswers.map((answerId) => normalizeOptionalString(answerId) ?? "").filter((answerId) => answerIds.has(answerId)).slice(0, parsed.maxSelections)));
726
+ latestVoteBySender.set(senderId, {
727
+ ts: eventTs,
728
+ eventId: typeof event.event_id === "string" ? event.event_id : "",
729
+ answerIds: normalizedAnswers
730
+ });
731
+ }
732
+ const voteCounts = new Map(parsed.answers.map((answer) => [answer.id, 0]));
733
+ let totalVotes = 0;
734
+ for (const latestVote of latestVoteBySender.values()) {
735
+ if (latestVote.answerIds.length === 0) continue;
736
+ totalVotes += 1;
737
+ for (const answerId of latestVote.answerIds) voteCounts.set(answerId, (voteCounts.get(answerId) ?? 0) + 1);
738
+ }
739
+ return {
740
+ eventId: params.pollEventId,
741
+ roomId: params.roomId,
742
+ sender: params.sender,
743
+ senderName: params.senderName,
744
+ question: parsed.question,
745
+ answers: parsed.answers.map((answer) => answer.text),
746
+ kind: parsed.kind,
747
+ maxSelections: parsed.maxSelections,
748
+ entries: parsed.answers.map((answer) => ({
749
+ id: answer.id,
750
+ text: answer.text,
751
+ votes: voteCounts.get(answer.id) ?? 0
752
+ })),
753
+ totalVotes,
754
+ closed: Number.isFinite(pollClosedAt)
755
+ };
756
+ }
757
+ function formatPollResultsAsText(summary) {
758
+ const lines = [
759
+ summary.closed ? "[Poll closed]" : "[Poll]",
760
+ summary.question,
761
+ ""
762
+ ];
763
+ const revealResults = summary.kind === "m.poll.disclosed" || summary.closed;
764
+ for (const [index, entry] of summary.entries.entries()) {
765
+ if (!revealResults) {
766
+ lines.push(`${index + 1}. ${entry.text}`);
767
+ continue;
768
+ }
769
+ lines.push(`${index + 1}. ${entry.text} (${entry.votes} vote${entry.votes === 1 ? "" : "s"})`);
770
+ }
771
+ lines.push("");
772
+ if (!revealResults) lines.push("Responses are hidden until the poll closes.");
773
+ else lines.push(`Total voters: ${summary.totalVotes}`);
774
+ return lines.join("\n");
775
+ }
776
+ function buildTextContent$1(body) {
777
+ return {
778
+ "m.text": body,
779
+ "org.matrix.msc1767.text": body
780
+ };
781
+ }
782
+ function buildPollFallbackText(question, answers) {
783
+ if (answers.length === 0) return question;
784
+ return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
785
+ }
786
+ function buildPollStartContent(poll) {
787
+ const normalized = normalizePollInput(poll);
788
+ const answers = normalized.options.map((option, idx) => ({
789
+ id: `answer${idx + 1}`,
790
+ ...buildTextContent$1(option)
791
+ }));
792
+ const isMultiple = normalized.maxSelections > 1;
793
+ const fallbackText = buildPollFallbackText(normalized.question, answers.map((answer) => getTextContent(answer)));
794
+ return {
795
+ [M_POLL_START]: {
796
+ question: buildTextContent$1(normalized.question),
797
+ kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed",
798
+ max_selections: normalized.maxSelections,
799
+ answers
800
+ },
801
+ "m.text": fallbackText,
802
+ "org.matrix.msc1767.text": fallbackText
803
+ };
804
+ }
805
+ function buildPollResponseContent(pollEventId, answerIds) {
806
+ return {
807
+ [M_POLL_RESPONSE]: { answers: answerIds },
808
+ [ORG_POLL_RESPONSE]: { answers: answerIds },
809
+ "m.relates_to": {
810
+ rel_type: "m.reference",
811
+ event_id: pollEventId
812
+ }
813
+ };
814
+ }
815
+ //#endregion
816
+ //#region extensions/matrix/src/matrix/format-profile.ts
817
+ function createMatrixPrivateMarkers(markdown, exhaustedMessage) {
234
818
  const used = new Set(Array.from(markdown, (character) => character.charCodeAt(0)));
235
819
  for (const match of markdown.matchAll(/&#(?:x([0-9a-f]+)|(\d+));/giu)) {
236
820
  const radix = match[1] ? 16 : 10;
@@ -759,937 +1343,559 @@ function mutateInlineTokensWithMentions(params) {
759
1343
  roomMentioned
760
1344
  };
761
1345
  }
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
1346
+ function compactLooseListTokens(tokens) {
1347
+ const listItemStack = [];
1348
+ for (const [index, token] of tokens.entries()) {
1349
+ if (token.type === "list_item_open") {
1350
+ listItemStack.push({
1351
+ level: token.level,
1352
+ immediateParagraphOpenIndexes: [],
1353
+ immediateParagraphCloseIndexes: []
1354
+ });
1355
+ continue;
1356
+ }
1357
+ if (token.type === "list_item_close") {
1358
+ const item = listItemStack.pop();
1359
+ if (item && item.immediateParagraphOpenIndexes.length === 1 && item.immediateParagraphCloseIndexes.length === 1) {
1360
+ const openIndex = item.immediateParagraphOpenIndexes[0];
1361
+ const closeIndex = item.immediateParagraphCloseIndexes[0];
1362
+ const openToken = openIndex === void 0 ? void 0 : tokens[openIndex];
1363
+ const closeToken = closeIndex === void 0 ? void 0 : tokens[closeIndex];
1364
+ if (openToken && closeToken) {
1365
+ openToken.hidden = true;
1366
+ closeToken.hidden = true;
1367
+ }
1331
1368
  }
1332
- };
1369
+ continue;
1370
+ }
1371
+ const currentItem = listItemStack.at(-1);
1372
+ if (!currentItem || token.level !== currentItem.level + 1) continue;
1373
+ if (token.type === "paragraph_open") currentItem.immediateParagraphOpenIndexes.push(index);
1374
+ else if (token.type === "paragraph_close") currentItem.immediateParagraphCloseIndexes.push(index);
1333
1375
  }
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
1376
  }
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));
1377
+ function markdownToMatrixHtml(markdown, options = {}) {
1378
+ if (hasMatrixSpoilerMetadataCollision(markdown)) return renderMatrixFallbackHtml(markdown);
1379
+ const tokens = parseMatrixMarkdown(projectMatrixMarkdown(markdown), options.tableMode);
1380
+ compactLooseListTokens(tokens);
1381
+ return md.renderer.render(tokens, md.options, {}).trimEnd();
1358
1382
  }
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));
1383
+ function protectMatrixSpoilerDelimiters(markdown) {
1384
+ const offsets = findMatrixSpoilerDelimiterOffsets(markdown);
1385
+ if (offsets.length === 0) return { markdown };
1386
+ const markers = createMatrixPrivateMarkers(markdown, "Matrix spoiler formatting exhausted its private marker pool");
1387
+ let protectedMarkdown = "";
1388
+ let cursor = 0;
1389
+ for (const [index, offset] of offsets.entries()) {
1390
+ const marker = index % 2 === 0 ? markers.open : markers.close;
1391
+ protectedMarkdown += `${markdown.slice(cursor, offset)}${marker}${markers.padding}`;
1392
+ cursor = offset + 2;
1393
+ }
1394
+ protectedMarkdown += markdown.slice(cursor);
1395
+ return {
1396
+ markdown: protectedMarkdown,
1397
+ markers
1398
+ };
1364
1399
  }
1365
- async function readJoinedMatrixMembers(client, roomId) {
1400
+ function parseMatrixMarkdown(markdown, tableMode) {
1401
+ const protectedSpoilers = protectMatrixSpoilerDelimiters(markdown);
1402
+ if (tableMode === "off") md.disable("table");
1366
1403
  try {
1367
- return normalizeJoinedMatrixMembers(await client.getJoinedRoomMembers(roomId));
1368
- } catch {
1369
- return null;
1404
+ return md.parse(protectedSpoilers.markdown, { matrixSpoilerMarkers: protectedSpoilers.markers });
1405
+ } finally {
1406
+ if (tableMode === "off") md.enable("table");
1370
1407
  }
1371
1408
  }
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;
1409
+ function markdownToMatrixBody(markdown) {
1410
+ const projected = projectMatrixMarkdown(markdown);
1411
+ const offsets = findMatrixSpoilerDelimiterOffsets(projected);
1412
+ const metadataCollision = hasMatrixSpoilerMetadataCollision(projected);
1413
+ if (offsets.length === 0 && !metadataCollision) return projected;
1414
+ let body = projected;
1415
+ if (metadataCollision) body = "[Spoiler]";
1416
+ else for (let index = offsets.length - 2; index >= 0; index -= 2) {
1417
+ const open = offsets[index];
1418
+ const close = offsets[index + 1];
1419
+ if (open !== void 0 && close !== void 0) body = `${body.slice(0, open)}[Spoiler]${body.slice(close + 2)}`;
1382
1420
  }
1421
+ return renderMarkdownWithMarkers(markdownToIR(body, {
1422
+ enableHtmlUnderline: true,
1423
+ headingStyle: "rich",
1424
+ linkify: true
1425
+ }), {
1426
+ styleMarkers: {},
1427
+ escapeText: (text) => text
1428
+ }, MATRIX_FORMAT_PROFILE);
1383
1429
  }
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);
1430
+ function renderMatrixFallbackHtml(markdown) {
1431
+ return `<p>${escapeHtml(markdownToMatrixBody(markdown)).replaceAll("\n", "<br>\n")}</p>`;
1432
+ }
1433
+ async function resolveMarkdownMentionState(params) {
1434
+ const tokens = parseMatrixMarkdown(maskEscapedMentions(projectMatrixMarkdown(params.markdown)), params.tableMode);
1435
+ restoreEscapedMentionsInBlockTokens(tokens);
1436
+ const selfUserId = await resolveMatrixSelfUserId(params.client);
1437
+ const userIds = [];
1438
+ const seenUserIds = /* @__PURE__ */ new Set();
1439
+ let roomMentioned = false;
1440
+ for (const token of tokens) {
1441
+ if (!token.children?.length) continue;
1442
+ const mutated = mutateInlineTokensWithMentions({
1443
+ children: token.children,
1444
+ userIds,
1445
+ seenUserIds,
1446
+ selfUserId
1447
+ });
1448
+ token.children = mutated.children;
1449
+ roomMentioned ||= mutated.roomMentioned;
1450
+ }
1451
+ const mentions = {};
1452
+ if (userIds.length > 0) mentions.user_ids = userIds;
1453
+ if (roomMentioned) mentions.room = true;
1399
1454
  return {
1400
- joinedMembers,
1401
- strict,
1402
- viaMemberState: memberStateFlag === true,
1403
- memberStateFlag
1455
+ tokens,
1456
+ mentions
1404
1457
  };
1405
1458
  }
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;
1459
+ async function resolveMatrixMentionsInMarkdown(params) {
1460
+ return (await resolveMarkdownMentionState(params)).mentions;
1413
1461
  }
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 {};
1462
+ async function renderMarkdownToMatrixHtmlWithMentions(params) {
1463
+ const state = await resolveMarkdownMentionState(params);
1464
+ if (hasMatrixSpoilerMetadataCollision(params.markdown)) {
1465
+ const redacted = markdownToMatrixBody(params.markdown);
1466
+ const redactedState = await resolveMarkdownMentionState({
1467
+ ...params,
1468
+ markdown: redacted
1469
+ });
1470
+ return {
1471
+ html: renderMatrixFallbackHtml(params.markdown),
1472
+ mentions: redactedState.mentions
1473
+ };
1430
1474
  }
1475
+ compactLooseListTokens(state.tokens);
1476
+ return {
1477
+ html: md.renderer.render(state.tokens, md.options, {}).trimEnd() || void 0,
1478
+ mentions: state.mentions
1479
+ };
1431
1480
  }
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;
1481
+ //#endregion
1482
+ //#region extensions/matrix/src/matrix/send/chunking.ts
1483
+ const getCore$2 = () => getMatrixRuntime();
1484
+ function normalizeMatrixEventLimit(limit) {
1485
+ if (!Number.isFinite(limit) || limit <= 0) return limit;
1486
+ return Math.max(1, Math.floor(limit));
1449
1487
  }
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;
1488
+ function resolveMatrixChunkOverflow(chunk, limit) {
1489
+ const body = markdownToMatrixBody(chunk);
1490
+ const renderedLength = Math.max(chunk.length, body.length);
1491
+ if (limit === 1 && Array.from(chunk).length === 1 && Array.from(body).length === 1) return 0;
1492
+ return Math.max(0, renderedLength - limit);
1460
1493
  }
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]);
1494
+ function protectMatrixUnderlineTags(markdown) {
1495
+ const codeRegions = findCodeRegions(markdown);
1496
+ const metadataRanges = findMatrixMarkdownMetadataRanges(markdown);
1497
+ 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));
1498
+ if (tags.length === 0) return { markdown };
1499
+ const markers = createMatrixPrivateMarkers(markdown, "Matrix underline chunking exhausted its private marker pool");
1500
+ let depth = 0;
1501
+ const replacements = tags.flatMap((tag) => {
1502
+ if (!tag.closing) {
1503
+ depth += 1;
1504
+ return [{
1505
+ tag,
1506
+ marker: depth === 1 ? markers.open : ""
1507
+ }];
1508
+ }
1509
+ if (depth === 0) return [];
1510
+ depth -= 1;
1511
+ return [{
1512
+ tag,
1513
+ marker: depth === 0 ? markers.close : ""
1514
+ }];
1515
+ });
1516
+ let protectedMarkdown = markdown;
1517
+ 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)}`;
1518
+ return {
1519
+ markdown: protectedMarkdown,
1520
+ markers
1521
+ };
1465
1522
  }
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;
1523
+ function restoreMatrixStyleChunks(chunks, spoiler, underline) {
1524
+ const stack = [];
1525
+ const syntax = {
1526
+ spoiler: {
1527
+ open: "||",
1528
+ close: "||",
1529
+ markers: spoiler
1530
+ },
1531
+ underline: {
1532
+ open: "<u>",
1533
+ close: "</u>",
1534
+ markers: underline
1535
+ }
1536
+ };
1537
+ return chunks.map((chunk) => {
1538
+ let restored = stack.map((style) => syntax[style].open).join("");
1539
+ for (const character of chunk) {
1540
+ const opening = Object.keys(syntax).find((style) => character === syntax[style].markers?.open);
1541
+ const closing = Object.keys(syntax).find((style) => character === syntax[style].markers?.close);
1542
+ if (opening) {
1543
+ stack.push(opening);
1544
+ restored += syntax[opening].open;
1545
+ } else if (closing) {
1546
+ const stackIndex = stack.lastIndexOf(closing);
1547
+ if (stackIndex >= 0) {
1548
+ const above = stack.slice(stackIndex + 1);
1549
+ restored += above.toReversed().map((style) => syntax[style].close).join("");
1550
+ restored += syntax[closing].close;
1551
+ stack.splice(stackIndex, 1);
1552
+ restored += above.map((style) => syntax[style].open).join("");
1553
+ }
1554
+ } else if (character !== spoiler?.padding && character !== underline?.padding) restored += character;
1555
+ }
1556
+ return restored + stack.toReversed().map((style) => syntax[style].close).join("");
1557
+ });
1472
1558
  }
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
1559
+ function splitMatrixTableSegments(markdown) {
1560
+ const segments = [];
1561
+ let cursor = 0;
1562
+ for (const range of findMatrixTableSourceRanges(markdown)) {
1563
+ const plain = markdown.slice(cursor, range.start).replace(/(?:[ \t]*\n)+$/u, "");
1564
+ if (plain.trim()) segments.push({
1565
+ table: false,
1566
+ text: plain
1480
1567
  });
1481
- const changed = !hasMatrixDirectRoomMappings({
1482
- directContent: directContentBefore,
1483
- remoteUserId: params.remoteUserId,
1484
- roomIds: params.roomIds
1568
+ const rawTable = markdown.slice(range.start, range.end).trimEnd();
1569
+ const indent = /^ +/u.exec(rawTable)?.[0] ?? "";
1570
+ const table = indent ? rawTable.split("\n").map((line) => line.startsWith(indent) ? line.slice(indent.length) : line).join("\n") : rawTable;
1571
+ segments.push({
1572
+ table: true,
1573
+ text: table
1485
1574
  });
1486
- if (changed) await params.client.setAccountData(EventType.Direct, directContentAfter);
1487
- return {
1488
- changed,
1489
- directContentBefore,
1490
- directContentAfter
1491
- };
1575
+ cursor = range.end;
1576
+ }
1577
+ const tail = markdown.slice(cursor).replace(/^(?:[ \t]*\n)+/u, "");
1578
+ if (tail.trim()) segments.push({
1579
+ table: false,
1580
+ text: tail
1492
1581
  });
1582
+ return segments;
1493
1583
  }
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
1584
+ function prepareMatrixSingleText(text, opts) {
1585
+ const normalizedText = text.replace(/\r\n?/gu, "\n");
1586
+ const trimmedText = opts.preserveWhitespace ? normalizedText : normalizedText.trim();
1587
+ const cfg = requireRuntimeConfig(opts.cfg, "Matrix text preparation");
1588
+ const tableMode = opts.tableMode ?? getCore$2().channel.text.resolveMarkdownTableMode({
1589
+ cfg,
1590
+ channel: "matrix",
1591
+ accountId: opts.accountId,
1592
+ supportsBlockTables: MATRIX_FORMAT_PROFILE.constructs.table === "native"
1500
1593
  });
1594
+ const convertedText = renderMatrixMarkdownTables(trimmedText, tableMode);
1595
+ const singleEventLimit = normalizeMatrixEventLimit(Math.min(getCore$2().channel.text.resolveTextChunkLimit(cfg, "matrix", opts.accountId), MATRIX_FORMAT_PROFILE.chunk.limit));
1596
+ const eventTextLength = Math.max(convertedText.length, markdownToMatrixBody(convertedText).length);
1501
1597
  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
1598
+ trimmedText,
1599
+ convertedText,
1600
+ singleEventLimit,
1601
+ eventTextLength,
1602
+ fitsInSingleEvent: eventTextLength <= singleEventLimit,
1603
+ tableMode
1507
1604
  };
1508
1605
  }
1509
- function buildNextDirectContent(params) {
1510
- const current = normalizeMappedRoomIds(params.directContent, params.remoteUserId);
1511
- const nextRooms = normalizeRoomIdList([...params.roomIds, ...current]);
1606
+ function chunkMatrixText(text, opts) {
1607
+ const preparedText = prepareMatrixSingleText(text, opts);
1608
+ if (preparedText.fitsInSingleEvent) return {
1609
+ ...preparedText,
1610
+ chunks: preparedText.convertedText ? [preparedText.convertedText] : []
1611
+ };
1612
+ const cfg = requireRuntimeConfig(opts.cfg, "Matrix text chunking");
1613
+ const chunkMode = getCore$2().channel.text.resolveChunkMode(cfg, "matrix", opts.accountId);
1614
+ const collisionRedacted = hasMatrixSpoilerMetadataCollision(preparedText.convertedText) ? markdownToMatrixBody(preparedText.convertedText) : void 0;
1615
+ const chunkSegment = (segmentText) => {
1616
+ const protectedUnderline = protectMatrixUnderlineTags(hasMatrixSpoilerMetadataCollision(segmentText) ? markdownToMatrixBody(segmentText) : segmentText);
1617
+ const protectedSpoilers = protectMatrixSpoilerDelimiters(protectedUnderline.markdown);
1618
+ const wrapperReserve = (protectedSpoilers.markers ? 4 : 0) + (protectedUnderline.markers ? 7 : 0);
1619
+ const privateMarkers = [protectedSpoilers.markers, protectedUnderline.markers].flatMap((markers) => markers ? [
1620
+ markers.open,
1621
+ markers.close,
1622
+ markers.padding
1623
+ ] : []);
1624
+ let reserve = wrapperReserve;
1625
+ while (reserve < preparedText.singleEventLimit) {
1626
+ const protectedChunks = getCore$2().channel.text.chunkMarkdownTextWithMode(protectedSpoilers.markdown, preparedText.singleEventLimit - reserve, chunkMode);
1627
+ const restored = restoreMatrixStyleChunks(protectedChunks, protectedSpoilers.markers, protectedUnderline.markers).filter((_, index) => {
1628
+ return privateMarkers.reduce((value, marker) => value.replaceAll(marker, ""), protectedChunks[index] ?? "").length > 0;
1629
+ });
1630
+ const overflow = Math.max(0, ...restored.map((chunk) => resolveMatrixChunkOverflow(chunk, preparedText.singleEventLimit)));
1631
+ if (overflow === 0) return restored;
1632
+ reserve += overflow;
1633
+ }
1634
+ throw new Error("Matrix text chunk limit is too small for formatted content");
1635
+ };
1636
+ const chunks = collisionRedacted !== void 0 ? chunkSegment(collisionRedacted) : preparedText.tableMode === "block" ? splitMatrixTableSegments(preparedText.convertedText).flatMap((segment) => {
1637
+ if (!segment.table) return chunkSegment(segment.text);
1638
+ return segment.text.length <= preparedText.singleEventLimit ? [segment.text] : chunkSegment(renderMatrixMarkdownTables(segment.text, "bullets"));
1639
+ }) : chunkSegment(preparedText.convertedText);
1512
1640
  return {
1513
- ...params.directContent,
1514
- [params.remoteUserId]: nextRooms
1641
+ ...preparedText,
1642
+ chunks
1515
1643
  };
1516
1644
  }
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({
1645
+ //#endregion
1646
+ //#region extensions/matrix/src/matrix/send/formatting.ts
1647
+ const getCore$1 = () => getMatrixRuntime();
1648
+ async function renderMatrixFormattedContent(params) {
1649
+ const markdown = params.markdown ?? "";
1650
+ const body = markdownToMatrixBody(markdown);
1651
+ if (params.includeMentions === false) return {
1652
+ body,
1653
+ html: markdownToMatrixHtml(markdown, { tableMode: params.tableMode }).trimEnd() || void 0
1654
+ };
1655
+ const { html, mentions } = await renderMarkdownToMatrixHtmlWithMentions({
1656
+ markdown,
1528
1657
  client: params.client,
1529
- roomId: params.roomId,
1530
- remoteUserId,
1531
- selfUserId: params.selfUserId
1658
+ tableMode: params.tableMode
1532
1659
  });
1533
- if (!evidence.strict) return {
1534
- classifyAsDirect: false,
1535
- repaired: false,
1536
- reason: "not-strict"
1660
+ return {
1661
+ body,
1662
+ html,
1663
+ mentions
1537
1664
  };
1538
- if (evidence.memberStateFlag === false) return {
1539
- classifyAsDirect: false,
1540
- repaired: false,
1541
- reason: "local-explicit-false"
1665
+ }
1666
+ function buildTextContent(body, relation, opts = {}) {
1667
+ const msgtype = opts.msgtype ?? MsgType.Text;
1668
+ return relation ? {
1669
+ msgtype,
1670
+ body,
1671
+ "m.relates_to": relation
1672
+ } : {
1673
+ msgtype,
1674
+ body
1542
1675
  };
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
1676
  }
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({
1677
+ async function enrichMatrixFormattedContent(params) {
1678
+ const { body, html, mentions } = await renderMatrixFormattedContent({
1569
1679
  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);
1680
+ markdown: params.markdown,
1681
+ includeMentions: params.includeMentions,
1682
+ tableMode: params.tableMode
1683
+ });
1684
+ params.content.body = body || params.content.body;
1685
+ if (mentions) params.content["m.mentions"] = mentions;
1686
+ else delete params.content["m.mentions"];
1687
+ if (!html) {
1688
+ delete params.content.format;
1689
+ delete params.content.formatted_body;
1690
+ return;
1594
1691
  }
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
- };
1692
+ params.content.format = "org.matrix.custom.html";
1693
+ params.content.formatted_body = html;
1694
+ }
1695
+ async function resolveMatrixMentionsForBody(params) {
1696
+ return await resolveMatrixMentionsInMarkdown({
1697
+ markdown: params.body ?? "",
1698
+ client: params.client
1699
+ });
1700
+ }
1701
+ function normalizeMentionUserIds(value) {
1702
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
1703
+ }
1704
+ function extractMatrixMentions(content) {
1705
+ const rawMentions = content?.["m.mentions"];
1706
+ if (!rawMentions || typeof rawMentions !== "object") return {};
1707
+ const mentions = rawMentions;
1708
+ const normalized = {};
1709
+ const userIds = normalizeMentionUserIds(mentions.user_ids);
1710
+ if (userIds.length > 0) normalized.user_ids = userIds;
1711
+ if (mentions.room === true) normalized.room = true;
1712
+ return normalized;
1713
+ }
1714
+ function diffMatrixMentions(current, previous) {
1715
+ const previousUserIds = new Set(previous.user_ids ?? []);
1716
+ const newUserIds = (current.user_ids ?? []).filter((userId) => !previousUserIds.has(userId));
1717
+ const delta = {};
1718
+ if (newUserIds.length > 0) delta.user_ids = newUserIds;
1719
+ if (current.room && !previous.room) delta.room = true;
1720
+ return delta;
1605
1721
  }
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
1722
+ function buildReplyRelation(replyToId) {
1723
+ const trimmed = replyToId?.trim();
1724
+ if (!trimmed) return;
1725
+ return { "m.in_reply_to": { event_id: trimmed } };
1726
+ }
1727
+ function buildThreadRelation(threadId, replyToId) {
1728
+ const trimmed = threadId.trim();
1729
+ const relation = {
1730
+ rel_type: RelationType.Thread,
1731
+ event_id: trimmed
1626
1732
  };
1733
+ const fallbackReplyToId = replyToId?.trim();
1734
+ if (fallbackReplyToId) {
1735
+ relation.is_falling_back = true;
1736
+ relation["m.in_reply_to"] = { event_id: fallbackReplyToId };
1737
+ }
1738
+ return relation;
1627
1739
  }
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;
1740
+ function resolveMatrixMsgType(contentType, _fileName) {
1741
+ switch (getCore$1().media.mediaKindFromMime(contentType ?? "")) {
1742
+ case "image": return MsgType.Image;
1743
+ case "audio": return MsgType.Audio;
1744
+ case "video": return MsgType.Video;
1745
+ default: return MsgType.File;
1746
+ }
1634
1747
  }
1635
- function normalizeThreadId(raw) {
1636
- return normalizeOptionalStringifiedId(raw) ?? null;
1748
+ function resolveMatrixVoiceDecision(opts) {
1749
+ if (!opts.wantsVoice) return { useVoice: false };
1750
+ if (isMatrixVoiceCompatibleAudio(opts)) return { useVoice: true };
1751
+ return { useVoice: false };
1637
1752
  }
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;
1753
+ function isMatrixVoiceCompatibleAudio(opts) {
1754
+ return isVoiceMessageCompatibleAudio({
1755
+ contentType: opts.contentType,
1756
+ fileName: opts.fileName
1757
+ });
1646
1758
  }
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);
1759
+ //#endregion
1760
+ //#region extensions/matrix/src/matrix/send/media.ts
1761
+ const getCore = () => getMatrixRuntime();
1762
+ function buildMatrixMediaInfo(params) {
1763
+ const base = {};
1764
+ if (Number.isFinite(params.size)) base.size = params.size;
1765
+ if (params.mimetype) base.mimetype = params.mimetype;
1766
+ if (params.imageInfo) {
1767
+ const dimensional = {
1768
+ ...base,
1769
+ ...params.imageInfo
1770
+ };
1771
+ if (typeof params.durationMs === "number") return {
1772
+ ...dimensional,
1773
+ duration: params.durationMs
1774
+ };
1775
+ return dimensional;
1653
1776
  }
1777
+ if (typeof params.durationMs === "number") return {
1778
+ ...base,
1779
+ duration: params.durationMs
1780
+ };
1781
+ if (Object.keys(base).length === 0) return;
1782
+ return base;
1654
1783
  }
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
1784
+ function buildMediaContent(params) {
1785
+ const info = buildMatrixMediaInfo({
1786
+ size: params.size,
1787
+ mimetype: params.mimetype,
1788
+ durationMs: params.durationMs,
1789
+ imageInfo: params.imageInfo
1671
1790
  });
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;
1791
+ const base = {
1792
+ msgtype: params.msgtype,
1793
+ body: params.body,
1794
+ filename: params.filename,
1795
+ info: info ?? void 0
1796
+ };
1797
+ if (!params.file && params.url) base.url = params.url;
1798
+ if (params.file) base.file = params.file;
1799
+ if (params.isVoice) {
1800
+ base["org.matrix.msc3245.voice"] = {};
1801
+ if (typeof params.durationMs === "number") base["org.matrix.msc1767.audio"] = { duration: params.durationMs };
1680
1802
  }
1681
- throw new Error(`No direct room found for ${trimmed} (m.direct missing)`);
1803
+ if (params.relation) base["m.relates_to"] = params.relation;
1804
+ return base;
1682
1805
  }
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;
1806
+ const THUMBNAIL_MAX_SIDE = 800;
1807
+ const THUMBNAIL_QUALITY = 80;
1808
+ const AIFC_IMA4_BYTES_PER_CHANNEL_PACKET = 34;
1809
+ const AIFC_IMA4_FRAMES_PER_PACKET = 64;
1810
+ function resolveAifcIma4DurationSeconds(buffer, sampleRate) {
1811
+ if (!sampleRate || !Number.isFinite(sampleRate) || buffer.length < 12 || buffer.toString("ascii", 0, 4) !== "FORM" || buffer.toString("ascii", 8, 12) !== "AIFC") return;
1812
+ let channels;
1813
+ let declaredFrameCount;
1814
+ let soundDataBytes;
1815
+ for (let offset = 12; offset + 8 <= buffer.length;) {
1816
+ const chunkType = buffer.toString("ascii", offset, offset + 4);
1817
+ const chunkSize = buffer.readUInt32BE(offset + 4);
1818
+ const chunkStart = offset + 8;
1819
+ if (chunkSize > buffer.length - chunkStart) return;
1820
+ if (chunkType === "COMM") {
1821
+ if (chunkSize < 22 || buffer.toString("ascii", chunkStart + 18, chunkStart + 22) !== "ima4") return;
1822
+ channels = buffer.readUInt16BE(chunkStart);
1823
+ declaredFrameCount = buffer.readUInt32BE(chunkStart + 2);
1824
+ } else if (chunkType === "SSND") {
1825
+ if (chunkSize < 8) return;
1826
+ const soundDataOffset = buffer.readUInt32BE(chunkStart);
1827
+ if (soundDataOffset > chunkSize - 8) return;
1828
+ soundDataBytes = chunkSize - 8 - soundDataOffset;
1829
+ }
1830
+ offset = chunkStart + chunkSize + (chunkSize & 1);
1691
1831
  }
1692
- return target;
1832
+ if (!channels || !declaredFrameCount || !soundDataBytes) return;
1833
+ const packetSize = channels * AIFC_IMA4_BYTES_PER_CHANNEL_PACKET;
1834
+ if (soundDataBytes % packetSize !== 0) return;
1835
+ const packetCount = soundDataBytes / packetSize;
1836
+ if (packetCount !== declaredFrameCount) return;
1837
+ return packetCount * AIFC_IMA4_FRAMES_PER_PACKET / sampleRate;
1838
+ }
1839
+ async function prepareImageInfo(params) {
1840
+ const meta = await getCore().media.getImageMetadata(params.buffer).catch(() => null);
1841
+ if (!meta) return;
1842
+ const imageInfo = {
1843
+ w: meta.width,
1844
+ h: meta.height
1845
+ };
1846
+ if (Math.max(meta.width, meta.height) > THUMBNAIL_MAX_SIDE) try {
1847
+ const thumbBuffer = await getCore().media.resizeToJpeg({
1848
+ buffer: params.buffer,
1849
+ maxSide: THUMBNAIL_MAX_SIDE,
1850
+ quality: THUMBNAIL_QUALITY,
1851
+ withoutEnlargement: true
1852
+ });
1853
+ const thumbMeta = await getCore().media.getImageMetadata(thumbBuffer).catch(() => null);
1854
+ const result = await uploadMediaWithEncryption(params.client, params.roomId, thumbBuffer, {
1855
+ contentType: "image/jpeg",
1856
+ filename: "thumbnail.jpg"
1857
+ });
1858
+ if (result.file) imageInfo.thumbnail_file = result.file;
1859
+ else imageInfo.thumbnail_url = result.url;
1860
+ if (thumbMeta) imageInfo.thumbnail_info = {
1861
+ w: thumbMeta.width,
1862
+ h: thumbMeta.height,
1863
+ mimetype: "image/jpeg",
1864
+ size: thumbBuffer.byteLength
1865
+ };
1866
+ } catch {}
1867
+ return imageInfo;
1868
+ }
1869
+ async function resolveMediaDurationMs(params) {
1870
+ if (params.kind !== "audio" && params.kind !== "video") return;
1871
+ try {
1872
+ const fileInfo = params.contentType || params.fileName ? {
1873
+ mimeType: params.contentType,
1874
+ size: params.buffer.byteLength,
1875
+ path: params.fileName
1876
+ } : void 0;
1877
+ const metadata = await parseBuffer(params.buffer, fileInfo, {
1878
+ duration: true,
1879
+ skipCovers: true
1880
+ });
1881
+ const durationSeconds = resolveAifcIma4DurationSeconds(params.buffer, metadata.format.sampleRate) ?? metadata.format.duration;
1882
+ if (typeof durationSeconds === "number" && Number.isFinite(durationSeconds)) return Math.max(0, Math.round(durationSeconds * 1e3));
1883
+ } catch {}
1884
+ }
1885
+ async function uploadMediaWithEncryption(client, roomId, buffer, params) {
1886
+ if (await client.prepareRoomForMessageSend(roomId) === "m.room.encrypted") {
1887
+ if (!client.crypto) throw new Error("Encrypted Matrix room: enable encryption before uploading media");
1888
+ const encrypted = await client.crypto.encryptMedia(buffer);
1889
+ const mxc = await client.uploadContent(encrypted.buffer, "application/octet-stream");
1890
+ return {
1891
+ url: mxc,
1892
+ file: {
1893
+ url: mxc,
1894
+ ...encrypted.file
1895
+ }
1896
+ };
1897
+ }
1898
+ return { url: await client.uploadContent(buffer, params.contentType, params.filename) };
1693
1899
  }
1694
1900
  //#endregion
1695
1901
  //#region extensions/matrix/src/matrix/send.ts
@@ -1707,16 +1913,28 @@ var send_exports = /* @__PURE__ */ __exportAll({
1707
1913
  sendTypingMatrix: () => sendTypingMatrix
1708
1914
  });
1709
1915
  function createMatrixSendReceipt(params) {
1710
- return createMessageReceiptFromOutboundResults({
1711
- kind: params.kind,
1712
- ...params.replyToId ? { replyToId: params.replyToId } : {},
1916
+ const firstEvent = params.events[0];
1917
+ const receipt = createMessageReceiptFromOutboundResults({
1918
+ kind: firstEvent?.kind ?? "text",
1919
+ ...firstEvent?.replyToId ? { replyToId: firstEvent.replyToId } : {},
1713
1920
  ...params.threadId ? { threadId: params.threadId } : {},
1714
- results: params.platformMessageIds.map((messageId) => ({
1921
+ results: params.events.map(({ messageId }) => ({
1715
1922
  channel: "matrix",
1716
1923
  messageId,
1717
1924
  roomId: params.roomId
1718
1925
  }))
1719
1926
  });
1927
+ receipt.parts = receipt.parts.map((part, index) => {
1928
+ const event = params.events[index];
1929
+ const actualPart = {
1930
+ ...part,
1931
+ kind: event.kind
1932
+ };
1933
+ if (event.replyToId) actualPart.replyToId = event.replyToId;
1934
+ else delete actualPart.replyToId;
1935
+ return actualPart;
1936
+ });
1937
+ return receipt;
1720
1938
  }
1721
1939
  function isMatrixClient(value) {
1722
1940
  return typeof value.sendEvent === "function";
@@ -1769,8 +1987,13 @@ async function resolvePreviousEditMentions(params) {
1769
1987
  });
1770
1988
  }
1771
1989
  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");
1990
+ const messageText = message?.trimEnd() ?? "";
1991
+ if (!messageText.trim() && !opts.mediaUrl) throw new Error("Matrix send requires text or media");
1992
+ const durableIdentity = resolveMatrixDurableDeliveryIdentity({
1993
+ queueId: opts.deliveryQueueId,
1994
+ partIndex: opts.deliveryPartIndex,
1995
+ partCount: opts.deliveryPartCount
1996
+ });
1774
1997
  return await withResolvedMatrixSendClient({
1775
1998
  client: opts.client,
1776
1999
  cfg: opts.cfg,
@@ -1778,135 +2001,174 @@ async function sendMessageMatrix(to, message, opts) {
1778
2001
  accountId: opts.accountId
1779
2002
  }, async (client) => {
1780
2003
  const roomId = await resolveMatrixRoomId(client, to);
2004
+ const wireEventType = await client.prepareRoomForMessageSend(roomId);
1781
2005
  const cfg = requireRuntimeConfig(opts.cfg, "Matrix send");
1782
- const { chunks, tableMode } = chunkMatrixText(trimmedMessage, {
1783
- cfg,
1784
- accountId: opts.accountId
1785
- });
1786
2006
  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
2007
+ const transactionScopeId = durableIdentity ? await client.getTransactionScopeId() : void 0;
2008
+ let plannedEvents = (durableIdentity ? await loadMatrixDeliveryPlan({
2009
+ identity: durableIdentity,
2010
+ accountId: opts.accountId,
2011
+ roomId,
2012
+ transactionScopeId,
2013
+ wireEventType
2014
+ }) : null)?.events;
2015
+ if (!plannedEvents) {
2016
+ const { chunks, tableMode } = chunkMatrixText(messageText, {
2017
+ cfg,
2018
+ accountId: opts.accountId,
2019
+ preserveWhitespace: true
1867
2020
  });
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);
2021
+ const relation = threadId ? buildThreadRelation(threadId, opts.replyToId) : buildReplyRelation(opts.replyToId);
2022
+ let pendingExtraContent = opts.extraContent;
2023
+ const events = [];
2024
+ const prepareContent = (content, receiptKind) => {
2025
+ events.push({
2026
+ content: withMatrixExtraContentFields(content, pendingExtraContent),
2027
+ receiptKind
2028
+ });
2029
+ pendingExtraContent = void 0;
2030
+ };
2031
+ if (opts.mediaUrl) {
2032
+ const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg);
2033
+ const media = await loadOutboundMediaFromUrl(opts.mediaUrl, {
2034
+ maxBytes,
2035
+ mediaAccess: opts.mediaAccess,
2036
+ mediaLocalRoots: opts.mediaLocalRoots,
2037
+ mediaReadFile: opts.mediaReadFile
2038
+ });
2039
+ const uploaded = await uploadMediaWithEncryption(client, roomId, media.buffer, {
2040
+ contentType: media.contentType,
2041
+ filename: media.fileName
2042
+ });
2043
+ const durationMs = await resolveMediaDurationMs({
2044
+ buffer: media.buffer,
2045
+ contentType: media.contentType,
2046
+ fileName: media.fileName,
2047
+ kind: media.kind === "sticker" ? "unknown" : media.kind ?? "unknown"
2048
+ });
2049
+ const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName);
2050
+ const { useVoice } = resolveMatrixVoiceDecision({
2051
+ wantsVoice: opts.audioAsVoice === true,
2052
+ contentType: media.contentType,
2053
+ fileName: media.fileName
2054
+ });
2055
+ const msgtype = useVoice ? MsgType.Audio : baseMsgType;
2056
+ const receiptKind = useVoice ? "voice" : "media";
2057
+ const imageInfo = msgtype === MsgType.Image ? await prepareImageInfo({
2058
+ buffer: media.buffer,
2059
+ client,
2060
+ roomId
2061
+ }) : void 0;
2062
+ const [firstChunk, ...rest] = chunks;
2063
+ const captionMarkdown = useVoice ? "" : firstChunk ?? "";
2064
+ const content = buildMediaContent({
2065
+ msgtype,
2066
+ body: useVoice ? "Voice message" : captionMarkdown || media.fileName || "(file)",
2067
+ url: uploaded.url,
2068
+ file: uploaded.file,
2069
+ filename: media.fileName,
2070
+ mimetype: media.contentType,
2071
+ size: media.buffer.byteLength,
2072
+ durationMs,
2073
+ relation,
2074
+ isVoice: useVoice,
2075
+ imageInfo
2076
+ });
2077
+ await enrichMatrixFormattedContent({
2078
+ client,
2079
+ content,
2080
+ markdown: captionMarkdown,
2081
+ tableMode
2082
+ });
2083
+ prepareContent(content, receiptKind);
2084
+ const textChunks = useVoice ? chunks : rest;
2085
+ const followupRelation = useVoice || threadId ? relation : void 0;
2086
+ for (const chunk of textChunks) {
2087
+ if (!chunk.trim()) continue;
2088
+ const followup = buildTextContent(chunk, followupRelation);
2089
+ await enrichMatrixFormattedContent({
2090
+ client,
2091
+ content: followup,
2092
+ markdown: chunk,
2093
+ tableMode
2094
+ });
2095
+ prepareContent(followup, "text");
2096
+ }
2097
+ } else for (const chunk of chunks.length ? chunks : [""]) {
2098
+ if (!chunk.trim()) continue;
2099
+ const content = buildTextContent(chunk, relation);
1877
2100
  await enrichMatrixFormattedContent({
1878
2101
  client,
1879
- content: followup,
1880
- markdown: text,
2102
+ content,
2103
+ markdown: chunk,
1881
2104
  tableMode
1882
2105
  });
1883
- const followupEventId = await sendContent(followup, "text");
1884
- lastMessageId = followupEventId ?? lastMessageId;
1885
- if (followupEventId) platformMessageIds.push(followupEventId);
2106
+ prepareContent(content, "text");
1886
2107
  }
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
2108
+ plannedEvents = durableIdentity ? createMatrixPlannedEvents({
2109
+ identity: durableIdentity,
2110
+ events
2111
+ }) : events.map((event) => ({
2112
+ content: event.content,
2113
+ receiptKind: event.receiptKind,
2114
+ transactionId: ""
2115
+ }));
2116
+ }
2117
+ if (opts.mediaUrl) await client.prepareRoomForMessageSend(roomId, plannedEvents[0]?.content);
2118
+ let platformDispatchStarted = false;
2119
+ if (!durableIdentity) {
2120
+ await opts.onPlatformSendDispatch?.();
2121
+ platformDispatchStarted = true;
2122
+ }
2123
+ const acceptedEvents = [];
2124
+ const acceptedContents = [];
2125
+ let lastMessageId = "";
2126
+ for (const planned of plannedEvents) {
2127
+ const eventId = await client.sendMessage(roomId, planned.content, planned.transactionId || void 0, durableIdentity ? async (dispatch) => {
2128
+ await persistMatrixDeliveryPlan({
2129
+ identity: durableIdentity,
2130
+ accountId: opts.accountId,
2131
+ roomId,
2132
+ transactionScopeId,
2133
+ wireEventType: dispatch.eventType,
2134
+ events: plannedEvents,
2135
+ dispatch
2136
+ });
2137
+ if (!platformDispatchStarted) {
2138
+ await opts.onPlatformSendDispatch?.();
2139
+ platformDispatchStarted = true;
2140
+ }
2141
+ } : void 0);
2142
+ lastMessageId = eventId || lastMessageId;
2143
+ if (!eventId) continue;
2144
+ const eventReplyToId = planned.content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
2145
+ const acceptedEvent = {
2146
+ messageId: eventId,
2147
+ kind: planned.receiptKind,
2148
+ ...eventReplyToId ? { replyToId: eventReplyToId } : {}
2149
+ };
2150
+ acceptedEvents.push(acceptedEvent);
2151
+ const visibleContent = planned.content.body ?? "";
2152
+ acceptedContents.push(visibleContent);
2153
+ await opts.onDeliveryResult?.({
2154
+ messageId: eventId,
2155
+ roomId,
2156
+ primaryMessageId: eventId,
2157
+ receipt: createMatrixSendReceipt({
2158
+ roomId,
2159
+ events: [acceptedEvent],
2160
+ threadId
2161
+ }),
2162
+ content: visibleContent
1896
2163
  });
1897
- const eventId = await sendContent(content, "text");
1898
- lastMessageId = eventId ?? lastMessageId;
1899
- if (eventId) platformMessageIds.push(eventId);
1900
2164
  }
1901
2165
  return {
1902
2166
  messageId: lastMessageId || "unknown",
1903
2167
  roomId,
1904
- primaryMessageId: platformMessageIds[0] ?? (lastMessageId || "unknown"),
2168
+ primaryMessageId: acceptedEvents[0]?.messageId ?? (lastMessageId || "unknown"),
1905
2169
  receipt: createMatrixSendReceipt({
1906
2170
  roomId,
1907
- platformMessageIds,
1908
- kind: receiptKind,
1909
- replyToId: opts.replyToId,
2171
+ events: acceptedEvents,
1910
2172
  threadId
1911
2173
  }),
1912
2174
  content: acceptedContents.join("\n")
@@ -1967,11 +2229,12 @@ async function sendReadReceiptMatrix(roomId, eventId, client) {
1967
2229
  });
1968
2230
  }
1969
2231
  async function sendSingleTextMessageMatrix(roomId, text, opts) {
1970
- const { trimmedText, convertedText, singleEventLimit, eventTextLength, fitsInSingleEvent, tableMode } = prepareMatrixSingleText(text, {
2232
+ const { trimmedText, convertedText, singleEventLimit, eventTextLength, fitsInSingleEvent, tableMode } = prepareMatrixSingleText(text.trimEnd(), {
1971
2233
  cfg: opts.cfg,
1972
- accountId: opts.accountId
2234
+ accountId: opts.accountId,
2235
+ preserveWhitespace: true
1973
2236
  });
1974
- if (!trimmedText) throw new Error("Matrix single-message send requires text");
2237
+ if (!trimmedText.trim()) throw new Error("Matrix single-message send requires text");
1975
2238
  if (!fitsInSingleEvent) throw new Error(`Matrix single-message text exceeds limit (${eventTextLength} > ${singleEventLimit})`);
1976
2239
  return await withResolvedMatrixSendClient({
1977
2240
  client: opts.client,
@@ -1991,15 +2254,18 @@ async function sendSingleTextMessageMatrix(roomId, text, opts) {
1991
2254
  });
1992
2255
  if (opts.live) content[MSC4357_LIVE_KEY] = {};
1993
2256
  const eventId = await client.sendMessage(resolvedRoom, content);
2257
+ const replyToId = content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
1994
2258
  return {
1995
2259
  messageId: eventId ?? "unknown",
1996
2260
  roomId: resolvedRoom,
1997
2261
  primaryMessageId: eventId ?? "unknown",
1998
2262
  receipt: createMatrixSendReceipt({
1999
2263
  roomId: resolvedRoom,
2000
- platformMessageIds: eventId ? [eventId] : [],
2001
- kind: "text",
2002
- replyToId: opts.replyToId,
2264
+ events: eventId ? [{
2265
+ messageId: eventId,
2266
+ kind: "text",
2267
+ ...replyToId ? { replyToId } : {}
2268
+ }] : [],
2003
2269
  threadId: normalizedThreadId
2004
2270
  }),
2005
2271
  content: content.body
@@ -2074,4 +2340,4 @@ async function reactMatrixMessage(roomId, messageId, emoji, opts) {
2074
2340
  });
2075
2341
  }
2076
2342
  //#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 };
2343
+ 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 };