mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:20:42 +00:00
Add structured Matrix approval metadata (#72432)
Merged via squash.
Prepared head SHA: 0e06533dff
Co-authored-by: kakahu2015 <17962485+kakahu2015@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/runtime: reuse the current plugin metadata snapshot for provider discovery so repeated model-provider discovery avoids rebuilding plugin manifest metadata. Thanks @shakkernerd.
|
||||
- Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd.
|
||||
- ACP/runtime: add an opt-in bundled Coven backend extension that routes ACP coding sessions through a local Coven daemon when `acp.backend="coven"`, while preserving the existing ACPX backend as the default fallback path. Thanks @BunsDev.
|
||||
- Matrix: attach versioned structured approval metadata to pending approval messages so capable Matrix clients can render richer approval UI while body text and reaction fallback keep working. (#72432) Thanks @kakahu2015.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -208,6 +208,12 @@ Notes:
|
||||
- Media replies always send attachments normally. If a stale preview can no longer be reused safely, OpenClaw redacts it before sending the final media reply.
|
||||
- Preview edits cost extra Matrix API calls. Leave `streaming: "off"` if you want the most conservative rate-limit profile.
|
||||
|
||||
## Approval metadata
|
||||
|
||||
Matrix native approval prompts are normal `m.room.message` events with OpenClaw-specific custom event content under `com.openclaw.approval`. Matrix permits custom event-content keys, so stock clients still render the text body while OpenClaw-aware clients can read the structured approval id, kind, state, available decisions, and exec/plugin details.
|
||||
|
||||
When an approval prompt is too long for one Matrix event, OpenClaw chunks the visible text and attaches `com.openclaw.approval` to the first chunk only. Reactions for allow/deny decisions are bound to that first event, so long prompts keep the same approval target as single-event prompts.
|
||||
|
||||
### Self-hosted push rules for quiet finalized previews
|
||||
|
||||
`streaming: "quiet"` only notifies recipients once a block or turn is finalized — a per-user push rule has to match the finalized preview marker. See [Matrix push rules for quiet previews](/channels/matrix-push-rules) for the full recipe (recipient token, pusher check, rule install, per-homeserver notes).
|
||||
|
||||
@@ -301,6 +301,9 @@ Shared behavior:
|
||||
without a second Slack-local fallback layer
|
||||
- Matrix native DM/channel routing and reaction shortcuts handle both exec and plugin approvals;
|
||||
plugin authorization still comes from `channels.matrix.dm.allowFrom`
|
||||
- Matrix native prompts include `com.openclaw.approval` custom event content on the first prompt
|
||||
event so OpenClaw-aware Matrix clients can read structured approval state while stock clients
|
||||
keep the plain-text `/approve` fallback
|
||||
- the requester does not need to be an approver
|
||||
- the originating chat can approve directly with `/approve` when that chat already supports commands and replies
|
||||
- native Discord approval buttons route by approval id kind: `plugin:` ids go
|
||||
|
||||
@@ -1,7 +1,251 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { matrixApprovalNativeRuntime } from "./approval-handler.runtime.js";
|
||||
|
||||
describe("matrixApprovalNativeRuntime", () => {
|
||||
it("sends versioned Matrix approval content with pending exec approvals", async () => {
|
||||
const sendSingleTextMessage = vi.fn().mockResolvedValue({
|
||||
messageId: "$approval",
|
||||
primaryMessageId: "$approval",
|
||||
messageIds: ["$approval"],
|
||||
roomId: "!room:example.org",
|
||||
});
|
||||
const reactMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const pendingPayload = await matrixApprovalNativeRuntime.presentation.buildPendingPayload({
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
context: { client: {} as never },
|
||||
request: {
|
||||
id: "req-1",
|
||||
request: {
|
||||
command: "echo hi",
|
||||
cwd: "/repo",
|
||||
host: "gateway",
|
||||
agentId: "agent-1",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1_000,
|
||||
},
|
||||
approvalKind: "exec",
|
||||
nowMs: 100,
|
||||
view: {
|
||||
approvalKind: "exec",
|
||||
approvalId: "req-1",
|
||||
phase: "pending",
|
||||
title: "Exec Approval Required",
|
||||
description: "A command needs your approval.",
|
||||
metadata: [],
|
||||
ask: "on-request",
|
||||
agentId: "agent-1",
|
||||
commandText: "echo hi",
|
||||
commandPreview: "echo hi",
|
||||
cwd: "/repo",
|
||||
host: "gateway",
|
||||
actions: [
|
||||
{
|
||||
decision: "allow-once",
|
||||
label: "Allow Once",
|
||||
style: "success",
|
||||
command: "/approve req-1 allow-once",
|
||||
},
|
||||
{
|
||||
decision: "deny",
|
||||
label: "Deny",
|
||||
style: "danger",
|
||||
command: "/approve req-1 deny",
|
||||
},
|
||||
],
|
||||
expiresAtMs: 1_000,
|
||||
} as never,
|
||||
});
|
||||
|
||||
await matrixApprovalNativeRuntime.transport.deliverPending({
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
context: {
|
||||
client: {} as never,
|
||||
deps: {
|
||||
sendSingleTextMessage,
|
||||
reactMessage,
|
||||
},
|
||||
},
|
||||
request: {} as never,
|
||||
approvalKind: "exec",
|
||||
preparedTarget: {
|
||||
to: "room:!room:example.org",
|
||||
roomId: "!room:example.org",
|
||||
},
|
||||
pendingPayload,
|
||||
});
|
||||
|
||||
expect(sendSingleTextMessage).toHaveBeenCalledWith(
|
||||
"room:!room:example.org",
|
||||
expect.stringContaining("echo hi"),
|
||||
expect.objectContaining({
|
||||
extraContent: {
|
||||
"com.openclaw.approval": expect.objectContaining({
|
||||
version: 1,
|
||||
type: "approval.request",
|
||||
state: "pending",
|
||||
id: "req-1",
|
||||
kind: "exec",
|
||||
commandText: "echo hi",
|
||||
cwd: "/repo",
|
||||
agentId: "agent-1",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes plugin approval fields in Matrix approval content", async () => {
|
||||
const pendingPayload = await matrixApprovalNativeRuntime.presentation.buildPendingPayload({
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
context: { client: {} as never },
|
||||
request: {
|
||||
id: "plugin:req-1",
|
||||
request: {
|
||||
title: "Plugin Approval Required",
|
||||
description: "Approve the tool call.",
|
||||
severity: "critical",
|
||||
toolName: "deploy",
|
||||
pluginId: "ops",
|
||||
agentId: "agent-1",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1_000,
|
||||
},
|
||||
approvalKind: "plugin",
|
||||
nowMs: 100,
|
||||
view: {
|
||||
approvalKind: "plugin",
|
||||
approvalId: "plugin:req-1",
|
||||
phase: "pending",
|
||||
title: "Plugin Approval Required",
|
||||
description: "Approve the tool call.",
|
||||
metadata: [],
|
||||
agentId: "agent-1",
|
||||
pluginId: "ops",
|
||||
toolName: "deploy",
|
||||
severity: "critical",
|
||||
actions: [
|
||||
{
|
||||
decision: "allow-once",
|
||||
label: "Allow Once",
|
||||
style: "success",
|
||||
command: "/approve plugin:req-1 allow-once",
|
||||
},
|
||||
],
|
||||
expiresAtMs: 1_000,
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(pendingPayload).toMatchObject({
|
||||
extraContent: {
|
||||
"com.openclaw.approval": {
|
||||
version: 1,
|
||||
type: "approval.request",
|
||||
state: "pending",
|
||||
id: "plugin:req-1",
|
||||
kind: "plugin",
|
||||
pluginId: "ops",
|
||||
toolName: "deploy",
|
||||
agentId: "agent-1",
|
||||
severity: "critical",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to chunked Matrix delivery when approval content exceeds one event", async () => {
|
||||
const sendSingleTextMessage = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("Matrix single-message text exceeds limit (5000 > 4000)"));
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
messageId: "$last",
|
||||
primaryMessageId: "$primary",
|
||||
messageIds: ["$primary", "$last"],
|
||||
roomId: "!room:example.org",
|
||||
});
|
||||
const reactMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const pendingPayload = await matrixApprovalNativeRuntime.presentation.buildPendingPayload({
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
context: { client: {} as never },
|
||||
request: {
|
||||
id: "req-1",
|
||||
request: {
|
||||
command: "echo hi",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 1_000,
|
||||
},
|
||||
approvalKind: "exec",
|
||||
nowMs: 100,
|
||||
view: {
|
||||
approvalKind: "exec",
|
||||
approvalId: "req-1",
|
||||
phase: "pending",
|
||||
title: "Exec Approval Required",
|
||||
description: "A command needs your approval.",
|
||||
metadata: [],
|
||||
commandText: "echo hi",
|
||||
actions: [
|
||||
{
|
||||
decision: "allow-once",
|
||||
label: "Allow Once",
|
||||
style: "success",
|
||||
command: "/approve req-1 allow-once",
|
||||
},
|
||||
],
|
||||
expiresAtMs: 1_000,
|
||||
} as never,
|
||||
});
|
||||
|
||||
const entry = await matrixApprovalNativeRuntime.transport.deliverPending({
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
context: {
|
||||
client: {} as never,
|
||||
deps: {
|
||||
sendSingleTextMessage,
|
||||
sendMessage,
|
||||
reactMessage,
|
||||
},
|
||||
},
|
||||
request: {} as never,
|
||||
approvalKind: "exec",
|
||||
preparedTarget: {
|
||||
to: "room:!room:example.org",
|
||||
roomId: "!room:example.org",
|
||||
},
|
||||
pendingPayload,
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(
|
||||
"room:!room:example.org",
|
||||
pendingPayload.text,
|
||||
expect.objectContaining({
|
||||
accountId: "default",
|
||||
extraContent: pendingPayload.extraContent,
|
||||
}),
|
||||
);
|
||||
expect(reactMessage).toHaveBeenCalledWith(
|
||||
"!room:example.org",
|
||||
"$primary",
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
accountId: "default",
|
||||
}),
|
||||
);
|
||||
expect(entry).toMatchObject({
|
||||
roomId: "!room:example.org",
|
||||
messageIds: ["$primary", "$last"],
|
||||
reactionEventId: "$primary",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses a longer code fence when resolved commands contain triple backticks", async () => {
|
||||
const result = await matrixApprovalNativeRuntime.presentation.buildResolvedResult({
|
||||
cfg: {} as never,
|
||||
|
||||
@@ -26,10 +26,17 @@ import { resolveMatrixAccount } from "./matrix/accounts.js";
|
||||
import { deleteMatrixMessage, editMatrixMessage } from "./matrix/actions/messages.js";
|
||||
import { repairMatrixDirectRooms } from "./matrix/direct-management.js";
|
||||
import type { MatrixClient } from "./matrix/sdk.js";
|
||||
import { reactMatrixMessage, sendMessageMatrix } from "./matrix/send.js";
|
||||
import {
|
||||
reactMatrixMessage,
|
||||
sendMessageMatrix,
|
||||
sendSingleTextMessageMatrix,
|
||||
} from "./matrix/send.js";
|
||||
import { resolveMatrixTargetIdentity } from "./matrix/target-ids.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
// OpenClaw Matrix custom event content for capable clients; body and reactions remain fallback.
|
||||
const MATRIX_APPROVAL_METADATA_KEY = "com.openclaw.approval" as const;
|
||||
|
||||
type PendingMessage = {
|
||||
roomId: string;
|
||||
messageIds: readonly string[];
|
||||
@@ -40,10 +47,58 @@ type PreparedMatrixTarget = {
|
||||
roomId: string;
|
||||
threadId?: string;
|
||||
};
|
||||
type MatrixApprovalMetadataAction = {
|
||||
decision: ExecApprovalReplyDecision;
|
||||
label: string;
|
||||
style: PendingApprovalView["actions"][number]["style"];
|
||||
command: string;
|
||||
};
|
||||
type MatrixApprovalMetadataBase = {
|
||||
version: 1;
|
||||
type: "approval.request";
|
||||
id: string;
|
||||
state: "pending";
|
||||
kind: PendingApprovalView["approvalKind"];
|
||||
phase: "pending";
|
||||
title: string;
|
||||
description?: string;
|
||||
expiresAtMs: number;
|
||||
metadata: PendingApprovalView["metadata"];
|
||||
allowedDecisions: ExecApprovalReplyDecision[];
|
||||
actions: MatrixApprovalMetadataAction[];
|
||||
};
|
||||
type MatrixExecApprovalMetadata = MatrixApprovalMetadataBase & {
|
||||
kind: "exec";
|
||||
ask?: string;
|
||||
agentId?: string;
|
||||
commandText: string;
|
||||
commandPreview?: string;
|
||||
cwd?: string;
|
||||
envKeys?: readonly string[];
|
||||
host?: string;
|
||||
nodeId?: string;
|
||||
sessionKey?: string;
|
||||
};
|
||||
type MatrixPluginApprovalSeverity = Extract<
|
||||
PendingApprovalView,
|
||||
{ approvalKind: "plugin" }
|
||||
>["severity"];
|
||||
type MatrixPluginApprovalMetadata = MatrixApprovalMetadataBase & {
|
||||
kind: "plugin";
|
||||
agentId?: string;
|
||||
pluginId?: string;
|
||||
toolName?: string;
|
||||
severity: MatrixPluginApprovalSeverity;
|
||||
};
|
||||
type MatrixApprovalMetadata = MatrixExecApprovalMetadata | MatrixPluginApprovalMetadata;
|
||||
type MatrixApprovalExtraContent = {
|
||||
[MATRIX_APPROVAL_METADATA_KEY]: MatrixApprovalMetadata;
|
||||
};
|
||||
type PendingApprovalContent = {
|
||||
approvalId: string;
|
||||
text: string;
|
||||
allowedDecisions: readonly ExecApprovalReplyDecision[];
|
||||
extraContent: MatrixApprovalExtraContent;
|
||||
};
|
||||
type ReactionTargetRef = {
|
||||
roomId: string;
|
||||
@@ -64,6 +119,7 @@ type MatrixPrepareTargetParams = {
|
||||
export type MatrixApprovalHandlerDeps = {
|
||||
nowMs?: () => number;
|
||||
sendMessage?: typeof sendMessageMatrix;
|
||||
sendSingleTextMessage?: typeof sendSingleTextMessageMatrix;
|
||||
reactMessage?: typeof reactMatrixMessage;
|
||||
editMessage?: typeof editMatrixMessage;
|
||||
deleteMessage?: typeof deleteMatrixMessage;
|
||||
@@ -105,6 +161,12 @@ function normalizeThreadId(value?: string | number | null): string | undefined {
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function isSingleMatrixMessageLimitError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Error && error.message.includes("Matrix single-message text exceeds limit")
|
||||
);
|
||||
}
|
||||
|
||||
async function prepareTarget(
|
||||
params: MatrixPrepareTargetParams,
|
||||
): Promise<PreparedMatrixTarget | null> {
|
||||
@@ -144,6 +206,56 @@ async function prepareTarget(
|
||||
};
|
||||
}
|
||||
|
||||
function buildMatrixApprovalMetadata(params: {
|
||||
view: PendingApprovalView;
|
||||
allowedDecisions: readonly ExecApprovalReplyDecision[];
|
||||
}): MatrixApprovalMetadata {
|
||||
const base: MatrixApprovalMetadataBase = {
|
||||
version: 1,
|
||||
type: "approval.request",
|
||||
id: params.view.approvalId,
|
||||
state: "pending",
|
||||
kind: params.view.approvalKind,
|
||||
phase: params.view.phase,
|
||||
title: params.view.title,
|
||||
expiresAtMs: params.view.expiresAtMs,
|
||||
metadata: params.view.metadata,
|
||||
allowedDecisions: Array.from(params.allowedDecisions),
|
||||
actions: params.view.actions.map((action) => ({
|
||||
decision: action.decision,
|
||||
label: action.label,
|
||||
style: action.style,
|
||||
command: action.command,
|
||||
})),
|
||||
...(params.view.description != null ? { description: params.view.description } : {}),
|
||||
};
|
||||
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return {
|
||||
...base,
|
||||
kind: "plugin",
|
||||
severity: params.view.severity,
|
||||
...(params.view.agentId != null ? { agentId: params.view.agentId } : {}),
|
||||
...(params.view.pluginId != null ? { pluginId: params.view.pluginId } : {}),
|
||||
...(params.view.toolName != null ? { toolName: params.view.toolName } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
kind: "exec",
|
||||
commandText: params.view.commandText,
|
||||
...(params.view.ask != null ? { ask: params.view.ask } : {}),
|
||||
...(params.view.agentId != null ? { agentId: params.view.agentId } : {}),
|
||||
...(params.view.commandPreview != null ? { commandPreview: params.view.commandPreview } : {}),
|
||||
...(params.view.cwd != null ? { cwd: params.view.cwd } : {}),
|
||||
...(params.view.envKeys != null ? { envKeys: params.view.envKeys } : {}),
|
||||
...(params.view.host != null ? { host: params.view.host } : {}),
|
||||
...(params.view.nodeId != null ? { nodeId: params.view.nodeId } : {}),
|
||||
...(params.view.sessionKey != null ? { sessionKey: params.view.sessionKey } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildPendingApprovalContent(params: {
|
||||
view: PendingApprovalView;
|
||||
nowMs: number;
|
||||
@@ -189,6 +301,12 @@ function buildPendingApprovalContent(params: {
|
||||
approvalId: params.view.approvalId,
|
||||
text: hint ? (text ? `${hint}\n\n${text}` : hint) : text,
|
||||
allowedDecisions,
|
||||
extraContent: {
|
||||
[MATRIX_APPROVAL_METADATA_KEY]: buildMatrixApprovalMetadata({
|
||||
view: params.view,
|
||||
allowedDecisions,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -292,14 +410,31 @@ export const matrixApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
const sendMessage = resolved.context.deps?.sendMessage ?? sendMessageMatrix;
|
||||
const sendSingleTextMessage =
|
||||
resolved.context.deps?.sendSingleTextMessage ?? sendSingleTextMessageMatrix;
|
||||
const reactMessage = resolved.context.deps?.reactMessage ?? reactMatrixMessage;
|
||||
const result = await sendMessage(preparedTarget.to, pendingPayload.text, {
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
result = await sendSingleTextMessage(preparedTarget.to, pendingPayload.text, {
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
extraContent: pendingPayload.extraContent,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isSingleMatrixMessageLimitError(error)) {
|
||||
throw error;
|
||||
}
|
||||
const sendMessage = resolved.context.deps?.sendMessage ?? sendMessageMatrix;
|
||||
result = await sendMessage(preparedTarget.to, pendingPayload.text, {
|
||||
cfg: cfg as CoreConfig,
|
||||
accountId: resolved.accountId,
|
||||
client: resolved.context.client,
|
||||
threadId: preparedTarget.threadId,
|
||||
extraContent: pendingPayload.extraContent,
|
||||
});
|
||||
}
|
||||
const messageIds = Array.from(
|
||||
new Set(
|
||||
(result.messageIds ?? [result.messageId])
|
||||
|
||||
@@ -630,6 +630,28 @@ describe("sendMessageMatrix threads", () => {
|
||||
messageIds: ["$m1", "$m2", "$m3"],
|
||||
});
|
||||
});
|
||||
|
||||
it("merges extra content into only the first chunked text event", async () => {
|
||||
const { client, sendMessage } = makeClient();
|
||||
convertMarkdownTablesMock.mockImplementation(() => "first|second|third");
|
||||
chunkMarkdownTextWithModeMock.mockImplementation((text: string) => text.split("|"));
|
||||
|
||||
await sendMessageMatrix("room:!room:example", "ignored", {
|
||||
client,
|
||||
cfg: {} as never,
|
||||
extraContent: { "com.openclaw.approval": { id: "req-1" } },
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(3);
|
||||
expect(sendMessage.mock.calls[0]?.[1]).toMatchObject({
|
||||
body: "first",
|
||||
"com.openclaw.approval": { id: "req-1" },
|
||||
});
|
||||
expect(sendMessage.mock.calls[1]?.[1]).toMatchObject({ body: "second" });
|
||||
expect(sendMessage.mock.calls[1]?.[1]).not.toHaveProperty("com.openclaw.approval");
|
||||
expect(sendMessage.mock.calls[2]?.[1]).toMatchObject({ body: "third" });
|
||||
expect(sendMessage.mock.calls[2]?.[1]).not.toHaveProperty("com.openclaw.approval");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendSingleTextMessageMatrix", () => {
|
||||
|
||||
@@ -211,8 +211,11 @@ export async function sendMessageMatrix(
|
||||
const relation = threadId
|
||||
? buildThreadRelation(threadId, opts.replyToId)
|
||||
: buildReplyRelation(opts.replyToId);
|
||||
let pendingExtraContent = opts.extraContent;
|
||||
const sendContent = async (content: MatrixOutboundContent) => {
|
||||
const eventId = await client.sendMessage(roomId, content);
|
||||
const contentWithExtra = withMatrixExtraContentFields(content, pendingExtraContent);
|
||||
pendingExtraContent = undefined;
|
||||
const eventId = await client.sendMessage(roomId, contentWithExtra);
|
||||
return eventId;
|
||||
};
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ export type MatrixSendOpts = {
|
||||
replyToId?: string;
|
||||
threadId?: string | number | null;
|
||||
timeoutMs?: number;
|
||||
/** Additional Matrix event content fields to merge into the first sent event. */
|
||||
extraContent?: MatrixExtraContentFields;
|
||||
/** Send audio as voice message instead of audio file. Defaults to false. */
|
||||
audioAsVoice?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user