diff --git a/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts b/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts
index 52b44de49e7..96a23a19f7d 100644
--- a/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts
+++ b/extensions/whatsapp/src/outbound-adapter.sendpayload.test.ts
@@ -89,4 +89,15 @@ describe("whatsappOutbound sendPayload", () => {
expect(result).toEqual({ channel: "whatsapp", messageId: "" });
expect(sendWhatsApp).not.toHaveBeenCalled();
});
+
+ it("sanitizes HTML-only text to whitespace-only payload", () => {
+ expect(
+ whatsappOutbound
+ .sanitizeText?.({
+ text: "
",
+ payload: { text: "
" },
+ })
+ ?.trim(),
+ ).toBe("");
+ });
});
diff --git a/src/infra/outbound/deliver.test-helpers.ts b/src/infra/outbound/deliver.test-helpers.ts
deleted file mode 100644
index f7431e1b802..00000000000
--- a/src/infra/outbound/deliver.test-helpers.ts
+++ /dev/null
@@ -1,239 +0,0 @@
-import { vi } from "vitest";
-import { createIMessageTestPlugin } from "../../../test/helpers/channels/imessage-test-plugin.js";
-import {
- imessageOutboundForTest,
- signalOutbound,
- whatsappOutbound,
-} from "../../../test/helpers/infra/deliver-test-outbounds.js";
-import type { OpenClawConfig } from "../../config/types.openclaw.js";
-import {
- releasePinnedPluginChannelRegistry,
- setActivePluginRegistry,
-} from "../../plugins/runtime.js";
-import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
-import { createInternalHookEventPayload } from "../../test-utils/internal-hook-event-payload.js";
-import type { DeliverOutboundPayloadsParams, OutboundDeliveryResult } from "./deliver.js";
-
-type DeliverMockState = {
- sessions: {
- appendAssistantMessageToSessionTranscript: (...args: unknown[]) => Promise<{
- ok: boolean;
- sessionFile: string;
- }>;
- };
- hooks: {
- runner: {
- hasHooks: (...args: unknown[]) => boolean;
- runMessageSent: (...args: unknown[]) => Promise;
- };
- };
- internalHooks: {
- createInternalHookEvent: typeof createInternalHookEventPayload;
- triggerInternalHook: (...args: unknown[]) => Promise;
- };
- queue: {
- enqueueDelivery: (...args: unknown[]) => Promise;
- ackDelivery: (...args: unknown[]) => Promise;
- failDelivery: (...args: unknown[]) => Promise;
- };
- log: {
- warn: (...args: unknown[]) => void;
- };
-};
-
-export const deliverMocks: DeliverMockState = {
- sessions: {
- appendAssistantMessageToSessionTranscript: async () => ({ ok: true, sessionFile: "x" }),
- },
- hooks: {
- runner: {
- hasHooks: () => false,
- runMessageSent: async () => {},
- },
- },
- internalHooks: {
- createInternalHookEvent: createInternalHookEventPayload,
- triggerInternalHook: async () => {},
- },
- queue: {
- enqueueDelivery: async () => "mock-queue-id",
- ackDelivery: async () => {},
- failDelivery: async () => {},
- },
- log: {
- warn: () => {},
- },
-};
-
-const _mocks = vi.hoisted(() => ({
- appendAssistantMessageToSessionTranscript: vi.fn(async () =>
- deliverMocks.sessions.appendAssistantMessageToSessionTranscript(),
- ),
-}));
-const _hookMocks = vi.hoisted(() => ({
- runner: {
- hasHooks: vi.fn(() => deliverMocks.hooks.runner.hasHooks()),
- runMessageSent: vi.fn(
- async (...args: unknown[]) => await deliverMocks.hooks.runner.runMessageSent(...args),
- ),
- },
-}));
-const _internalHookMocks = vi.hoisted(() => ({
- createInternalHookEvent: vi.fn((...args: Parameters) =>
- deliverMocks.internalHooks.createInternalHookEvent(...args),
- ),
- triggerInternalHook: vi.fn(
- async (...args: unknown[]) => await deliverMocks.internalHooks.triggerInternalHook(...args),
- ),
-}));
-const _queueMocks = vi.hoisted(() => ({
- enqueueDelivery: vi.fn(
- async (...args: unknown[]) => await deliverMocks.queue.enqueueDelivery(...args),
- ),
- ackDelivery: vi.fn(async (...args: unknown[]) => await deliverMocks.queue.ackDelivery(...args)),
- failDelivery: vi.fn(async (...args: unknown[]) => await deliverMocks.queue.failDelivery(...args)),
-}));
-const _logMocks = vi.hoisted(() => ({
- warn: vi.fn((...args: unknown[]) => deliverMocks.log.warn(...args)),
-}));
-
-export const mocks = _mocks;
-export const hookMocks = _hookMocks;
-export const internalHookMocks = _internalHookMocks;
-export const queueMocks = _queueMocks;
-export const logMocks = _logMocks;
-
-vi.mock("../../config/sessions/transcript.runtime.js", async () => {
- const actual = await vi.importActual<
- typeof import("../../config/sessions/transcript.runtime.js")
- >("../../config/sessions/transcript.runtime.js");
- return {
- ...actual,
- appendAssistantMessageToSessionTranscript: _mocks.appendAssistantMessageToSessionTranscript,
- };
-});
-vi.mock("../../config/sessions/transcript.js", async () => {
- const actual = await vi.importActual(
- "../../config/sessions/transcript.js",
- );
- return {
- ...actual,
- appendAssistantMessageToSessionTranscript: _mocks.appendAssistantMessageToSessionTranscript,
- };
-});
-vi.mock("../../plugins/hook-runner-global.js", () => ({
- getGlobalHookRunner: () => _hookMocks.runner,
-}));
-vi.mock("../../hooks/internal-hooks.js", () => ({
- createInternalHookEvent: _internalHookMocks.createInternalHookEvent,
- triggerInternalHook: _internalHookMocks.triggerInternalHook,
-}));
-vi.mock("./delivery-queue.js", () => ({
- enqueueDelivery: _queueMocks.enqueueDelivery,
- ackDelivery: _queueMocks.ackDelivery,
- failDelivery: _queueMocks.failDelivery,
-}));
-vi.mock("../../logging/subsystem.js", () => ({
- createSubsystemLogger: () => {
- const makeLogger = () => ({
- warn: _logMocks.warn,
- info: vi.fn(),
- error: vi.fn(),
- debug: vi.fn(),
- child: vi.fn(() => makeLogger()),
- });
- return makeLogger();
- },
-}));
-
-export const whatsappChunkConfig: OpenClawConfig = {
- channels: { whatsapp: { textChunkLimit: 4000 } },
-};
-
-export const defaultRegistry = createTestRegistry([
- {
- pluginId: "signal",
- source: "test",
- plugin: createOutboundTestPlugin({
- id: "signal",
- outbound: signalOutbound,
- }),
- },
- {
- pluginId: "whatsapp",
- source: "test",
- plugin: createOutboundTestPlugin({
- id: "whatsapp",
- outbound: whatsappOutbound,
- }),
- },
- {
- pluginId: "imessage",
- source: "test",
- plugin: createIMessageTestPlugin({ outbound: imessageOutboundForTest }),
- },
-]);
-
-export const emptyRegistry = createTestRegistry([]);
-
-export function resetDeliverTestState() {
- releasePinnedPluginChannelRegistry();
- setActivePluginRegistry(defaultRegistry);
- deliverMocks.hooks.runner.hasHooks = () => false;
- deliverMocks.hooks.runner.runMessageSent = async () => {};
- deliverMocks.internalHooks.createInternalHookEvent = createInternalHookEventPayload;
- deliverMocks.internalHooks.triggerInternalHook = async () => {};
- deliverMocks.queue.enqueueDelivery = async () => "mock-queue-id";
- deliverMocks.queue.ackDelivery = async () => {};
- deliverMocks.queue.failDelivery = async () => {};
- deliverMocks.log.warn = () => {};
- deliverMocks.sessions.appendAssistantMessageToSessionTranscript = async () => ({
- ok: true,
- sessionFile: "x",
- });
-}
-
-export function clearDeliverTestRegistry() {
- releasePinnedPluginChannelRegistry();
- setActivePluginRegistry(emptyRegistry);
-}
-
-export function resetDeliverTestMocks(params?: { includeSessionMocks?: boolean }) {
- hookMocks.runner.hasHooks.mockClear();
- hookMocks.runner.runMessageSent.mockClear();
- internalHookMocks.createInternalHookEvent.mockClear();
- internalHookMocks.triggerInternalHook.mockClear();
- queueMocks.enqueueDelivery.mockClear();
- queueMocks.ackDelivery.mockClear();
- queueMocks.failDelivery.mockClear();
- logMocks.warn.mockClear();
- if (params?.includeSessionMocks) {
- mocks.appendAssistantMessageToSessionTranscript.mockClear();
- }
-}
-
-export async function runChunkedWhatsAppDelivery(params: {
- deliverOutboundPayloads: (
- params: DeliverOutboundPayloadsParams,
- ) => Promise;
- mirror?: DeliverOutboundPayloadsParams["mirror"];
-}) {
- const sendWhatsApp = vi
- .fn<
- (to: string, text: string, opts?: unknown) => Promise<{ messageId: string; toJid: string }>
- >()
- .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" })
- .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" });
- const cfg: OpenClawConfig = {
- channels: { whatsapp: { textChunkLimit: 2 } },
- };
- const results = await params.deliverOutboundPayloads({
- cfg,
- channel: "whatsapp",
- to: "+1555",
- payloads: [{ text: "abcd" }],
- deps: { whatsapp: sendWhatsApp },
- ...(params.mirror ? { mirror: params.mirror } : {}),
- });
- return { sendWhatsApp, results };
-}
diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts
index 4348626e356..d3eb0365e07 100644
--- a/src/infra/outbound/deliver.test.ts
+++ b/src/infra/outbound/deliver.test.ts
@@ -1,11 +1,7 @@
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
-import { createIMessageTestPlugin } from "../../../test/helpers/channels/imessage-test-plugin.js";
-import {
- imessageOutboundForTest,
- signalOutbound,
- whatsappOutbound,
-} from "../../../test/helpers/infra/deliver-test-outbounds.js";
+import { chunkText } from "../../auto-reply/chunk.js";
+import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import * as mediaCapabilityModule from "../../media/read-capability.js";
import { createHookRunner } from "../../plugins/hooks.js";
@@ -93,84 +89,139 @@ type DeliverModule = typeof import("./deliver.js");
let deliverOutboundPayloads: DeliverModule["deliverOutboundPayloads"];
let normalizeOutboundPayloads: DeliverModule["normalizeOutboundPayloads"];
-const whatsappChunkConfig: OpenClawConfig = {
- channels: { whatsapp: { textChunkLimit: 4000 } },
+const matrixChunkConfig: OpenClawConfig = {
+ channels: { matrix: { textChunkLimit: 4000 } } as OpenClawConfig["channels"],
};
const expectedPreferredTmpRoot = resolvePreferredOpenClawTmpDir();
type DeliverOutboundArgs = Parameters[0];
type DeliverOutboundPayload = DeliverOutboundArgs["payloads"][number];
+type MatrixSendFn = (
+ to: string,
+ text: string,
+ options?: Record,
+) => Promise<{ messageId: string } & Record>;
-async function deliverWhatsAppPayload(params: {
- sendWhatsApp: NonNullable<
- NonNullable[0]["deps"]>["whatsapp"]
- >;
+function resolveMatrixSender(deps: DeliverOutboundArgs["deps"]): MatrixSendFn {
+ const sender = deps?.matrix;
+ if (typeof sender !== "function") {
+ throw new Error("missing matrix sender");
+ }
+ return sender as MatrixSendFn;
+}
+
+function withMatrixChannel(result: Awaited>) {
+ return {
+ channel: "matrix" as const,
+ ...result,
+ };
+}
+
+const matrixOutboundForTest: ChannelOutboundAdapter = {
+ deliveryMode: "direct",
+ chunker: chunkText,
+ chunkerMode: "text",
+ textChunkLimit: 4000,
+ sanitizeText: ({ text }) => (text === "
" || text === "
" ? "" : text),
+ sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) =>
+ withMatrixChannel(
+ await resolveMatrixSender(deps)(to, text, {
+ cfg,
+ accountId: accountId ?? undefined,
+ gifPlayback,
+ }),
+ ),
+ sendMedia: async ({
+ cfg,
+ to,
+ text,
+ mediaUrl,
+ mediaLocalRoots,
+ mediaReadFile,
+ accountId,
+ deps,
+ gifPlayback,
+ }) =>
+ withMatrixChannel(
+ await resolveMatrixSender(deps)(to, text, {
+ cfg,
+ mediaUrl,
+ mediaLocalRoots,
+ mediaReadFile,
+ accountId: accountId ?? undefined,
+ gifPlayback,
+ }),
+ ),
+};
+
+async function deliverMatrixPayload(params: {
+ sendMatrix: MatrixSendFn;
payload: DeliverOutboundPayload;
cfg?: OpenClawConfig;
}) {
return deliverOutboundPayloads({
- cfg: params.cfg ?? whatsappChunkConfig,
- channel: "whatsapp",
- to: "+1555",
+ cfg: params.cfg ?? matrixChunkConfig,
+ channel: "matrix",
+ to: "!room:example",
payloads: [params.payload],
- deps: { whatsapp: params.sendWhatsApp },
+ deps: { matrix: params.sendMatrix },
});
}
-async function runChunkedWhatsAppDelivery(params?: {
+async function runChunkedMatrixDelivery(params?: {
mirror?: Parameters[0]["mirror"];
}) {
- const sendWhatsApp = vi
+ const sendMatrix = vi
.fn()
- .mockResolvedValueOnce({ messageId: "w1", toJid: "jid" })
- .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" });
+ .mockResolvedValueOnce({ messageId: "m1", roomId: "!room:example" })
+ .mockResolvedValueOnce({ messageId: "m2", roomId: "!room:example" });
const cfg: OpenClawConfig = {
- channels: { whatsapp: { textChunkLimit: 2 } },
+ channels: { matrix: { textChunkLimit: 2 } } as OpenClawConfig["channels"],
};
const results = await deliverOutboundPayloads({
cfg,
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "abcd" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
...(params?.mirror ? { mirror: params.mirror } : {}),
});
- return { sendWhatsApp, results };
+ return { sendMatrix, results };
}
-async function deliverSingleWhatsAppForHookTest(params?: { sessionKey?: string }) {
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+async function deliverSingleMatrixForHookTest(params?: { sessionKey?: string }) {
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
await deliverOutboundPayloads({
- cfg: whatsappChunkConfig,
- channel: "whatsapp",
- to: "+1555",
+ cfg: matrixChunkConfig,
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
...(params?.sessionKey ? { session: { key: params.sessionKey } } : {}),
});
}
async function runBestEffortPartialFailureDelivery() {
- const sendWhatsApp = vi
+ const sendMatrix = vi
.fn()
.mockRejectedValueOnce(new Error("fail"))
- .mockResolvedValueOnce({ messageId: "w2", toJid: "jid" });
+ .mockResolvedValueOnce({ messageId: "m2", roomId: "!room:example" });
const onError = vi.fn();
const cfg: OpenClawConfig = {};
const results = await deliverOutboundPayloads({
cfg,
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "a" }, { text: "b" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
bestEffort: true,
onError,
});
- return { sendWhatsApp, onError, results };
+ return { sendMatrix, onError, results };
}
-function expectSuccessfulWhatsAppInternalHookPayload(
+function expectSuccessfulMatrixInternalHookPayload(
expected: Partial<{
content: string;
messageId: string;
@@ -179,10 +230,10 @@ function expectSuccessfulWhatsAppInternalHookPayload(
}>,
) {
return expect.objectContaining({
- to: "+1555",
+ to: "!room:example",
success: true,
- channelId: "whatsapp",
- conversationId: "+1555",
+ channelId: "matrix",
+ conversationId: "!room:example",
...expected,
});
}
@@ -224,23 +275,23 @@ describe("deliverOutboundPayloads", () => {
mediaCapabilityModule,
"resolveAgentScopedOutboundMediaAccess",
);
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
await deliverOutboundPayloads({
cfg: {},
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
session: {
- key: "agent:main:whatsapp:group:ops",
+ key: "agent:main:matrix:room:ops",
requesterSenderId: "attacker",
},
});
expect(resolveMediaAccessSpy).toHaveBeenCalledWith(
expect.objectContaining({
- sessionKey: "agent:main:whatsapp:group:ops",
+ sessionKey: "agent:main:matrix:room:ops",
messageProvider: undefined,
requesterSenderId: "attacker",
}),
@@ -253,17 +304,17 @@ describe("deliverOutboundPayloads", () => {
mediaCapabilityModule,
"resolveAgentScopedOutboundMediaAccess",
);
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w2", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m2", roomId: "!room:example" });
await deliverOutboundPayloads({
cfg: {},
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
session: {
- key: "agent:main:whatsapp:group:ops",
- requesterSenderId: "id:whatsapp:123",
+ key: "agent:main:matrix:room:ops",
+ requesterSenderId: "id:matrix:123",
requesterSenderName: "Alice",
requesterSenderUsername: "alice_u",
requesterSenderE164: "+15551234567",
@@ -272,7 +323,7 @@ describe("deliverOutboundPayloads", () => {
expect(resolveMediaAccessSpy).toHaveBeenCalledWith(
expect.objectContaining({
- requesterSenderId: "id:whatsapp:123",
+ requesterSenderId: "id:matrix:123",
requesterSenderName: "Alice",
requesterSenderUsername: "alice_u",
requesterSenderE164: "+15551234567",
@@ -286,17 +337,17 @@ describe("deliverOutboundPayloads", () => {
mediaCapabilityModule,
"resolveAgentScopedOutboundMediaAccess",
);
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w3", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m3", roomId: "!room:example" });
await deliverOutboundPayloads({
cfg: {},
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
accountId: "destination-account",
payloads: [{ text: "hello", mediaUrl: "file:///tmp/policy.png" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
session: {
- key: "agent:main:whatsapp:group:ops",
+ key: "agent:main:matrix:room:ops",
requesterAccountId: "source-account",
requesterSenderId: "attacker",
},
@@ -304,7 +355,7 @@ describe("deliverOutboundPayloads", () => {
expect(resolveMediaAccessSpy).toHaveBeenCalledWith(
expect.objectContaining({
- sessionKey: "agent:main:whatsapp:group:ops",
+ sessionKey: "agent:main:matrix:room:ops",
accountId: "source-account",
requesterSenderId: "attacker",
}),
@@ -317,16 +368,16 @@ describe("deliverOutboundPayloads", () => {
mediaCapabilityModule,
"resolveAgentScopedOutboundMediaAccess",
);
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w4", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m4", roomId: "!room:example" });
await deliverOutboundPayloads({
cfg: {},
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
session: {
- key: "agent:main:whatsapp:group:ops",
+ key: "agent:main:matrix:room:ops",
requesterSenderId: "attacker",
},
});
@@ -473,19 +524,19 @@ describe("deliverOutboundPayloads", () => {
expect(sendMedia).not.toHaveBeenCalled();
});
- it("includes OpenClaw tmp root in signal mediaLocalRoots", async () => {
- const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", timestamp: 123 });
+ it("includes OpenClaw tmp root in plugin mediaLocalRoots", async () => {
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m-media", roomId: "!room" });
await deliverOutboundPayloads({
- cfg: { channels: { signal: {} } },
- channel: "signal",
- to: "+1555",
+ cfg: { channels: { matrix: {} } } as OpenClawConfig,
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }],
- deps: { sendSignal },
+ deps: { matrix: sendMatrix },
});
- expect(sendSignal).toHaveBeenCalledWith(
- "+1555",
+ expect(sendMatrix).toHaveBeenCalledWith(
+ "!room:example",
"hi",
expect.objectContaining({
mediaLocalRoots: expect.arrayContaining([expectedPreferredTmpRoot]),
@@ -493,18 +544,18 @@ describe("deliverOutboundPayloads", () => {
);
});
- it("sends telegram media to an explicit target once instead of fanning out over allowFrom", async () => {
- const sendMedia = vi.fn().mockResolvedValue({ channel: "telegram", messageId: "t1" });
+ it("sends plugin media to an explicit target once instead of fanning out over allowFrom", async () => {
+ const sendMedia = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "m1" });
setActivePluginRegistry(
createTestRegistry([
{
- pluginId: "telegram",
+ pluginId: "matrix",
source: "test",
plugin: createOutboundTestPlugin({
- id: "telegram",
+ id: "matrix",
outbound: {
deliveryMode: "direct",
- sendText: vi.fn().mockResolvedValue({ channel: "telegram", messageId: "text-1" }),
+ sendText: vi.fn().mockResolvedValue({ channel: "matrix", messageId: "text-1" }),
sendMedia,
},
}),
@@ -515,14 +566,13 @@ describe("deliverOutboundPayloads", () => {
await deliverOutboundPayloads({
cfg: {
channels: {
- telegram: {
- botToken: "tok",
+ matrix: {
allowFrom: ["111", "222", "333"],
},
- },
+ } as OpenClawConfig["channels"],
},
- channel: "telegram",
- to: "123",
+ channel: "matrix",
+ to: "!explicit:example",
payloads: [{ text: "HEARTBEAT_OK", mediaUrl: "https://example.com/img.png" }],
skipQueue: true,
});
@@ -530,7 +580,7 @@ describe("deliverOutboundPayloads", () => {
expect(sendMedia).toHaveBeenCalledTimes(1);
expect(sendMedia).toHaveBeenCalledWith(
expect.objectContaining({
- to: "123",
+ to: "!explicit:example",
text: "HEARTBEAT_OK",
mediaUrl: "https://example.com/img.png",
accountId: undefined,
@@ -581,104 +631,66 @@ describe("deliverOutboundPayloads", () => {
);
});
- it("includes OpenClaw tmp root in whatsapp mediaLocalRoots", async () => {
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+ it("chunks plugin text and returns all results", async () => {
+ const { sendMatrix, results } = await runChunkedMatrixDelivery();
- await deliverOutboundPayloads({
- cfg: whatsappChunkConfig,
- channel: "whatsapp",
- to: "+1555",
- payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }],
- deps: { whatsapp: sendWhatsApp },
- });
-
- expect(sendWhatsApp).toHaveBeenCalledWith(
- "+1555",
- "hi",
- expect.objectContaining({
- mediaLocalRoots: expect.arrayContaining([expectedPreferredTmpRoot]),
- }),
- );
+ expect(sendMatrix).toHaveBeenCalledTimes(2);
+ expect(results.map((r) => r.messageId)).toEqual(["m1", "m2"]);
});
- it("includes OpenClaw tmp root in imessage mediaLocalRoots", async () => {
- const sendIMessage = vi.fn().mockResolvedValue({ messageId: "i1", chatId: "chat-1" });
-
- await deliverOutboundPayloads({
- cfg: {},
- channel: "imessage",
- to: "imessage:+15551234567",
- payloads: [{ text: "hi", mediaUrl: "https://example.com/x.png" }],
- deps: { imessage: sendIMessage },
- });
-
- expect(sendIMessage).toHaveBeenCalledWith(
- "imessage:+15551234567",
- "hi",
- expect.objectContaining({
- mediaLocalRoots: expect.arrayContaining([expectedPreferredTmpRoot]),
- }),
- );
- });
-
- it("chunks WhatsApp text and returns all results", async () => {
- const { sendWhatsApp, results } = await runChunkedWhatsAppDelivery();
-
- expect(sendWhatsApp).toHaveBeenCalledTimes(2);
- expect(results.map((r) => r.messageId)).toEqual(["w1", "w2"]);
- });
-
- it("respects newline chunk mode for WhatsApp", async () => {
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+ it("respects newline chunk mode for plugin text", async () => {
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
const cfg: OpenClawConfig = {
- channels: { whatsapp: { textChunkLimit: 4000, chunkMode: "newline" } },
+ channels: {
+ matrix: { textChunkLimit: 4000, chunkMode: "newline" },
+ } as OpenClawConfig["channels"],
};
await deliverOutboundPayloads({
cfg,
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "Line one\n\nLine two" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
});
- expect(sendWhatsApp).toHaveBeenCalledTimes(2);
- expect(sendWhatsApp).toHaveBeenNthCalledWith(
+ expect(sendMatrix).toHaveBeenCalledTimes(2);
+ expect(sendMatrix).toHaveBeenNthCalledWith(
1,
- "+1555",
+ "!room:example",
"Line one",
- expect.objectContaining({ verbose: false }),
+ expect.objectContaining({ cfg }),
);
- expect(sendWhatsApp).toHaveBeenNthCalledWith(
+ expect(sendMatrix).toHaveBeenNthCalledWith(
2,
- "+1555",
+ "!room:example",
"Line two",
- expect.objectContaining({ verbose: false }),
+ expect.objectContaining({ cfg }),
);
});
- it("drops HTML-only WhatsApp text payloads after sanitization", async () => {
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
- const results = await deliverWhatsAppPayload({
- sendWhatsApp,
+ it("drops text payloads after adapter sanitization removes all content", async () => {
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
+ const results = await deliverMatrixPayload({
+ sendMatrix,
payload: { text: "
" },
});
- expect(sendWhatsApp).not.toHaveBeenCalled();
+ expect(sendMatrix).not.toHaveBeenCalled();
expect(results).toEqual([]);
});
- it("drops non-WhatsApp HTML-only text payloads after sanitization", async () => {
- const sendSignal = vi.fn().mockResolvedValue({ messageId: "s1", toJid: "jid" });
+ it("drops plugin HTML-only text payloads after sanitization", async () => {
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
const results = await deliverOutboundPayloads({
cfg: {},
- channel: "signal",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "
" }],
- deps: { sendSignal },
+ deps: { matrix: sendMatrix },
});
- expect(sendSignal).not.toHaveBeenCalled();
+ expect(sendMatrix).not.toHaveBeenCalled();
expect(results).toEqual([]);
});
@@ -730,14 +742,14 @@ describe("deliverOutboundPayloads", () => {
expect(chunker).toHaveBeenNthCalledWith(1, text, 4000);
});
- it("passes config through for iMessage media sends so the channel runtime can resolve limits", async () => {
- const sendIMessage = vi.fn().mockResolvedValue({ messageId: "i1" });
+ it("passes config through for plugin media sends", async () => {
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m-media", roomId: "!room" });
setActivePluginRegistry(
createTestRegistry([
{
- pluginId: "imessage",
+ pluginId: "matrix",
source: "test",
- plugin: createIMessageTestPlugin(),
+ plugin: createOutboundTestPlugin({ id: "matrix", outbound: matrixOutboundForTest }),
},
]),
);
@@ -747,17 +759,17 @@ describe("deliverOutboundPayloads", () => {
await deliverOutboundPayloads({
cfg,
- channel: "imessage",
- to: "chat_id:42",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello", mediaUrls: ["https://example.com/a.png"] }],
- deps: { imessage: sendIMessage },
+ deps: { matrix: sendMatrix },
});
- expect(sendIMessage).toHaveBeenCalledWith(
- "chat_id:42",
+ expect(sendMatrix).toHaveBeenCalledWith(
+ "!room:example",
"hello",
expect.objectContaining({
- config: cfg,
+ cfg,
mediaUrl: "https://example.com/a.png",
}),
);
@@ -776,74 +788,74 @@ describe("deliverOutboundPayloads", () => {
});
it("continues on errors when bestEffort is enabled", async () => {
- const { sendWhatsApp, onError, results } = await runBestEffortPartialFailureDelivery();
+ const { sendMatrix, onError, results } = await runBestEffortPartialFailureDelivery();
- expect(sendWhatsApp).toHaveBeenCalledTimes(2);
+ expect(sendMatrix).toHaveBeenCalledTimes(2);
expect(onError).toHaveBeenCalledTimes(1);
- expect(results).toEqual([{ channel: "whatsapp", messageId: "w2", toJid: "jid" }]);
+ expect(results).toEqual([{ channel: "matrix", messageId: "m2", roomId: "!room:example" }]);
});
it("emits internal message:sent hook with success=true for chunked payload delivery", async () => {
- const { sendWhatsApp } = await runChunkedWhatsAppDelivery({
+ const { sendMatrix } = await runChunkedMatrixDelivery({
mirror: {
sessionKey: "agent:main:main",
isGroup: true,
- groupId: "whatsapp:group:123",
+ groupId: "matrix:room:123",
},
});
- expect(sendWhatsApp).toHaveBeenCalledTimes(2);
+ expect(sendMatrix).toHaveBeenCalledTimes(2);
expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1);
expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith(
"message",
"sent",
"agent:main:main",
- expectSuccessfulWhatsAppInternalHookPayload({
+ expectSuccessfulMatrixInternalHookPayload({
content: "abcd",
- messageId: "w2",
+ messageId: "m2",
isGroup: true,
- groupId: "whatsapp:group:123",
+ groupId: "matrix:room:123",
}),
);
expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1);
});
it("does not emit internal message:sent hook when neither mirror nor sessionKey is provided", async () => {
- await deliverSingleWhatsAppForHookTest();
+ await deliverSingleMatrixForHookTest();
expect(internalHookMocks.createInternalHookEvent).not.toHaveBeenCalled();
expect(internalHookMocks.triggerInternalHook).not.toHaveBeenCalled();
});
it("emits internal message:sent hook when sessionKey is provided without mirror", async () => {
- await deliverSingleWhatsAppForHookTest({ sessionKey: "agent:main:main" });
+ await deliverSingleMatrixForHookTest({ sessionKey: "agent:main:main" });
expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledTimes(1);
expect(internalHookMocks.createInternalHookEvent).toHaveBeenCalledWith(
"message",
"sent",
"agent:main:main",
- expectSuccessfulWhatsAppInternalHookPayload({ content: "hello", messageId: "w1" }),
+ expectSuccessfulMatrixInternalHookPayload({ content: "hello", messageId: "m1" }),
);
expect(internalHookMocks.triggerInternalHook).toHaveBeenCalledTimes(1);
});
it("warns when session.agentId is set without a session key", async () => {
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
hookMocks.runner.hasHooks.mockReturnValue(true);
await deliverOutboundPayloads({
- cfg: whatsappChunkConfig,
- channel: "whatsapp",
- to: "+1555",
+ cfg: matrixChunkConfig,
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
session: { agentId: "agent-main" },
});
expect(logMocks.warn).toHaveBeenCalledWith(
"deliverOutboundPayloads: session.agentId present without session key; internal message:sent hook will be skipped",
- expect.objectContaining({ channel: "whatsapp", to: "+1555", agentId: "agent-main" }),
+ expect.objectContaining({ channel: "matrix", to: "!room:example", agentId: "agent-main" }),
);
});
@@ -862,7 +874,7 @@ describe("deliverOutboundPayloads", () => {
});
it("writes raw payloads to the queue before normalization", async () => {
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w-raw", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m-raw", roomId: "!room:example" });
const rawPayloads: DeliverOutboundPayload[] = [
{ text: "NO_REPLY" },
{ text: '{"action":"NO_REPLY"}' },
@@ -871,11 +883,11 @@ describe("deliverOutboundPayloads", () => {
];
await deliverOutboundPayloads({
- cfg: whatsappChunkConfig,
- channel: "whatsapp",
- to: "+1555",
+ cfg: matrixChunkConfig,
+ channel: "matrix",
+ to: "!room:example",
payloads: rawPayloads,
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
});
expect(queueMocks.enqueueDelivery).toHaveBeenCalledTimes(1);
@@ -892,7 +904,7 @@ describe("deliverOutboundPayloads", () => {
});
it("acks the queue entry when delivery is aborted", async () => {
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
const abortController = new AbortController();
abortController.abort();
const cfg: OpenClawConfig = {};
@@ -900,30 +912,30 @@ describe("deliverOutboundPayloads", () => {
await expect(
deliverOutboundPayloads({
cfg,
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "a" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
abortSignal: abortController.signal,
}),
).rejects.toThrow("Operation aborted");
expect(queueMocks.ackDelivery).toHaveBeenCalledWith("mock-queue-id");
expect(queueMocks.failDelivery).not.toHaveBeenCalled();
- expect(sendWhatsApp).not.toHaveBeenCalled();
+ expect(sendMatrix).not.toHaveBeenCalled();
});
it("passes normalized payload to onError", async () => {
- const sendWhatsApp = vi.fn().mockRejectedValue(new Error("boom"));
+ const sendMatrix = vi.fn().mockRejectedValue(new Error("boom"));
const onError = vi.fn();
const cfg: OpenClawConfig = {};
await deliverOutboundPayloads({
cfg,
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hi", mediaUrl: "https://x.test/a.jpg" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
bestEffort: true,
onError,
});
@@ -977,19 +989,19 @@ describe("deliverOutboundPayloads", () => {
it("emits message_sent success for text-only deliveries", async () => {
hookMocks.runner.hasHooks.mockReturnValue(true);
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
await deliverOutboundPayloads({
cfg: {},
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
});
expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith(
- expect.objectContaining({ to: "+1555", content: "hello", success: true }),
- expect.objectContaining({ channelId: "whatsapp" }),
+ expect.objectContaining({ to: "!room:example", content: "hello", success: true }),
+ expect.objectContaining({ channelId: "matrix" }),
);
});
@@ -1019,19 +1031,19 @@ describe("deliverOutboundPayloads", () => {
realRunner.runMessageSending(event as never, ctx as never),
);
- const sendWhatsApp = vi.fn().mockResolvedValue({ messageId: "w1", toJid: "jid" });
+ const sendMatrix = vi.fn().mockResolvedValue({ messageId: "m1", roomId: "!room:example" });
await deliverOutboundPayloads({
cfg: {},
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hello" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
});
expect(hookMocks.runner.runMessageSending).toHaveBeenCalledTimes(1);
expect(high).toHaveBeenCalledTimes(1);
expect(low).not.toHaveBeenCalled();
- expect(sendWhatsApp).not.toHaveBeenCalled();
+ expect(sendMatrix).not.toHaveBeenCalled();
expect(hookMocks.runner.runMessageSent).not.toHaveBeenCalled();
});
@@ -1066,7 +1078,7 @@ describe("deliverOutboundPayloads", () => {
);
});
- it("preserves channelData-only payloads with empty text for non-WhatsApp sendPayload channels", async () => {
+ it("preserves channelData-only payloads with empty text for sendPayload channels", async () => {
const sendPayload = vi.fn().mockResolvedValue({ channel: "line", messageId: "ln-1" });
const sendText = vi.fn();
const sendMedia = vi.fn();
@@ -1229,26 +1241,26 @@ describe("deliverOutboundPayloads", () => {
it("emits message_sent failure when delivery errors", async () => {
hookMocks.runner.hasHooks.mockReturnValue(true);
- const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed"));
+ const sendMatrix = vi.fn().mockRejectedValue(new Error("downstream failed"));
await expect(
deliverOutboundPayloads({
cfg: {},
- channel: "whatsapp",
- to: "+1555",
+ channel: "matrix",
+ to: "!room:example",
payloads: [{ text: "hi" }],
- deps: { whatsapp: sendWhatsApp },
+ deps: { matrix: sendMatrix },
}),
).rejects.toThrow("downstream failed");
expect(hookMocks.runner.runMessageSent).toHaveBeenCalledWith(
expect.objectContaining({
- to: "+1555",
+ to: "!room:example",
content: "hi",
success: false,
error: "downstream failed",
}),
- expect.objectContaining({ channelId: "whatsapp" }),
+ expect.objectContaining({ channelId: "matrix" }),
);
});
});
@@ -1256,18 +1268,8 @@ describe("deliverOutboundPayloads", () => {
const emptyRegistry = createTestRegistry([]);
const defaultRegistry = createTestRegistry([
{
- pluginId: "signal",
- plugin: createOutboundTestPlugin({ id: "signal", outbound: signalOutbound }),
- source: "test",
- },
- {
- pluginId: "whatsapp",
- plugin: createOutboundTestPlugin({ id: "whatsapp", outbound: whatsappOutbound }),
- source: "test",
- },
- {
- pluginId: "imessage",
- plugin: createIMessageTestPlugin({ outbound: imessageOutboundForTest }),
+ pluginId: "matrix",
+ plugin: createOutboundTestPlugin({ id: "matrix", outbound: matrixOutboundForTest }),
source: "test",
},
]);
diff --git a/test/helpers/infra/deliver-test-outbounds.ts b/test/helpers/infra/deliver-test-outbounds.ts
deleted file mode 100644
index f4b09fbada4..00000000000
--- a/test/helpers/infra/deliver-test-outbounds.ts
+++ /dev/null
@@ -1,269 +0,0 @@
-import { chunkMarkdownTextWithMode, chunkText } from "../../../src/auto-reply/chunk.js";
-import { resolveChannelMediaMaxBytes } from "../../../src/channels/plugins/media-limits.js";
-import type { ChannelOutboundAdapter } from "../../../src/channels/plugins/types.js";
-import type { OpenClawConfig } from "../../../src/config/config.js";
-import {
- resolveOutboundSendDep,
- type OutboundSendDeps,
-} from "../../../src/infra/outbound/send-deps.js";
-import { sanitizeForPlainText } from "../../../src/plugin-sdk/outbound-runtime.js";
-
-type SignalSendFn = (
- to: string,
- text: string,
- options?: Record,
-) => Promise<{ messageId: string } & Record>;
-
-const MB = 1024 * 1024;
-
-function resolveSignalMaxBytes(cfg: OpenClawConfig, accountId?: string): number | undefined {
- const signalCfg = cfg.channels?.signal as
- | {
- mediaMaxMb?: number;
- accounts?: Record;
- }
- | undefined;
- const accountMb = accountId ? signalCfg?.accounts?.[accountId]?.mediaMaxMb : undefined;
- const mediaMaxMb = accountMb ?? signalCfg?.mediaMaxMb;
- return typeof mediaMaxMb === "number" ? mediaMaxMb * MB : undefined;
-}
-
-function resolveSignalSender(deps: OutboundSendDeps | undefined): SignalSendFn {
- const sender = resolveOutboundSendDep(deps, "signal");
- if (!sender) {
- throw new Error("missing sendSignal dep");
- }
- return sender;
-}
-
-function resolveSignalTextChunkLimit(cfg: OpenClawConfig, accountId?: string | null): number {
- const signalCfg = cfg.channels?.signal as
- | {
- textChunkLimit?: number;
- accounts?: Record;
- }
- | undefined;
- const accountLimit = accountId ? signalCfg?.accounts?.[accountId]?.textChunkLimit : undefined;
- if (typeof accountLimit === "number") {
- return accountLimit;
- }
- return typeof signalCfg?.textChunkLimit === "number" ? signalCfg.textChunkLimit : 4000;
-}
-
-function withSignalChannel(result: Awaited>) {
- return {
- channel: "signal" as const,
- ...result,
- };
-}
-
-export const signalOutbound: ChannelOutboundAdapter = {
- deliveryMode: "direct",
- textChunkLimit: 4000,
- sanitizeText: ({ text }) => sanitizeForPlainText(text),
- sendFormattedText: async ({ cfg, to, text, accountId, deps, abortSignal }) => {
- const send = resolveSignalSender(deps);
- const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined);
- const limit = resolveSignalTextChunkLimit(cfg, accountId);
- const chunks = chunkMarkdownTextWithMode(text, limit, "length");
- const outputChunks = chunks.length === 0 && text ? [text] : chunks;
- const results = [];
- for (const chunk of outputChunks) {
- abortSignal?.throwIfAborted();
- results.push(
- withSignalChannel(
- await send(to, chunk, {
- cfg,
- maxBytes,
- accountId: accountId ?? undefined,
- textMode: "plain",
- textStyles: [],
- }),
- ),
- );
- }
- return results;
- },
- sendFormattedMedia: async ({
- cfg,
- to,
- text,
- mediaUrl,
- mediaLocalRoots,
- mediaReadFile,
- accountId,
- deps,
- abortSignal,
- }) => {
- abortSignal?.throwIfAborted();
- const send = resolveSignalSender(deps);
- const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined);
- return withSignalChannel(
- await send(to, text, {
- cfg,
- mediaUrl,
- maxBytes,
- accountId: accountId ?? undefined,
- textMode: "plain",
- textStyles: [],
- mediaLocalRoots,
- mediaReadFile,
- }),
- );
- },
- sendText: async ({ cfg, to, text, accountId, deps }) => {
- const send = resolveSignalSender(deps);
- const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined);
- return withSignalChannel(
- await send(to, text, {
- cfg,
- maxBytes,
- accountId: accountId ?? undefined,
- }),
- );
- },
- sendMedia: async ({
- cfg,
- to,
- text,
- mediaUrl,
- mediaLocalRoots,
- mediaReadFile,
- accountId,
- deps,
- }) => {
- const send = resolveSignalSender(deps);
- const maxBytes = resolveSignalMaxBytes(cfg, accountId ?? undefined);
- return withSignalChannel(
- await send(to, text, {
- cfg,
- mediaUrl,
- maxBytes,
- accountId: accountId ?? undefined,
- mediaLocalRoots,
- mediaReadFile,
- }),
- );
- },
-};
-
-type WhatsAppSendFn = (
- to: string,
- text: string,
- options?: Record,
-) => Promise<{ messageId: string } & Record>;
-
-function resolveWhatsAppSender(deps: OutboundSendDeps | undefined): WhatsAppSendFn {
- const sender = resolveOutboundSendDep(deps, "whatsapp");
- if (!sender) {
- throw new Error("missing whatsapp dep");
- }
- return sender;
-}
-
-function withWhatsAppChannel(result: Awaited>) {
- return {
- channel: "whatsapp" as const,
- ...result,
- };
-}
-
-export const whatsappOutbound: ChannelOutboundAdapter = {
- deliveryMode: "gateway",
- chunker: chunkText,
- chunkerMode: "text",
- textChunkLimit: 4000,
- sanitizeText: ({ text }) => sanitizeForPlainText(text),
- sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) => {
- const send = resolveWhatsAppSender(deps);
- return withWhatsAppChannel(
- await send(to, text, {
- verbose: false,
- cfg,
- accountId: accountId ?? undefined,
- gifPlayback,
- }),
- );
- },
- sendMedia: async ({
- cfg,
- to,
- text,
- mediaUrl,
- mediaLocalRoots,
- mediaReadFile,
- accountId,
- deps,
- gifPlayback,
- }) => {
- const send = resolveWhatsAppSender(deps);
- return withWhatsAppChannel(
- await send(to, text, {
- verbose: false,
- cfg,
- mediaUrl,
- mediaLocalRoots,
- mediaReadFile,
- accountId: accountId ?? undefined,
- gifPlayback,
- }),
- );
- },
-};
-
-function resolveIMessageSender(deps: OutboundSendDeps | undefined) {
- const sender = resolveOutboundSendDep<
- (
- to: string,
- text: string,
- options?: Record,
- ) => Promise<{ messageId: string; chatId?: string }>
- >(deps, "imessage");
- if (!sender) {
- throw new Error("missing sendIMessage dep");
- }
- return sender;
-}
-
-function withIMessageChannel(
- result: Awaited>>,
-) {
- return {
- channel: "imessage" as const,
- ...result,
- };
-}
-
-function resolveIMessageMaxBytes(
- cfg: OpenClawConfig,
- accountId?: string | null,
-): number | undefined {
- return resolveChannelMediaMaxBytes({
- cfg,
- resolveChannelLimitMb: ({ cfg, accountId }) =>
- cfg.channels?.imessage?.accounts?.[accountId]?.mediaMaxMb ??
- cfg.channels?.imessage?.mediaMaxMb,
- accountId,
- });
-}
-
-export const imessageOutboundForTest: ChannelOutboundAdapter = {
- deliveryMode: "direct",
- sanitizeText: ({ text }) => text,
- sendText: async ({ to, text, accountId, deps }) =>
- withIMessageChannel(
- await resolveIMessageSender(deps)(to, text, {
- accountId: accountId ?? undefined,
- }),
- ),
- sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, mediaReadFile, accountId, deps }) =>
- withIMessageChannel(
- await resolveIMessageSender(deps)(to, text, {
- mediaUrl,
- mediaLocalRoots,
- mediaReadFile,
- maxBytes: resolveIMessageMaxBytes(cfg, accountId),
- accountId: accountId ?? undefined,
- }),
- ),
-};