@gakr-gakr/matrix 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/SPEC-SUPPORT.md +116 -0
  3. package/api.ts +38 -0
  4. package/auth-presence.ts +56 -0
  5. package/autobot.plugin.json +28 -0
  6. package/channel-plugin-api.ts +3 -0
  7. package/cli-metadata.ts +11 -0
  8. package/contract-api.ts +17 -0
  9. package/doctor-contract-api.ts +1 -0
  10. package/helper-api.ts +3 -0
  11. package/index.ts +55 -0
  12. package/package.json +101 -0
  13. package/plugin-entry.handlers.runtime.ts +1 -0
  14. package/runtime-api.ts +72 -0
  15. package/runtime-heavy-api.ts +1 -0
  16. package/runtime-setter-api.ts +3 -0
  17. package/secret-contract-api.ts +5 -0
  18. package/setup-entry.ts +17 -0
  19. package/setup-plugin-api.ts +3 -0
  20. package/src/account-selection.ts +223 -0
  21. package/src/actions.ts +346 -0
  22. package/src/approval-auth.ts +25 -0
  23. package/src/approval-handler.runtime.ts +595 -0
  24. package/src/approval-ids.ts +6 -0
  25. package/src/approval-native.ts +348 -0
  26. package/src/approval-reaction-auth.ts +45 -0
  27. package/src/approval-reactions.ts +313 -0
  28. package/src/auth-precedence.ts +61 -0
  29. package/src/channel-account-paths.ts +97 -0
  30. package/src/channel.runtime.ts +17 -0
  31. package/src/channel.setup.ts +48 -0
  32. package/src/channel.ts +667 -0
  33. package/src/cli-metadata.ts +19 -0
  34. package/src/cli.ts +2298 -0
  35. package/src/config-adapter.ts +41 -0
  36. package/src/config-schema.ts +159 -0
  37. package/src/config-ui-hints.ts +56 -0
  38. package/src/directory-live.ts +238 -0
  39. package/src/doctor-contract.ts +287 -0
  40. package/src/doctor.ts +262 -0
  41. package/src/env-vars.ts +92 -0
  42. package/src/exec-approval-resolver.ts +23 -0
  43. package/src/exec-approvals.ts +293 -0
  44. package/src/group-mentions.ts +41 -0
  45. package/src/legacy-crypto-inspector-availability.ts +60 -0
  46. package/src/legacy-crypto.ts +531 -0
  47. package/src/legacy-state.ts +156 -0
  48. package/src/matrix/account-config.ts +175 -0
  49. package/src/matrix/accounts.ts +194 -0
  50. package/src/matrix/actions/client.ts +31 -0
  51. package/src/matrix/actions/devices.ts +34 -0
  52. package/src/matrix/actions/limits.ts +6 -0
  53. package/src/matrix/actions/messages.ts +129 -0
  54. package/src/matrix/actions/pins.ts +63 -0
  55. package/src/matrix/actions/polls.ts +109 -0
  56. package/src/matrix/actions/profile.ts +37 -0
  57. package/src/matrix/actions/reactions.ts +59 -0
  58. package/src/matrix/actions/room.ts +71 -0
  59. package/src/matrix/actions/summary.ts +88 -0
  60. package/src/matrix/actions/types.ts +63 -0
  61. package/src/matrix/actions/verification.ts +589 -0
  62. package/src/matrix/actions.ts +37 -0
  63. package/src/matrix/active-client.ts +26 -0
  64. package/src/matrix/async-lock.ts +18 -0
  65. package/src/matrix/backup-health.ts +124 -0
  66. package/src/matrix/client/config-runtime-api.ts +9 -0
  67. package/src/matrix/client/config-secret-input.runtime.ts +1 -0
  68. package/src/matrix/client/config.ts +853 -0
  69. package/src/matrix/client/create-client.ts +105 -0
  70. package/src/matrix/client/env-auth.ts +95 -0
  71. package/src/matrix/client/file-sync-store.ts +289 -0
  72. package/src/matrix/client/logging.ts +140 -0
  73. package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
  74. package/src/matrix/client/private-network-host.ts +1 -0
  75. package/src/matrix/client/runtime.ts +4 -0
  76. package/src/matrix/client/shared.ts +316 -0
  77. package/src/matrix/client/storage.ts +543 -0
  78. package/src/matrix/client/types.ts +50 -0
  79. package/src/matrix/client/url-validation.ts +76 -0
  80. package/src/matrix/client-bootstrap.ts +173 -0
  81. package/src/matrix/client.ts +23 -0
  82. package/src/matrix/config-paths.ts +31 -0
  83. package/src/matrix/config-update.ts +292 -0
  84. package/src/matrix/credentials-read.ts +207 -0
  85. package/src/matrix/credentials-write.runtime.ts +35 -0
  86. package/src/matrix/credentials.ts +95 -0
  87. package/src/matrix/deps.ts +309 -0
  88. package/src/matrix/device-health.ts +31 -0
  89. package/src/matrix/direct-management.ts +349 -0
  90. package/src/matrix/direct-room.ts +128 -0
  91. package/src/matrix/draft-stream.ts +225 -0
  92. package/src/matrix/encryption-guidance.ts +24 -0
  93. package/src/matrix/errors.ts +21 -0
  94. package/src/matrix/format.ts +426 -0
  95. package/src/matrix/legacy-crypto-inspector.ts +95 -0
  96. package/src/matrix/media-errors.ts +20 -0
  97. package/src/matrix/media-text.ts +162 -0
  98. package/src/matrix/monitor/access-state.ts +145 -0
  99. package/src/matrix/monitor/ack-config.ts +27 -0
  100. package/src/matrix/monitor/allowlist.ts +92 -0
  101. package/src/matrix/monitor/auto-join.ts +86 -0
  102. package/src/matrix/monitor/config.ts +569 -0
  103. package/src/matrix/monitor/context-summary.ts +43 -0
  104. package/src/matrix/monitor/direct.ts +296 -0
  105. package/src/matrix/monitor/events.ts +397 -0
  106. package/src/matrix/monitor/handler.ts +2271 -0
  107. package/src/matrix/monitor/inbound-dedupe.ts +267 -0
  108. package/src/matrix/monitor/index.ts +540 -0
  109. package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
  110. package/src/matrix/monitor/location.ts +108 -0
  111. package/src/matrix/monitor/media.ts +119 -0
  112. package/src/matrix/monitor/mentions.ts +256 -0
  113. package/src/matrix/monitor/reaction-events.ts +197 -0
  114. package/src/matrix/monitor/recent-invite.ts +30 -0
  115. package/src/matrix/monitor/replies.ts +136 -0
  116. package/src/matrix/monitor/reply-context.ts +92 -0
  117. package/src/matrix/monitor/room-history.ts +301 -0
  118. package/src/matrix/monitor/room-info.ts +126 -0
  119. package/src/matrix/monitor/rooms.ts +52 -0
  120. package/src/matrix/monitor/route.ts +179 -0
  121. package/src/matrix/monitor/runtime-api.ts +28 -0
  122. package/src/matrix/monitor/startup-verification.ts +237 -0
  123. package/src/matrix/monitor/startup.ts +218 -0
  124. package/src/matrix/monitor/status.ts +120 -0
  125. package/src/matrix/monitor/sync-lifecycle.ts +91 -0
  126. package/src/matrix/monitor/task-runner.ts +38 -0
  127. package/src/matrix/monitor/test-events.ts +21 -0
  128. package/src/matrix/monitor/thread-context.ts +108 -0
  129. package/src/matrix/monitor/threads.ts +85 -0
  130. package/src/matrix/monitor/types.ts +30 -0
  131. package/src/matrix/monitor/verification-events.ts +643 -0
  132. package/src/matrix/monitor/verification-utils.ts +46 -0
  133. package/src/matrix/outbound-media-runtime.ts +1 -0
  134. package/src/matrix/poll-summary.ts +110 -0
  135. package/src/matrix/poll-types.ts +429 -0
  136. package/src/matrix/probe.runtime.ts +4 -0
  137. package/src/matrix/probe.ts +97 -0
  138. package/src/matrix/profile.ts +184 -0
  139. package/src/matrix/reaction-common.ts +147 -0
  140. package/src/matrix/sdk/crypto-bootstrap.ts +438 -0
  141. package/src/matrix/sdk/crypto-facade.ts +242 -0
  142. package/src/matrix/sdk/crypto-node.runtime.ts +17 -0
  143. package/src/matrix/sdk/crypto-runtime.ts +14 -0
  144. package/src/matrix/sdk/decrypt-bridge.ts +410 -0
  145. package/src/matrix/sdk/event-helpers.ts +83 -0
  146. package/src/matrix/sdk/http-client.ts +87 -0
  147. package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
  148. package/src/matrix/sdk/idb-persistence.ts +286 -0
  149. package/src/matrix/sdk/logger.ts +108 -0
  150. package/src/matrix/sdk/read-response-with-limit.ts +19 -0
  151. package/src/matrix/sdk/recovery-key-store.ts +453 -0
  152. package/src/matrix/sdk/timeout-abort-signal.ts +1 -0
  153. package/src/matrix/sdk/transport-runtime-api.ts +18 -0
  154. package/src/matrix/sdk/transport.ts +352 -0
  155. package/src/matrix/sdk/types.ts +245 -0
  156. package/src/matrix/sdk/verification-manager.ts +795 -0
  157. package/src/matrix/sdk/verification-status.ts +23 -0
  158. package/src/matrix/sdk.ts +2152 -0
  159. package/src/matrix/send/client.ts +93 -0
  160. package/src/matrix/send/formatting.ts +189 -0
  161. package/src/matrix/send/media.ts +244 -0
  162. package/src/matrix/send/targets.ts +104 -0
  163. package/src/matrix/send/types.ts +131 -0
  164. package/src/matrix/send.ts +660 -0
  165. package/src/matrix/session-store-metadata.ts +108 -0
  166. package/src/matrix/startup-abort.ts +44 -0
  167. package/src/matrix/subagent-hooks.ts +308 -0
  168. package/src/matrix/sync-state.ts +27 -0
  169. package/src/matrix/target-ids.ts +79 -0
  170. package/src/matrix/thread-bindings-shared.ts +206 -0
  171. package/src/matrix/thread-bindings.ts +580 -0
  172. package/src/matrix-migration.runtime.ts +9 -0
  173. package/src/migration-config.ts +243 -0
  174. package/src/migration-snapshot-backup.ts +116 -0
  175. package/src/migration-snapshot.ts +53 -0
  176. package/src/onboarding.ts +775 -0
  177. package/src/outbound.ts +248 -0
  178. package/src/plugin-entry.runtime.js +115 -0
  179. package/src/plugin-entry.runtime.ts +70 -0
  180. package/src/profile-update.ts +71 -0
  181. package/src/record-shared.ts +3 -0
  182. package/src/resolve-targets.ts +175 -0
  183. package/src/resolver.runtime.ts +5 -0
  184. package/src/resolver.ts +21 -0
  185. package/src/runtime-api.ts +106 -0
  186. package/src/runtime.ts +13 -0
  187. package/src/secret-contract.ts +174 -0
  188. package/src/session-route.ts +126 -0
  189. package/src/setup-bootstrap.ts +102 -0
  190. package/src/setup-config.ts +222 -0
  191. package/src/setup-contract.ts +90 -0
  192. package/src/setup-core.ts +146 -0
  193. package/src/setup-dm-policy.ts +15 -0
  194. package/src/setup-surface.ts +4 -0
  195. package/src/startup-maintenance.ts +114 -0
  196. package/src/storage-paths.ts +92 -0
  197. package/src/thread-binding-api.ts +23 -0
  198. package/src/tool-actions.runtime.ts +1 -0
  199. package/src/tool-actions.ts +498 -0
  200. package/src/types.ts +257 -0
  201. package/subagent-hooks-api.ts +31 -0
  202. package/test-api.ts +21 -0
  203. package/thread-binding-api.ts +4 -0
  204. package/thread-bindings-runtime.ts +4 -0
  205. package/tsconfig.json +16 -0
@@ -0,0 +1,93 @@
1
+ import { requireRuntimeConfig } from "autobot/plugin-sdk/plugin-config-runtime";
2
+ import type { CoreConfig } from "../../types.js";
3
+ import { resolveMatrixAccountConfig } from "../account-config.js";
4
+ import type { MatrixClient } from "../sdk.js";
5
+
6
+ type MatrixSendClientRuntime = Pick<
7
+ typeof import("../client-bootstrap.js"),
8
+ "withResolvedRuntimeMatrixClient"
9
+ >;
10
+
11
+ let matrixSendClientRuntimePromise: Promise<MatrixSendClientRuntime> | null = null;
12
+
13
+ async function loadMatrixSendClientRuntime(): Promise<MatrixSendClientRuntime> {
14
+ matrixSendClientRuntimePromise ??= import("../client-bootstrap.js");
15
+ return await matrixSendClientRuntimePromise;
16
+ }
17
+
18
+ export function resolveMediaMaxBytes(
19
+ accountId?: string | null,
20
+ cfg?: CoreConfig,
21
+ ): number | undefined {
22
+ if (!cfg) {
23
+ throw new Error(
24
+ "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
+ );
26
+ }
27
+ const resolvedCfg = requireRuntimeConfig(cfg, "Matrix media limits") as CoreConfig;
28
+ const matrixCfg = resolveMatrixAccountConfig({ cfg: resolvedCfg, accountId });
29
+ const mediaMaxMb = typeof matrixCfg.mediaMaxMb === "number" ? matrixCfg.mediaMaxMb : undefined;
30
+ if (typeof mediaMaxMb === "number") {
31
+ return mediaMaxMb * 1024 * 1024;
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export async function withResolvedMatrixSendClient<T>(
37
+ opts: {
38
+ client?: MatrixClient;
39
+ cfg?: CoreConfig;
40
+ timeoutMs?: number;
41
+ accountId?: string | null;
42
+ },
43
+ run: (client: MatrixClient) => Promise<T>,
44
+ ): Promise<T> {
45
+ return await withResolvedMatrixClient(
46
+ {
47
+ ...opts,
48
+ // One-off outbound sends still need a started client so room encryption
49
+ // state and live crypto sessions are available before sendMessage/sendEvent.
50
+ readiness: "started",
51
+ },
52
+ run,
53
+ // Started one-off send clients should flush sync/crypto state before CLI
54
+ // shutdown paths can tear down the process.
55
+ "persist",
56
+ );
57
+ }
58
+
59
+ export async function withResolvedMatrixControlClient<T>(
60
+ opts: {
61
+ client?: MatrixClient;
62
+ cfg?: CoreConfig;
63
+ timeoutMs?: number;
64
+ accountId?: string | null;
65
+ },
66
+ run: (client: MatrixClient) => Promise<T>,
67
+ ): Promise<T> {
68
+ return await withResolvedMatrixClient(
69
+ {
70
+ ...opts,
71
+ readiness: "none",
72
+ },
73
+ run,
74
+ );
75
+ }
76
+
77
+ async function withResolvedMatrixClient<T>(
78
+ opts: {
79
+ client?: MatrixClient;
80
+ cfg?: CoreConfig;
81
+ timeoutMs?: number;
82
+ accountId?: string | null;
83
+ readiness: "started" | "none";
84
+ },
85
+ run: (client: MatrixClient) => Promise<T>,
86
+ shutdownBehavior?: "persist",
87
+ ): Promise<T> {
88
+ if (opts.client) {
89
+ return await run(opts.client);
90
+ }
91
+ const { withResolvedRuntimeMatrixClient } = await loadMatrixSendClientRuntime();
92
+ return await withResolvedRuntimeMatrixClient(opts, run, shutdownBehavior);
93
+ }
@@ -0,0 +1,189 @@
1
+ import { getMatrixRuntime } from "../../runtime.js";
2
+ import {
3
+ markdownToMatrixHtml,
4
+ resolveMatrixMentionsInMarkdown,
5
+ renderMarkdownToMatrixHtmlWithMentions,
6
+ type MatrixMentions,
7
+ } from "../format.js";
8
+ import type { MatrixClient } from "../sdk.js";
9
+ import {
10
+ MsgType,
11
+ RelationType,
12
+ type MatrixFormattedContent,
13
+ type MatrixMediaMsgType,
14
+ type MatrixRelation,
15
+ type MatrixReplyRelation,
16
+ type MatrixTextContent,
17
+ type MatrixTextMsgType,
18
+ type MatrixThreadRelation,
19
+ } from "./types.js";
20
+
21
+ const getCore = () => getMatrixRuntime();
22
+
23
+ async function renderMatrixFormattedContent(params: {
24
+ client: MatrixClient;
25
+ markdown?: string | null;
26
+ includeMentions?: boolean;
27
+ }): Promise<{ html?: string; mentions?: MatrixMentions }> {
28
+ const markdown = params.markdown ?? "";
29
+ if (params.includeMentions === false) {
30
+ const html = markdownToMatrixHtml(markdown).trimEnd();
31
+ return { html: html || undefined };
32
+ }
33
+ const { html, mentions } = await renderMarkdownToMatrixHtmlWithMentions({
34
+ markdown,
35
+ client: params.client,
36
+ });
37
+ return { html, mentions };
38
+ }
39
+
40
+ export function buildTextContent(
41
+ body: string,
42
+ relation?: MatrixRelation,
43
+ opts: {
44
+ msgtype?: MatrixTextMsgType;
45
+ } = {},
46
+ ): MatrixTextContent {
47
+ const msgtype = opts.msgtype ?? MsgType.Text;
48
+ return relation
49
+ ? {
50
+ msgtype,
51
+ body,
52
+ "m.relates_to": relation,
53
+ }
54
+ : {
55
+ msgtype,
56
+ body,
57
+ };
58
+ }
59
+
60
+ export async function enrichMatrixFormattedContent(params: {
61
+ client: MatrixClient;
62
+ content: MatrixFormattedContent;
63
+ markdown?: string | null;
64
+ includeMentions?: boolean;
65
+ }): Promise<void> {
66
+ const { html, mentions } = await renderMatrixFormattedContent({
67
+ client: params.client,
68
+ markdown: params.markdown,
69
+ includeMentions: params.includeMentions,
70
+ });
71
+ if (mentions) {
72
+ params.content["m.mentions"] = mentions;
73
+ } else {
74
+ delete params.content["m.mentions"];
75
+ }
76
+ if (!html) {
77
+ delete params.content.format;
78
+ delete params.content.formatted_body;
79
+ return;
80
+ }
81
+ params.content.format = "org.matrix.custom.html";
82
+ params.content.formatted_body = html;
83
+ }
84
+
85
+ export async function resolveMatrixMentionsForBody(params: {
86
+ client: MatrixClient;
87
+ body: string;
88
+ }): Promise<MatrixMentions> {
89
+ return await resolveMatrixMentionsInMarkdown({
90
+ markdown: params.body ?? "",
91
+ client: params.client,
92
+ });
93
+ }
94
+
95
+ function normalizeMentionUserIds(value: unknown): string[] {
96
+ return Array.isArray(value)
97
+ ? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
98
+ : [];
99
+ }
100
+
101
+ export function extractMatrixMentions(
102
+ content: Record<string, unknown> | undefined,
103
+ ): MatrixMentions {
104
+ const rawMentions = content?.["m.mentions"];
105
+ if (!rawMentions || typeof rawMentions !== "object") {
106
+ return {};
107
+ }
108
+ const mentions = rawMentions as { room?: unknown; user_ids?: unknown };
109
+ const normalized: MatrixMentions = {};
110
+ const userIds = normalizeMentionUserIds(mentions.user_ids);
111
+ if (userIds.length > 0) {
112
+ normalized.user_ids = userIds;
113
+ }
114
+ if (mentions.room === true) {
115
+ normalized.room = true;
116
+ }
117
+ return normalized;
118
+ }
119
+
120
+ export function diffMatrixMentions(
121
+ current: MatrixMentions,
122
+ previous: MatrixMentions,
123
+ ): MatrixMentions {
124
+ const previousUserIds = new Set(previous.user_ids ?? []);
125
+ const newUserIds = (current.user_ids ?? []).filter((userId) => !previousUserIds.has(userId));
126
+ const delta: MatrixMentions = {};
127
+ if (newUserIds.length > 0) {
128
+ delta.user_ids = newUserIds;
129
+ }
130
+ if (current.room && !previous.room) {
131
+ delta.room = true;
132
+ }
133
+ return delta;
134
+ }
135
+
136
+ export function buildReplyRelation(replyToId?: string): MatrixReplyRelation | undefined {
137
+ const trimmed = replyToId?.trim();
138
+ if (!trimmed) {
139
+ return undefined;
140
+ }
141
+ return { "m.in_reply_to": { event_id: trimmed } };
142
+ }
143
+
144
+ export function buildThreadRelation(threadId: string, replyToId?: string): MatrixThreadRelation {
145
+ const trimmed = threadId.trim();
146
+ return {
147
+ rel_type: RelationType.Thread,
148
+ event_id: trimmed,
149
+ is_falling_back: true,
150
+ "m.in_reply_to": { event_id: replyToId?.trim() || trimmed },
151
+ };
152
+ }
153
+
154
+ export function resolveMatrixMsgType(contentType?: string, _fileName?: string): MatrixMediaMsgType {
155
+ const kind = getCore().media.mediaKindFromMime(contentType ?? "");
156
+ switch (kind) {
157
+ case "image":
158
+ return MsgType.Image;
159
+ case "audio":
160
+ return MsgType.Audio;
161
+ case "video":
162
+ return MsgType.Video;
163
+ default:
164
+ return MsgType.File;
165
+ }
166
+ }
167
+
168
+ export function resolveMatrixVoiceDecision(opts: {
169
+ wantsVoice: boolean;
170
+ contentType?: string;
171
+ fileName?: string;
172
+ }): { useVoice: boolean } {
173
+ if (!opts.wantsVoice) {
174
+ return { useVoice: false };
175
+ }
176
+ if (isMatrixVoiceCompatibleAudio(opts)) {
177
+ return { useVoice: true };
178
+ }
179
+ return { useVoice: false };
180
+ }
181
+
182
+ function isMatrixVoiceCompatibleAudio(opts: { contentType?: string; fileName?: string }): boolean {
183
+ // Matrix currently shares the core voice compatibility policy.
184
+ // Keep this wrapper as the seam if Matrix policy diverges later.
185
+ return getCore().media.isVoiceCompatibleAudio({
186
+ contentType: opts.contentType,
187
+ fileName: opts.fileName,
188
+ });
189
+ }
@@ -0,0 +1,244 @@
1
+ import { parseBuffer, type IFileInfo } from "music-metadata";
2
+ import { getMatrixRuntime } from "../../runtime.js";
3
+ import type {
4
+ DimensionalFileInfo,
5
+ EncryptedFile,
6
+ FileWithThumbnailInfo,
7
+ MatrixClient,
8
+ TimedFileInfo,
9
+ VideoFileInfo,
10
+ } from "../sdk.js";
11
+ import {
12
+ type MatrixMediaContent,
13
+ type MatrixMediaInfo,
14
+ type MatrixMediaMsgType,
15
+ type MatrixRelation,
16
+ type MediaKind,
17
+ } from "./types.js";
18
+
19
+ const getCore = () => getMatrixRuntime();
20
+
21
+ function buildMatrixMediaInfo(params: {
22
+ size: number;
23
+ mimetype?: string;
24
+ durationMs?: number;
25
+ imageInfo?: DimensionalFileInfo;
26
+ }): MatrixMediaInfo | undefined {
27
+ const base: FileWithThumbnailInfo = {};
28
+ if (Number.isFinite(params.size)) {
29
+ base.size = params.size;
30
+ }
31
+ if (params.mimetype) {
32
+ base.mimetype = params.mimetype;
33
+ }
34
+ if (params.imageInfo) {
35
+ const dimensional: DimensionalFileInfo = {
36
+ ...base,
37
+ ...params.imageInfo,
38
+ };
39
+ if (typeof params.durationMs === "number") {
40
+ const videoInfo: VideoFileInfo = {
41
+ ...dimensional,
42
+ duration: params.durationMs,
43
+ };
44
+ return videoInfo;
45
+ }
46
+ return dimensional;
47
+ }
48
+ if (typeof params.durationMs === "number") {
49
+ const timedInfo: TimedFileInfo = {
50
+ ...base,
51
+ duration: params.durationMs,
52
+ };
53
+ return timedInfo;
54
+ }
55
+ if (Object.keys(base).length === 0) {
56
+ return undefined;
57
+ }
58
+ return base;
59
+ }
60
+
61
+ export function buildMediaContent(params: {
62
+ msgtype: MatrixMediaMsgType;
63
+ body: string;
64
+ url?: string;
65
+ filename?: string;
66
+ mimetype?: string;
67
+ size: number;
68
+ relation?: MatrixRelation;
69
+ isVoice?: boolean;
70
+ durationMs?: number;
71
+ imageInfo?: DimensionalFileInfo;
72
+ file?: EncryptedFile;
73
+ }): MatrixMediaContent {
74
+ const info = buildMatrixMediaInfo({
75
+ size: params.size,
76
+ mimetype: params.mimetype,
77
+ durationMs: params.durationMs,
78
+ imageInfo: params.imageInfo,
79
+ });
80
+ const base: MatrixMediaContent = {
81
+ msgtype: params.msgtype,
82
+ body: params.body,
83
+ filename: params.filename,
84
+ info: info ?? undefined,
85
+ };
86
+ // Encrypted media should only include the "file" payload, not top-level "url".
87
+ if (!params.file && params.url) {
88
+ base.url = params.url;
89
+ }
90
+ // For encrypted files, add the file object
91
+ if (params.file) {
92
+ base.file = params.file;
93
+ }
94
+ if (params.isVoice) {
95
+ base["org.matrix.msc3245.voice"] = {};
96
+ if (typeof params.durationMs === "number") {
97
+ base["org.matrix.msc1767.audio"] = {
98
+ duration: params.durationMs,
99
+ };
100
+ }
101
+ }
102
+ if (params.relation) {
103
+ base["m.relates_to"] = params.relation;
104
+ }
105
+ return base;
106
+ }
107
+
108
+ const THUMBNAIL_MAX_SIDE = 800;
109
+ const THUMBNAIL_QUALITY = 80;
110
+
111
+ export async function prepareImageInfo(params: {
112
+ buffer: Buffer;
113
+ client: MatrixClient;
114
+ encrypted?: boolean;
115
+ }): Promise<DimensionalFileInfo | undefined> {
116
+ const meta = await getCore()
117
+ .media.getImageMetadata(params.buffer)
118
+ .catch(() => null);
119
+ if (!meta) {
120
+ return undefined;
121
+ }
122
+ const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height };
123
+ const maxDim = Math.max(meta.width, meta.height);
124
+ if (maxDim > THUMBNAIL_MAX_SIDE) {
125
+ try {
126
+ const thumbBuffer = await getCore().media.resizeToJpeg({
127
+ buffer: params.buffer,
128
+ maxSide: THUMBNAIL_MAX_SIDE,
129
+ quality: THUMBNAIL_QUALITY,
130
+ withoutEnlargement: true,
131
+ });
132
+ const thumbMeta = await getCore()
133
+ .media.getImageMetadata(thumbBuffer)
134
+ .catch(() => null);
135
+ const result = await uploadMediaWithEncryption(params.client, thumbBuffer, {
136
+ contentType: "image/jpeg",
137
+ filename: "thumbnail.jpg",
138
+ encrypted: params.encrypted === true,
139
+ });
140
+ if (result.file) {
141
+ imageInfo.thumbnail_file = result.file;
142
+ } else {
143
+ imageInfo.thumbnail_url = result.url;
144
+ }
145
+ if (thumbMeta) {
146
+ imageInfo.thumbnail_info = {
147
+ w: thumbMeta.width,
148
+ h: thumbMeta.height,
149
+ mimetype: "image/jpeg",
150
+ size: thumbBuffer.byteLength,
151
+ };
152
+ }
153
+ } catch {
154
+ // Thumbnail generation failed, continue without it
155
+ }
156
+ }
157
+ return imageInfo;
158
+ }
159
+
160
+ export async function resolveMediaDurationMs(params: {
161
+ buffer: Buffer;
162
+ contentType?: string;
163
+ fileName?: string;
164
+ kind: MediaKind;
165
+ }): Promise<number | undefined> {
166
+ if (params.kind !== "audio" && params.kind !== "video") {
167
+ return undefined;
168
+ }
169
+ try {
170
+ const fileInfo: IFileInfo | string | undefined =
171
+ params.contentType || params.fileName
172
+ ? {
173
+ mimeType: params.contentType,
174
+ size: params.buffer.byteLength,
175
+ path: params.fileName,
176
+ }
177
+ : undefined;
178
+ const metadata = await parseBuffer(params.buffer, fileInfo, {
179
+ duration: true,
180
+ skipCovers: true,
181
+ });
182
+ const durationSeconds = metadata.format.duration;
183
+ if (typeof durationSeconds === "number" && Number.isFinite(durationSeconds)) {
184
+ return Math.max(0, Math.round(durationSeconds * 1000));
185
+ }
186
+ } catch {
187
+ // Duration is optional; ignore parse failures.
188
+ }
189
+ return undefined;
190
+ }
191
+
192
+ async function uploadFile(
193
+ client: MatrixClient,
194
+ file: Buffer,
195
+ params: {
196
+ contentType?: string;
197
+ filename?: string;
198
+ },
199
+ ): Promise<string> {
200
+ return await client.uploadContent(file, params.contentType, params.filename);
201
+ }
202
+
203
+ async function uploadMediaWithEncryption(
204
+ client: MatrixClient,
205
+ buffer: Buffer,
206
+ params: {
207
+ contentType?: string;
208
+ filename?: string;
209
+ encrypted: boolean;
210
+ },
211
+ ): Promise<{ url: string; file?: EncryptedFile }> {
212
+ if (params.encrypted && client.crypto) {
213
+ const encrypted = await client.crypto.encryptMedia(buffer);
214
+ const mxc = await client.uploadContent(encrypted.buffer, params.contentType, params.filename);
215
+ const file: EncryptedFile = { url: mxc, ...encrypted.file };
216
+ return {
217
+ url: mxc,
218
+ file,
219
+ };
220
+ }
221
+
222
+ const mxc = await uploadFile(client, buffer, params);
223
+ return { url: mxc };
224
+ }
225
+
226
+ /**
227
+ * Upload media with optional encryption for E2EE rooms.
228
+ */
229
+ export async function uploadMediaMaybeEncrypted(
230
+ client: MatrixClient,
231
+ roomId: string,
232
+ buffer: Buffer,
233
+ params: {
234
+ contentType?: string;
235
+ filename?: string;
236
+ },
237
+ ): Promise<{ url: string; file?: EncryptedFile }> {
238
+ // Check if room is encrypted and crypto is available
239
+ const isEncrypted = Boolean(client.crypto && (await client.crypto.isRoomEncrypted(roomId)));
240
+ return await uploadMediaWithEncryption(client, buffer, {
241
+ ...params,
242
+ encrypted: isEncrypted,
243
+ });
244
+ }
@@ -0,0 +1,104 @@
1
+ import {
2
+ normalizeLowercaseStringOrEmpty,
3
+ normalizeOptionalStringifiedId,
4
+ } from "autobot/plugin-sdk/string-coerce-runtime";
5
+ import { inspectMatrixDirectRooms, persistMatrixDirectRoomMapping } from "../direct-management.js";
6
+ import { isStrictDirectRoom } from "../direct-room.js";
7
+ import type { MatrixClient } from "../sdk.js";
8
+ import { isMatrixQualifiedUserId, normalizeMatrixResolvableTarget } from "../target-ids.js";
9
+
10
+ function normalizeTarget(raw: string): string {
11
+ const trimmed = raw.trim();
12
+ if (!trimmed) {
13
+ throw new Error("Matrix target is required (room:<id> or #alias)");
14
+ }
15
+ return trimmed;
16
+ }
17
+
18
+ export function normalizeThreadId(raw?: string | number | null): string | null {
19
+ return normalizeOptionalStringifiedId(raw) ?? null;
20
+ }
21
+
22
+ // Size-capped to prevent unbounded growth (#4948)
23
+ const MAX_DIRECT_ROOM_CACHE_SIZE = 1024;
24
+ const directRoomCacheByClient = new WeakMap<MatrixClient, Map<string, string>>();
25
+
26
+ function resolveDirectRoomCache(client: MatrixClient): Map<string, string> {
27
+ const existing = directRoomCacheByClient.get(client);
28
+ if (existing) {
29
+ return existing;
30
+ }
31
+ const created = new Map<string, string>();
32
+ directRoomCacheByClient.set(client, created);
33
+ return created;
34
+ }
35
+
36
+ function setDirectRoomCached(client: MatrixClient, key: string, value: string): void {
37
+ const directRoomCache = resolveDirectRoomCache(client);
38
+ directRoomCache.set(key, value);
39
+ if (directRoomCache.size > MAX_DIRECT_ROOM_CACHE_SIZE) {
40
+ const oldest = directRoomCache.keys().next().value;
41
+ if (oldest !== undefined) {
42
+ directRoomCache.delete(oldest);
43
+ }
44
+ }
45
+ }
46
+
47
+ async function resolveDirectRoomId(client: MatrixClient, userId: string): Promise<string> {
48
+ const trimmed = userId.trim();
49
+ if (!isMatrixQualifiedUserId(trimmed)) {
50
+ throw new Error(`Matrix user IDs must be fully qualified (got "${trimmed}")`);
51
+ }
52
+ const selfUserId = (await client.getUserId().catch(() => null))?.trim() || null;
53
+
54
+ const directRoomCache = resolveDirectRoomCache(client);
55
+ const cached = directRoomCache.get(trimmed);
56
+ if (
57
+ cached &&
58
+ (await isStrictDirectRoom({ client, roomId: cached, remoteUserId: trimmed, selfUserId }))
59
+ ) {
60
+ return cached;
61
+ }
62
+ if (cached) {
63
+ directRoomCache.delete(trimmed);
64
+ }
65
+
66
+ const inspection = await inspectMatrixDirectRooms({
67
+ client,
68
+ remoteUserId: trimmed,
69
+ });
70
+ if (inspection.activeRoomId) {
71
+ setDirectRoomCached(client, trimmed, inspection.activeRoomId);
72
+ if (inspection.mappedRoomIds[0] !== inspection.activeRoomId) {
73
+ await persistMatrixDirectRoomMapping({
74
+ client,
75
+ remoteUserId: trimmed,
76
+ roomId: inspection.activeRoomId,
77
+ }).catch(() => {
78
+ // Ignore persistence errors when send resolution has already found a usable room.
79
+ });
80
+ }
81
+ return inspection.activeRoomId;
82
+ }
83
+
84
+ throw new Error(`No direct room found for ${trimmed} (m.direct missing)`);
85
+ }
86
+
87
+ export async function resolveMatrixRoomId(client: MatrixClient, raw: string): Promise<string> {
88
+ const target = normalizeMatrixResolvableTarget(normalizeTarget(raw));
89
+ const lowered = normalizeLowercaseStringOrEmpty(target);
90
+ if (lowered.startsWith("user:")) {
91
+ return await resolveDirectRoomId(client, target.slice("user:".length));
92
+ }
93
+ if (isMatrixQualifiedUserId(target)) {
94
+ return await resolveDirectRoomId(client, target);
95
+ }
96
+ if (target.startsWith("#")) {
97
+ const resolved = await client.resolveRoom(target);
98
+ if (!resolved) {
99
+ throw new Error(`Matrix alias ${target} could not be resolved`);
100
+ }
101
+ return resolved;
102
+ }
103
+ return target;
104
+ }