import { Type } from "@sinclair/typebox"; import { requiresExplicitMatrixDefaultAccount } from "./account-selection.js"; import { resolveDefaultMatrixAccountId, resolveMatrixAccount } from "./matrix/accounts.js"; import { createActionGate, readNumberParam, readStringParam, type ChannelMessageActionAdapter, type ChannelMessageActionContext, type ChannelMessageActionName, type ChannelMessageToolDiscovery, type ChannelToolSend, } from "./runtime-api.js"; import type { CoreConfig } from "./types.js"; const MATRIX_PLUGIN_HANDLED_ACTIONS = new Set([ "send", "poll-vote", "react", "reactions", "read", "edit", "delete", "pin", "unpin", "list-pins", "set-profile", "member-info", "channel-info", "permissions", ]); function createMatrixExposedActions(params: { gate: ReturnType; encryptionEnabled: boolean; }) { const actions = new Set(["poll", "poll-vote"]); if (params.gate("messages")) { actions.add("send"); actions.add("read"); actions.add("edit"); actions.add("delete"); } if (params.gate("reactions")) { actions.add("react"); actions.add("reactions"); } if (params.gate("pins")) { actions.add("pin"); actions.add("unpin"); actions.add("list-pins"); } if (params.gate("profile")) { actions.add("set-profile"); } if (params.gate("memberInfo")) { actions.add("member-info"); } if (params.gate("channelInfo")) { actions.add("channel-info"); } if (params.encryptionEnabled && params.gate("verification")) { actions.add("permissions"); } return actions; } function buildMatrixProfileToolSchema(): NonNullable { return { properties: { displayName: Type.Optional( Type.String({ description: "Profile display name for Matrix self-profile update actions.", }), ), display_name: Type.Optional( Type.String({ description: "snake_case alias of displayName for Matrix self-profile update actions.", }), ), avatarUrl: Type.Optional( Type.String({ description: "Profile avatar URL for Matrix self-profile update actions. Matrix accepts mxc:// and http(s) URLs.", }), ), avatar_url: Type.Optional( Type.String({ description: "snake_case alias of avatarUrl for Matrix self-profile update actions. Matrix accepts mxc:// and http(s) URLs.", }), ), avatarPath: Type.Optional( Type.String({ description: "Local avatar file path for Matrix self-profile update actions. Matrix uploads this file and sets the resulting MXC URI.", }), ), avatar_path: Type.Optional( Type.String({ description: "snake_case alias of avatarPath for Matrix self-profile update actions. Matrix uploads this file and sets the resulting MXC URI.", }), ), }, }; } export const matrixMessageActions: ChannelMessageActionAdapter = { describeMessageTool: ({ cfg, accountId }) => { const resolvedCfg = cfg as CoreConfig; if (!accountId && requiresExplicitMatrixDefaultAccount(resolvedCfg)) { return { actions: [], capabilities: [] }; } const account = resolveMatrixAccount({ cfg: resolvedCfg, accountId: accountId ?? resolveDefaultMatrixAccountId(resolvedCfg), }); if (!account.enabled || !account.configured) { return { actions: [], capabilities: [] }; } const gate = createActionGate(account.config.actions); const actions = createMatrixExposedActions({ gate, encryptionEnabled: account.config.encryption === true, }); const listedActions = Array.from(actions); return { actions: listedActions, capabilities: [], schema: listedActions.includes("set-profile") ? buildMatrixProfileToolSchema() : null, }; }, supportsAction: ({ action }) => MATRIX_PLUGIN_HANDLED_ACTIONS.has(action), extractToolSend: ({ args }): ChannelToolSend | null => { const action = typeof args.action === "string" ? args.action.trim() : ""; if (action !== "sendMessage") { return null; } const to = typeof args.to === "string" ? args.to : undefined; if (!to) { return null; } return { to }; }, handleAction: async (ctx: ChannelMessageActionContext) => { const { handleMatrixAction } = await import("./tool-actions.runtime.js"); const { action, params, cfg, accountId, mediaLocalRoots } = ctx; const dispatch = async (actionParams: Record) => await handleMatrixAction( { ...actionParams, ...(accountId ? { accountId } : {}), }, cfg as CoreConfig, { mediaLocalRoots }, ); const resolveRoomId = () => readStringParam(params, "roomId") ?? readStringParam(params, "channelId") ?? readStringParam(params, "to", { required: true }); if (action === "send") { const to = readStringParam(params, "to", { required: true }); const mediaUrl = readStringParam(params, "media", { trim: false }) ?? readStringParam(params, "mediaUrl", { trim: false }) ?? readStringParam(params, "filePath", { trim: false }) ?? readStringParam(params, "path", { trim: false }); const content = readStringParam(params, "message", { required: !mediaUrl, allowEmpty: true, }); const replyTo = readStringParam(params, "replyTo"); const threadId = readStringParam(params, "threadId"); const audioAsVoice = typeof params.asVoice === "boolean" ? params.asVoice : typeof params.audioAsVoice === "boolean" ? params.audioAsVoice : undefined; return await dispatch({ action: "sendMessage", to, content, mediaUrl: mediaUrl ?? undefined, replyToId: replyTo ?? undefined, threadId: threadId ?? undefined, audioAsVoice, }); } if (action === "poll-vote") { return await dispatch({ ...params, action: "pollVote", }); } if (action === "react") { const messageId = readStringParam(params, "messageId", { required: true }); const emoji = readStringParam(params, "emoji", { allowEmpty: true }); const remove = typeof params.remove === "boolean" ? params.remove : undefined; return await dispatch({ action: "react", roomId: resolveRoomId(), messageId, emoji, remove, }); } if (action === "reactions") { const messageId = readStringParam(params, "messageId", { required: true }); const limit = readNumberParam(params, "limit", { integer: true }); return await dispatch({ action: "reactions", roomId: resolveRoomId(), messageId, limit, }); } if (action === "read") { const limit = readNumberParam(params, "limit", { integer: true }); return await dispatch({ action: "readMessages", roomId: resolveRoomId(), limit, before: readStringParam(params, "before"), after: readStringParam(params, "after"), }); } if (action === "edit") { const messageId = readStringParam(params, "messageId", { required: true }); const content = readStringParam(params, "message", { required: true }); return await dispatch({ action: "editMessage", roomId: resolveRoomId(), messageId, content, }); } if (action === "delete") { const messageId = readStringParam(params, "messageId", { required: true }); return await dispatch({ action: "deleteMessage", roomId: resolveRoomId(), messageId, }); } if (action === "pin" || action === "unpin" || action === "list-pins") { const messageId = action === "list-pins" ? undefined : readStringParam(params, "messageId", { required: true }); return await dispatch({ action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins", roomId: resolveRoomId(), messageId, }); } if (action === "set-profile") { const avatarPath = readStringParam(params, "avatarPath") ?? readStringParam(params, "path") ?? readStringParam(params, "filePath"); return await dispatch({ action: "setProfile", displayName: readStringParam(params, "displayName") ?? readStringParam(params, "name"), avatarUrl: readStringParam(params, "avatarUrl"), avatarPath, }); } if (action === "member-info") { const userId = readStringParam(params, "userId", { required: true }); return await dispatch({ action: "memberInfo", userId, roomId: readStringParam(params, "roomId") ?? readStringParam(params, "channelId"), }); } if (action === "channel-info") { return await dispatch({ action: "channelInfo", roomId: resolveRoomId(), }); } if (action === "permissions") { const operation = ( readStringParam(params, "operation") ?? readStringParam(params, "mode") ?? "verification-list" ) .trim() .toLowerCase(); const operationToAction: Record = { "encryption-status": "encryptionStatus", "verification-status": "verificationStatus", "verification-bootstrap": "verificationBootstrap", "verification-recovery-key": "verificationRecoveryKey", "verification-backup-status": "verificationBackupStatus", "verification-backup-restore": "verificationBackupRestore", "verification-list": "verificationList", "verification-request": "verificationRequest", "verification-accept": "verificationAccept", "verification-cancel": "verificationCancel", "verification-start": "verificationStart", "verification-generate-qr": "verificationGenerateQr", "verification-scan-qr": "verificationScanQr", "verification-sas": "verificationSas", "verification-confirm": "verificationConfirm", "verification-mismatch": "verificationMismatch", "verification-confirm-qr": "verificationConfirmQr", }; const resolvedAction = operationToAction[operation]; if (!resolvedAction) { throw new Error( `Unsupported Matrix permissions operation: ${operation}. Supported values: ${Object.keys( operationToAction, ).join(", ")}`, ); } return await dispatch({ ...params, action: resolvedAction, }); } throw new Error(`Action ${action} is not supported for provider matrix.`); }, };