mirror of https://github.com/openclaw/openclaw.git
499 lines
18 KiB
TypeScript
499 lines
18 KiB
TypeScript
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
import { resolveMatrixAccountConfig } from "./matrix/accounts.js";
|
|
import {
|
|
bootstrapMatrixVerification,
|
|
acceptMatrixVerification,
|
|
cancelMatrixVerification,
|
|
confirmMatrixVerificationReciprocateQr,
|
|
confirmMatrixVerificationSas,
|
|
deleteMatrixMessage,
|
|
editMatrixMessage,
|
|
generateMatrixVerificationQr,
|
|
getMatrixEncryptionStatus,
|
|
getMatrixRoomKeyBackupStatus,
|
|
getMatrixVerificationStatus,
|
|
getMatrixMemberInfo,
|
|
getMatrixRoomInfo,
|
|
getMatrixVerificationSas,
|
|
listMatrixPins,
|
|
listMatrixReactions,
|
|
listMatrixVerifications,
|
|
mismatchMatrixVerificationSas,
|
|
pinMatrixMessage,
|
|
readMatrixMessages,
|
|
requestMatrixVerification,
|
|
restoreMatrixRoomKeyBackup,
|
|
removeMatrixReactions,
|
|
scanMatrixVerificationQr,
|
|
sendMatrixMessage,
|
|
startMatrixVerification,
|
|
unpinMatrixMessage,
|
|
voteMatrixPoll,
|
|
verifyMatrixRecoveryKey,
|
|
} from "./matrix/actions.js";
|
|
import { reactMatrixMessage } from "./matrix/send.js";
|
|
import { applyMatrixProfileUpdate } from "./profile-update.js";
|
|
import {
|
|
createActionGate,
|
|
jsonResult,
|
|
readNumberParam,
|
|
readReactionParams,
|
|
readStringArrayParam,
|
|
readStringParam,
|
|
} from "./runtime-api.js";
|
|
import type { CoreConfig } from "./types.js";
|
|
|
|
const messageActions = new Set(["sendMessage", "editMessage", "deleteMessage", "readMessages"]);
|
|
const reactionActions = new Set(["react", "reactions"]);
|
|
const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]);
|
|
const pollActions = new Set(["pollVote"]);
|
|
const profileActions = new Set(["setProfile"]);
|
|
const verificationActions = new Set([
|
|
"encryptionStatus",
|
|
"verificationList",
|
|
"verificationRequest",
|
|
"verificationAccept",
|
|
"verificationCancel",
|
|
"verificationStart",
|
|
"verificationGenerateQr",
|
|
"verificationScanQr",
|
|
"verificationSas",
|
|
"verificationConfirm",
|
|
"verificationMismatch",
|
|
"verificationConfirmQr",
|
|
"verificationStatus",
|
|
"verificationBootstrap",
|
|
"verificationRecoveryKey",
|
|
"verificationBackupStatus",
|
|
"verificationBackupRestore",
|
|
]);
|
|
|
|
function readRoomId(params: Record<string, unknown>, required = true): string {
|
|
const direct = readStringParam(params, "roomId") ?? readStringParam(params, "channelId");
|
|
if (direct) {
|
|
return direct;
|
|
}
|
|
if (!required) {
|
|
return readStringParam(params, "to") ?? "";
|
|
}
|
|
return readStringParam(params, "to", { required: true });
|
|
}
|
|
|
|
function toSnakeCaseKey(key: string): string {
|
|
return key
|
|
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
.toLowerCase();
|
|
}
|
|
|
|
function readRawParam(params: Record<string, unknown>, key: string): unknown {
|
|
if (Object.hasOwn(params, key)) {
|
|
return params[key];
|
|
}
|
|
const snakeKey = toSnakeCaseKey(key);
|
|
if (snakeKey !== key && Object.hasOwn(params, snakeKey)) {
|
|
return params[snakeKey];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function readStringAliasParam(
|
|
params: Record<string, unknown>,
|
|
keys: string[],
|
|
options: { required?: boolean } = {},
|
|
): string | undefined {
|
|
for (const key of keys) {
|
|
const raw = readRawParam(params, key);
|
|
if (typeof raw !== "string") {
|
|
continue;
|
|
}
|
|
const trimmed = raw.trim();
|
|
if (trimmed) {
|
|
return trimmed;
|
|
}
|
|
}
|
|
if (options.required) {
|
|
throw new Error(`${keys[0]} required`);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function readNumericArrayParam(
|
|
params: Record<string, unknown>,
|
|
key: string,
|
|
options: { integer?: boolean } = {},
|
|
): number[] {
|
|
const { integer = false } = options;
|
|
const raw = readRawParam(params, key);
|
|
if (raw === undefined) {
|
|
return [];
|
|
}
|
|
return (Array.isArray(raw) ? raw : [raw])
|
|
.map((value) => {
|
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
const parsed = Number(trimmed);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
})
|
|
.filter((value): value is number => value !== null)
|
|
.map((value) => (integer ? Math.trunc(value) : value));
|
|
}
|
|
|
|
export async function handleMatrixAction(
|
|
params: Record<string, unknown>,
|
|
cfg: CoreConfig,
|
|
opts: { mediaLocalRoots?: readonly string[] } = {},
|
|
): Promise<AgentToolResult<unknown>> {
|
|
const action = readStringParam(params, "action", { required: true });
|
|
const accountId = readStringParam(params, "accountId") ?? undefined;
|
|
const isActionEnabled = createActionGate(resolveMatrixAccountConfig({ cfg, accountId }).actions);
|
|
const clientOpts = {
|
|
cfg,
|
|
...(accountId ? { accountId } : {}),
|
|
};
|
|
|
|
if (reactionActions.has(action)) {
|
|
if (!isActionEnabled("reactions")) {
|
|
throw new Error("Matrix reactions are disabled.");
|
|
}
|
|
const roomId = readRoomId(params);
|
|
const messageId = readStringParam(params, "messageId", { required: true });
|
|
if (action === "react") {
|
|
const { emoji, remove, isEmpty } = readReactionParams(params, {
|
|
removeErrorMessage: "Emoji is required to remove a Matrix reaction.",
|
|
});
|
|
if (remove || isEmpty) {
|
|
const result = await removeMatrixReactions(roomId, messageId, {
|
|
...clientOpts,
|
|
emoji: remove ? emoji : undefined,
|
|
});
|
|
return jsonResult({ ok: true, removed: result.removed });
|
|
}
|
|
await reactMatrixMessage(roomId, messageId, emoji, clientOpts);
|
|
return jsonResult({ ok: true, added: emoji });
|
|
}
|
|
const limit = readNumberParam(params, "limit", { integer: true });
|
|
const reactions = await listMatrixReactions(roomId, messageId, {
|
|
...clientOpts,
|
|
limit: limit ?? undefined,
|
|
});
|
|
return jsonResult({ ok: true, reactions });
|
|
}
|
|
|
|
if (pollActions.has(action)) {
|
|
const roomId = readRoomId(params);
|
|
const pollId = readStringAliasParam(params, ["pollId", "messageId"], { required: true });
|
|
if (!pollId) {
|
|
throw new Error("pollId required");
|
|
}
|
|
const optionId = readStringParam(params, "pollOptionId");
|
|
const optionIndex = readNumberParam(params, "pollOptionIndex", { integer: true });
|
|
const optionIds = [
|
|
...(readStringArrayParam(params, "pollOptionIds") ?? []),
|
|
...(optionId ? [optionId] : []),
|
|
];
|
|
const optionIndexes = [
|
|
...readNumericArrayParam(params, "pollOptionIndexes", { integer: true }),
|
|
...(optionIndex !== undefined ? [optionIndex] : []),
|
|
];
|
|
const result = await voteMatrixPoll(roomId, pollId, {
|
|
...clientOpts,
|
|
optionIds,
|
|
optionIndexes,
|
|
});
|
|
return jsonResult({ ok: true, result });
|
|
}
|
|
|
|
if (messageActions.has(action)) {
|
|
if (!isActionEnabled("messages")) {
|
|
throw new Error("Matrix messages are disabled.");
|
|
}
|
|
switch (action) {
|
|
case "sendMessage": {
|
|
const to = readStringParam(params, "to", { required: true });
|
|
const mediaUrl =
|
|
readStringParam(params, "mediaUrl", { trim: false }) ??
|
|
readStringParam(params, "media", { trim: false }) ??
|
|
readStringParam(params, "filePath", { trim: false }) ??
|
|
readStringParam(params, "path", { trim: false });
|
|
const content = readStringParam(params, "content", {
|
|
required: !mediaUrl,
|
|
allowEmpty: true,
|
|
});
|
|
const replyToId =
|
|
readStringParam(params, "replyToId") ?? readStringParam(params, "replyTo");
|
|
const threadId = readStringParam(params, "threadId");
|
|
const audioAsVoice =
|
|
typeof readRawParam(params, "audioAsVoice") === "boolean"
|
|
? (readRawParam(params, "audioAsVoice") as boolean)
|
|
: typeof readRawParam(params, "asVoice") === "boolean"
|
|
? (readRawParam(params, "asVoice") as boolean)
|
|
: undefined;
|
|
const result = await sendMatrixMessage(to, content, {
|
|
mediaUrl: mediaUrl ?? undefined,
|
|
mediaLocalRoots: opts.mediaLocalRoots,
|
|
replyToId: replyToId ?? undefined,
|
|
threadId: threadId ?? undefined,
|
|
audioAsVoice,
|
|
...clientOpts,
|
|
});
|
|
return jsonResult({ ok: true, result });
|
|
}
|
|
case "editMessage": {
|
|
const roomId = readRoomId(params);
|
|
const messageId = readStringParam(params, "messageId", { required: true });
|
|
const content = readStringParam(params, "content", { required: true });
|
|
const result = await editMatrixMessage(roomId, messageId, content, clientOpts);
|
|
return jsonResult({ ok: true, result });
|
|
}
|
|
case "deleteMessage": {
|
|
const roomId = readRoomId(params);
|
|
const messageId = readStringParam(params, "messageId", { required: true });
|
|
const reason = readStringParam(params, "reason");
|
|
await deleteMatrixMessage(roomId, messageId, {
|
|
reason: reason ?? undefined,
|
|
...clientOpts,
|
|
});
|
|
return jsonResult({ ok: true, deleted: true });
|
|
}
|
|
case "readMessages": {
|
|
const roomId = readRoomId(params);
|
|
const limit = readNumberParam(params, "limit", { integer: true });
|
|
const before = readStringParam(params, "before");
|
|
const after = readStringParam(params, "after");
|
|
const result = await readMatrixMessages(roomId, {
|
|
limit: limit ?? undefined,
|
|
before: before ?? undefined,
|
|
after: after ?? undefined,
|
|
...clientOpts,
|
|
});
|
|
return jsonResult({ ok: true, ...result });
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pinActions.has(action)) {
|
|
if (!isActionEnabled("pins")) {
|
|
throw new Error("Matrix pins are disabled.");
|
|
}
|
|
const roomId = readRoomId(params);
|
|
if (action === "pinMessage") {
|
|
const messageId = readStringParam(params, "messageId", { required: true });
|
|
const result = await pinMatrixMessage(roomId, messageId, clientOpts);
|
|
return jsonResult({ ok: true, pinned: result.pinned });
|
|
}
|
|
if (action === "unpinMessage") {
|
|
const messageId = readStringParam(params, "messageId", { required: true });
|
|
const result = await unpinMatrixMessage(roomId, messageId, clientOpts);
|
|
return jsonResult({ ok: true, pinned: result.pinned });
|
|
}
|
|
const result = await listMatrixPins(roomId, clientOpts);
|
|
return jsonResult({ ok: true, pinned: result.pinned, events: result.events });
|
|
}
|
|
|
|
if (profileActions.has(action)) {
|
|
if (!isActionEnabled("profile")) {
|
|
throw new Error("Matrix profile updates are disabled.");
|
|
}
|
|
const avatarPath =
|
|
readStringParam(params, "avatarPath") ??
|
|
readStringParam(params, "path") ??
|
|
readStringParam(params, "filePath");
|
|
const result = await applyMatrixProfileUpdate({
|
|
cfg,
|
|
account: accountId,
|
|
displayName: readStringParam(params, "displayName") ?? readStringParam(params, "name"),
|
|
avatarUrl: readStringParam(params, "avatarUrl"),
|
|
avatarPath,
|
|
mediaLocalRoots: opts.mediaLocalRoots,
|
|
});
|
|
return jsonResult({ ok: true, ...result });
|
|
}
|
|
|
|
if (action === "memberInfo") {
|
|
if (!isActionEnabled("memberInfo")) {
|
|
throw new Error("Matrix member info is disabled.");
|
|
}
|
|
const userId = readStringParam(params, "userId", { required: true });
|
|
const roomId = readStringParam(params, "roomId") ?? readStringParam(params, "channelId");
|
|
const result = await getMatrixMemberInfo(userId, {
|
|
roomId: roomId ?? undefined,
|
|
...clientOpts,
|
|
});
|
|
return jsonResult({ ok: true, member: result });
|
|
}
|
|
|
|
if (action === "channelInfo") {
|
|
if (!isActionEnabled("channelInfo")) {
|
|
throw new Error("Matrix room info is disabled.");
|
|
}
|
|
const roomId = readRoomId(params);
|
|
const result = await getMatrixRoomInfo(roomId, clientOpts);
|
|
return jsonResult({ ok: true, room: result });
|
|
}
|
|
|
|
if (verificationActions.has(action)) {
|
|
if (!isActionEnabled("verification")) {
|
|
throw new Error("Matrix verification actions are disabled.");
|
|
}
|
|
|
|
const requestId =
|
|
readStringParam(params, "requestId") ??
|
|
readStringParam(params, "verificationId") ??
|
|
readStringParam(params, "id");
|
|
|
|
if (action === "encryptionStatus") {
|
|
const includeRecoveryKey = params.includeRecoveryKey === true;
|
|
const status = await getMatrixEncryptionStatus({ includeRecoveryKey, ...clientOpts });
|
|
return jsonResult({ ok: true, status });
|
|
}
|
|
if (action === "verificationStatus") {
|
|
const includeRecoveryKey = params.includeRecoveryKey === true;
|
|
const status = await getMatrixVerificationStatus({ includeRecoveryKey, ...clientOpts });
|
|
return jsonResult({ ok: true, status });
|
|
}
|
|
if (action === "verificationBootstrap") {
|
|
const recoveryKey =
|
|
readStringParam(params, "recoveryKey", { trim: false }) ??
|
|
readStringParam(params, "key", { trim: false });
|
|
const result = await bootstrapMatrixVerification({
|
|
recoveryKey: recoveryKey ?? undefined,
|
|
forceResetCrossSigning: params.forceResetCrossSigning === true,
|
|
...clientOpts,
|
|
});
|
|
return jsonResult({ ok: result.success, result });
|
|
}
|
|
if (action === "verificationRecoveryKey") {
|
|
const recoveryKey =
|
|
readStringParam(params, "recoveryKey", { trim: false }) ??
|
|
readStringParam(params, "key", { trim: false });
|
|
const result = await verifyMatrixRecoveryKey(
|
|
readStringParam({ recoveryKey }, "recoveryKey", { required: true, trim: false }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: result.success, result });
|
|
}
|
|
if (action === "verificationBackupStatus") {
|
|
const status = await getMatrixRoomKeyBackupStatus(clientOpts);
|
|
return jsonResult({ ok: true, status });
|
|
}
|
|
if (action === "verificationBackupRestore") {
|
|
const recoveryKey =
|
|
readStringParam(params, "recoveryKey", { trim: false }) ??
|
|
readStringParam(params, "key", { trim: false });
|
|
const result = await restoreMatrixRoomKeyBackup({
|
|
recoveryKey: recoveryKey ?? undefined,
|
|
...clientOpts,
|
|
});
|
|
return jsonResult({ ok: result.success, result });
|
|
}
|
|
if (action === "verificationList") {
|
|
const verifications = await listMatrixVerifications(clientOpts);
|
|
return jsonResult({ ok: true, verifications });
|
|
}
|
|
if (action === "verificationRequest") {
|
|
const userId = readStringParam(params, "userId");
|
|
const deviceId = readStringParam(params, "deviceId");
|
|
const roomId = readStringParam(params, "roomId") ?? readStringParam(params, "channelId");
|
|
const ownUser = typeof params.ownUser === "boolean" ? params.ownUser : undefined;
|
|
const verification = await requestMatrixVerification({
|
|
ownUser,
|
|
userId: userId ?? undefined,
|
|
deviceId: deviceId ?? undefined,
|
|
roomId: roomId ?? undefined,
|
|
...clientOpts,
|
|
});
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
if (action === "verificationAccept") {
|
|
const verification = await acceptMatrixVerification(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
if (action === "verificationCancel") {
|
|
const reason = readStringParam(params, "reason");
|
|
const code = readStringParam(params, "code");
|
|
const verification = await cancelMatrixVerification(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
{ reason: reason ?? undefined, code: code ?? undefined, ...clientOpts },
|
|
);
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
if (action === "verificationStart") {
|
|
const methodRaw = readStringParam(params, "method");
|
|
const method = methodRaw?.trim().toLowerCase();
|
|
if (method && method !== "sas") {
|
|
throw new Error(
|
|
"Matrix verificationStart only supports method=sas; use verificationGenerateQr/verificationScanQr for QR flows.",
|
|
);
|
|
}
|
|
const verification = await startMatrixVerification(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
{ method: "sas", ...clientOpts },
|
|
);
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
if (action === "verificationGenerateQr") {
|
|
const qr = await generateMatrixVerificationQr(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: true, ...qr });
|
|
}
|
|
if (action === "verificationScanQr") {
|
|
const qrDataBase64 =
|
|
readStringParam(params, "qrDataBase64") ??
|
|
readStringParam(params, "qrData") ??
|
|
readStringParam(params, "qr");
|
|
const verification = await scanMatrixVerificationQr(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
readStringParam({ qrDataBase64 }, "qrDataBase64", { required: true }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
if (action === "verificationSas") {
|
|
const sas = await getMatrixVerificationSas(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: true, sas });
|
|
}
|
|
if (action === "verificationConfirm") {
|
|
const verification = await confirmMatrixVerificationSas(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
if (action === "verificationMismatch") {
|
|
const verification = await mismatchMatrixVerificationSas(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
if (action === "verificationConfirmQr") {
|
|
const verification = await confirmMatrixVerificationReciprocateQr(
|
|
readStringParam({ requestId }, "requestId", { required: true }),
|
|
clientOpts,
|
|
);
|
|
return jsonResult({ ok: true, verification });
|
|
}
|
|
}
|
|
|
|
throw new Error(`Unsupported Matrix action: ${action}`);
|
|
}
|