@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,62 @@
|
|
|
1
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
2
|
+
|
|
3
|
+
// Type for room message content with mentions
|
|
4
|
+
type MessageContentWithMentions = {
|
|
5
|
+
msgtype: string;
|
|
6
|
+
body: string;
|
|
7
|
+
formatted_body?: string;
|
|
8
|
+
"m.mentions"?: {
|
|
9
|
+
user_ids?: string[];
|
|
10
|
+
room?: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if the formatted_body contains a matrix.to mention link for the given user ID.
|
|
16
|
+
* Many Matrix clients (including Element) use HTML links in formatted_body instead of
|
|
17
|
+
* or in addition to the m.mentions field.
|
|
18
|
+
*/
|
|
19
|
+
function checkFormattedBodyMention(formattedBody: string | undefined, userId: string): boolean {
|
|
20
|
+
if (!formattedBody || !userId) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
// Escape special regex characters in the user ID (e.g., @user:matrix.org)
|
|
24
|
+
const escapedUserId = userId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
// Match matrix.to links with the user ID, handling both URL-encoded and plain formats
|
|
26
|
+
// Example: href="https://matrix.to/#/@user:matrix.org" or href="https://matrix.to/#/%40user%3Amatrix.org"
|
|
27
|
+
const plainPattern = new RegExp(`href=["']https://matrix\\.to/#/${escapedUserId}["']`, "i");
|
|
28
|
+
if (plainPattern.test(formattedBody)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// Also check URL-encoded version (@ -> %40, : -> %3A)
|
|
32
|
+
const encodedUserId = encodeURIComponent(userId).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
const encodedPattern = new RegExp(`href=["']https://matrix\\.to/#/${encodedUserId}["']`, "i");
|
|
34
|
+
return encodedPattern.test(formattedBody);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveMentions(params: {
|
|
38
|
+
content: MessageContentWithMentions;
|
|
39
|
+
userId?: string | null;
|
|
40
|
+
text?: string;
|
|
41
|
+
mentionRegexes: RegExp[];
|
|
42
|
+
}) {
|
|
43
|
+
const mentions = params.content["m.mentions"];
|
|
44
|
+
const mentionedUsers = Array.isArray(mentions?.user_ids)
|
|
45
|
+
? new Set(mentions.user_ids)
|
|
46
|
+
: new Set<string>();
|
|
47
|
+
|
|
48
|
+
// Check formatted_body for matrix.to mention links (legacy/alternative mention format)
|
|
49
|
+
const mentionedInFormattedBody = params.userId
|
|
50
|
+
? checkFormattedBodyMention(params.content.formatted_body, params.userId)
|
|
51
|
+
: false;
|
|
52
|
+
|
|
53
|
+
const wasMentioned =
|
|
54
|
+
Boolean(mentions?.room) ||
|
|
55
|
+
(params.userId ? mentionedUsers.has(params.userId) : false) ||
|
|
56
|
+
mentionedInFormattedBody ||
|
|
57
|
+
getMatrixRuntime().channel.mentions.matchesMentionPatterns(
|
|
58
|
+
params.text ?? "",
|
|
59
|
+
params.mentionRegexes,
|
|
60
|
+
);
|
|
61
|
+
return { wasMentioned, hasExplicitMention: Boolean(mentions) };
|
|
62
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk/matrix";
|
|
3
|
+
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
|
+
import { sendMessageMatrix } from "../send.js";
|
|
5
|
+
|
|
6
|
+
export async function deliverMatrixReplies(params: {
|
|
7
|
+
replies: ReplyPayload[];
|
|
8
|
+
roomId: string;
|
|
9
|
+
client: MatrixClient;
|
|
10
|
+
runtime: RuntimeEnv;
|
|
11
|
+
textLimit: number;
|
|
12
|
+
replyToMode: "off" | "first" | "all";
|
|
13
|
+
threadId?: string;
|
|
14
|
+
accountId?: string;
|
|
15
|
+
tableMode?: MarkdownTableMode;
|
|
16
|
+
}): Promise<void> {
|
|
17
|
+
const core = getMatrixRuntime();
|
|
18
|
+
const cfg = core.config.loadConfig();
|
|
19
|
+
const tableMode =
|
|
20
|
+
params.tableMode ??
|
|
21
|
+
core.channel.text.resolveMarkdownTableMode({
|
|
22
|
+
cfg,
|
|
23
|
+
channel: "badgerclaw",
|
|
24
|
+
accountId: params.accountId,
|
|
25
|
+
});
|
|
26
|
+
const logVerbose = (message: string) => {
|
|
27
|
+
if (core.logging.shouldLogVerbose()) {
|
|
28
|
+
params.runtime.log?.(message);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const chunkLimit = Math.min(params.textLimit, 4000);
|
|
32
|
+
const chunkMode = core.channel.text.resolveChunkMode(cfg, "badgerclaw", params.accountId);
|
|
33
|
+
let hasReplied = false;
|
|
34
|
+
for (const reply of params.replies) {
|
|
35
|
+
const hasMedia = Boolean(reply?.mediaUrl) || (reply?.mediaUrls?.length ?? 0) > 0;
|
|
36
|
+
if (!reply?.text && !hasMedia) {
|
|
37
|
+
if (reply?.audioAsVoice) {
|
|
38
|
+
logVerbose("badgerclaw reply has audioAsVoice without media/text; skipping");
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
params.runtime.error?.("badgerclaw reply missing text/media");
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Skip pure reasoning messages so internal thinking traces are never delivered.
|
|
45
|
+
if (reply.text && isReasoningOnlyMessage(reply.text)) {
|
|
46
|
+
logVerbose("badgerclaw reply is reasoning-only; skipping");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const replyToIdRaw = reply.replyToId?.trim();
|
|
50
|
+
const replyToId = params.threadId || params.replyToMode === "off" ? undefined : replyToIdRaw;
|
|
51
|
+
const rawText = reply.text ?? "";
|
|
52
|
+
const text = core.channel.text.convertMarkdownTables(rawText, tableMode);
|
|
53
|
+
const mediaList = reply.mediaUrls?.length
|
|
54
|
+
? reply.mediaUrls
|
|
55
|
+
: reply.mediaUrl
|
|
56
|
+
? [reply.mediaUrl]
|
|
57
|
+
: [];
|
|
58
|
+
|
|
59
|
+
const shouldIncludeReply = (id?: string) =>
|
|
60
|
+
Boolean(id) && (params.replyToMode === "all" || !hasReplied);
|
|
61
|
+
const replyToIdForReply = shouldIncludeReply(replyToId) ? replyToId : undefined;
|
|
62
|
+
|
|
63
|
+
if (mediaList.length === 0) {
|
|
64
|
+
let sentTextChunk = false;
|
|
65
|
+
for (const chunk of core.channel.text.chunkMarkdownTextWithMode(
|
|
66
|
+
text,
|
|
67
|
+
chunkLimit,
|
|
68
|
+
chunkMode,
|
|
69
|
+
)) {
|
|
70
|
+
const trimmed = chunk.trim();
|
|
71
|
+
if (!trimmed) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
await sendMessageMatrix(params.roomId, trimmed, {
|
|
75
|
+
client: params.client,
|
|
76
|
+
replyToId: replyToIdForReply,
|
|
77
|
+
threadId: params.threadId,
|
|
78
|
+
accountId: params.accountId,
|
|
79
|
+
});
|
|
80
|
+
sentTextChunk = true;
|
|
81
|
+
}
|
|
82
|
+
if (replyToIdForReply && !hasReplied && sentTextChunk) {
|
|
83
|
+
hasReplied = true;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let first = true;
|
|
89
|
+
for (const mediaUrl of mediaList) {
|
|
90
|
+
const caption = first ? text : "";
|
|
91
|
+
await sendMessageMatrix(params.roomId, caption, {
|
|
92
|
+
client: params.client,
|
|
93
|
+
mediaUrl,
|
|
94
|
+
replyToId: replyToIdForReply,
|
|
95
|
+
threadId: params.threadId,
|
|
96
|
+
audioAsVoice: reply.audioAsVoice,
|
|
97
|
+
accountId: params.accountId,
|
|
98
|
+
});
|
|
99
|
+
first = false;
|
|
100
|
+
}
|
|
101
|
+
if (replyToIdForReply && !hasReplied) {
|
|
102
|
+
hasReplied = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const REASONING_PREFIX = "Reasoning:\n";
|
|
108
|
+
const THINKING_TAG_RE = /^\s*<\s*(?:think(?:ing)?|thought|antthinking)\b/i;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect messages that contain only reasoning/thinking content and no user-facing answer.
|
|
112
|
+
* These are emitted by the agent when `includeReasoning` is active but should not
|
|
113
|
+
* be forwarded to channels that do not support a dedicated reasoning lane.
|
|
114
|
+
*/
|
|
115
|
+
function isReasoningOnlyMessage(text: string): boolean {
|
|
116
|
+
const trimmed = text.trim();
|
|
117
|
+
if (trimmed.startsWith(REASONING_PREFIX)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (THINKING_TAG_RE.test(trimmed)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
|
|
3
|
+
export type MatrixRoomInfo = {
|
|
4
|
+
name?: string;
|
|
5
|
+
canonicalAlias?: string;
|
|
6
|
+
altAliases: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createMatrixRoomInfoResolver(client: MatrixClient) {
|
|
10
|
+
const roomInfoCache = new Map<string, MatrixRoomInfo>();
|
|
11
|
+
|
|
12
|
+
const getRoomInfo = async (roomId: string): Promise<MatrixRoomInfo> => {
|
|
13
|
+
const cached = roomInfoCache.get(roomId);
|
|
14
|
+
if (cached) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
let name: string | undefined;
|
|
18
|
+
let canonicalAlias: string | undefined;
|
|
19
|
+
let altAliases: string[] = [];
|
|
20
|
+
try {
|
|
21
|
+
const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "").catch(() => null);
|
|
22
|
+
name = nameState?.name;
|
|
23
|
+
} catch {
|
|
24
|
+
// ignore
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const aliasState = await client
|
|
28
|
+
.getRoomStateEvent(roomId, "m.room.canonical_alias", "")
|
|
29
|
+
.catch(() => null);
|
|
30
|
+
canonicalAlias = aliasState?.alias;
|
|
31
|
+
altAliases = aliasState?.alt_aliases ?? [];
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
const info = { name, canonicalAlias, altAliases };
|
|
36
|
+
roomInfoCache.set(roomId, info);
|
|
37
|
+
return info;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getMemberDisplayName = async (roomId: string, userId: string): Promise<string> => {
|
|
41
|
+
try {
|
|
42
|
+
const memberState = await client
|
|
43
|
+
.getRoomStateEvent(roomId, "m.room.member", userId)
|
|
44
|
+
.catch(() => null);
|
|
45
|
+
return memberState?.displayname ?? userId;
|
|
46
|
+
} catch {
|
|
47
|
+
return userId;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
getRoomInfo,
|
|
53
|
+
getMemberDisplayName,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "openclaw/plugin-sdk/matrix";
|
|
2
|
+
import type { MatrixRoomConfig } from "../../types.js";
|
|
3
|
+
|
|
4
|
+
export type MatrixRoomConfigResolved = {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
allowlistConfigured: boolean;
|
|
7
|
+
config?: MatrixRoomConfig;
|
|
8
|
+
matchKey?: string;
|
|
9
|
+
matchSource?: "direct" | "wildcard";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function resolveMatrixRoomConfig(params: {
|
|
13
|
+
rooms?: Record<string, MatrixRoomConfig>;
|
|
14
|
+
roomId: string;
|
|
15
|
+
aliases: string[];
|
|
16
|
+
name?: string | null;
|
|
17
|
+
}): MatrixRoomConfigResolved {
|
|
18
|
+
const rooms = params.rooms ?? {};
|
|
19
|
+
const keys = Object.keys(rooms);
|
|
20
|
+
const allowlistConfigured = keys.length > 0;
|
|
21
|
+
const candidates = buildChannelKeyCandidates(
|
|
22
|
+
params.roomId,
|
|
23
|
+
`room:${params.roomId}`,
|
|
24
|
+
...params.aliases,
|
|
25
|
+
);
|
|
26
|
+
const {
|
|
27
|
+
entry: matched,
|
|
28
|
+
key: matchedKey,
|
|
29
|
+
wildcardEntry,
|
|
30
|
+
wildcardKey,
|
|
31
|
+
} = resolveChannelEntryMatch({
|
|
32
|
+
entries: rooms,
|
|
33
|
+
keys: candidates,
|
|
34
|
+
wildcardKey: "*",
|
|
35
|
+
});
|
|
36
|
+
const resolved = matched ?? wildcardEntry;
|
|
37
|
+
const allowed = resolved ? resolved.enabled !== false && resolved.allow !== false : false;
|
|
38
|
+
const matchKey = matchedKey ?? wildcardKey;
|
|
39
|
+
const matchSource = matched ? "direct" : wildcardEntry ? "wildcard" : undefined;
|
|
40
|
+
return {
|
|
41
|
+
allowed,
|
|
42
|
+
allowlistConfigured,
|
|
43
|
+
config: resolved,
|
|
44
|
+
matchKey,
|
|
45
|
+
matchSource,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Type for raw Matrix event from @vector-im/matrix-bot-sdk
|
|
2
|
+
type MatrixRawEvent = {
|
|
3
|
+
event_id: string;
|
|
4
|
+
sender: string;
|
|
5
|
+
type: string;
|
|
6
|
+
origin_server_ts: number;
|
|
7
|
+
content: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type RoomMessageEventContent = {
|
|
11
|
+
msgtype: string;
|
|
12
|
+
body: string;
|
|
13
|
+
"m.relates_to"?: {
|
|
14
|
+
rel_type?: string;
|
|
15
|
+
event_id?: string;
|
|
16
|
+
"m.in_reply_to"?: { event_id?: string };
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const RelationType = {
|
|
21
|
+
Thread: "m.thread",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export function resolveMatrixThreadTarget(params: {
|
|
25
|
+
threadReplies: "off" | "inbound" | "always";
|
|
26
|
+
messageId: string;
|
|
27
|
+
threadRootId?: string;
|
|
28
|
+
isThreadRoot?: boolean;
|
|
29
|
+
}): string | undefined {
|
|
30
|
+
const { threadReplies, messageId, threadRootId } = params;
|
|
31
|
+
if (threadReplies === "off") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const isThreadRoot = params.isThreadRoot === true;
|
|
35
|
+
const hasInboundThread = Boolean(threadRootId && threadRootId !== messageId && !isThreadRoot);
|
|
36
|
+
if (threadReplies === "inbound") {
|
|
37
|
+
return hasInboundThread ? threadRootId : undefined;
|
|
38
|
+
}
|
|
39
|
+
if (threadReplies === "always") {
|
|
40
|
+
return threadRootId ?? messageId;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveMatrixThreadRootId(params: {
|
|
46
|
+
event: MatrixRawEvent;
|
|
47
|
+
content: RoomMessageEventContent;
|
|
48
|
+
}): string | undefined {
|
|
49
|
+
const relates = params.content["m.relates_to"];
|
|
50
|
+
if (!relates || typeof relates !== "object") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if ("rel_type" in relates && relates.rel_type === RelationType.Thread) {
|
|
54
|
+
if ("event_id" in relates && typeof relates.event_id === "string") {
|
|
55
|
+
return relates.event_id;
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
"m.in_reply_to" in relates &&
|
|
59
|
+
typeof relates["m.in_reply_to"] === "object" &&
|
|
60
|
+
relates["m.in_reply_to"] &&
|
|
61
|
+
"event_id" in relates["m.in_reply_to"] &&
|
|
62
|
+
typeof relates["m.in_reply_to"].event_id === "string"
|
|
63
|
+
) {
|
|
64
|
+
return relates["m.in_reply_to"].event_id;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EncryptedFile, MessageEventContent } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
|
|
3
|
+
export const EventType = {
|
|
4
|
+
RoomMessage: "m.room.message",
|
|
5
|
+
RoomMessageEncrypted: "m.room.encrypted",
|
|
6
|
+
RoomMember: "m.room.member",
|
|
7
|
+
Location: "m.location",
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export const RelationType = {
|
|
11
|
+
Replace: "m.replace",
|
|
12
|
+
Thread: "m.thread",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type MatrixRawEvent = {
|
|
16
|
+
event_id: string;
|
|
17
|
+
sender: string;
|
|
18
|
+
type: string;
|
|
19
|
+
origin_server_ts: number;
|
|
20
|
+
content: Record<string, unknown>;
|
|
21
|
+
unsigned?: {
|
|
22
|
+
age?: number;
|
|
23
|
+
redacted_because?: unknown;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type RoomMessageEventContent = MessageEventContent & {
|
|
28
|
+
url?: string;
|
|
29
|
+
file?: EncryptedFile;
|
|
30
|
+
info?: {
|
|
31
|
+
mimetype?: string;
|
|
32
|
+
size?: number;
|
|
33
|
+
};
|
|
34
|
+
"m.relates_to"?: {
|
|
35
|
+
rel_type?: string;
|
|
36
|
+
event_id?: string;
|
|
37
|
+
"m.in_reply_to"?: { event_id?: string };
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matrix Poll Types (MSC3381)
|
|
3
|
+
*
|
|
4
|
+
* Defines types for Matrix poll events:
|
|
5
|
+
* - m.poll.start - Creates a new poll
|
|
6
|
+
* - m.poll.response - Records a vote
|
|
7
|
+
* - m.poll.end - Closes a poll
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PollInput } from "openclaw/plugin-sdk/matrix";
|
|
11
|
+
|
|
12
|
+
export const M_POLL_START = "m.poll.start" as const;
|
|
13
|
+
export const M_POLL_RESPONSE = "m.poll.response" as const;
|
|
14
|
+
export const M_POLL_END = "m.poll.end" as const;
|
|
15
|
+
|
|
16
|
+
export const ORG_POLL_START = "org.matrix.msc3381.poll.start" as const;
|
|
17
|
+
export const ORG_POLL_RESPONSE = "org.matrix.msc3381.poll.response" as const;
|
|
18
|
+
export const ORG_POLL_END = "org.matrix.msc3381.poll.end" as const;
|
|
19
|
+
|
|
20
|
+
export const POLL_EVENT_TYPES = [
|
|
21
|
+
M_POLL_START,
|
|
22
|
+
M_POLL_RESPONSE,
|
|
23
|
+
M_POLL_END,
|
|
24
|
+
ORG_POLL_START,
|
|
25
|
+
ORG_POLL_RESPONSE,
|
|
26
|
+
ORG_POLL_END,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export const POLL_START_TYPES = [M_POLL_START, ORG_POLL_START];
|
|
30
|
+
export const POLL_RESPONSE_TYPES = [M_POLL_RESPONSE, ORG_POLL_RESPONSE];
|
|
31
|
+
export const POLL_END_TYPES = [M_POLL_END, ORG_POLL_END];
|
|
32
|
+
|
|
33
|
+
export type PollKind = "m.poll.disclosed" | "m.poll.undisclosed";
|
|
34
|
+
|
|
35
|
+
export type TextContent = {
|
|
36
|
+
"m.text"?: string;
|
|
37
|
+
"org.matrix.msc1767.text"?: string;
|
|
38
|
+
body?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type PollAnswer = {
|
|
42
|
+
id: string;
|
|
43
|
+
} & TextContent;
|
|
44
|
+
|
|
45
|
+
export type PollStartSubtype = {
|
|
46
|
+
question: TextContent;
|
|
47
|
+
kind?: PollKind;
|
|
48
|
+
max_selections?: number;
|
|
49
|
+
answers: PollAnswer[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type LegacyPollStartContent = {
|
|
53
|
+
"m.poll"?: PollStartSubtype;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type PollStartContent = {
|
|
57
|
+
[M_POLL_START]?: PollStartSubtype;
|
|
58
|
+
[ORG_POLL_START]?: PollStartSubtype;
|
|
59
|
+
"m.poll"?: PollStartSubtype;
|
|
60
|
+
"m.text"?: string;
|
|
61
|
+
"org.matrix.msc1767.text"?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type PollSummary = {
|
|
65
|
+
eventId: string;
|
|
66
|
+
roomId: string;
|
|
67
|
+
sender: string;
|
|
68
|
+
senderName: string;
|
|
69
|
+
question: string;
|
|
70
|
+
answers: string[];
|
|
71
|
+
kind: PollKind;
|
|
72
|
+
maxSelections: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function isPollStartType(eventType: string): boolean {
|
|
76
|
+
return (POLL_START_TYPES as readonly string[]).includes(eventType);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getTextContent(text?: TextContent): string {
|
|
80
|
+
if (!text) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
return text["m.text"] ?? text["org.matrix.msc1767.text"] ?? text.body ?? "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parsePollStartContent(content: PollStartContent): PollSummary | null {
|
|
87
|
+
const poll =
|
|
88
|
+
(content as Record<string, PollStartSubtype | undefined>)[M_POLL_START] ??
|
|
89
|
+
(content as Record<string, PollStartSubtype | undefined>)[ORG_POLL_START] ??
|
|
90
|
+
(content as Record<string, PollStartSubtype | undefined>)["m.poll"];
|
|
91
|
+
if (!poll) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const question = getTextContent(poll.question);
|
|
96
|
+
if (!question) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const answers = poll.answers
|
|
101
|
+
.map((answer) => getTextContent(answer))
|
|
102
|
+
.filter((a) => a.trim().length > 0);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
eventId: "",
|
|
106
|
+
roomId: "",
|
|
107
|
+
sender: "",
|
|
108
|
+
senderName: "",
|
|
109
|
+
question,
|
|
110
|
+
answers,
|
|
111
|
+
kind: poll.kind ?? "m.poll.disclosed",
|
|
112
|
+
maxSelections: poll.max_selections ?? 1,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatPollAsText(summary: PollSummary): string {
|
|
117
|
+
const lines = [
|
|
118
|
+
"[Poll]",
|
|
119
|
+
summary.question,
|
|
120
|
+
"",
|
|
121
|
+
...summary.answers.map((answer, idx) => `${idx + 1}. ${answer}`),
|
|
122
|
+
];
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildTextContent(body: string): TextContent {
|
|
127
|
+
return {
|
|
128
|
+
"m.text": body,
|
|
129
|
+
"org.matrix.msc1767.text": body,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildPollFallbackText(question: string, answers: string[]): string {
|
|
134
|
+
if (answers.length === 0) {
|
|
135
|
+
return question;
|
|
136
|
+
}
|
|
137
|
+
return `${question}\n${answers.map((answer, idx) => `${idx + 1}. ${answer}`).join("\n")}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildPollStartContent(poll: PollInput): PollStartContent {
|
|
141
|
+
const question = poll.question.trim();
|
|
142
|
+
const answers = poll.options
|
|
143
|
+
.map((option) => option.trim())
|
|
144
|
+
.filter((option) => option.length > 0)
|
|
145
|
+
.map((option, idx) => ({
|
|
146
|
+
id: `answer${idx + 1}`,
|
|
147
|
+
...buildTextContent(option),
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
const isMultiple = (poll.maxSelections ?? 1) > 1;
|
|
151
|
+
const maxSelections = isMultiple ? Math.max(1, answers.length) : 1;
|
|
152
|
+
const fallbackText = buildPollFallbackText(
|
|
153
|
+
question,
|
|
154
|
+
answers.map((answer) => getTextContent(answer)),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
[M_POLL_START]: {
|
|
159
|
+
question: buildTextContent(question),
|
|
160
|
+
kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed",
|
|
161
|
+
max_selections: maxSelections,
|
|
162
|
+
answers,
|
|
163
|
+
},
|
|
164
|
+
"m.text": fallbackText,
|
|
165
|
+
"org.matrix.msc1767.text": fallbackText,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { BaseProbeResult } from "openclaw/plugin-sdk/matrix";
|
|
2
|
+
import { createMatrixClient, isBunRuntime } from "./client.js";
|
|
3
|
+
|
|
4
|
+
export type MatrixProbe = BaseProbeResult & {
|
|
5
|
+
status?: number | null;
|
|
6
|
+
elapsedMs: number;
|
|
7
|
+
userId?: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function probeMatrix(params: {
|
|
11
|
+
homeserver: string;
|
|
12
|
+
accessToken: string;
|
|
13
|
+
userId?: string;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
}): Promise<MatrixProbe> {
|
|
16
|
+
const started = Date.now();
|
|
17
|
+
const result: MatrixProbe = {
|
|
18
|
+
ok: false,
|
|
19
|
+
status: null,
|
|
20
|
+
error: null,
|
|
21
|
+
elapsedMs: 0,
|
|
22
|
+
};
|
|
23
|
+
if (isBunRuntime()) {
|
|
24
|
+
return {
|
|
25
|
+
...result,
|
|
26
|
+
error: "BadgerClaw probe requires Node (bun runtime not supported)",
|
|
27
|
+
elapsedMs: Date.now() - started,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!params.homeserver?.trim()) {
|
|
31
|
+
return {
|
|
32
|
+
...result,
|
|
33
|
+
error: "missing homeserver",
|
|
34
|
+
elapsedMs: Date.now() - started,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!params.accessToken?.trim()) {
|
|
38
|
+
return {
|
|
39
|
+
...result,
|
|
40
|
+
error: "missing access token",
|
|
41
|
+
elapsedMs: Date.now() - started,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const client = await createMatrixClient({
|
|
46
|
+
homeserver: params.homeserver,
|
|
47
|
+
userId: params.userId ?? "",
|
|
48
|
+
accessToken: params.accessToken,
|
|
49
|
+
localTimeoutMs: params.timeoutMs,
|
|
50
|
+
});
|
|
51
|
+
// @vector-im/matrix-bot-sdk uses getUserId() which calls whoami internally
|
|
52
|
+
const userId = await client.getUserId();
|
|
53
|
+
result.ok = true;
|
|
54
|
+
result.userId = userId ?? null;
|
|
55
|
+
|
|
56
|
+
result.elapsedMs = Date.now() - started;
|
|
57
|
+
return result;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
...result,
|
|
61
|
+
status:
|
|
62
|
+
typeof err === "object" && err && "statusCode" in err
|
|
63
|
+
? Number((err as { statusCode?: number }).statusCode)
|
|
64
|
+
: result.status,
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
elapsedMs: Date.now() - started,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
type MatrixSdkRuntime = typeof import("@vector-im/matrix-bot-sdk");
|
|
4
|
+
|
|
5
|
+
let cachedMatrixSdkRuntime: MatrixSdkRuntime | null = null;
|
|
6
|
+
|
|
7
|
+
export function loadMatrixSdk(): MatrixSdkRuntime {
|
|
8
|
+
if (cachedMatrixSdkRuntime) {
|
|
9
|
+
return cachedMatrixSdkRuntime;
|
|
10
|
+
}
|
|
11
|
+
const req = createRequire(import.meta.url);
|
|
12
|
+
cachedMatrixSdkRuntime = req("@vector-im/matrix-bot-sdk") as MatrixSdkRuntime;
|
|
13
|
+
return cachedMatrixSdkRuntime;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getMatrixLogService() {
|
|
17
|
+
return loadMatrixSdk().LogService;
|
|
18
|
+
}
|