@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.
- package/CHANGELOG.md +59 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +36 -0
- package/src/actions.ts +185 -0
- package/src/channel.directory.test.ts +56 -0
- package/src/channel.ts +417 -0
- package/src/config-schema.ts +62 -0
- package/src/directory-live.ts +175 -0
- package/src/group-mentions.ts +61 -0
- package/src/matrix/accounts.test.ts +83 -0
- package/src/matrix/accounts.ts +63 -0
- package/src/matrix/actions/client.ts +53 -0
- package/src/matrix/actions/messages.ts +120 -0
- package/src/matrix/actions/pins.ts +70 -0
- package/src/matrix/actions/reactions.ts +84 -0
- package/src/matrix/actions/room.ts +88 -0
- package/src/matrix/actions/summary.ts +77 -0
- package/src/matrix/actions/types.ts +84 -0
- package/src/matrix/actions.ts +15 -0
- package/src/matrix/active-client.ts +11 -0
- package/src/matrix/client/config.ts +165 -0
- package/src/matrix/client/create-client.ts +127 -0
- package/src/matrix/client/logging.ts +35 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.ts +169 -0
- package/src/matrix/client/storage.ts +131 -0
- package/src/matrix/client/types.ts +34 -0
- package/src/matrix/client.test.ts +57 -0
- package/src/matrix/client.ts +9 -0
- package/src/matrix/credentials.ts +103 -0
- package/src/matrix/deps.ts +57 -0
- package/src/matrix/format.test.ts +34 -0
- package/src/matrix/format.ts +22 -0
- package/src/matrix/index.ts +11 -0
- package/src/matrix/monitor/allowlist.ts +58 -0
- package/src/matrix/monitor/auto-join.ts +68 -0
- package/src/matrix/monitor/direct.ts +105 -0
- package/src/matrix/monitor/events.ts +103 -0
- package/src/matrix/monitor/handler.ts +645 -0
- package/src/matrix/monitor/index.ts +279 -0
- package/src/matrix/monitor/location.ts +83 -0
- package/src/matrix/monitor/media.test.ts +103 -0
- package/src/matrix/monitor/media.ts +113 -0
- package/src/matrix/monitor/mentions.ts +31 -0
- package/src/matrix/monitor/replies.ts +96 -0
- package/src/matrix/monitor/room-info.ts +58 -0
- package/src/matrix/monitor/rooms.ts +43 -0
- package/src/matrix/monitor/threads.ts +64 -0
- package/src/matrix/monitor/types.ts +39 -0
- package/src/matrix/poll-types.test.ts +22 -0
- package/src/matrix/poll-types.ts +157 -0
- package/src/matrix/probe.ts +70 -0
- package/src/matrix/send/client.ts +63 -0
- package/src/matrix/send/formatting.ts +92 -0
- package/src/matrix/send/media.ts +220 -0
- package/src/matrix/send/targets.test.ts +102 -0
- package/src/matrix/send/targets.ts +144 -0
- package/src/matrix/send/types.ts +109 -0
- package/src/matrix/send.test.ts +172 -0
- package/src/matrix/send.ts +255 -0
- package/src/onboarding.ts +432 -0
- package/src/outbound.ts +53 -0
- package/src/resolve-targets.ts +89 -0
- package/src/runtime.ts +14 -0
- package/src/tool-actions.ts +160 -0
- 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
|
+
}
|