@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.
- package/CHANGELOG.md +104 -0
- package/SETUP.md +131 -0
- package/index.ts +23 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +32 -0
- package/src/actions.ts +195 -0
- package/src/channel.ts +461 -0
- package/src/config-schema.ts +62 -0
- package/src/connect.ts +17 -0
- package/src/directory-live.ts +209 -0
- package/src/group-mentions.ts +52 -0
- package/src/matrix/accounts.ts +114 -0
- package/src/matrix/actions/client.ts +47 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.ts +126 -0
- package/src/matrix/actions/pins.ts +84 -0
- package/src/matrix/actions/reactions.ts +102 -0
- package/src/matrix/actions/room.ts +85 -0
- package/src/matrix/actions/summary.ts +75 -0
- package/src/matrix/actions/types.ts +85 -0
- package/src/matrix/actions.ts +15 -0
- package/src/matrix/active-client.ts +32 -0
- package/src/matrix/client/config.ts +245 -0
- package/src/matrix/client/create-client.ts +125 -0
- package/src/matrix/client/logging.ts +46 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.ts +210 -0
- package/src/matrix/client/startup.ts +29 -0
- package/src/matrix/client/storage.ts +131 -0
- package/src/matrix/client/types.ts +34 -0
- package/src/matrix/client-bootstrap.ts +47 -0
- package/src/matrix/client.ts +14 -0
- package/src/matrix/credentials.ts +125 -0
- package/src/matrix/deps.ts +126 -0
- package/src/matrix/format.ts +22 -0
- package/src/matrix/index.ts +11 -0
- package/src/matrix/monitor/access-policy.ts +126 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.ts +72 -0
- package/src/matrix/monitor/direct.ts +152 -0
- package/src/matrix/monitor/events.ts +168 -0
- package/src/matrix/monitor/handler.ts +768 -0
- package/src/matrix/monitor/inbound-body.ts +28 -0
- package/src/matrix/monitor/index.ts +414 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.ts +118 -0
- package/src/matrix/monitor/mentions.ts +62 -0
- package/src/matrix/monitor/replies.ts +124 -0
- package/src/matrix/monitor/room-info.ts +55 -0
- package/src/matrix/monitor/rooms.ts +47 -0
- package/src/matrix/monitor/threads.ts +68 -0
- package/src/matrix/monitor/types.ts +39 -0
- package/src/matrix/poll-types.ts +167 -0
- package/src/matrix/probe.ts +69 -0
- package/src/matrix/sdk-runtime.ts +18 -0
- package/src/matrix/send/client.ts +99 -0
- package/src/matrix/send/formatting.ts +93 -0
- package/src/matrix/send/media.ts +230 -0
- package/src/matrix/send/targets.ts +150 -0
- package/src/matrix/send/types.ts +110 -0
- package/src/matrix/send-queue.ts +28 -0
- package/src/matrix/send.ts +267 -0
- package/src/onboarding.ts +331 -0
- package/src/outbound.ts +58 -0
- package/src/resolve-targets.ts +125 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/test-mocks.ts +53 -0
- package/src/tool-actions.ts +164 -0
- package/src/types.ts +118 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { resolveMatrixRoomId } from "../send.js";
|
|
2
|
+
import { resolveActionClient } from "./client.js";
|
|
3
|
+
import { resolveMatrixActionLimit } from "./limits.js";
|
|
4
|
+
import {
|
|
5
|
+
EventType,
|
|
6
|
+
RelationType,
|
|
7
|
+
type MatrixActionClientOpts,
|
|
8
|
+
type MatrixRawEvent,
|
|
9
|
+
type MatrixReactionSummary,
|
|
10
|
+
type ReactionEventContent,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
function getReactionsPath(roomId: string, messageId: string): string {
|
|
14
|
+
return `/_matrix/client/v1/rooms/${encodeURIComponent(roomId)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function listReactionEvents(
|
|
18
|
+
client: NonNullable<MatrixActionClientOpts["client"]>,
|
|
19
|
+
roomId: string,
|
|
20
|
+
messageId: string,
|
|
21
|
+
limit: number,
|
|
22
|
+
): Promise<MatrixRawEvent[]> {
|
|
23
|
+
const res = (await client.doRequest("GET", getReactionsPath(roomId, messageId), {
|
|
24
|
+
dir: "b",
|
|
25
|
+
limit,
|
|
26
|
+
})) as { chunk: MatrixRawEvent[] };
|
|
27
|
+
return res.chunk;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function listMatrixReactions(
|
|
31
|
+
roomId: string,
|
|
32
|
+
messageId: string,
|
|
33
|
+
opts: MatrixActionClientOpts & { limit?: number } = {},
|
|
34
|
+
): Promise<MatrixReactionSummary[]> {
|
|
35
|
+
const { client, stopOnDone } = await resolveActionClient(opts);
|
|
36
|
+
try {
|
|
37
|
+
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
|
38
|
+
const limit = resolveMatrixActionLimit(opts.limit, 100);
|
|
39
|
+
const chunk = await listReactionEvents(client, resolvedRoom, messageId, limit);
|
|
40
|
+
const summaries = new Map<string, MatrixReactionSummary>();
|
|
41
|
+
for (const event of chunk) {
|
|
42
|
+
const content = event.content as ReactionEventContent;
|
|
43
|
+
const key = content["m.relates_to"]?.key;
|
|
44
|
+
if (!key) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const sender = event.sender ?? "";
|
|
48
|
+
const entry: MatrixReactionSummary = summaries.get(key) ?? {
|
|
49
|
+
key,
|
|
50
|
+
count: 0,
|
|
51
|
+
users: [],
|
|
52
|
+
};
|
|
53
|
+
entry.count += 1;
|
|
54
|
+
if (sender && !entry.users.includes(sender)) {
|
|
55
|
+
entry.users.push(sender);
|
|
56
|
+
}
|
|
57
|
+
summaries.set(key, entry);
|
|
58
|
+
}
|
|
59
|
+
return Array.from(summaries.values());
|
|
60
|
+
} finally {
|
|
61
|
+
if (stopOnDone) {
|
|
62
|
+
client.stop();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function removeMatrixReactions(
|
|
68
|
+
roomId: string,
|
|
69
|
+
messageId: string,
|
|
70
|
+
opts: MatrixActionClientOpts & { emoji?: string } = {},
|
|
71
|
+
): Promise<{ removed: number }> {
|
|
72
|
+
const { client, stopOnDone } = await resolveActionClient(opts);
|
|
73
|
+
try {
|
|
74
|
+
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
|
75
|
+
const chunk = await listReactionEvents(client, resolvedRoom, messageId, 200);
|
|
76
|
+
const userId = await client.getUserId();
|
|
77
|
+
if (!userId) {
|
|
78
|
+
return { removed: 0 };
|
|
79
|
+
}
|
|
80
|
+
const targetEmoji = opts.emoji?.trim();
|
|
81
|
+
const toRemove = chunk
|
|
82
|
+
.filter((event) => event.sender === userId)
|
|
83
|
+
.filter((event) => {
|
|
84
|
+
if (!targetEmoji) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const content = event.content as ReactionEventContent;
|
|
88
|
+
return content["m.relates_to"]?.key === targetEmoji;
|
|
89
|
+
})
|
|
90
|
+
.map((event) => event.event_id)
|
|
91
|
+
.filter((id): id is string => Boolean(id));
|
|
92
|
+
if (toRemove.length === 0) {
|
|
93
|
+
return { removed: 0 };
|
|
94
|
+
}
|
|
95
|
+
await Promise.all(toRemove.map((id) => client.redactEvent(resolvedRoom, id)));
|
|
96
|
+
return { removed: toRemove.length };
|
|
97
|
+
} finally {
|
|
98
|
+
if (stopOnDone) {
|
|
99
|
+
client.stop();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolveMatrixRoomId } from "../send.js";
|
|
2
|
+
import { resolveActionClient } from "./client.js";
|
|
3
|
+
import { EventType, type MatrixActionClientOpts } from "./types.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) {
|
|
29
|
+
client.stop();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function getMatrixRoomInfo(roomId: string, opts: MatrixActionClientOpts = {}) {
|
|
35
|
+
const { client, stopOnDone } = await resolveActionClient(opts);
|
|
36
|
+
try {
|
|
37
|
+
const resolvedRoom = await resolveMatrixRoomId(client, roomId);
|
|
38
|
+
// @vector-im/matrix-bot-sdk uses getRoomState for state events
|
|
39
|
+
let name: string | null = null;
|
|
40
|
+
let topic: string | null = null;
|
|
41
|
+
let canonicalAlias: string | null = null;
|
|
42
|
+
let memberCount: number | null = null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const nameState = await client.getRoomStateEvent(resolvedRoom, "m.room.name", "");
|
|
46
|
+
name = nameState?.name ?? null;
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const topicState = await client.getRoomStateEvent(resolvedRoom, EventType.RoomTopic, "");
|
|
53
|
+
topic = topicState?.topic ?? null;
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const aliasState = await client.getRoomStateEvent(resolvedRoom, "m.room.canonical_alias", "");
|
|
60
|
+
canonicalAlias = aliasState?.alias ?? null;
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const members = await client.getJoinedRoomMembers(resolvedRoom);
|
|
67
|
+
memberCount = members.length;
|
|
68
|
+
} catch {
|
|
69
|
+
// ignore
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
roomId: resolvedRoom,
|
|
74
|
+
name,
|
|
75
|
+
topic,
|
|
76
|
+
canonicalAlias,
|
|
77
|
+
altAliases: [], // Would need separate query
|
|
78
|
+
memberCount,
|
|
79
|
+
};
|
|
80
|
+
} finally {
|
|
81
|
+
if (stopOnDone) {
|
|
82
|
+
client.stop();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import {
|
|
3
|
+
EventType,
|
|
4
|
+
type MatrixMessageSummary,
|
|
5
|
+
type MatrixRawEvent,
|
|
6
|
+
type RoomMessageEventContent,
|
|
7
|
+
type RoomPinnedEventsEventContent,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
export function summarizeMatrixRawEvent(event: MatrixRawEvent): MatrixMessageSummary {
|
|
11
|
+
const content = event.content as RoomMessageEventContent;
|
|
12
|
+
const relates = content["m.relates_to"];
|
|
13
|
+
let relType: string | undefined;
|
|
14
|
+
let eventId: string | undefined;
|
|
15
|
+
if (relates) {
|
|
16
|
+
if ("rel_type" in relates) {
|
|
17
|
+
relType = relates.rel_type;
|
|
18
|
+
eventId = relates.event_id;
|
|
19
|
+
} else if ("m.in_reply_to" in relates) {
|
|
20
|
+
eventId = relates["m.in_reply_to"]?.event_id;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const relatesTo =
|
|
24
|
+
relType || eventId
|
|
25
|
+
? {
|
|
26
|
+
relType,
|
|
27
|
+
eventId,
|
|
28
|
+
}
|
|
29
|
+
: undefined;
|
|
30
|
+
return {
|
|
31
|
+
eventId: event.event_id,
|
|
32
|
+
sender: event.sender,
|
|
33
|
+
body: content.body,
|
|
34
|
+
msgtype: content.msgtype,
|
|
35
|
+
timestamp: event.origin_server_ts,
|
|
36
|
+
relatesTo,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function readPinnedEvents(client: MatrixClient, roomId: string): Promise<string[]> {
|
|
41
|
+
try {
|
|
42
|
+
const content = (await client.getRoomStateEvent(
|
|
43
|
+
roomId,
|
|
44
|
+
EventType.RoomPinnedEvents,
|
|
45
|
+
"",
|
|
46
|
+
)) as RoomPinnedEventsEventContent;
|
|
47
|
+
const pinned = content.pinned;
|
|
48
|
+
return pinned.filter((id) => id.trim().length > 0);
|
|
49
|
+
} catch (err: unknown) {
|
|
50
|
+
const errObj = err as { statusCode?: number; body?: { errcode?: string } };
|
|
51
|
+
const httpStatus = errObj.statusCode;
|
|
52
|
+
const errcode = errObj.body?.errcode;
|
|
53
|
+
if (httpStatus === 404 || errcode === "M_NOT_FOUND") {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchEventSummary(
|
|
61
|
+
client: MatrixClient,
|
|
62
|
+
roomId: string,
|
|
63
|
+
eventId: string,
|
|
64
|
+
): Promise<MatrixMessageSummary | null> {
|
|
65
|
+
try {
|
|
66
|
+
const raw = (await client.getEvent(roomId, eventId)) as unknown as MatrixRawEvent;
|
|
67
|
+
if (raw.unsigned?.redacted_because) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return summarizeMatrixRawEvent(raw);
|
|
71
|
+
} catch {
|
|
72
|
+
// Event not found, redacted, or inaccessible - return null
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
|
|
3
|
+
export const MsgType = {
|
|
4
|
+
Text: "m.text",
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export const RelationType = {
|
|
8
|
+
Replace: "m.replace",
|
|
9
|
+
Annotation: "m.annotation",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export const EventType = {
|
|
13
|
+
RoomMessage: "m.room.message",
|
|
14
|
+
RoomPinnedEvents: "m.room.pinned_events",
|
|
15
|
+
RoomTopic: "m.room.topic",
|
|
16
|
+
Reaction: "m.reaction",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type RoomMessageEventContent = {
|
|
20
|
+
msgtype: string;
|
|
21
|
+
body: string;
|
|
22
|
+
"m.new_content"?: RoomMessageEventContent;
|
|
23
|
+
"m.relates_to"?: {
|
|
24
|
+
rel_type?: string;
|
|
25
|
+
event_id?: string;
|
|
26
|
+
"m.in_reply_to"?: { event_id?: string };
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ReactionEventContent = {
|
|
31
|
+
"m.relates_to": {
|
|
32
|
+
rel_type: string;
|
|
33
|
+
event_id: string;
|
|
34
|
+
key: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type RoomPinnedEventsEventContent = {
|
|
39
|
+
pinned: string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type RoomTopicEventContent = {
|
|
43
|
+
topic?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type MatrixRawEvent = {
|
|
47
|
+
event_id: string;
|
|
48
|
+
sender: string;
|
|
49
|
+
type: string;
|
|
50
|
+
origin_server_ts: number;
|
|
51
|
+
content: Record<string, unknown>;
|
|
52
|
+
unsigned?: {
|
|
53
|
+
redacted_because?: unknown;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type MatrixActionClientOpts = {
|
|
58
|
+
client?: MatrixClient;
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
accountId?: string | null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type MatrixMessageSummary = {
|
|
64
|
+
eventId?: string;
|
|
65
|
+
sender?: string;
|
|
66
|
+
body?: string;
|
|
67
|
+
msgtype?: string;
|
|
68
|
+
timestamp?: number;
|
|
69
|
+
relatesTo?: {
|
|
70
|
+
relType?: string;
|
|
71
|
+
eventId?: string;
|
|
72
|
+
key?: string;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type MatrixReactionSummary = {
|
|
77
|
+
key: string;
|
|
78
|
+
count: number;
|
|
79
|
+
users: string[];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type MatrixActionClient = {
|
|
83
|
+
client: MatrixClient;
|
|
84
|
+
stopOnDone: boolean;
|
|
85
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
MatrixActionClientOpts,
|
|
3
|
+
MatrixMessageSummary,
|
|
4
|
+
MatrixReactionSummary,
|
|
5
|
+
} from "./actions/types.js";
|
|
6
|
+
export {
|
|
7
|
+
sendMatrixMessage,
|
|
8
|
+
editMatrixMessage,
|
|
9
|
+
deleteMatrixMessage,
|
|
10
|
+
readMatrixMessages,
|
|
11
|
+
} from "./actions/messages.js";
|
|
12
|
+
export { listMatrixReactions, removeMatrixReactions } from "./actions/reactions.js";
|
|
13
|
+
export { pinMatrixMessage, unpinMatrixMessage, listMatrixPins } from "./actions/pins.js";
|
|
14
|
+
export { getMatrixMemberInfo, getMatrixRoomInfo } from "./actions/room.js";
|
|
15
|
+
export { reactMatrixMessage } from "./send.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
3
|
+
|
|
4
|
+
// Support multiple active clients for multi-account
|
|
5
|
+
const activeClients = new Map<string, MatrixClient>();
|
|
6
|
+
|
|
7
|
+
export function setActiveMatrixClient(
|
|
8
|
+
client: MatrixClient | null,
|
|
9
|
+
accountId?: string | null,
|
|
10
|
+
): void {
|
|
11
|
+
const key = normalizeAccountId(accountId);
|
|
12
|
+
if (client) {
|
|
13
|
+
activeClients.set(key, client);
|
|
14
|
+
} else {
|
|
15
|
+
activeClients.delete(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getActiveMatrixClient(accountId?: string | null): MatrixClient | null {
|
|
20
|
+
const key = normalizeAccountId(accountId);
|
|
21
|
+
return activeClients.get(key) ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getAnyActiveMatrixClient(): MatrixClient | null {
|
|
25
|
+
// Return any available client (for backward compatibility)
|
|
26
|
+
const first = activeClients.values().next();
|
|
27
|
+
return first.done ? null : first.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function clearAllActiveMatrixClients(): void {
|
|
31
|
+
activeClients.clear();
|
|
32
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/matrix";
|
|
3
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
|
+
import {
|
|
5
|
+
normalizeResolvedSecretInputString,
|
|
6
|
+
normalizeSecretInputString,
|
|
7
|
+
} from "../../secret-input.js";
|
|
8
|
+
import type { CoreConfig } from "../../types.js";
|
|
9
|
+
import { loadMatrixSdk } from "../sdk-runtime.js";
|
|
10
|
+
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
|
11
|
+
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
|
12
|
+
|
|
13
|
+
function clean(value: unknown, path: string): string {
|
|
14
|
+
return normalizeResolvedSecretInputString({ value, path }) ?? "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Shallow-merge known nested config sub-objects so partial overrides inherit base values. */
|
|
18
|
+
function deepMergeConfig<T extends Record<string, unknown>>(base: T, override: Partial<T>): T {
|
|
19
|
+
const merged = { ...base, ...override } as Record<string, unknown>;
|
|
20
|
+
// Merge known nested objects (dm, actions) so partial overrides keep base fields
|
|
21
|
+
for (const key of ["dm", "actions"] as const) {
|
|
22
|
+
const b = base[key];
|
|
23
|
+
const o = override[key];
|
|
24
|
+
if (typeof b === "object" && b !== null && typeof o === "object" && o !== null) {
|
|
25
|
+
merged[key] = { ...(b as Record<string, unknown>), ...(o as Record<string, unknown>) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return merged as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve Matrix config for a specific account, with fallback to top-level config.
|
|
33
|
+
* This supports both multi-account (channels.badgerclaw.accounts.*) and
|
|
34
|
+
* single-account (channels.badgerclaw.*) configurations.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveMatrixConfigForAccount(
|
|
37
|
+
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
38
|
+
accountId?: string | null,
|
|
39
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
40
|
+
): MatrixResolvedConfig {
|
|
41
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
42
|
+
const matrixBase = cfg.channels?.badgerclaw ?? {};
|
|
43
|
+
const accounts = cfg.channels?.badgerclaw?.accounts;
|
|
44
|
+
|
|
45
|
+
// Try to get account-specific config first (direct lookup, then case-insensitive fallback)
|
|
46
|
+
let accountConfig = accounts?.[normalizedAccountId];
|
|
47
|
+
if (!accountConfig && accounts) {
|
|
48
|
+
for (const key of Object.keys(accounts)) {
|
|
49
|
+
if (normalizeAccountId(key) === normalizedAccountId) {
|
|
50
|
+
accountConfig = accounts[key];
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Deep merge: account-specific values override top-level values, preserving
|
|
57
|
+
// nested object inheritance (dm, actions, groups) so partial overrides work.
|
|
58
|
+
const matrix = accountConfig ? deepMergeConfig(matrixBase, accountConfig) : matrixBase;
|
|
59
|
+
|
|
60
|
+
const homeserver =
|
|
61
|
+
clean(matrix.homeserver, "channels.badgerclaw.homeserver") ||
|
|
62
|
+
clean(env.MATRIX_HOMESERVER, "MATRIX_HOMESERVER");
|
|
63
|
+
const userId =
|
|
64
|
+
clean(matrix.userId, "channels.badgerclaw.userId") || clean(env.MATRIX_USER_ID, "MATRIX_USER_ID");
|
|
65
|
+
const accessToken =
|
|
66
|
+
clean(matrix.accessToken, "channels.badgerclaw.accessToken") ||
|
|
67
|
+
clean(env.MATRIX_ACCESS_TOKEN, "MATRIX_ACCESS_TOKEN") ||
|
|
68
|
+
undefined;
|
|
69
|
+
const password =
|
|
70
|
+
clean(matrix.password, "channels.badgerclaw.password") ||
|
|
71
|
+
clean(env.MATRIX_PASSWORD, "MATRIX_PASSWORD") ||
|
|
72
|
+
undefined;
|
|
73
|
+
const deviceName =
|
|
74
|
+
clean(matrix.deviceName, "channels.badgerclaw.deviceName") ||
|
|
75
|
+
clean(env.MATRIX_DEVICE_NAME, "MATRIX_DEVICE_NAME") ||
|
|
76
|
+
undefined;
|
|
77
|
+
const initialSyncLimit =
|
|
78
|
+
typeof matrix.initialSyncLimit === "number"
|
|
79
|
+
? Math.max(0, Math.floor(matrix.initialSyncLimit))
|
|
80
|
+
: undefined;
|
|
81
|
+
const encryption = matrix.encryption ?? false;
|
|
82
|
+
return {
|
|
83
|
+
homeserver,
|
|
84
|
+
userId,
|
|
85
|
+
accessToken,
|
|
86
|
+
password,
|
|
87
|
+
deviceName,
|
|
88
|
+
initialSyncLimit,
|
|
89
|
+
encryption,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Single-account function for backward compatibility - resolves default account config.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveMatrixConfig(
|
|
97
|
+
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
|
98
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
99
|
+
): MatrixResolvedConfig {
|
|
100
|
+
return resolveMatrixConfigForAccount(cfg, DEFAULT_ACCOUNT_ID, env);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function resolveMatrixAuth(params?: {
|
|
104
|
+
cfg?: CoreConfig;
|
|
105
|
+
env?: NodeJS.ProcessEnv;
|
|
106
|
+
accountId?: string | null;
|
|
107
|
+
}): Promise<MatrixAuth> {
|
|
108
|
+
const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig);
|
|
109
|
+
const env = params?.env ?? process.env;
|
|
110
|
+
const resolved = resolveMatrixConfigForAccount(cfg, params?.accountId, env);
|
|
111
|
+
if (!resolved.homeserver) {
|
|
112
|
+
throw new Error("BadgerClaw homeserver is required (matrix.homeserver)");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const {
|
|
116
|
+
loadMatrixCredentials,
|
|
117
|
+
saveMatrixCredentials,
|
|
118
|
+
credentialsMatchConfig,
|
|
119
|
+
touchMatrixCredentials,
|
|
120
|
+
} = await import("../credentials.js");
|
|
121
|
+
|
|
122
|
+
const accountId = params?.accountId;
|
|
123
|
+
const cached = loadMatrixCredentials(env, accountId);
|
|
124
|
+
const cachedCredentials =
|
|
125
|
+
cached &&
|
|
126
|
+
credentialsMatchConfig(cached, {
|
|
127
|
+
homeserver: resolved.homeserver,
|
|
128
|
+
userId: resolved.userId || "",
|
|
129
|
+
})
|
|
130
|
+
? cached
|
|
131
|
+
: null;
|
|
132
|
+
|
|
133
|
+
// If we have an access token, we can fetch userId via whoami if not provided
|
|
134
|
+
if (resolved.accessToken) {
|
|
135
|
+
let userId = resolved.userId;
|
|
136
|
+
if (!userId) {
|
|
137
|
+
// Fetch userId from access token via whoami
|
|
138
|
+
ensureMatrixSdkLoggingConfigured();
|
|
139
|
+
const { MatrixClient } = loadMatrixSdk();
|
|
140
|
+
const tempClient = new MatrixClient(resolved.homeserver, resolved.accessToken);
|
|
141
|
+
const whoami = await tempClient.getUserId();
|
|
142
|
+
userId = whoami;
|
|
143
|
+
// Save the credentials with the fetched userId
|
|
144
|
+
saveMatrixCredentials(
|
|
145
|
+
{
|
|
146
|
+
homeserver: resolved.homeserver,
|
|
147
|
+
userId,
|
|
148
|
+
accessToken: resolved.accessToken,
|
|
149
|
+
},
|
|
150
|
+
env,
|
|
151
|
+
accountId,
|
|
152
|
+
);
|
|
153
|
+
} else if (cachedCredentials && cachedCredentials.accessToken === resolved.accessToken) {
|
|
154
|
+
touchMatrixCredentials(env, accountId);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
homeserver: resolved.homeserver,
|
|
158
|
+
userId,
|
|
159
|
+
accessToken: resolved.accessToken,
|
|
160
|
+
deviceName: resolved.deviceName,
|
|
161
|
+
initialSyncLimit: resolved.initialSyncLimit,
|
|
162
|
+
encryption: resolved.encryption,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (cachedCredentials) {
|
|
167
|
+
touchMatrixCredentials(env, accountId);
|
|
168
|
+
return {
|
|
169
|
+
homeserver: cachedCredentials.homeserver,
|
|
170
|
+
userId: cachedCredentials.userId,
|
|
171
|
+
accessToken: cachedCredentials.accessToken,
|
|
172
|
+
deviceName: resolved.deviceName,
|
|
173
|
+
initialSyncLimit: resolved.initialSyncLimit,
|
|
174
|
+
encryption: resolved.encryption,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!resolved.userId) {
|
|
179
|
+
throw new Error("BadgerClaw userId is required when no access token is configured (matrix.userId)");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!resolved.password) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"BadgerClaw password is required when no access token is configured (matrix.password)",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Login with password using HTTP API.
|
|
189
|
+
const { response: loginResponse, release: releaseLoginResponse } = await fetchWithSsrFGuard({
|
|
190
|
+
url: `${resolved.homeserver}/_matrix/client/v3/login`,
|
|
191
|
+
init: {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "Content-Type": "application/json" },
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
type: "m.login.password",
|
|
196
|
+
identifier: { type: "m.id.user", user: resolved.userId },
|
|
197
|
+
password: resolved.password,
|
|
198
|
+
initial_device_display_name: resolved.deviceName ?? "OpenClaw Gateway",
|
|
199
|
+
}),
|
|
200
|
+
},
|
|
201
|
+
auditContext: "matrix.login",
|
|
202
|
+
});
|
|
203
|
+
const login = await (async () => {
|
|
204
|
+
try {
|
|
205
|
+
if (!loginResponse.ok) {
|
|
206
|
+
const errorText = await loginResponse.text();
|
|
207
|
+
throw new Error(`BadgerClaw login failed: ${errorText}`);
|
|
208
|
+
}
|
|
209
|
+
return (await loginResponse.json()) as {
|
|
210
|
+
access_token?: string;
|
|
211
|
+
user_id?: string;
|
|
212
|
+
device_id?: string;
|
|
213
|
+
};
|
|
214
|
+
} finally {
|
|
215
|
+
await releaseLoginResponse();
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
|
|
219
|
+
const accessToken = login.access_token?.trim();
|
|
220
|
+
if (!accessToken) {
|
|
221
|
+
throw new Error("BadgerClaw login did not return an access token");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const auth: MatrixAuth = {
|
|
225
|
+
homeserver: resolved.homeserver,
|
|
226
|
+
userId: login.user_id ?? resolved.userId,
|
|
227
|
+
accessToken,
|
|
228
|
+
deviceName: resolved.deviceName,
|
|
229
|
+
initialSyncLimit: resolved.initialSyncLimit,
|
|
230
|
+
encryption: resolved.encryption,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
saveMatrixCredentials(
|
|
234
|
+
{
|
|
235
|
+
homeserver: auth.homeserver,
|
|
236
|
+
userId: auth.userId,
|
|
237
|
+
accessToken: auth.accessToken,
|
|
238
|
+
deviceId: login.device_id,
|
|
239
|
+
},
|
|
240
|
+
env,
|
|
241
|
+
accountId,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return auth;
|
|
245
|
+
}
|