@openclaw/matrix 2026.1.29

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 (67) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/index.ts +18 -0
  3. package/openclaw.plugin.json +11 -0
  4. package/package.json +36 -0
  5. package/src/actions.ts +185 -0
  6. package/src/channel.directory.test.ts +56 -0
  7. package/src/channel.ts +417 -0
  8. package/src/config-schema.ts +62 -0
  9. package/src/directory-live.ts +175 -0
  10. package/src/group-mentions.ts +61 -0
  11. package/src/matrix/accounts.test.ts +83 -0
  12. package/src/matrix/accounts.ts +63 -0
  13. package/src/matrix/actions/client.ts +53 -0
  14. package/src/matrix/actions/messages.ts +120 -0
  15. package/src/matrix/actions/pins.ts +70 -0
  16. package/src/matrix/actions/reactions.ts +84 -0
  17. package/src/matrix/actions/room.ts +88 -0
  18. package/src/matrix/actions/summary.ts +77 -0
  19. package/src/matrix/actions/types.ts +84 -0
  20. package/src/matrix/actions.ts +15 -0
  21. package/src/matrix/active-client.ts +11 -0
  22. package/src/matrix/client/config.ts +165 -0
  23. package/src/matrix/client/create-client.ts +127 -0
  24. package/src/matrix/client/logging.ts +35 -0
  25. package/src/matrix/client/runtime.ts +4 -0
  26. package/src/matrix/client/shared.ts +169 -0
  27. package/src/matrix/client/storage.ts +131 -0
  28. package/src/matrix/client/types.ts +34 -0
  29. package/src/matrix/client.test.ts +57 -0
  30. package/src/matrix/client.ts +9 -0
  31. package/src/matrix/credentials.ts +103 -0
  32. package/src/matrix/deps.ts +57 -0
  33. package/src/matrix/format.test.ts +34 -0
  34. package/src/matrix/format.ts +22 -0
  35. package/src/matrix/index.ts +11 -0
  36. package/src/matrix/monitor/allowlist.ts +58 -0
  37. package/src/matrix/monitor/auto-join.ts +68 -0
  38. package/src/matrix/monitor/direct.ts +105 -0
  39. package/src/matrix/monitor/events.ts +103 -0
  40. package/src/matrix/monitor/handler.ts +645 -0
  41. package/src/matrix/monitor/index.ts +279 -0
  42. package/src/matrix/monitor/location.ts +83 -0
  43. package/src/matrix/monitor/media.test.ts +103 -0
  44. package/src/matrix/monitor/media.ts +113 -0
  45. package/src/matrix/monitor/mentions.ts +31 -0
  46. package/src/matrix/monitor/replies.ts +96 -0
  47. package/src/matrix/monitor/room-info.ts +58 -0
  48. package/src/matrix/monitor/rooms.ts +43 -0
  49. package/src/matrix/monitor/threads.ts +64 -0
  50. package/src/matrix/monitor/types.ts +39 -0
  51. package/src/matrix/poll-types.test.ts +22 -0
  52. package/src/matrix/poll-types.ts +157 -0
  53. package/src/matrix/probe.ts +70 -0
  54. package/src/matrix/send/client.ts +63 -0
  55. package/src/matrix/send/formatting.ts +92 -0
  56. package/src/matrix/send/media.ts +220 -0
  57. package/src/matrix/send/targets.test.ts +102 -0
  58. package/src/matrix/send/targets.ts +144 -0
  59. package/src/matrix/send/types.ts +109 -0
  60. package/src/matrix/send.test.ts +172 -0
  61. package/src/matrix/send.ts +255 -0
  62. package/src/onboarding.ts +432 -0
  63. package/src/outbound.ts +53 -0
  64. package/src/resolve-targets.ts +89 -0
  65. package/src/runtime.ts +14 -0
  66. package/src/tool-actions.ts +160 -0
  67. package/src/types.ts +95 -0
@@ -0,0 +1,58 @@
1
+ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+
3
+ export type MatrixRoomInfo = {
4
+ name?: string;
5
+ canonicalAlias?: string;
6
+ altAliases: string[];
7
+ };
8
+
9
+ export function createMatrixRoomInfoResolver(client: MatrixClient) {
10
+ const roomInfoCache = new Map<string, MatrixRoomInfo>();
11
+
12
+ const getRoomInfo = async (roomId: string): Promise<MatrixRoomInfo> => {
13
+ const cached = roomInfoCache.get(roomId);
14
+ if (cached) return cached;
15
+ let name: string | undefined;
16
+ let canonicalAlias: string | undefined;
17
+ let altAliases: string[] = [];
18
+ try {
19
+ const nameState = await client
20
+ .getRoomStateEvent(roomId, "m.room.name", "")
21
+ .catch(() => null);
22
+ name = nameState?.name;
23
+ } catch {
24
+ // ignore
25
+ }
26
+ try {
27
+ const aliasState = await client
28
+ .getRoomStateEvent(roomId, "m.room.canonical_alias", "")
29
+ .catch(() => null);
30
+ canonicalAlias = aliasState?.alias;
31
+ altAliases = aliasState?.alt_aliases ?? [];
32
+ } catch {
33
+ // ignore
34
+ }
35
+ const info = { name, canonicalAlias, altAliases };
36
+ roomInfoCache.set(roomId, info);
37
+ return info;
38
+ };
39
+
40
+ const getMemberDisplayName = async (
41
+ roomId: string,
42
+ userId: string,
43
+ ): Promise<string> => {
44
+ try {
45
+ const memberState = await client
46
+ .getRoomStateEvent(roomId, "m.room.member", userId)
47
+ .catch(() => null);
48
+ return memberState?.displayname ?? userId;
49
+ } catch {
50
+ return userId;
51
+ }
52
+ };
53
+
54
+ return {
55
+ getRoomInfo,
56
+ getMemberDisplayName,
57
+ };
58
+ }
@@ -0,0 +1,43 @@
1
+ import type { MatrixRoomConfig } from "../../types.js";
2
+ import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "openclaw/plugin-sdk";
3
+
4
+ export type MatrixRoomConfigResolved = {
5
+ allowed: boolean;
6
+ allowlistConfigured: boolean;
7
+ config?: MatrixRoomConfig;
8
+ matchKey?: string;
9
+ matchSource?: "direct" | "wildcard";
10
+ };
11
+
12
+ export function resolveMatrixRoomConfig(params: {
13
+ rooms?: Record<string, MatrixRoomConfig>;
14
+ roomId: string;
15
+ aliases: string[];
16
+ name?: string | null;
17
+ }): MatrixRoomConfigResolved {
18
+ const rooms = params.rooms ?? {};
19
+ const keys = Object.keys(rooms);
20
+ const allowlistConfigured = keys.length > 0;
21
+ const candidates = buildChannelKeyCandidates(
22
+ params.roomId,
23
+ `room:${params.roomId}`,
24
+ ...params.aliases,
25
+ params.name ?? "",
26
+ );
27
+ const { entry: matched, key: matchedKey, wildcardEntry, wildcardKey } = resolveChannelEntryMatch({
28
+ entries: rooms,
29
+ keys: candidates,
30
+ wildcardKey: "*",
31
+ });
32
+ const resolved = matched ?? wildcardEntry;
33
+ const allowed = resolved ? resolved.enabled !== false && resolved.allow !== false : false;
34
+ const matchKey = matchedKey ?? wildcardKey;
35
+ const matchSource = matched ? "direct" : wildcardEntry ? "wildcard" : undefined;
36
+ return {
37
+ allowed,
38
+ allowlistConfigured,
39
+ config: resolved,
40
+ matchKey,
41
+ matchSource,
42
+ };
43
+ }
@@ -0,0 +1,64 @@
1
+ // Type for raw Matrix event from @vector-im/matrix-bot-sdk
2
+ type MatrixRawEvent = {
3
+ event_id: string;
4
+ sender: string;
5
+ type: string;
6
+ origin_server_ts: number;
7
+ content: Record<string, unknown>;
8
+ };
9
+
10
+ type RoomMessageEventContent = {
11
+ msgtype: string;
12
+ body: string;
13
+ "m.relates_to"?: {
14
+ rel_type?: string;
15
+ event_id?: string;
16
+ "m.in_reply_to"?: { event_id?: string };
17
+ };
18
+ };
19
+
20
+ const RelationType = {
21
+ Thread: "m.thread",
22
+ } as const;
23
+
24
+ export function resolveMatrixThreadTarget(params: {
25
+ threadReplies: "off" | "inbound" | "always";
26
+ messageId: string;
27
+ threadRootId?: string;
28
+ isThreadRoot?: boolean;
29
+ }): string | undefined {
30
+ const { threadReplies, messageId, threadRootId } = params;
31
+ if (threadReplies === "off") return undefined;
32
+ const isThreadRoot = params.isThreadRoot === true;
33
+ const hasInboundThread = Boolean(threadRootId && threadRootId !== messageId && !isThreadRoot);
34
+ if (threadReplies === "inbound") {
35
+ return hasInboundThread ? threadRootId : undefined;
36
+ }
37
+ if (threadReplies === "always") {
38
+ return threadRootId ?? messageId;
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ export function resolveMatrixThreadRootId(params: {
44
+ event: MatrixRawEvent;
45
+ content: RoomMessageEventContent;
46
+ }): string | undefined {
47
+ const relates = params.content["m.relates_to"];
48
+ if (!relates || typeof relates !== "object") return undefined;
49
+ if ("rel_type" in relates && relates.rel_type === RelationType.Thread) {
50
+ if ("event_id" in relates && typeof relates.event_id === "string") {
51
+ return relates.event_id;
52
+ }
53
+ if (
54
+ "m.in_reply_to" in relates &&
55
+ typeof relates["m.in_reply_to"] === "object" &&
56
+ relates["m.in_reply_to"] &&
57
+ "event_id" in relates["m.in_reply_to"] &&
58
+ typeof relates["m.in_reply_to"].event_id === "string"
59
+ ) {
60
+ return relates["m.in_reply_to"].event_id;
61
+ }
62
+ }
63
+ return undefined;
64
+ }
@@ -0,0 +1,39 @@
1
+ import type { EncryptedFile, MessageEventContent } from "@vector-im/matrix-bot-sdk";
2
+
3
+ export const EventType = {
4
+ RoomMessage: "m.room.message",
5
+ RoomMessageEncrypted: "m.room.encrypted",
6
+ RoomMember: "m.room.member",
7
+ Location: "m.location",
8
+ } as const;
9
+
10
+ export const RelationType = {
11
+ Replace: "m.replace",
12
+ Thread: "m.thread",
13
+ } as const;
14
+
15
+ export type MatrixRawEvent = {
16
+ event_id: string;
17
+ sender: string;
18
+ type: string;
19
+ origin_server_ts: number;
20
+ content: Record<string, unknown>;
21
+ unsigned?: {
22
+ age?: number;
23
+ redacted_because?: unknown;
24
+ };
25
+ };
26
+
27
+ export type RoomMessageEventContent = MessageEventContent & {
28
+ url?: string;
29
+ file?: EncryptedFile;
30
+ info?: {
31
+ mimetype?: string;
32
+ size?: number;
33
+ };
34
+ "m.relates_to"?: {
35
+ rel_type?: string;
36
+ event_id?: string;
37
+ "m.in_reply_to"?: { event_id?: string };
38
+ };
39
+ };
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { parsePollStartContent } from "./poll-types.js";
4
+
5
+ describe("parsePollStartContent", () => {
6
+ it("parses legacy m.poll payloads", () => {
7
+ const summary = parsePollStartContent({
8
+ "m.poll": {
9
+ question: { "m.text": "Lunch?" },
10
+ kind: "m.poll.disclosed",
11
+ max_selections: 1,
12
+ answers: [
13
+ { id: "answer1", "m.text": "Yes" },
14
+ { id: "answer2", "m.text": "No" },
15
+ ],
16
+ },
17
+ });
18
+
19
+ expect(summary?.question).toBe("Lunch?");
20
+ expect(summary?.answers).toEqual(["Yes", "No"]);
21
+ });
22
+ });
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Matrix Poll Types (MSC3381)
3
+ *
4
+ * Defines types for Matrix poll events:
5
+ * - m.poll.start - Creates a new poll
6
+ * - m.poll.response - Records a vote
7
+ * - m.poll.end - Closes a poll
8
+ */
9
+
10
+ import type { PollInput } from "openclaw/plugin-sdk";
11
+
12
+ export const M_POLL_START = "m.poll.start" as const;
13
+ export const M_POLL_RESPONSE = "m.poll.response" as const;
14
+ export const M_POLL_END = "m.poll.end" as const;
15
+
16
+ export const ORG_POLL_START = "org.matrix.msc3381.poll.start" as const;
17
+ export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response" as const;
18
+ export const ORG_POLL_END = "org.matrix.msc3381.poll.end" as const;
19
+
20
+ export const POLL_EVENT_TYPES = [
21
+ M_POLL_START,
22
+ M_POLL_RESPONSE,
23
+ M_POLL_END,
24
+ ORG_POLL_START,
25
+ ORG_POLL_RESPONSE,
26
+ ORG_POLL_END,
27
+ ];
28
+
29
+ export const POLL_START_TYPES = [M_POLL_START, ORG_POLL_START];
30
+ export const POLL_RESPONSE_TYPES = [M_POLL_RESPONSE, ORG_POLL_RESPONSE];
31
+ export const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END];
32
+
33
+ export type PollKind = "m.poll.disclosed" | "m.poll.undisclosed";
34
+
35
+ export type TextContent = {
36
+ "m.text"?: string;
37
+ "org.matrix.msc1767.text"?: string;
38
+ body?: string;
39
+ };
40
+
41
+ export type PollAnswer = {
42
+ id: string;
43
+ } & TextContent;
44
+
45
+ export type PollStartSubtype = {
46
+ question: TextContent;
47
+ kind?: PollKind;
48
+ max_selections?: number;
49
+ answers: PollAnswer[];
50
+ };
51
+
52
+ export type LegacyPollStartContent = {
53
+ "m.poll"?: PollStartSubtype;
54
+ };
55
+
56
+ export type PollStartContent = {
57
+ [M_POLL_START]?: PollStartSubtype;
58
+ [ORG_POLL_START]?: PollStartSubtype;
59
+ "m.poll"?: PollStartSubtype;
60
+ "m.text"?: string;
61
+ "org.matrix.msc1767.text"?: string;
62
+ };
63
+
64
+ export type PollSummary = {
65
+ eventId: string;
66
+ roomId: string;
67
+ sender: string;
68
+ senderName: string;
69
+ question: string;
70
+ answers: string[];
71
+ kind: PollKind;
72
+ maxSelections: number;
73
+ };
74
+
75
+ export function isPollStartType(eventType: string): boolean {
76
+ return POLL_START_TYPES.includes(eventType);
77
+ }
78
+
79
+ export function getTextContent(text?: TextContent): string {
80
+ if (!text) return "";
81
+ return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? "";
82
+ }
83
+
84
+ export function parsePollStartContent(content: PollStartContent): PollSummary | null {
85
+ const poll = (content as Record<string, PollStartSubtype | undefined>)[M_POLL_START]
86
+ ?? (content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START]
87
+ ?? (content as Record<string, PollStartSubtype | undefined>)["m.poll"];
88
+ if (!poll) return null;
89
+
90
+ const question = getTextContent(poll.question);
91
+ if (!question) return null;
92
+
93
+ const answers = poll.answers
94
+ .map((answer) => getTextContent(answer))
95
+ .filter((a) => a.trim().length > 0);
96
+
97
+ return {
98
+ eventId: "",
99
+ roomId: "",
100
+ sender: "",
101
+ senderName: "",
102
+ question,
103
+ answers,
104
+ kind: poll.kind ?? "m.poll.disclosed",
105
+ maxSelections: poll.max_selections ?? 1,
106
+ };
107
+ }
108
+
109
+ export function formatPollAsText(summary: PollSummary): string {
110
+ const lines = [
111
+ "[Poll]",
112
+ summary.question,
113
+ "",
114
+ ...summary.answers.map((answer, idx) => `${idx + 1}. ${answer}`),
115
+ ];
116
+ return lines.join("\n");
117
+ }
118
+
119
+ function buildTextContent(body: string): TextContent {
120
+ return {
121
+ "m.text": body,
122
+ "org.matrix.msc1767.text": body,
123
+ };
124
+ }
125
+
126
+ function buildPollFallbackText(question: string, answers: string[]): string {
127
+ if (answers.length === 0) return question;
128
+ return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
129
+ }
130
+
131
+ export function buildPollStartContent(poll: PollInput): PollStartContent {
132
+ const question = poll.question.trim();
133
+ const answers = poll.options
134
+ .map((option) => option.trim())
135
+ .filter((option) => option.length > 0)
136
+ .map((option, idx) => ({
137
+ id: `answer${idx + 1}`,
138
+ ...buildTextContent(option),
139
+ }));
140
+
141
+ const maxSelections = poll.multiple ? Math.max(1, answers.length) : 1;
142
+ const fallbackText = buildPollFallbackText(
143
+ question,
144
+ answers.map((answer) => getTextContent(answer)),
145
+ );
146
+
147
+ return {
148
+ [M_POLL_START]: {
149
+ question: buildTextContent(question),
150
+ kind: poll.multiple ? "m.poll.undisclosed" : "m.poll.disclosed",
151
+ max_selections: maxSelections,
152
+ answers,
153
+ },
154
+ "m.text": fallbackText,
155
+ "org.matrix.msc1767.text": fallbackText,
156
+ };
157
+ }
@@ -0,0 +1,70 @@
1
+ import { createMatrixClient, isBunRuntime } from "./client.js";
2
+
3
+ export type MatrixProbe = {
4
+ ok: boolean;
5
+ status?: number | null;
6
+ error?: string | null;
7
+ elapsedMs: number;
8
+ userId?: string | null;
9
+ };
10
+
11
+ export async function probeMatrix(params: {
12
+ homeserver: string;
13
+ accessToken: string;
14
+ userId?: string;
15
+ timeoutMs: number;
16
+ }): Promise<MatrixProbe> {
17
+ const started = Date.now();
18
+ const result: MatrixProbe = {
19
+ ok: false,
20
+ status: null,
21
+ error: null,
22
+ elapsedMs: 0,
23
+ };
24
+ if (isBunRuntime()) {
25
+ return {
26
+ ...result,
27
+ error: "Matrix probe requires Node (bun runtime not supported)",
28
+ elapsedMs: Date.now() - started,
29
+ };
30
+ }
31
+ if (!params.homeserver?.trim()) {
32
+ return {
33
+ ...result,
34
+ error: "missing homeserver",
35
+ elapsedMs: Date.now() - started,
36
+ };
37
+ }
38
+ if (!params.accessToken?.trim()) {
39
+ return {
40
+ ...result,
41
+ error: "missing access token",
42
+ elapsedMs: Date.now() - started,
43
+ };
44
+ }
45
+ try {
46
+ const client = await createMatrixClient({
47
+ homeserver: params.homeserver,
48
+ userId: params.userId ?? "",
49
+ accessToken: params.accessToken,
50
+ localTimeoutMs: params.timeoutMs,
51
+ });
52
+ // @vector-im/matrix-bot-sdk uses getUserId() which calls whoami internally
53
+ const userId = await client.getUserId();
54
+ result.ok = true;
55
+ result.userId = userId ?? null;
56
+
57
+ result.elapsedMs = Date.now() - started;
58
+ return result;
59
+ } catch (err) {
60
+ return {
61
+ ...result,
62
+ status:
63
+ typeof err === "object" && err && "statusCode" in err
64
+ ? Number((err as { statusCode?: number }).statusCode)
65
+ : result.status,
66
+ error: err instanceof Error ? err.message : String(err),
67
+ elapsedMs: Date.now() - started,
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,63 @@
1
+ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+
3
+ import { getMatrixRuntime } from "../../runtime.js";
4
+ import { getActiveMatrixClient } from "../active-client.js";
5
+ import {
6
+ createMatrixClient,
7
+ isBunRuntime,
8
+ resolveMatrixAuth,
9
+ resolveSharedMatrixClient,
10
+ } from "../client.js";
11
+ import type { CoreConfig } from "../types.js";
12
+
13
+ const getCore = () => getMatrixRuntime();
14
+
15
+ export function ensureNodeRuntime() {
16
+ if (isBunRuntime()) {
17
+ throw new Error("Matrix support requires Node (bun runtime not supported)");
18
+ }
19
+ }
20
+
21
+ export function resolveMediaMaxBytes(): number | undefined {
22
+ const cfg = getCore().config.loadConfig() as CoreConfig;
23
+ if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") {
24
+ return cfg.channels.matrix.mediaMaxMb * 1024 * 1024;
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ export async function resolveMatrixClient(opts: {
30
+ client?: MatrixClient;
31
+ timeoutMs?: number;
32
+ }): Promise<{ client: MatrixClient; stopOnDone: boolean }> {
33
+ ensureNodeRuntime();
34
+ if (opts.client) return { client: opts.client, stopOnDone: false };
35
+ const active = getActiveMatrixClient();
36
+ if (active) return { client: active, stopOnDone: false };
37
+ const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT);
38
+ if (shouldShareClient) {
39
+ const client = await resolveSharedMatrixClient({
40
+ timeoutMs: opts.timeoutMs,
41
+ });
42
+ return { client, stopOnDone: false };
43
+ }
44
+ const auth = await resolveMatrixAuth();
45
+ const client = await createMatrixClient({
46
+ homeserver: auth.homeserver,
47
+ userId: auth.userId,
48
+ accessToken: auth.accessToken,
49
+ encryption: auth.encryption,
50
+ localTimeoutMs: opts.timeoutMs,
51
+ });
52
+ if (auth.encryption && client.crypto) {
53
+ try {
54
+ const joinedRooms = await client.getJoinedRooms();
55
+ await client.crypto.prepare(joinedRooms);
56
+ } catch {
57
+ // Ignore crypto prep failures for one-off sends; normal sync will retry.
58
+ }
59
+ }
60
+ // @vector-im/matrix-bot-sdk uses start() instead of startClient()
61
+ await client.start();
62
+ return { client, stopOnDone: true };
63
+ }
@@ -0,0 +1,92 @@
1
+ import { markdownToMatrixHtml } from "../format.js";
2
+ import { getMatrixRuntime } from "../../runtime.js";
3
+ import {
4
+ MsgType,
5
+ RelationType,
6
+ type MatrixFormattedContent,
7
+ type MatrixMediaMsgType,
8
+ type MatrixRelation,
9
+ type MatrixReplyRelation,
10
+ type MatrixTextContent,
11
+ type MatrixThreadRelation,
12
+ } from "./types.js";
13
+
14
+ const getCore = () => getMatrixRuntime();
15
+
16
+ export function buildTextContent(
17
+ body: string,
18
+ relation?: MatrixRelation,
19
+ ): MatrixTextContent {
20
+ const content: MatrixTextContent = relation
21
+ ? {
22
+ msgtype: MsgType.Text,
23
+ body,
24
+ "m.relates_to": relation,
25
+ }
26
+ : {
27
+ msgtype: MsgType.Text,
28
+ body,
29
+ };
30
+ applyMatrixFormatting(content, body);
31
+ return content;
32
+ }
33
+
34
+ export function applyMatrixFormatting(content: MatrixFormattedContent, body: string): void {
35
+ const formatted = markdownToMatrixHtml(body ?? "");
36
+ if (!formatted) return;
37
+ content.format = "org.matrix.custom.html";
38
+ content.formatted_body = formatted;
39
+ }
40
+
41
+ export function buildReplyRelation(replyToId?: string): MatrixReplyRelation | undefined {
42
+ const trimmed = replyToId?.trim();
43
+ if (!trimmed) return undefined;
44
+ return { "m.in_reply_to": { event_id: trimmed } };
45
+ }
46
+
47
+ export function buildThreadRelation(
48
+ threadId: string,
49
+ replyToId?: string,
50
+ ): MatrixThreadRelation {
51
+ const trimmed = threadId.trim();
52
+ return {
53
+ rel_type: RelationType.Thread,
54
+ event_id: trimmed,
55
+ is_falling_back: true,
56
+ "m.in_reply_to": { event_id: (replyToId?.trim() || trimmed) },
57
+ };
58
+ }
59
+
60
+ export function resolveMatrixMsgType(
61
+ contentType?: string,
62
+ _fileName?: string,
63
+ ): MatrixMediaMsgType {
64
+ const kind = getCore().media.mediaKindFromMime(contentType ?? "");
65
+ switch (kind) {
66
+ case "image":
67
+ return MsgType.Image;
68
+ case "audio":
69
+ return MsgType.Audio;
70
+ case "video":
71
+ return MsgType.Video;
72
+ default:
73
+ return MsgType.File;
74
+ }
75
+ }
76
+
77
+ export function resolveMatrixVoiceDecision(opts: {
78
+ wantsVoice: boolean;
79
+ contentType?: string;
80
+ fileName?: string;
81
+ }): { useVoice: boolean } {
82
+ if (!opts.wantsVoice) return { useVoice: false };
83
+ if (
84
+ getCore().media.isVoiceCompatibleAudio({
85
+ contentType: opts.contentType,
86
+ fileName: opts.fileName,
87
+ })
88
+ ) {
89
+ return { useVoice: true };
90
+ }
91
+ return { useVoice: false };
92
+ }