@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,131 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
7
|
+
import type { MatrixStoragePaths } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_ACCOUNT_KEY = "default";
|
|
10
|
+
const STORAGE_META_FILENAME = "storage-meta.json";
|
|
11
|
+
|
|
12
|
+
function sanitizePathSegment(value: string): string {
|
|
13
|
+
const cleaned = value
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9._-]+/g, "_")
|
|
17
|
+
.replace(/^_+|_+$/g, "");
|
|
18
|
+
return cleaned || "unknown";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveHomeserverKey(homeserver: string): string {
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(homeserver);
|
|
24
|
+
if (url.host) return sanitizePathSegment(url.host);
|
|
25
|
+
} catch {
|
|
26
|
+
// fall through
|
|
27
|
+
}
|
|
28
|
+
return sanitizePathSegment(homeserver);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hashAccessToken(accessToken: string): string {
|
|
32
|
+
return crypto.createHash("sha256").update(accessToken).digest("hex").slice(0, 16);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): {
|
|
36
|
+
storagePath: string;
|
|
37
|
+
cryptoPath: string;
|
|
38
|
+
} {
|
|
39
|
+
const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir);
|
|
40
|
+
return {
|
|
41
|
+
storagePath: path.join(stateDir, "matrix", "bot-storage.json"),
|
|
42
|
+
cryptoPath: path.join(stateDir, "matrix", "crypto"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveMatrixStoragePaths(params: {
|
|
47
|
+
homeserver: string;
|
|
48
|
+
userId: string;
|
|
49
|
+
accessToken: string;
|
|
50
|
+
accountId?: string | null;
|
|
51
|
+
env?: NodeJS.ProcessEnv;
|
|
52
|
+
}): MatrixStoragePaths {
|
|
53
|
+
const env = params.env ?? process.env;
|
|
54
|
+
const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir);
|
|
55
|
+
const accountKey = sanitizePathSegment(params.accountId ?? DEFAULT_ACCOUNT_KEY);
|
|
56
|
+
const userKey = sanitizePathSegment(params.userId);
|
|
57
|
+
const serverKey = resolveHomeserverKey(params.homeserver);
|
|
58
|
+
const tokenHash = hashAccessToken(params.accessToken);
|
|
59
|
+
const rootDir = path.join(
|
|
60
|
+
stateDir,
|
|
61
|
+
"matrix",
|
|
62
|
+
"accounts",
|
|
63
|
+
accountKey,
|
|
64
|
+
`${serverKey}__${userKey}`,
|
|
65
|
+
tokenHash,
|
|
66
|
+
);
|
|
67
|
+
return {
|
|
68
|
+
rootDir,
|
|
69
|
+
storagePath: path.join(rootDir, "bot-storage.json"),
|
|
70
|
+
cryptoPath: path.join(rootDir, "crypto"),
|
|
71
|
+
metaPath: path.join(rootDir, STORAGE_META_FILENAME),
|
|
72
|
+
accountKey,
|
|
73
|
+
tokenHash,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function maybeMigrateLegacyStorage(params: {
|
|
78
|
+
storagePaths: MatrixStoragePaths;
|
|
79
|
+
env?: NodeJS.ProcessEnv;
|
|
80
|
+
}): void {
|
|
81
|
+
const legacy = resolveLegacyStoragePaths(params.env);
|
|
82
|
+
const hasLegacyStorage = fs.existsSync(legacy.storagePath);
|
|
83
|
+
const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath);
|
|
84
|
+
const hasNewStorage =
|
|
85
|
+
fs.existsSync(params.storagePaths.storagePath) ||
|
|
86
|
+
fs.existsSync(params.storagePaths.cryptoPath);
|
|
87
|
+
|
|
88
|
+
if (!hasLegacyStorage && !hasLegacyCrypto) return;
|
|
89
|
+
if (hasNewStorage) return;
|
|
90
|
+
|
|
91
|
+
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
|
|
92
|
+
if (hasLegacyStorage) {
|
|
93
|
+
try {
|
|
94
|
+
fs.renameSync(legacy.storagePath, params.storagePaths.storagePath);
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore migration failures; new store will be created.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (hasLegacyCrypto) {
|
|
100
|
+
try {
|
|
101
|
+
fs.renameSync(legacy.cryptoPath, params.storagePaths.cryptoPath);
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore migration failures; new store will be created.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function writeStorageMeta(params: {
|
|
109
|
+
storagePaths: MatrixStoragePaths;
|
|
110
|
+
homeserver: string;
|
|
111
|
+
userId: string;
|
|
112
|
+
accountId?: string | null;
|
|
113
|
+
}): void {
|
|
114
|
+
try {
|
|
115
|
+
const payload = {
|
|
116
|
+
homeserver: params.homeserver,
|
|
117
|
+
userId: params.userId,
|
|
118
|
+
accountId: params.accountId ?? DEFAULT_ACCOUNT_KEY,
|
|
119
|
+
accessTokenHash: params.storagePaths.tokenHash,
|
|
120
|
+
createdAt: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
|
|
123
|
+
fs.writeFileSync(
|
|
124
|
+
params.storagePaths.metaPath,
|
|
125
|
+
JSON.stringify(payload, null, 2),
|
|
126
|
+
"utf-8",
|
|
127
|
+
);
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore meta write failures
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type MatrixResolvedConfig = {
|
|
2
|
+
homeserver: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
accessToken?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
deviceName?: string;
|
|
7
|
+
initialSyncLimit?: number;
|
|
8
|
+
encryption?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Authenticated Matrix configuration.
|
|
13
|
+
* Note: deviceId is NOT included here because it's implicit in the accessToken.
|
|
14
|
+
* The crypto storage assumes the device ID (and thus access token) does not change
|
|
15
|
+
* between restarts. If the access token becomes invalid or crypto storage is lost,
|
|
16
|
+
* both will need to be recreated together.
|
|
17
|
+
*/
|
|
18
|
+
export type MatrixAuth = {
|
|
19
|
+
homeserver: string;
|
|
20
|
+
userId: string;
|
|
21
|
+
accessToken: string;
|
|
22
|
+
deviceName?: string;
|
|
23
|
+
initialSyncLimit?: number;
|
|
24
|
+
encryption?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MatrixStoragePaths = {
|
|
28
|
+
rootDir: string;
|
|
29
|
+
storagePath: string;
|
|
30
|
+
cryptoPath: string;
|
|
31
|
+
metaPath: string;
|
|
32
|
+
accountKey: string;
|
|
33
|
+
tokenHash: string;
|
|
34
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { CoreConfig } from "../types.js";
|
|
4
|
+
import { resolveMatrixConfig } from "./client.js";
|
|
5
|
+
|
|
6
|
+
describe("resolveMatrixConfig", () => {
|
|
7
|
+
it("prefers config over env", () => {
|
|
8
|
+
const cfg = {
|
|
9
|
+
channels: {
|
|
10
|
+
matrix: {
|
|
11
|
+
homeserver: "https://cfg.example.org",
|
|
12
|
+
userId: "@cfg:example.org",
|
|
13
|
+
accessToken: "cfg-token",
|
|
14
|
+
password: "cfg-pass",
|
|
15
|
+
deviceName: "CfgDevice",
|
|
16
|
+
initialSyncLimit: 5,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} as CoreConfig;
|
|
20
|
+
const env = {
|
|
21
|
+
MATRIX_HOMESERVER: "https://env.example.org",
|
|
22
|
+
MATRIX_USER_ID: "@env:example.org",
|
|
23
|
+
MATRIX_ACCESS_TOKEN: "env-token",
|
|
24
|
+
MATRIX_PASSWORD: "env-pass",
|
|
25
|
+
MATRIX_DEVICE_NAME: "EnvDevice",
|
|
26
|
+
} as NodeJS.ProcessEnv;
|
|
27
|
+
const resolved = resolveMatrixConfig(cfg, env);
|
|
28
|
+
expect(resolved).toEqual({
|
|
29
|
+
homeserver: "https://cfg.example.org",
|
|
30
|
+
userId: "@cfg:example.org",
|
|
31
|
+
accessToken: "cfg-token",
|
|
32
|
+
password: "cfg-pass",
|
|
33
|
+
deviceName: "CfgDevice",
|
|
34
|
+
initialSyncLimit: 5,
|
|
35
|
+
encryption: false,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("uses env when config is missing", () => {
|
|
40
|
+
const cfg = {} as CoreConfig;
|
|
41
|
+
const env = {
|
|
42
|
+
MATRIX_HOMESERVER: "https://env.example.org",
|
|
43
|
+
MATRIX_USER_ID: "@env:example.org",
|
|
44
|
+
MATRIX_ACCESS_TOKEN: "env-token",
|
|
45
|
+
MATRIX_PASSWORD: "env-pass",
|
|
46
|
+
MATRIX_DEVICE_NAME: "EnvDevice",
|
|
47
|
+
} as NodeJS.ProcessEnv;
|
|
48
|
+
const resolved = resolveMatrixConfig(cfg, env);
|
|
49
|
+
expect(resolved.homeserver).toBe("https://env.example.org");
|
|
50
|
+
expect(resolved.userId).toBe("@env:example.org");
|
|
51
|
+
expect(resolved.accessToken).toBe("env-token");
|
|
52
|
+
expect(resolved.password).toBe("env-pass");
|
|
53
|
+
expect(resolved.deviceName).toBe("EnvDevice");
|
|
54
|
+
expect(resolved.initialSyncLimit).toBeUndefined();
|
|
55
|
+
expect(resolved.encryption).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { MatrixAuth, MatrixResolvedConfig } from "./client/types.js";
|
|
2
|
+
export { isBunRuntime } from "./client/runtime.js";
|
|
3
|
+
export { resolveMatrixConfig, resolveMatrixAuth } from "./client/config.js";
|
|
4
|
+
export { createMatrixClient } from "./client/create-client.js";
|
|
5
|
+
export {
|
|
6
|
+
resolveSharedMatrixClient,
|
|
7
|
+
waitForMatrixSync,
|
|
8
|
+
stopSharedClient,
|
|
9
|
+
} from "./client/shared.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { getMatrixRuntime } from "../runtime.js";
|
|
6
|
+
|
|
7
|
+
export type MatrixStoredCredentials = {
|
|
8
|
+
homeserver: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
accessToken: string;
|
|
11
|
+
deviceId?: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
lastUsedAt?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const CREDENTIALS_FILENAME = "credentials.json";
|
|
17
|
+
|
|
18
|
+
export function resolveMatrixCredentialsDir(
|
|
19
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
20
|
+
stateDir?: string,
|
|
21
|
+
): string {
|
|
22
|
+
const resolvedStateDir =
|
|
23
|
+
stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
|
|
24
|
+
return path.join(resolvedStateDir, "credentials", "matrix");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveMatrixCredentialsPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
28
|
+
const dir = resolveMatrixCredentialsDir(env);
|
|
29
|
+
return path.join(dir, CREDENTIALS_FILENAME);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadMatrixCredentials(
|
|
33
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
34
|
+
): MatrixStoredCredentials | null {
|
|
35
|
+
const credPath = resolveMatrixCredentialsPath(env);
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(credPath)) return null;
|
|
38
|
+
const raw = fs.readFileSync(credPath, "utf-8");
|
|
39
|
+
const parsed = JSON.parse(raw) as Partial<MatrixStoredCredentials>;
|
|
40
|
+
if (
|
|
41
|
+
typeof parsed.homeserver !== "string" ||
|
|
42
|
+
typeof parsed.userId !== "string" ||
|
|
43
|
+
typeof parsed.accessToken !== "string"
|
|
44
|
+
) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return parsed as MatrixStoredCredentials;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function saveMatrixCredentials(
|
|
54
|
+
credentials: Omit<MatrixStoredCredentials, "createdAt" | "lastUsedAt">,
|
|
55
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
56
|
+
): void {
|
|
57
|
+
const dir = resolveMatrixCredentialsDir(env);
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
const credPath = resolveMatrixCredentialsPath(env);
|
|
61
|
+
|
|
62
|
+
const existing = loadMatrixCredentials(env);
|
|
63
|
+
const now = new Date().toISOString();
|
|
64
|
+
|
|
65
|
+
const toSave: MatrixStoredCredentials = {
|
|
66
|
+
...credentials,
|
|
67
|
+
createdAt: existing?.createdAt ?? now,
|
|
68
|
+
lastUsedAt: now,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
fs.writeFileSync(credPath, JSON.stringify(toSave, null, 2), "utf-8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function touchMatrixCredentials(env: NodeJS.ProcessEnv = process.env): void {
|
|
75
|
+
const existing = loadMatrixCredentials(env);
|
|
76
|
+
if (!existing) return;
|
|
77
|
+
|
|
78
|
+
existing.lastUsedAt = new Date().toISOString();
|
|
79
|
+
const credPath = resolveMatrixCredentialsPath(env);
|
|
80
|
+
fs.writeFileSync(credPath, JSON.stringify(existing, null, 2), "utf-8");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function clearMatrixCredentials(env: NodeJS.ProcessEnv = process.env): void {
|
|
84
|
+
const credPath = resolveMatrixCredentialsPath(env);
|
|
85
|
+
try {
|
|
86
|
+
if (fs.existsSync(credPath)) {
|
|
87
|
+
fs.unlinkSync(credPath);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function credentialsMatchConfig(
|
|
95
|
+
stored: MatrixStoredCredentials,
|
|
96
|
+
config: { homeserver: string; userId: string },
|
|
97
|
+
): boolean {
|
|
98
|
+
// If userId is empty (token-based auth), only match homeserver
|
|
99
|
+
if (!config.userId) {
|
|
100
|
+
return stored.homeserver === config.homeserver;
|
|
101
|
+
}
|
|
102
|
+
return stored.homeserver === config.homeserver && stored.userId === config.userId;
|
|
103
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
7
|
+
import { getMatrixRuntime } from "../runtime.js";
|
|
8
|
+
|
|
9
|
+
const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk";
|
|
10
|
+
|
|
11
|
+
export function isMatrixSdkAvailable(): boolean {
|
|
12
|
+
try {
|
|
13
|
+
const req = createRequire(import.meta.url);
|
|
14
|
+
req.resolve(MATRIX_SDK_PACKAGE);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolvePluginRoot(): string {
|
|
22
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
return path.resolve(currentDir, "..", "..");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function ensureMatrixSdkInstalled(params: {
|
|
27
|
+
runtime: RuntimeEnv;
|
|
28
|
+
confirm?: (message: string) => Promise<boolean>;
|
|
29
|
+
}): Promise<void> {
|
|
30
|
+
if (isMatrixSdkAvailable()) return;
|
|
31
|
+
const confirm = params.confirm;
|
|
32
|
+
if (confirm) {
|
|
33
|
+
const ok = await confirm("Matrix requires @vector-im/matrix-bot-sdk. Install now?");
|
|
34
|
+
if (!ok) {
|
|
35
|
+
throw new Error("Matrix requires @vector-im/matrix-bot-sdk (install dependencies first).");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const root = resolvePluginRoot();
|
|
40
|
+
const command = fs.existsSync(path.join(root, "pnpm-lock.yaml"))
|
|
41
|
+
? ["pnpm", "install"]
|
|
42
|
+
: ["npm", "install", "--omit=dev", "--silent"];
|
|
43
|
+
params.runtime.log?.(`matrix: installing dependencies via ${command[0]} (${root})…`);
|
|
44
|
+
const result = await getMatrixRuntime().system.runCommandWithTimeout(command, {
|
|
45
|
+
cwd: root,
|
|
46
|
+
timeoutMs: 300_000,
|
|
47
|
+
env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
|
|
48
|
+
});
|
|
49
|
+
if (result.code !== 0) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
result.stderr.trim() || result.stdout.trim() || "Matrix dependency install failed.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (!isMatrixSdkAvailable()) {
|
|
55
|
+
throw new Error("Matrix dependency install completed but @vector-im/matrix-bot-sdk is still missing.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { markdownToMatrixHtml } from "./format.js";
|
|
4
|
+
|
|
5
|
+
describe("markdownToMatrixHtml", () => {
|
|
6
|
+
it("renders basic inline formatting", () => {
|
|
7
|
+
const html = markdownToMatrixHtml("hi _there_ **boss** `code`");
|
|
8
|
+
expect(html).toContain("<em>there</em>");
|
|
9
|
+
expect(html).toContain("<strong>boss</strong>");
|
|
10
|
+
expect(html).toContain("<code>code</code>");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("renders links as HTML", () => {
|
|
14
|
+
const html = markdownToMatrixHtml("see [docs](https://example.com)");
|
|
15
|
+
expect(html).toContain('<a href="https://example.com">docs</a>');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("escapes raw HTML", () => {
|
|
19
|
+
const html = markdownToMatrixHtml("<b>nope</b>");
|
|
20
|
+
expect(html).toContain("<b>nope</b>");
|
|
21
|
+
expect(html).not.toContain("<b>nope</b>");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("flattens images into alt text", () => {
|
|
25
|
+
const html = markdownToMatrixHtml("");
|
|
26
|
+
expect(html).toContain("alt");
|
|
27
|
+
expect(html).not.toContain("<img");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("preserves line breaks", () => {
|
|
31
|
+
const html = markdownToMatrixHtml("line1\nline2");
|
|
32
|
+
expect(html).toContain("<br");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
|
|
3
|
+
const md = new MarkdownIt({
|
|
4
|
+
html: false,
|
|
5
|
+
linkify: true,
|
|
6
|
+
breaks: true,
|
|
7
|
+
typographer: false,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
md.enable("strikethrough");
|
|
11
|
+
|
|
12
|
+
const { escapeHtml } = md.utils;
|
|
13
|
+
|
|
14
|
+
md.renderer.rules.image = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
|
|
15
|
+
|
|
16
|
+
md.renderer.rules.html_block = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
|
|
17
|
+
md.renderer.rules.html_inline = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
|
|
18
|
+
|
|
19
|
+
export function markdownToMatrixHtml(markdown: string): string {
|
|
20
|
+
const rendered = md.render(markdown ?? "");
|
|
21
|
+
return rendered.trimEnd();
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { monitorMatrixProvider } from "./monitor/index.js";
|
|
2
|
+
export { probeMatrix } from "./probe.js";
|
|
3
|
+
export {
|
|
4
|
+
reactMatrixMessage,
|
|
5
|
+
resolveMatrixRoomId,
|
|
6
|
+
sendReadReceiptMatrix,
|
|
7
|
+
sendMessageMatrix,
|
|
8
|
+
sendPollMatrix,
|
|
9
|
+
sendTypingMatrix,
|
|
10
|
+
} from "./send.js";
|
|
11
|
+
export { resolveMatrixAuth, resolveSharedMatrixClient } from "./client.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AllowlistMatch } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
function normalizeAllowList(list?: Array<string | number>) {
|
|
4
|
+
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function normalizeAllowListLower(list?: Array<string | number>) {
|
|
8
|
+
return normalizeAllowList(list).map((entry) => entry.toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeMatrixUser(raw?: string | null): string {
|
|
12
|
+
return (raw ?? "").trim().toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type MatrixAllowListMatch = AllowlistMatch<
|
|
16
|
+
"wildcard" | "id" | "prefixed-id" | "prefixed-user" | "name" | "localpart"
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
export function resolveMatrixAllowListMatch(params: {
|
|
20
|
+
allowList: string[];
|
|
21
|
+
userId?: string;
|
|
22
|
+
userName?: string;
|
|
23
|
+
}): MatrixAllowListMatch {
|
|
24
|
+
const allowList = params.allowList;
|
|
25
|
+
if (allowList.length === 0) return { allowed: false };
|
|
26
|
+
if (allowList.includes("*")) {
|
|
27
|
+
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
28
|
+
}
|
|
29
|
+
const userId = normalizeMatrixUser(params.userId);
|
|
30
|
+
const userName = normalizeMatrixUser(params.userName);
|
|
31
|
+
const localPart = userId.startsWith("@") ? (userId.slice(1).split(":")[0] ?? "") : "";
|
|
32
|
+
const candidates: Array<{ value?: string; source: MatrixAllowListMatch["matchSource"] }> = [
|
|
33
|
+
{ value: userId, source: "id" },
|
|
34
|
+
{ value: userId ? `matrix:${userId}` : "", source: "prefixed-id" },
|
|
35
|
+
{ value: userId ? `user:${userId}` : "", source: "prefixed-user" },
|
|
36
|
+
{ value: userName, source: "name" },
|
|
37
|
+
{ value: localPart, source: "localpart" },
|
|
38
|
+
];
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
if (!candidate.value) continue;
|
|
41
|
+
if (allowList.includes(candidate.value)) {
|
|
42
|
+
return {
|
|
43
|
+
allowed: true,
|
|
44
|
+
matchKey: candidate.value,
|
|
45
|
+
matchSource: candidate.source,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { allowed: false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveMatrixAllowListMatches(params: {
|
|
53
|
+
allowList: string[];
|
|
54
|
+
userId?: string;
|
|
55
|
+
userName?: string;
|
|
56
|
+
}) {
|
|
57
|
+
return resolveMatrixAllowListMatch(params).allowed;
|
|
58
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { AutojoinRoomsMixin } from "@vector-im/matrix-bot-sdk";
|
|
3
|
+
|
|
4
|
+
import type { RuntimeEnv } from "openclaw/plugin-sdk";
|
|
5
|
+
import type { CoreConfig } from "../../types.js";
|
|
6
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
7
|
+
|
|
8
|
+
export function registerMatrixAutoJoin(params: {
|
|
9
|
+
client: MatrixClient;
|
|
10
|
+
cfg: CoreConfig;
|
|
11
|
+
runtime: RuntimeEnv;
|
|
12
|
+
}) {
|
|
13
|
+
const { client, cfg, runtime } = params;
|
|
14
|
+
const core = getMatrixRuntime();
|
|
15
|
+
const logVerbose = (message: string) => {
|
|
16
|
+
if (!core.logging.shouldLogVerbose()) return;
|
|
17
|
+
runtime.log?.(message);
|
|
18
|
+
};
|
|
19
|
+
const autoJoin = cfg.channels?.matrix?.autoJoin ?? "always";
|
|
20
|
+
const autoJoinAllowlist = cfg.channels?.matrix?.autoJoinAllowlist ?? [];
|
|
21
|
+
|
|
22
|
+
if (autoJoin === "off") {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (autoJoin === "always") {
|
|
27
|
+
// Use the built-in autojoin mixin for "always" mode
|
|
28
|
+
AutojoinRoomsMixin.setupOnClient(client);
|
|
29
|
+
logVerbose("matrix: auto-join enabled for all invites");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// For "allowlist" mode, handle invites manually
|
|
34
|
+
client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => {
|
|
35
|
+
if (autoJoin !== "allowlist") return;
|
|
36
|
+
|
|
37
|
+
// Get room alias if available
|
|
38
|
+
let alias: string | undefined;
|
|
39
|
+
let altAliases: string[] = [];
|
|
40
|
+
try {
|
|
41
|
+
const aliasState = await client
|
|
42
|
+
.getRoomStateEvent(roomId, "m.room.canonical_alias", "")
|
|
43
|
+
.catch(() => null);
|
|
44
|
+
alias = aliasState?.alias;
|
|
45
|
+
altAliases = Array.isArray(aliasState?.alt_aliases) ? aliasState.alt_aliases : [];
|
|
46
|
+
} catch {
|
|
47
|
+
// Ignore errors
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const allowed =
|
|
51
|
+
autoJoinAllowlist.includes("*") ||
|
|
52
|
+
autoJoinAllowlist.includes(roomId) ||
|
|
53
|
+
(alias ? autoJoinAllowlist.includes(alias) : false) ||
|
|
54
|
+
altAliases.some((value) => autoJoinAllowlist.includes(value));
|
|
55
|
+
|
|
56
|
+
if (!allowed) {
|
|
57
|
+
logVerbose(`matrix: invite ignored (not in allowlist) room=${roomId}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await client.joinRoom(roomId);
|
|
63
|
+
logVerbose(`matrix: joined room ${roomId}`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
runtime.error?.(`matrix: failed to join room ${roomId}: ${String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|