@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,152 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
|
|
3
|
+
type DirectMessageCheck = {
|
|
4
|
+
roomId: string;
|
|
5
|
+
senderId?: string;
|
|
6
|
+
selfUserId?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type DirectRoomTrackerOptions = {
|
|
10
|
+
log?: (message: string) => void;
|
|
11
|
+
includeMemberCountInLogs?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DM_CACHE_TTL_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if an error is a Matrix M_NOT_FOUND response (missing state event).
|
|
18
|
+
* The bot-sdk throws MatrixError with errcode/statusCode on the error object.
|
|
19
|
+
*/
|
|
20
|
+
function isMatrixNotFoundError(err: unknown): boolean {
|
|
21
|
+
if (typeof err !== "object" || err === null) return false;
|
|
22
|
+
const e = err as { errcode?: string; statusCode?: number };
|
|
23
|
+
return e.errcode === "M_NOT_FOUND" || e.statusCode === 404;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTrackerOptions = {}) {
|
|
27
|
+
const log = opts.log ?? (() => {});
|
|
28
|
+
const includeMemberCountInLogs = opts.includeMemberCountInLogs === true;
|
|
29
|
+
let lastDmUpdateMs = 0;
|
|
30
|
+
let cachedSelfUserId: string | null = null;
|
|
31
|
+
const memberCountCache = new Map<string, { count: number; ts: number }>();
|
|
32
|
+
|
|
33
|
+
const ensureSelfUserId = async (): Promise<string | null> => {
|
|
34
|
+
if (cachedSelfUserId) {
|
|
35
|
+
return cachedSelfUserId;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
cachedSelfUserId = await client.getUserId();
|
|
39
|
+
} catch {
|
|
40
|
+
cachedSelfUserId = null;
|
|
41
|
+
}
|
|
42
|
+
return cachedSelfUserId;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const refreshDmCache = async (): Promise<void> => {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
if (now - lastDmUpdateMs < DM_CACHE_TTL_MS) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
lastDmUpdateMs = now;
|
|
51
|
+
try {
|
|
52
|
+
await client.dms.update();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log(`badgerclaw: dm cache refresh failed (${String(err)})`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const resolveMemberCount = async (roomId: string): Promise<number | null> => {
|
|
59
|
+
const cached = memberCountCache.get(roomId);
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
if (cached && now - cached.ts < DM_CACHE_TTL_MS) {
|
|
62
|
+
return cached.count;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const members = await client.getJoinedRoomMembers(roomId);
|
|
66
|
+
const count = members.length;
|
|
67
|
+
memberCountCache.set(roomId, { count, ts: now });
|
|
68
|
+
return count;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log(`badgerclaw: dm member count failed room=${roomId} (${String(err)})`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hasDirectFlag = async (roomId: string, userId?: string): Promise<boolean> => {
|
|
76
|
+
const target = userId?.trim();
|
|
77
|
+
if (!target) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const state = await client.getRoomStateEvent(roomId, "m.room.member", target);
|
|
82
|
+
return state?.is_direct === true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
isDirectMessage: async (params: DirectMessageCheck): Promise<boolean> => {
|
|
90
|
+
const { roomId, senderId } = params;
|
|
91
|
+
await refreshDmCache();
|
|
92
|
+
|
|
93
|
+
// Check m.direct account data (most authoritative)
|
|
94
|
+
if (client.dms.isDm(roomId)) {
|
|
95
|
+
log(`badgerclaw: dm detected via m.direct room=${roomId}`);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selfUserId = params.selfUserId ?? (await ensureSelfUserId());
|
|
100
|
+
const directViaState =
|
|
101
|
+
(await hasDirectFlag(roomId, senderId)) || (await hasDirectFlag(roomId, selfUserId ?? ""));
|
|
102
|
+
if (directViaState) {
|
|
103
|
+
log(`badgerclaw: dm detected via member state room=${roomId}`);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Conservative fallback: 2-member rooms without an explicit room name are likely
|
|
108
|
+
// DMs with broken m.direct / is_direct flags. This has been observed on Continuwuity
|
|
109
|
+
// where m.direct pointed to the wrong room and is_direct was never set on the invite.
|
|
110
|
+
// Unlike the removed heuristic, this requires two signals (member count + no name)
|
|
111
|
+
// to avoid false positives on named 2-person group rooms.
|
|
112
|
+
//
|
|
113
|
+
// Performance: member count is cached (resolveMemberCount). The room name state
|
|
114
|
+
// check is not cached but only runs for the subset of 2-member rooms that reach
|
|
115
|
+
// this fallback path (no m.direct, no is_direct). In typical deployments this is
|
|
116
|
+
// a small minority of rooms.
|
|
117
|
+
//
|
|
118
|
+
// Note: there is a narrow race where a room name is being set concurrently with
|
|
119
|
+
// this check. The consequence is a one-time misclassification that self-corrects
|
|
120
|
+
// on the next message (once the state event is synced). This is acceptable given
|
|
121
|
+
// the alternative of an additional API call on every message.
|
|
122
|
+
const memberCount = await resolveMemberCount(roomId);
|
|
123
|
+
if (memberCount === 2) {
|
|
124
|
+
try {
|
|
125
|
+
const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "");
|
|
126
|
+
if (!nameState?.name?.trim()) {
|
|
127
|
+
log(`badgerclaw: dm detected via fallback (2 members, no room name) room=${roomId}`);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
// Missing state events (M_NOT_FOUND) are expected for unnamed rooms and
|
|
132
|
+
// strongly indicate a DM. Any other error (network, auth) is ambiguous,
|
|
133
|
+
// so we fall through to classify as group rather than guess.
|
|
134
|
+
if (isMatrixNotFoundError(err)) {
|
|
135
|
+
log(`badgerclaw: dm detected via fallback (2 members, no room name) room=${roomId}`);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
log(
|
|
139
|
+
`badgerclaw: dm fallback skipped (room name check failed: ${String(err)}) room=${roomId}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!includeMemberCountInLogs) {
|
|
145
|
+
log(`badgerclaw: dm check room=${roomId} result=group`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
log(`badgerclaw: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`);
|
|
149
|
+
return false;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/matrix";
|
|
3
|
+
import type { MatrixAuth } from "../client.js";
|
|
4
|
+
import { sendReadReceiptMatrix } from "../send.js";
|
|
5
|
+
import type { MatrixRawEvent } from "./types.js";
|
|
6
|
+
import { EventType } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const matrixMonitorListenerRegistry = (() => {
|
|
9
|
+
// Prevent duplicate listener registration when both bundled and extension
|
|
10
|
+
// paths attempt to start monitors against the same shared client.
|
|
11
|
+
const registeredClients = new WeakSet<object>();
|
|
12
|
+
return {
|
|
13
|
+
tryRegister(client: object): boolean {
|
|
14
|
+
if (registeredClients.has(client)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
registeredClients.add(client);
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
function createSelfUserIdResolver(client: Pick<MatrixClient, "getUserId">) {
|
|
24
|
+
let selfUserId: string | undefined;
|
|
25
|
+
let selfUserIdLookup: Promise<string | undefined> | undefined;
|
|
26
|
+
|
|
27
|
+
return async (): Promise<string | undefined> => {
|
|
28
|
+
if (selfUserId) {
|
|
29
|
+
return selfUserId;
|
|
30
|
+
}
|
|
31
|
+
if (!selfUserIdLookup) {
|
|
32
|
+
selfUserIdLookup = client
|
|
33
|
+
.getUserId()
|
|
34
|
+
.then((userId) => {
|
|
35
|
+
selfUserId = userId;
|
|
36
|
+
return userId;
|
|
37
|
+
})
|
|
38
|
+
.catch(() => undefined)
|
|
39
|
+
.finally(() => {
|
|
40
|
+
if (!selfUserId) {
|
|
41
|
+
selfUserIdLookup = undefined;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return await selfUserIdLookup;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerMatrixMonitorEvents(params: {
|
|
50
|
+
client: MatrixClient;
|
|
51
|
+
auth: MatrixAuth;
|
|
52
|
+
logVerboseMessage: (message: string) => void;
|
|
53
|
+
warnedEncryptedRooms: Set<string>;
|
|
54
|
+
warnedCryptoMissingRooms: Set<string>;
|
|
55
|
+
logger: RuntimeLogger;
|
|
56
|
+
formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"];
|
|
57
|
+
onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise<void>;
|
|
58
|
+
}): void {
|
|
59
|
+
if (!matrixMonitorListenerRegistry.tryRegister(params.client)) {
|
|
60
|
+
params.logVerboseMessage("badgerclaw: skipping duplicate listener registration for client");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const {
|
|
65
|
+
client,
|
|
66
|
+
auth,
|
|
67
|
+
logVerboseMessage,
|
|
68
|
+
warnedEncryptedRooms,
|
|
69
|
+
warnedCryptoMissingRooms,
|
|
70
|
+
logger,
|
|
71
|
+
formatNativeDependencyHint,
|
|
72
|
+
onRoomMessage,
|
|
73
|
+
} = params;
|
|
74
|
+
|
|
75
|
+
const resolveSelfUserId = createSelfUserIdResolver(client);
|
|
76
|
+
client.on("room.message", (roomId: string, event: MatrixRawEvent) => {
|
|
77
|
+
const eventId = event?.event_id;
|
|
78
|
+
const senderId = event?.sender;
|
|
79
|
+
if (eventId && senderId) {
|
|
80
|
+
void (async () => {
|
|
81
|
+
const currentSelfUserId = await resolveSelfUserId();
|
|
82
|
+
if (!currentSelfUserId || senderId === currentSelfUserId) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await sendReadReceiptMatrix(roomId, eventId, client).catch((err) => {
|
|
86
|
+
logVerboseMessage(
|
|
87
|
+
`badgerclaw: early read receipt failed room=${roomId} id=${eventId}: ${String(err)}`,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
})();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
onRoomMessage(roomId, event);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
client.on("room.encrypted_event", (roomId: string, event: MatrixRawEvent) => {
|
|
97
|
+
const eventId = event?.event_id ?? "unknown";
|
|
98
|
+
const eventType = event?.type ?? "unknown";
|
|
99
|
+
logVerboseMessage(`badgerclaw: encrypted event room=${roomId} type=${eventType} id=${eventId}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
client.on("room.decrypted_event", (roomId: string, event: MatrixRawEvent) => {
|
|
103
|
+
const eventId = event?.event_id ?? "unknown";
|
|
104
|
+
const eventType = event?.type ?? "unknown";
|
|
105
|
+
logVerboseMessage(`badgerclaw: decrypted event room=${roomId} type=${eventType} id=${eventId}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
client.on(
|
|
109
|
+
"room.failed_decryption",
|
|
110
|
+
async (roomId: string, event: MatrixRawEvent, error: Error) => {
|
|
111
|
+
logger.warn("Failed to decrypt message", {
|
|
112
|
+
roomId,
|
|
113
|
+
eventId: event.event_id,
|
|
114
|
+
error: error.message,
|
|
115
|
+
});
|
|
116
|
+
logVerboseMessage(
|
|
117
|
+
`badgerclaw: failed decrypt room=${roomId} id=${event.event_id ?? "unknown"} error=${error.message}`,
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
client.on("room.invite", (roomId: string, event: MatrixRawEvent) => {
|
|
123
|
+
const eventId = event?.event_id ?? "unknown";
|
|
124
|
+
const sender = event?.sender ?? "unknown";
|
|
125
|
+
const isDirect = (event?.content as { is_direct?: boolean } | undefined)?.is_direct === true;
|
|
126
|
+
logVerboseMessage(
|
|
127
|
+
`badgerclaw: invite room=${roomId} sender=${sender} direct=${String(isDirect)} id=${eventId}`,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
client.on("room.join", (roomId: string, event: MatrixRawEvent) => {
|
|
132
|
+
const eventId = event?.event_id ?? "unknown";
|
|
133
|
+
logVerboseMessage(`badgerclaw: join room=${roomId} id=${eventId}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
client.on("room.event", (roomId: string, event: MatrixRawEvent) => {
|
|
137
|
+
const eventType = event?.type ?? "unknown";
|
|
138
|
+
if (eventType === EventType.RoomMessageEncrypted) {
|
|
139
|
+
logVerboseMessage(
|
|
140
|
+
`badgerclaw: encrypted raw event room=${roomId} id=${event?.event_id ?? "unknown"}`,
|
|
141
|
+
);
|
|
142
|
+
if (auth.encryption !== true && !warnedEncryptedRooms.has(roomId)) {
|
|
143
|
+
warnedEncryptedRooms.add(roomId);
|
|
144
|
+
const warning =
|
|
145
|
+
"badgerclaw: encrypted event received without encryption enabled; set channels.badgerclaw.encryption=true and verify the device to decrypt";
|
|
146
|
+
logger.warn(warning, { roomId });
|
|
147
|
+
}
|
|
148
|
+
if (auth.encryption === true && !client.crypto && !warnedCryptoMissingRooms.has(roomId)) {
|
|
149
|
+
warnedCryptoMissingRooms.add(roomId);
|
|
150
|
+
const hint = formatNativeDependencyHint({
|
|
151
|
+
packageName: "@matrix-org/matrix-sdk-crypto-nodejs",
|
|
152
|
+
manager: "pnpm",
|
|
153
|
+
downloadCommand: "node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js",
|
|
154
|
+
});
|
|
155
|
+
const warning = `badgerclaw: encryption enabled but crypto is unavailable; ${hint}`;
|
|
156
|
+
logger.warn(warning, { roomId });
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (eventType === EventType.RoomMember) {
|
|
161
|
+
const membership = (event?.content as { membership?: string } | undefined)?.membership;
|
|
162
|
+
const stateKey = (event as { state_key?: string }).state_key ?? "";
|
|
163
|
+
logVerboseMessage(
|
|
164
|
+
`badgerclaw: member event room=${roomId} stateKey=${stateKey} membership=${membership ?? "unknown"}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|