@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,61 @@
1
+ import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
2
+
3
+ import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
4
+ import type { CoreConfig } from "./types.js";
5
+
6
+ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): boolean {
7
+ const rawGroupId = params.groupId?.trim() ?? "";
8
+ let roomId = rawGroupId;
9
+ const lower = roomId.toLowerCase();
10
+ if (lower.startsWith("matrix:")) {
11
+ roomId = roomId.slice("matrix:".length).trim();
12
+ }
13
+ if (roomId.toLowerCase().startsWith("channel:")) {
14
+ roomId = roomId.slice("channel:".length).trim();
15
+ }
16
+ if (roomId.toLowerCase().startsWith("room:")) {
17
+ roomId = roomId.slice("room:".length).trim();
18
+ }
19
+ const groupChannel = params.groupChannel?.trim() ?? "";
20
+ const aliases = groupChannel ? [groupChannel] : [];
21
+ const cfg = params.cfg as CoreConfig;
22
+ const resolved = resolveMatrixRoomConfig({
23
+ rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
24
+ roomId,
25
+ aliases,
26
+ name: groupChannel || undefined,
27
+ }).config;
28
+ if (resolved) {
29
+ if (resolved.autoReply === true) return false;
30
+ if (resolved.autoReply === false) return true;
31
+ if (typeof resolved.requireMention === "boolean") return resolved.requireMention;
32
+ }
33
+ return true;
34
+ }
35
+
36
+ export function resolveMatrixGroupToolPolicy(
37
+ params: ChannelGroupContext,
38
+ ): GroupToolPolicyConfig | undefined {
39
+ const rawGroupId = params.groupId?.trim() ?? "";
40
+ let roomId = rawGroupId;
41
+ const lower = roomId.toLowerCase();
42
+ if (lower.startsWith("matrix:")) {
43
+ roomId = roomId.slice("matrix:".length).trim();
44
+ }
45
+ if (roomId.toLowerCase().startsWith("channel:")) {
46
+ roomId = roomId.slice("channel:".length).trim();
47
+ }
48
+ if (roomId.toLowerCase().startsWith("room:")) {
49
+ roomId = roomId.slice("room:".length).trim();
50
+ }
51
+ const groupChannel = params.groupChannel?.trim() ?? "";
52
+ const aliases = groupChannel ? [groupChannel] : [];
53
+ const cfg = params.cfg as CoreConfig;
54
+ const resolved = resolveMatrixRoomConfig({
55
+ rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
56
+ roomId,
57
+ aliases,
58
+ name: groupChannel || undefined,
59
+ }).config;
60
+ return resolved?.tools;
61
+ }
@@ -0,0 +1,83 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import type { CoreConfig } from "../types.js";
4
+ import { resolveMatrixAccount } from "./accounts.js";
5
+
6
+ vi.mock("./credentials.js", () => ({
7
+ loadMatrixCredentials: () => null,
8
+ credentialsMatchConfig: () => false,
9
+ }));
10
+
11
+ const envKeys = [
12
+ "MATRIX_HOMESERVER",
13
+ "MATRIX_USER_ID",
14
+ "MATRIX_ACCESS_TOKEN",
15
+ "MATRIX_PASSWORD",
16
+ "MATRIX_DEVICE_NAME",
17
+ ];
18
+
19
+ describe("resolveMatrixAccount", () => {
20
+ let prevEnv: Record<string, string | undefined> = {};
21
+
22
+ beforeEach(() => {
23
+ prevEnv = {};
24
+ for (const key of envKeys) {
25
+ prevEnv[key] = process.env[key];
26
+ delete process.env[key];
27
+ }
28
+ });
29
+
30
+ afterEach(() => {
31
+ for (const key of envKeys) {
32
+ const value = prevEnv[key];
33
+ if (value === undefined) {
34
+ delete process.env[key];
35
+ } else {
36
+ process.env[key] = value;
37
+ }
38
+ }
39
+ });
40
+
41
+ it("treats access-token-only config as configured", () => {
42
+ const cfg: CoreConfig = {
43
+ channels: {
44
+ matrix: {
45
+ homeserver: "https://matrix.example.org",
46
+ accessToken: "tok-access",
47
+ },
48
+ },
49
+ };
50
+
51
+ const account = resolveMatrixAccount({ cfg });
52
+ expect(account.configured).toBe(true);
53
+ });
54
+
55
+ it("requires userId + password when no access token is set", () => {
56
+ const cfg: CoreConfig = {
57
+ channels: {
58
+ matrix: {
59
+ homeserver: "https://matrix.example.org",
60
+ userId: "@bot:example.org",
61
+ },
62
+ },
63
+ };
64
+
65
+ const account = resolveMatrixAccount({ cfg });
66
+ expect(account.configured).toBe(false);
67
+ });
68
+
69
+ it("marks password auth as configured when userId is present", () => {
70
+ const cfg: CoreConfig = {
71
+ channels: {
72
+ matrix: {
73
+ homeserver: "https://matrix.example.org",
74
+ userId: "@bot:example.org",
75
+ password: "secret",
76
+ },
77
+ },
78
+ };
79
+
80
+ const account = resolveMatrixAccount({ cfg });
81
+ expect(account.configured).toBe(true);
82
+ });
83
+ });
@@ -0,0 +1,63 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk";
2
+ import type { CoreConfig, MatrixConfig } from "../types.js";
3
+ import { resolveMatrixConfig } from "./client.js";
4
+ import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
5
+
6
+ export type ResolvedMatrixAccount = {
7
+ accountId: string;
8
+ enabled: boolean;
9
+ name?: string;
10
+ configured: boolean;
11
+ homeserver?: string;
12
+ userId?: string;
13
+ config: MatrixConfig;
14
+ };
15
+
16
+ export function listMatrixAccountIds(_cfg: CoreConfig): string[] {
17
+ return [DEFAULT_ACCOUNT_ID];
18
+ }
19
+
20
+ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
21
+ const ids = listMatrixAccountIds(cfg);
22
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
23
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
24
+ }
25
+
26
+ export function resolveMatrixAccount(params: {
27
+ cfg: CoreConfig;
28
+ accountId?: string | null;
29
+ }): ResolvedMatrixAccount {
30
+ const accountId = normalizeAccountId(params.accountId);
31
+ const base = (params.cfg.channels?.matrix ?? {}) as MatrixConfig;
32
+ const enabled = base.enabled !== false;
33
+ const resolved = resolveMatrixConfig(params.cfg, process.env);
34
+ const hasHomeserver = Boolean(resolved.homeserver);
35
+ const hasUserId = Boolean(resolved.userId);
36
+ const hasAccessToken = Boolean(resolved.accessToken);
37
+ const hasPassword = Boolean(resolved.password);
38
+ const hasPasswordAuth = hasUserId && hasPassword;
39
+ const stored = loadMatrixCredentials(process.env);
40
+ const hasStored =
41
+ stored && resolved.homeserver
42
+ ? credentialsMatchConfig(stored, {
43
+ homeserver: resolved.homeserver,
44
+ userId: resolved.userId || "",
45
+ })
46
+ : false;
47
+ const configured = hasHomeserver && (hasAccessToken || hasPasswordAuth || Boolean(hasStored));
48
+ return {
49
+ accountId,
50
+ enabled,
51
+ name: base.name?.trim() || undefined,
52
+ configured,
53
+ homeserver: resolved.homeserver || undefined,
54
+ userId: resolved.userId || undefined,
55
+ config: base,
56
+ };
57
+ }
58
+
59
+ export function listEnabledMatrixAccounts(cfg: CoreConfig): ResolvedMatrixAccount[] {
60
+ return listMatrixAccountIds(cfg)
61
+ .map((accountId) => resolveMatrixAccount({ cfg, accountId }))
62
+ .filter((account) => account.enabled);
63
+ }
@@ -0,0 +1,53 @@
1
+ import { getMatrixRuntime } from "../../runtime.js";
2
+ import type { CoreConfig } from "../types.js";
3
+ import { getActiveMatrixClient } from "../active-client.js";
4
+ import {
5
+ createMatrixClient,
6
+ isBunRuntime,
7
+ resolveMatrixAuth,
8
+ resolveSharedMatrixClient,
9
+ } from "../client.js";
10
+ import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
11
+
12
+ export function ensureNodeRuntime() {
13
+ if (isBunRuntime()) {
14
+ throw new Error("Matrix support requires Node (bun runtime not supported)");
15
+ }
16
+ }
17
+
18
+ export async function resolveActionClient(
19
+ opts: MatrixActionClientOpts = {},
20
+ ): Promise<MatrixActionClient> {
21
+ ensureNodeRuntime();
22
+ if (opts.client) return { client: opts.client, stopOnDone: false };
23
+ const active = getActiveMatrixClient();
24
+ if (active) return { client: active, stopOnDone: false };
25
+ const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT);
26
+ if (shouldShareClient) {
27
+ const client = await resolveSharedMatrixClient({
28
+ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
29
+ timeoutMs: opts.timeoutMs,
30
+ });
31
+ return { client, stopOnDone: false };
32
+ }
33
+ const auth = await resolveMatrixAuth({
34
+ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
35
+ });
36
+ const client = await createMatrixClient({
37
+ homeserver: auth.homeserver,
38
+ userId: auth.userId,
39
+ accessToken: auth.accessToken,
40
+ encryption: auth.encryption,
41
+ localTimeoutMs: opts.timeoutMs,
42
+ });
43
+ if (auth.encryption && client.crypto) {
44
+ try {
45
+ const joinedRooms = await client.getJoinedRooms();
46
+ await client.crypto.prepare(joinedRooms);
47
+ } catch {
48
+ // Ignore crypto prep failures for one-off actions.
49
+ }
50
+ }
51
+ await client.start();
52
+ return { client, stopOnDone: true };
53
+ }
@@ -0,0 +1,120 @@
1
+ import {
2
+ EventType,
3
+ MsgType,
4
+ RelationType,
5
+ type MatrixActionClientOpts,
6
+ type MatrixMessageSummary,
7
+ type MatrixRawEvent,
8
+ type RoomMessageEventContent,
9
+ } from "./types.js";
10
+ import { resolveActionClient } from "./client.js";
11
+ import { summarizeMatrixRawEvent } from "./summary.js";
12
+ import { resolveMatrixRoomId, sendMessageMatrix } from "../send.js";
13
+
14
+ export async function sendMatrixMessage(
15
+ to: string,
16
+ content: string,
17
+ opts: MatrixActionClientOpts & {
18
+ mediaUrl?: string;
19
+ replyToId?: string;
20
+ threadId?: string;
21
+ } = {},
22
+ ) {
23
+ return await sendMessageMatrix(to, content, {
24
+ mediaUrl: opts.mediaUrl,
25
+ replyToId: opts.replyToId,
26
+ threadId: opts.threadId,
27
+ client: opts.client,
28
+ timeoutMs: opts.timeoutMs,
29
+ });
30
+ }
31
+
32
+ export async function editMatrixMessage(
33
+ roomId: string,
34
+ messageId: string,
35
+ content: string,
36
+ opts: MatrixActionClientOpts = {},
37
+ ) {
38
+ const trimmed = content.trim();
39
+ if (!trimmed) throw new Error("Matrix edit requires content");
40
+ const { client, stopOnDone } = await resolveActionClient(opts);
41
+ try {
42
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
43
+ const newContent = {
44
+ msgtype: MsgType.Text,
45
+ body: trimmed,
46
+ } satisfies RoomMessageEventContent;
47
+ const payload: RoomMessageEventContent = {
48
+ msgtype: MsgType.Text,
49
+ body: `* ${trimmed}`,
50
+ "m.new_content": newContent,
51
+ "m.relates_to": {
52
+ rel_type: RelationType.Replace,
53
+ event_id: messageId,
54
+ },
55
+ };
56
+ const eventId = await client.sendMessage(resolvedRoom, payload);
57
+ return { eventId: eventId ?? null };
58
+ } finally {
59
+ if (stopOnDone) client.stop();
60
+ }
61
+ }
62
+
63
+ export async function deleteMatrixMessage(
64
+ roomId: string,
65
+ messageId: string,
66
+ opts: MatrixActionClientOpts & { reason?: string } = {},
67
+ ) {
68
+ const { client, stopOnDone } = await resolveActionClient(opts);
69
+ try {
70
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
71
+ await client.redactEvent(resolvedRoom, messageId, opts.reason);
72
+ } finally {
73
+ if (stopOnDone) client.stop();
74
+ }
75
+ }
76
+
77
+ export async function readMatrixMessages(
78
+ roomId: string,
79
+ opts: MatrixActionClientOpts & {
80
+ limit?: number;
81
+ before?: string;
82
+ after?: string;
83
+ } = {},
84
+ ): Promise<{
85
+ messages: MatrixMessageSummary[];
86
+ nextBatch?: string | null;
87
+ prevBatch?: string | null;
88
+ }> {
89
+ const { client, stopOnDone } = await resolveActionClient(opts);
90
+ try {
91
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
92
+ const limit =
93
+ typeof opts.limit === "number" && Number.isFinite(opts.limit)
94
+ ? Math.max(1, Math.floor(opts.limit))
95
+ : 20;
96
+ const token = opts.before?.trim() || opts.after?.trim() || undefined;
97
+ const dir = opts.after ? "f" : "b";
98
+ // @vector-im/matrix-bot-sdk uses doRequest for room messages
99
+ const res = await client.doRequest(
100
+ "GET",
101
+ `/_matrix/client/v3/rooms/${encodeURIComponent(resolvedRoom)}/messages`,
102
+ {
103
+ dir,
104
+ limit,
105
+ from: token,
106
+ },
107
+ ) as { chunk: MatrixRawEvent[]; start?: string; end?: string };
108
+ const messages = res.chunk
109
+ .filter((event) => event.type === EventType.RoomMessage)
110
+ .filter((event) => !event.unsigned?.redacted_because)
111
+ .map(summarizeMatrixRawEvent);
112
+ return {
113
+ messages,
114
+ nextBatch: res.end ?? null,
115
+ prevBatch: res.start ?? null,
116
+ };
117
+ } finally {
118
+ if (stopOnDone) client.stop();
119
+ }
120
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ EventType,
3
+ type MatrixActionClientOpts,
4
+ type MatrixMessageSummary,
5
+ type RoomPinnedEventsEventContent,
6
+ } from "./types.js";
7
+ import { resolveActionClient } from "./client.js";
8
+ import { fetchEventSummary, readPinnedEvents } from "./summary.js";
9
+ import { resolveMatrixRoomId } from "../send.js";
10
+
11
+ export async function pinMatrixMessage(
12
+ roomId: string,
13
+ messageId: string,
14
+ opts: MatrixActionClientOpts = {},
15
+ ): Promise<{ pinned: string[] }> {
16
+ const { client, stopOnDone } = await resolveActionClient(opts);
17
+ try {
18
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
19
+ const current = await readPinnedEvents(client, resolvedRoom);
20
+ const next = current.includes(messageId) ? current : [...current, messageId];
21
+ const payload: RoomPinnedEventsEventContent = { pinned: next };
22
+ await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload);
23
+ return { pinned: next };
24
+ } finally {
25
+ if (stopOnDone) client.stop();
26
+ }
27
+ }
28
+
29
+ export async function unpinMatrixMessage(
30
+ roomId: string,
31
+ messageId: string,
32
+ opts: MatrixActionClientOpts = {},
33
+ ): Promise<{ pinned: string[] }> {
34
+ const { client, stopOnDone } = await resolveActionClient(opts);
35
+ try {
36
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
37
+ const current = await readPinnedEvents(client, resolvedRoom);
38
+ const next = current.filter((id) => id !== messageId);
39
+ const payload: RoomPinnedEventsEventContent = { pinned: next };
40
+ await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload);
41
+ return { pinned: next };
42
+ } finally {
43
+ if (stopOnDone) client.stop();
44
+ }
45
+ }
46
+
47
+ export async function listMatrixPins(
48
+ roomId: string,
49
+ opts: MatrixActionClientOpts = {},
50
+ ): Promise<{ pinned: string[]; events: MatrixMessageSummary[] }> {
51
+ const { client, stopOnDone } = await resolveActionClient(opts);
52
+ try {
53
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
54
+ const pinned = await readPinnedEvents(client, resolvedRoom);
55
+ const events = (
56
+ await Promise.all(
57
+ pinned.map(async (eventId) => {
58
+ try {
59
+ return await fetchEventSummary(client, resolvedRoom, eventId);
60
+ } catch {
61
+ return null;
62
+ }
63
+ }),
64
+ )
65
+ ).filter((event): event is MatrixMessageSummary => Boolean(event));
66
+ return { pinned, events };
67
+ } finally {
68
+ if (stopOnDone) client.stop();
69
+ }
70
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ EventType,
3
+ RelationType,
4
+ type MatrixActionClientOpts,
5
+ type MatrixRawEvent,
6
+ type MatrixReactionSummary,
7
+ type ReactionEventContent,
8
+ } from "./types.js";
9
+ import { resolveActionClient } from "./client.js";
10
+ import { resolveMatrixRoomId } from "../send.js";
11
+
12
+ export async function listMatrixReactions(
13
+ roomId: string,
14
+ messageId: string,
15
+ opts: MatrixActionClientOpts & { limit?: number } = {},
16
+ ): Promise<MatrixReactionSummary[]> {
17
+ const { client, stopOnDone } = await resolveActionClient(opts);
18
+ try {
19
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
20
+ const limit =
21
+ typeof opts.limit === "number" && Number.isFinite(opts.limit)
22
+ ? Math.max(1, Math.floor(opts.limit))
23
+ : 100;
24
+ // @vector-im/matrix-bot-sdk uses doRequest for relations
25
+ const res = await client.doRequest(
26
+ "GET",
27
+ `/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`,
28
+ { dir: "b", limit },
29
+ ) as { chunk: MatrixRawEvent[] };
30
+ const summaries = new Map<string, MatrixReactionSummary>();
31
+ for (const event of res.chunk) {
32
+ const content = event.content as ReactionEventContent;
33
+ const key = content["m.relates_to"]?.key;
34
+ if (!key) continue;
35
+ const sender = event.sender ?? "";
36
+ const entry: MatrixReactionSummary = summaries.get(key) ?? {
37
+ key,
38
+ count: 0,
39
+ users: [],
40
+ };
41
+ entry.count += 1;
42
+ if (sender && !entry.users.includes(sender)) {
43
+ entry.users.push(sender);
44
+ }
45
+ summaries.set(key, entry);
46
+ }
47
+ return Array.from(summaries.values());
48
+ } finally {
49
+ if (stopOnDone) client.stop();
50
+ }
51
+ }
52
+
53
+ export async function removeMatrixReactions(
54
+ roomId: string,
55
+ messageId: string,
56
+ opts: MatrixActionClientOpts & { emoji?: string } = {},
57
+ ): Promise<{ removed: number }> {
58
+ const { client, stopOnDone } = await resolveActionClient(opts);
59
+ try {
60
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
61
+ const res = await client.doRequest(
62
+ "GET",
63
+ `/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`,
64
+ { dir: "b", limit: 200 },
65
+ ) as { chunk: MatrixRawEvent[] };
66
+ const userId = await client.getUserId();
67
+ if (!userId) return { removed: 0 };
68
+ const targetEmoji = opts.emoji?.trim();
69
+ const toRemove = res.chunk
70
+ .filter((event) => event.sender === userId)
71
+ .filter((event) => {
72
+ if (!targetEmoji) return true;
73
+ const content = event.content as ReactionEventContent;
74
+ return content["m.relates_to"]?.key === targetEmoji;
75
+ })
76
+ .map((event) => event.event_id)
77
+ .filter((id): id is string => Boolean(id));
78
+ if (toRemove.length === 0) return { removed: 0 };
79
+ await Promise.all(toRemove.map((id) => client.redactEvent(resolvedRoom, id)));
80
+ return { removed: toRemove.length };
81
+ } finally {
82
+ if (stopOnDone) client.stop();
83
+ }
84
+ }
@@ -0,0 +1,88 @@
1
+ import { EventType, type MatrixActionClientOpts } from "./types.js";
2
+ import { resolveActionClient } from "./client.js";
3
+ import { resolveMatrixRoomId } from "../send.js";
4
+
5
+ export async function getMatrixMemberInfo(
6
+ userId: string,
7
+ opts: MatrixActionClientOpts & { roomId?: string } = {},
8
+ ) {
9
+ const { client, stopOnDone } = await resolveActionClient(opts);
10
+ try {
11
+ const roomId = opts.roomId ? await resolveMatrixRoomId(client, opts.roomId) : undefined;
12
+ // @vector-im/matrix-bot-sdk uses getUserProfile
13
+ const profile = await client.getUserProfile(userId);
14
+ // Note: @vector-im/matrix-bot-sdk doesn't have getRoom().getMember() like matrix-js-sdk
15
+ // We'd need to fetch room state separately if needed
16
+ return {
17
+ userId,
18
+ profile: {
19
+ displayName: profile?.displayname ?? null,
20
+ avatarUrl: profile?.avatar_url ?? null,
21
+ },
22
+ membership: null, // Would need separate room state query
23
+ powerLevel: null, // Would need separate power levels state query
24
+ displayName: profile?.displayname ?? null,
25
+ roomId: roomId ?? null,
26
+ };
27
+ } finally {
28
+ if (stopOnDone) client.stop();
29
+ }
30
+ }
31
+
32
+ export async function getMatrixRoomInfo(
33
+ roomId: string,
34
+ opts: MatrixActionClientOpts = {},
35
+ ) {
36
+ const { client, stopOnDone } = await resolveActionClient(opts);
37
+ try {
38
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
39
+ // @vector-im/matrix-bot-sdk uses getRoomState for state events
40
+ let name: string | null = null;
41
+ let topic: string | null = null;
42
+ let canonicalAlias: string | null = null;
43
+ let memberCount: number | null = null;
44
+
45
+ try {
46
+ const nameState = await client.getRoomStateEvent(resolvedRoom, "m.room.name", "");
47
+ name = nameState?.name ?? null;
48
+ } catch {
49
+ // ignore
50
+ }
51
+
52
+ try {
53
+ const topicState = await client.getRoomStateEvent(resolvedRoom, EventType.RoomTopic, "");
54
+ topic = topicState?.topic ?? null;
55
+ } catch {
56
+ // ignore
57
+ }
58
+
59
+ try {
60
+ const aliasState = await client.getRoomStateEvent(
61
+ resolvedRoom,
62
+ "m.room.canonical_alias",
63
+ "",
64
+ );
65
+ canonicalAlias = aliasState?.alias ?? null;
66
+ } catch {
67
+ // ignore
68
+ }
69
+
70
+ try {
71
+ const members = await client.getJoinedRoomMembers(resolvedRoom);
72
+ memberCount = members.length;
73
+ } catch {
74
+ // ignore
75
+ }
76
+
77
+ return {
78
+ roomId: resolvedRoom,
79
+ name,
80
+ topic,
81
+ canonicalAlias,
82
+ altAliases: [], // Would need separate query
83
+ memberCount,
84
+ };
85
+ } finally {
86
+ if (stopOnDone) client.stop();
87
+ }
88
+ }
@@ -0,0 +1,77 @@
1
+ import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
+
3
+ import {
4
+ EventType,
5
+ type MatrixMessageSummary,
6
+ type MatrixRawEvent,
7
+ type RoomMessageEventContent,
8
+ type RoomPinnedEventsEventContent,
9
+ } from "./types.js";
10
+
11
+ export function summarizeMatrixRawEvent(event: MatrixRawEvent): MatrixMessageSummary {
12
+ const content = event.content as RoomMessageEventContent;
13
+ const relates = content["m.relates_to"];
14
+ let relType: string | undefined;
15
+ let eventId: string | undefined;
16
+ if (relates) {
17
+ if ("rel_type" in relates) {
18
+ relType = relates.rel_type;
19
+ eventId = relates.event_id;
20
+ } else if ("m.in_reply_to" in relates) {
21
+ eventId = relates["m.in_reply_to"]?.event_id;
22
+ }
23
+ }
24
+ const relatesTo =
25
+ relType || eventId
26
+ ? {
27
+ relType,
28
+ eventId,
29
+ }
30
+ : undefined;
31
+ return {
32
+ eventId: event.event_id,
33
+ sender: event.sender,
34
+ body: content.body,
35
+ msgtype: content.msgtype,
36
+ timestamp: event.origin_server_ts,
37
+ relatesTo,
38
+ };
39
+ }
40
+
41
+ export async function readPinnedEvents(
42
+ client: MatrixClient,
43
+ roomId: string,
44
+ ): Promise<string[]> {
45
+ try {
46
+ const content = (await client.getRoomStateEvent(
47
+ roomId,
48
+ EventType.RoomPinnedEvents,
49
+ "",
50
+ )) as RoomPinnedEventsEventContent;
51
+ const pinned = content.pinned;
52
+ return pinned.filter((id) => id.trim().length > 0);
53
+ } catch (err: unknown) {
54
+ const errObj = err as { statusCode?: number; body?: { errcode?: string } };
55
+ const httpStatus = errObj.statusCode;
56
+ const errcode = errObj.body?.errcode;
57
+ if (httpStatus === 404 || errcode === "M_NOT_FOUND") {
58
+ return [];
59
+ }
60
+ throw err;
61
+ }
62
+ }
63
+
64
+ export async function fetchEventSummary(
65
+ client: MatrixClient,
66
+ roomId: string,
67
+ eventId: string,
68
+ ): Promise<MatrixMessageSummary | null> {
69
+ try {
70
+ const raw = (await client.getEvent(roomId, eventId)) as MatrixRawEvent;
71
+ if (raw.unsigned?.redacted_because) return null;
72
+ return summarizeMatrixRawEvent(raw);
73
+ } catch {
74
+ // Event not found, redacted, or inaccessible - return null
75
+ return null;
76
+ }
77
+ }