@badgerclaw/connect 1.0.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 (70) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/SETUP.md +131 -0
  3. package/index.ts +23 -0
  4. package/openclaw.plugin.json +1 -0
  5. package/package.json +32 -0
  6. package/src/actions.ts +195 -0
  7. package/src/channel.ts +461 -0
  8. package/src/config-schema.ts +62 -0
  9. package/src/connect.ts +17 -0
  10. package/src/directory-live.ts +209 -0
  11. package/src/group-mentions.ts +52 -0
  12. package/src/matrix/accounts.ts +114 -0
  13. package/src/matrix/actions/client.ts +47 -0
  14. package/src/matrix/actions/limits.ts +6 -0
  15. package/src/matrix/actions/messages.ts +126 -0
  16. package/src/matrix/actions/pins.ts +84 -0
  17. package/src/matrix/actions/reactions.ts +102 -0
  18. package/src/matrix/actions/room.ts +85 -0
  19. package/src/matrix/actions/summary.ts +75 -0
  20. package/src/matrix/actions/types.ts +85 -0
  21. package/src/matrix/actions.ts +15 -0
  22. package/src/matrix/active-client.ts +32 -0
  23. package/src/matrix/client/config.ts +245 -0
  24. package/src/matrix/client/create-client.ts +125 -0
  25. package/src/matrix/client/logging.ts +46 -0
  26. package/src/matrix/client/runtime.ts +4 -0
  27. package/src/matrix/client/shared.ts +210 -0
  28. package/src/matrix/client/startup.ts +29 -0
  29. package/src/matrix/client/storage.ts +131 -0
  30. package/src/matrix/client/types.ts +34 -0
  31. package/src/matrix/client-bootstrap.ts +47 -0
  32. package/src/matrix/client.ts +14 -0
  33. package/src/matrix/credentials.ts +125 -0
  34. package/src/matrix/deps.ts +126 -0
  35. package/src/matrix/format.ts +22 -0
  36. package/src/matrix/index.ts +11 -0
  37. package/src/matrix/monitor/access-policy.ts +126 -0
  38. package/src/matrix/monitor/allowlist.ts +94 -0
  39. package/src/matrix/monitor/auto-join.ts +72 -0
  40. package/src/matrix/monitor/direct.ts +152 -0
  41. package/src/matrix/monitor/events.ts +168 -0
  42. package/src/matrix/monitor/handler.ts +768 -0
  43. package/src/matrix/monitor/inbound-body.ts +28 -0
  44. package/src/matrix/monitor/index.ts +414 -0
  45. package/src/matrix/monitor/location.ts +100 -0
  46. package/src/matrix/monitor/media.ts +118 -0
  47. package/src/matrix/monitor/mentions.ts +62 -0
  48. package/src/matrix/monitor/replies.ts +124 -0
  49. package/src/matrix/monitor/room-info.ts +55 -0
  50. package/src/matrix/monitor/rooms.ts +47 -0
  51. package/src/matrix/monitor/threads.ts +68 -0
  52. package/src/matrix/monitor/types.ts +39 -0
  53. package/src/matrix/poll-types.ts +167 -0
  54. package/src/matrix/probe.ts +69 -0
  55. package/src/matrix/sdk-runtime.ts +18 -0
  56. package/src/matrix/send/client.ts +99 -0
  57. package/src/matrix/send/formatting.ts +93 -0
  58. package/src/matrix/send/media.ts +230 -0
  59. package/src/matrix/send/targets.ts +150 -0
  60. package/src/matrix/send/types.ts +110 -0
  61. package/src/matrix/send-queue.ts +28 -0
  62. package/src/matrix/send.ts +267 -0
  63. package/src/onboarding.ts +331 -0
  64. package/src/outbound.ts +58 -0
  65. package/src/resolve-targets.ts +125 -0
  66. package/src/runtime.ts +6 -0
  67. package/src/secret-input.ts +13 -0
  68. package/src/test-mocks.ts +53 -0
  69. package/src/tool-actions.ts +164 -0
  70. package/src/types.ts +118 -0
@@ -0,0 +1,209 @@
1
+ import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/matrix";
2
+ import { resolveMatrixAuth } from "./matrix/client.js";
3
+
4
+ type MatrixUserResult = {
5
+ user_id?: string;
6
+ display_name?: string;
7
+ };
8
+
9
+ type MatrixUserDirectoryResponse = {
10
+ results?: MatrixUserResult[];
11
+ };
12
+
13
+ type MatrixJoinedRoomsResponse = {
14
+ joined_rooms?: string[];
15
+ };
16
+
17
+ type MatrixRoomNameState = {
18
+ name?: string;
19
+ };
20
+
21
+ type MatrixAliasLookup = {
22
+ room_id?: string;
23
+ };
24
+
25
+ type MatrixDirectoryLiveParams = {
26
+ cfg: unknown;
27
+ accountId?: string | null;
28
+ query?: string | null;
29
+ limit?: number | null;
30
+ };
31
+
32
+ type MatrixResolvedAuth = Awaited<ReturnType<typeof resolveMatrixAuth>>;
33
+
34
+ async function fetchMatrixJson<T>(params: {
35
+ homeserver: string;
36
+ path: string;
37
+ accessToken: string;
38
+ method?: "GET" | "POST";
39
+ body?: unknown;
40
+ }): Promise<T> {
41
+ const res = await fetch(`${params.homeserver}${params.path}`, {
42
+ method: params.method ?? "GET",
43
+ headers: {
44
+ Authorization: `Bearer ${params.accessToken}`,
45
+ "Content-Type": "application/json",
46
+ },
47
+ body: params.body ? JSON.stringify(params.body) : undefined,
48
+ });
49
+ if (!res.ok) {
50
+ const text = await res.text().catch(() => "");
51
+ throw new Error(`BadgerClaw API ${params.path} failed (${res.status}): ${text || "unknown error"}`);
52
+ }
53
+ return (await res.json()) as T;
54
+ }
55
+
56
+ function normalizeQuery(value?: string | null): string {
57
+ return value?.trim().toLowerCase() ?? "";
58
+ }
59
+
60
+ function resolveMatrixDirectoryLimit(limit?: number | null): number {
61
+ return typeof limit === "number" && limit > 0 ? limit : 20;
62
+ }
63
+
64
+ async function resolveMatrixDirectoryContext(
65
+ params: MatrixDirectoryLiveParams,
66
+ ): Promise<{ query: string; auth: MatrixResolvedAuth } | null> {
67
+ const query = normalizeQuery(params.query);
68
+ if (!query) {
69
+ return null;
70
+ }
71
+ const auth = await resolveMatrixAuth({ cfg: params.cfg as never, accountId: params.accountId });
72
+ return { query, auth };
73
+ }
74
+
75
+ function createGroupDirectoryEntry(params: {
76
+ id: string;
77
+ name: string;
78
+ handle?: string;
79
+ }): ChannelDirectoryEntry {
80
+ return {
81
+ kind: "group",
82
+ id: params.id,
83
+ name: params.name,
84
+ handle: params.handle,
85
+ } satisfies ChannelDirectoryEntry;
86
+ }
87
+
88
+ export async function listMatrixDirectoryPeersLive(
89
+ params: MatrixDirectoryLiveParams,
90
+ ): Promise<ChannelDirectoryEntry[]> {
91
+ const context = await resolveMatrixDirectoryContext(params);
92
+ if (!context) {
93
+ return [];
94
+ }
95
+ const { query, auth } = context;
96
+ const res = await fetchMatrixJson<MatrixUserDirectoryResponse>({
97
+ homeserver: auth.homeserver,
98
+ accessToken: auth.accessToken,
99
+ path: "/_matrix/client/v3/user_directory/search",
100
+ method: "POST",
101
+ body: {
102
+ search_term: query,
103
+ limit: resolveMatrixDirectoryLimit(params.limit),
104
+ },
105
+ });
106
+ const results = res.results ?? [];
107
+ return results
108
+ .map((entry) => {
109
+ const userId = entry.user_id?.trim();
110
+ if (!userId) {
111
+ return null;
112
+ }
113
+ return {
114
+ kind: "user",
115
+ id: userId,
116
+ name: entry.display_name?.trim() || undefined,
117
+ handle: entry.display_name ? `@${entry.display_name.trim()}` : undefined,
118
+ raw: entry,
119
+ } satisfies ChannelDirectoryEntry;
120
+ })
121
+ .filter(Boolean) as ChannelDirectoryEntry[];
122
+ }
123
+
124
+ async function resolveMatrixRoomAlias(
125
+ homeserver: string,
126
+ accessToken: string,
127
+ alias: string,
128
+ ): Promise<string | null> {
129
+ try {
130
+ const res = await fetchMatrixJson<MatrixAliasLookup>({
131
+ homeserver,
132
+ accessToken,
133
+ path: `/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`,
134
+ });
135
+ return res.room_id?.trim() || null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ async function fetchMatrixRoomName(
142
+ homeserver: string,
143
+ accessToken: string,
144
+ roomId: string,
145
+ ): Promise<string | null> {
146
+ try {
147
+ const res = await fetchMatrixJson<MatrixRoomNameState>({
148
+ homeserver,
149
+ accessToken,
150
+ path: `/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.name`,
151
+ });
152
+ return res.name?.trim() || null;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ export async function listMatrixDirectoryGroupsLive(
159
+ params: MatrixDirectoryLiveParams,
160
+ ): Promise<ChannelDirectoryEntry[]> {
161
+ const context = await resolveMatrixDirectoryContext(params);
162
+ if (!context) {
163
+ return [];
164
+ }
165
+ const { query, auth } = context;
166
+ const limit = resolveMatrixDirectoryLimit(params.limit);
167
+
168
+ if (query.startsWith("#")) {
169
+ const roomId = await resolveMatrixRoomAlias(auth.homeserver, auth.accessToken, query);
170
+ if (!roomId) {
171
+ return [];
172
+ }
173
+ return [createGroupDirectoryEntry({ id: roomId, name: query, handle: query })];
174
+ }
175
+
176
+ if (query.startsWith("!")) {
177
+ const originalId = params.query?.trim() ?? query;
178
+ return [createGroupDirectoryEntry({ id: originalId, name: originalId })];
179
+ }
180
+
181
+ const joined = await fetchMatrixJson<MatrixJoinedRoomsResponse>({
182
+ homeserver: auth.homeserver,
183
+ accessToken: auth.accessToken,
184
+ path: "/_matrix/client/v3/joined_rooms",
185
+ });
186
+ const rooms = joined.joined_rooms ?? [];
187
+ const results: ChannelDirectoryEntry[] = [];
188
+
189
+ for (const roomId of rooms) {
190
+ const name = await fetchMatrixRoomName(auth.homeserver, auth.accessToken, roomId);
191
+ if (!name) {
192
+ continue;
193
+ }
194
+ if (!name.toLowerCase().includes(query)) {
195
+ continue;
196
+ }
197
+ results.push({
198
+ kind: "group",
199
+ id: roomId,
200
+ name,
201
+ handle: `#${name}`,
202
+ });
203
+ if (results.length >= limit) {
204
+ break;
205
+ }
206
+ }
207
+
208
+ return results;
209
+ }
@@ -0,0 +1,52 @@
1
+ import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk/matrix";
2
+ import { resolveMatrixAccountConfig } from "./matrix/accounts.js";
3
+ import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
4
+ import type { CoreConfig } from "./types.js";
5
+
6
+ function stripLeadingPrefixCaseInsensitive(value: string, prefix: string): string {
7
+ return value.toLowerCase().startsWith(prefix.toLowerCase())
8
+ ? value.slice(prefix.length).trim()
9
+ : value;
10
+ }
11
+
12
+ function resolveMatrixRoomConfigForGroup(params: ChannelGroupContext) {
13
+ const rawGroupId = params.groupId?.trim() ?? "";
14
+ let roomId = rawGroupId;
15
+ roomId = stripLeadingPrefixCaseInsensitive(roomId, "badgerclaw:");
16
+ roomId = stripLeadingPrefixCaseInsensitive(roomId, "channel:");
17
+ roomId = stripLeadingPrefixCaseInsensitive(roomId, "room:");
18
+
19
+ const groupChannel = params.groupChannel?.trim() ?? "";
20
+ const aliases = groupChannel ? [groupChannel] : [];
21
+ const cfg = params.cfg as CoreConfig;
22
+ const matrixConfig = resolveMatrixAccountConfig({ cfg, accountId: params.accountId });
23
+ return resolveMatrixRoomConfig({
24
+ rooms: matrixConfig.groups ?? matrixConfig.rooms,
25
+ roomId,
26
+ aliases,
27
+ name: groupChannel || undefined,
28
+ }).config;
29
+ }
30
+
31
+ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): boolean {
32
+ const resolved = resolveMatrixRoomConfigForGroup(params);
33
+ if (resolved) {
34
+ if (resolved.autoReply === true) {
35
+ return false;
36
+ }
37
+ if (resolved.autoReply === false) {
38
+ return true;
39
+ }
40
+ if (typeof resolved.requireMention === "boolean") {
41
+ return resolved.requireMention;
42
+ }
43
+ }
44
+ return true;
45
+ }
46
+
47
+ export function resolveMatrixGroupToolPolicy(
48
+ params: ChannelGroupContext,
49
+ ): GroupToolPolicyConfig | undefined {
50
+ const resolved = resolveMatrixRoomConfigForGroup(params);
51
+ return resolved?.tools;
52
+ }
@@ -0,0 +1,114 @@
1
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import { createAccountListHelpers } from "openclaw/plugin-sdk/matrix";
3
+ import { hasConfiguredSecretInput } from "../secret-input.js";
4
+ import type { CoreConfig, MatrixConfig } from "../types.js";
5
+ import { resolveMatrixConfigForAccount } from "./client.js";
6
+ import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
7
+
8
+ /** Merge account config with top-level defaults, preserving nested objects. */
9
+ function mergeAccountConfig(base: MatrixConfig, account: MatrixConfig): MatrixConfig {
10
+ const merged = { ...base, ...account };
11
+ // Deep-merge known nested objects so partial overrides inherit base fields
12
+ for (const key of ["dm", "actions"] as const) {
13
+ const b = base[key];
14
+ const o = account[key];
15
+ if (typeof b === "object" && b != null && typeof o === "object" && o != null) {
16
+ (merged as Record<string, unknown>)[key] = { ...b, ...o };
17
+ }
18
+ }
19
+ // Don't propagate the accounts map into the merged per-account config
20
+ delete (merged as Record<string, unknown>).accounts;
21
+ delete (merged as Record<string, unknown>).defaultAccount;
22
+ return merged;
23
+ }
24
+
25
+ export type ResolvedMatrixAccount = {
26
+ accountId: string;
27
+ enabled: boolean;
28
+ name?: string;
29
+ configured: boolean;
30
+ homeserver?: string;
31
+ userId?: string;
32
+ config: MatrixConfig;
33
+ };
34
+
35
+ const {
36
+ listAccountIds: listMatrixAccountIds,
37
+ resolveDefaultAccountId: resolveDefaultMatrixAccountId,
38
+ } = createAccountListHelpers("badgerclaw", { normalizeAccountId });
39
+ export { listMatrixAccountIds, resolveDefaultMatrixAccountId };
40
+
41
+ function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined {
42
+ const accounts = cfg.channels?.badgerclaw?.accounts;
43
+ if (!accounts || typeof accounts !== "object") {
44
+ return undefined;
45
+ }
46
+ // Direct lookup first (fast path for already-normalized keys)
47
+ if (accounts[accountId]) {
48
+ return accounts[accountId] as MatrixConfig;
49
+ }
50
+ // Fall back to case-insensitive match (user may have mixed-case keys in config)
51
+ const normalized = normalizeAccountId(accountId);
52
+ for (const key of Object.keys(accounts)) {
53
+ if (normalizeAccountId(key) === normalized) {
54
+ return accounts[key] as MatrixConfig;
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ export function resolveMatrixAccount(params: {
61
+ cfg: CoreConfig;
62
+ accountId?: string | null;
63
+ }): ResolvedMatrixAccount {
64
+ const accountId = normalizeAccountId(params.accountId);
65
+ const matrixBase = params.cfg.channels?.badgerclaw ?? {};
66
+ const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId });
67
+ const enabled = base.enabled !== false && matrixBase.enabled !== false;
68
+
69
+ const resolved = resolveMatrixConfigForAccount(params.cfg, accountId, process.env);
70
+ const hasHomeserver = Boolean(resolved.homeserver);
71
+ const hasUserId = Boolean(resolved.userId);
72
+ const hasAccessToken = Boolean(resolved.accessToken);
73
+ const hasPassword = Boolean(resolved.password);
74
+ const hasPasswordAuth = hasUserId && (hasPassword || hasConfiguredSecretInput(base.password));
75
+ const stored = loadMatrixCredentials(process.env, accountId);
76
+ const hasStored =
77
+ stored && resolved.homeserver
78
+ ? credentialsMatchConfig(stored, {
79
+ homeserver: resolved.homeserver,
80
+ userId: resolved.userId || "",
81
+ })
82
+ : false;
83
+ const configured = hasHomeserver && (hasAccessToken || hasPasswordAuth || Boolean(hasStored));
84
+ return {
85
+ accountId,
86
+ enabled,
87
+ name: base.name?.trim() || undefined,
88
+ configured,
89
+ homeserver: resolved.homeserver || undefined,
90
+ userId: resolved.userId || undefined,
91
+ config: base,
92
+ };
93
+ }
94
+
95
+ export function resolveMatrixAccountConfig(params: {
96
+ cfg: CoreConfig;
97
+ accountId?: string | null;
98
+ }): MatrixConfig {
99
+ const accountId = normalizeAccountId(params.accountId);
100
+ const matrixBase = params.cfg.channels?.badgerclaw ?? {};
101
+ const accountConfig = resolveAccountConfig(params.cfg, accountId);
102
+ if (!accountConfig) {
103
+ return matrixBase;
104
+ }
105
+ // Merge account-specific config with top-level defaults so settings like
106
+ // groupPolicy and blockStreaming inherit when not overridden.
107
+ return mergeAccountConfig(matrixBase, accountConfig);
108
+ }
109
+
110
+ export function listEnabledMatrixAccounts(cfg: CoreConfig): ResolvedMatrixAccount[] {
111
+ return listMatrixAccountIds(cfg)
112
+ .map((accountId) => resolveMatrixAccount({ cfg, accountId }))
113
+ .filter((account) => account.enabled);
114
+ }
@@ -0,0 +1,47 @@
1
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import { getMatrixRuntime } from "../../runtime.js";
3
+ import type { CoreConfig } from "../../types.js";
4
+ import { getActiveMatrixClient } from "../active-client.js";
5
+ import { createPreparedMatrixClient } from "../client-bootstrap.js";
6
+ import { isBunRuntime, resolveMatrixAuth, resolveSharedMatrixClient } from "../client.js";
7
+ import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js";
8
+
9
+ export function ensureNodeRuntime() {
10
+ if (isBunRuntime()) {
11
+ throw new Error("BadgerClaw support requires Node (bun runtime not supported)");
12
+ }
13
+ }
14
+
15
+ export async function resolveActionClient(
16
+ opts: MatrixActionClientOpts = {},
17
+ ): Promise<MatrixActionClient> {
18
+ ensureNodeRuntime();
19
+ if (opts.client) {
20
+ return { client: opts.client, stopOnDone: false };
21
+ }
22
+ // Normalize accountId early to ensure consistent keying across all lookups
23
+ const accountId = normalizeAccountId(opts.accountId);
24
+ const active = getActiveMatrixClient(accountId);
25
+ if (active) {
26
+ return { client: active, stopOnDone: false };
27
+ }
28
+ const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT);
29
+ if (shouldShareClient) {
30
+ const client = await resolveSharedMatrixClient({
31
+ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
32
+ timeoutMs: opts.timeoutMs,
33
+ accountId,
34
+ });
35
+ return { client, stopOnDone: false };
36
+ }
37
+ const auth = await resolveMatrixAuth({
38
+ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig,
39
+ accountId,
40
+ });
41
+ const client = await createPreparedMatrixClient({
42
+ auth,
43
+ timeoutMs: opts.timeoutMs,
44
+ accountId,
45
+ });
46
+ return { client, stopOnDone: true };
47
+ }
@@ -0,0 +1,6 @@
1
+ export function resolveMatrixActionLimit(raw: unknown, fallback: number): number {
2
+ if (typeof raw !== "number" || !Number.isFinite(raw)) {
3
+ return fallback;
4
+ }
5
+ return Math.max(1, Math.floor(raw));
6
+ }
@@ -0,0 +1,126 @@
1
+ import { resolveMatrixRoomId, sendMessageMatrix } from "../send.js";
2
+ import { resolveActionClient } from "./client.js";
3
+ import { resolveMatrixActionLimit } from "./limits.js";
4
+ import { summarizeMatrixRawEvent } from "./summary.js";
5
+ import {
6
+ EventType,
7
+ MsgType,
8
+ RelationType,
9
+ type MatrixActionClientOpts,
10
+ type MatrixMessageSummary,
11
+ type MatrixRawEvent,
12
+ type RoomMessageEventContent,
13
+ } from "./types.js";
14
+
15
+ export async function sendMatrixMessage(
16
+ to: string,
17
+ content: string,
18
+ opts: MatrixActionClientOpts & {
19
+ mediaUrl?: string;
20
+ replyToId?: string;
21
+ threadId?: string;
22
+ } = {},
23
+ ) {
24
+ return await sendMessageMatrix(to, content, {
25
+ mediaUrl: opts.mediaUrl,
26
+ replyToId: opts.replyToId,
27
+ threadId: opts.threadId,
28
+ client: opts.client,
29
+ timeoutMs: opts.timeoutMs,
30
+ });
31
+ }
32
+
33
+ export async function editMatrixMessage(
34
+ roomId: string,
35
+ messageId: string,
36
+ content: string,
37
+ opts: MatrixActionClientOpts = {},
38
+ ) {
39
+ const trimmed = content.trim();
40
+ if (!trimmed) {
41
+ throw new Error("BadgerClaw edit requires content");
42
+ }
43
+ const { client, stopOnDone } = await resolveActionClient(opts);
44
+ try {
45
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
46
+ const newContent = {
47
+ msgtype: MsgType.Text,
48
+ body: trimmed,
49
+ } satisfies RoomMessageEventContent;
50
+ const payload: RoomMessageEventContent = {
51
+ msgtype: MsgType.Text,
52
+ body: `* ${trimmed}`,
53
+ "m.new_content": newContent,
54
+ "m.relates_to": {
55
+ rel_type: RelationType.Replace,
56
+ event_id: messageId,
57
+ },
58
+ };
59
+ const eventId = await client.sendMessage(resolvedRoom, payload);
60
+ return { eventId: eventId ?? null };
61
+ } finally {
62
+ if (stopOnDone) {
63
+ client.stop();
64
+ }
65
+ }
66
+ }
67
+
68
+ export async function deleteMatrixMessage(
69
+ roomId: string,
70
+ messageId: string,
71
+ opts: MatrixActionClientOpts & { reason?: string } = {},
72
+ ) {
73
+ const { client, stopOnDone } = await resolveActionClient(opts);
74
+ try {
75
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
76
+ await client.redactEvent(resolvedRoom, messageId, opts.reason);
77
+ } finally {
78
+ if (stopOnDone) {
79
+ client.stop();
80
+ }
81
+ }
82
+ }
83
+
84
+ export async function readMatrixMessages(
85
+ roomId: string,
86
+ opts: MatrixActionClientOpts & {
87
+ limit?: number;
88
+ before?: string;
89
+ after?: string;
90
+ } = {},
91
+ ): Promise<{
92
+ messages: MatrixMessageSummary[];
93
+ nextBatch?: string | null;
94
+ prevBatch?: string | null;
95
+ }> {
96
+ const { client, stopOnDone } = await resolveActionClient(opts);
97
+ try {
98
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
99
+ const limit = resolveMatrixActionLimit(opts.limit, 20);
100
+ const token = opts.before?.trim() || opts.after?.trim() || undefined;
101
+ const dir = opts.after ? "f" : "b";
102
+ // @vector-im/matrix-bot-sdk uses doRequest for room messages
103
+ const res = (await client.doRequest(
104
+ "GET",
105
+ `/_matrix/client/v3/rooms/${encodeURIComponent(resolvedRoom)}/messages`,
106
+ {
107
+ dir,
108
+ limit,
109
+ from: token,
110
+ },
111
+ )) as { chunk: MatrixRawEvent[]; start?: string; end?: string };
112
+ const messages = res.chunk
113
+ .filter((event) => event.type === EventType.RoomMessage)
114
+ .filter((event) => !event.unsigned?.redacted_because)
115
+ .map(summarizeMatrixRawEvent);
116
+ return {
117
+ messages,
118
+ nextBatch: res.end ?? null,
119
+ prevBatch: res.start ?? null,
120
+ };
121
+ } finally {
122
+ if (stopOnDone) {
123
+ client.stop();
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,84 @@
1
+ import { resolveMatrixRoomId } from "../send.js";
2
+ import { resolveActionClient } from "./client.js";
3
+ import { fetchEventSummary, readPinnedEvents } from "./summary.js";
4
+ import {
5
+ EventType,
6
+ type MatrixActionClientOpts,
7
+ type MatrixActionClient,
8
+ type MatrixMessageSummary,
9
+ type RoomPinnedEventsEventContent,
10
+ } from "./types.js";
11
+
12
+ type ActionClient = MatrixActionClient["client"];
13
+
14
+ async function withResolvedPinRoom<T>(
15
+ roomId: string,
16
+ opts: MatrixActionClientOpts,
17
+ run: (client: ActionClient, resolvedRoom: string) => Promise<T>,
18
+ ): Promise<T> {
19
+ const { client, stopOnDone } = await resolveActionClient(opts);
20
+ try {
21
+ const resolvedRoom = await resolveMatrixRoomId(client, roomId);
22
+ return await run(client, resolvedRoom);
23
+ } finally {
24
+ if (stopOnDone) {
25
+ client.stop();
26
+ }
27
+ }
28
+ }
29
+
30
+ async function updateMatrixPins(
31
+ roomId: string,
32
+ messageId: string,
33
+ opts: MatrixActionClientOpts,
34
+ update: (current: string[]) => string[],
35
+ ): Promise<{ pinned: string[] }> {
36
+ return await withResolvedPinRoom(roomId, opts, async (client, resolvedRoom) => {
37
+ const current = await readPinnedEvents(client, resolvedRoom);
38
+ const next = update(current);
39
+ const payload: RoomPinnedEventsEventContent = { pinned: next };
40
+ await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload);
41
+ return { pinned: next };
42
+ });
43
+ }
44
+
45
+ export async function pinMatrixMessage(
46
+ roomId: string,
47
+ messageId: string,
48
+ opts: MatrixActionClientOpts = {},
49
+ ): Promise<{ pinned: string[] }> {
50
+ return await updateMatrixPins(roomId, messageId, opts, (current) =>
51
+ current.includes(messageId) ? current : [...current, messageId],
52
+ );
53
+ }
54
+
55
+ export async function unpinMatrixMessage(
56
+ roomId: string,
57
+ messageId: string,
58
+ opts: MatrixActionClientOpts = {},
59
+ ): Promise<{ pinned: string[] }> {
60
+ return await updateMatrixPins(roomId, messageId, opts, (current) =>
61
+ current.filter((id) => id !== messageId),
62
+ );
63
+ }
64
+
65
+ export async function listMatrixPins(
66
+ roomId: string,
67
+ opts: MatrixActionClientOpts = {},
68
+ ): Promise<{ pinned: string[]; events: MatrixMessageSummary[] }> {
69
+ return await withResolvedPinRoom(roomId, opts, async (client, resolvedRoom) => {
70
+ const pinned = await readPinnedEvents(client, resolvedRoom);
71
+ const events = (
72
+ await Promise.all(
73
+ pinned.map(async (eventId) => {
74
+ try {
75
+ return await fetchEventSummary(client, resolvedRoom, eventId);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }),
80
+ )
81
+ ).filter((event): event is MatrixMessageSummary => Boolean(event));
82
+ return { pinned, events };
83
+ });
84
+ }