refactor: unify markdown formatting pipeline
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { markdownToSignalTextChunks } from "../../signal/format.js";
|
||||
import { deliverOutboundPayloads, normalizeOutboundPayloads } from "./deliver.js";
|
||||
|
||||
describe("deliverOutboundPayloads", () => {
|
||||
@@ -22,7 +23,9 @@ describe("deliverOutboundPayloads", () => {
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledTimes(2);
|
||||
for (const call of sendTelegram.mock.calls) {
|
||||
expect(call[2]).toEqual(expect.objectContaining({ accountId: undefined, verbose: false }));
|
||||
expect(call[2]).toEqual(
|
||||
expect.objectContaining({ accountId: undefined, verbose: false, textMode: "html" }),
|
||||
);
|
||||
}
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]).toMatchObject({ channel: "telegram", chatId: "c1" });
|
||||
@@ -53,7 +56,7 @@ describe("deliverOutboundPayloads", () => {
|
||||
expect(sendTelegram).toHaveBeenCalledWith(
|
||||
"123",
|
||||
"hi",
|
||||
expect.objectContaining({ accountId: "default", verbose: false }),
|
||||
expect.objectContaining({ accountId: "default", verbose: false, textMode: "html" }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -75,11 +78,46 @@ describe("deliverOutboundPayloads", () => {
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://x.test/a.jpg",
|
||||
maxBytes: 2 * 1024 * 1024,
|
||||
textMode: "plain",
|
||||
textStyles: [],
|
||||
}),
|
||||
);
|
||||
expect(results[0]).toMatchObject({ channel: "signal", messageId: "s1" });
|
||||
});
|
||||
|
||||
it("chunks Signal markdown using the format-first chunker", async () => {
|
||||
const sendSignal = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ messageId: "s1", timestamp: 123 });
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: { signal: { textChunkLimit: 20 } },
|
||||
};
|
||||
const text = `Intro\\n\\n\`\`\`\`md\\n${"y".repeat(60)}\\n\`\`\`\\n\\nOutro`;
|
||||
const expectedChunks = markdownToSignalTextChunks(text, 20);
|
||||
|
||||
await deliverOutboundPayloads({
|
||||
cfg,
|
||||
channel: "signal",
|
||||
to: "+1555",
|
||||
payloads: [{ text }],
|
||||
deps: { sendSignal },
|
||||
});
|
||||
|
||||
expect(sendSignal).toHaveBeenCalledTimes(expectedChunks.length);
|
||||
expectedChunks.forEach((chunk, index) => {
|
||||
expect(sendSignal).toHaveBeenNthCalledWith(
|
||||
index + 1,
|
||||
"+1555",
|
||||
chunk.text,
|
||||
expect.objectContaining({
|
||||
accountId: undefined,
|
||||
textMode: "plain",
|
||||
textStyles: chunk.styles,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("chunks WhatsApp text and returns all results", async () => {
|
||||
const sendWhatsApp = vi
|
||||
.fn()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import { resolveChannelMediaMaxBytes } from "../../channels/plugins/media-limits.js";
|
||||
import { loadChannelOutboundAdapter } from "../../channels/plugins/outbound/load.js";
|
||||
import type { ChannelOutboundAdapter } from "../../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { sendMessageDiscord } from "../../discord/send.js";
|
||||
import type { sendMessageIMessage } from "../../imessage/send.js";
|
||||
import type { sendMessageSignal } from "../../signal/send.js";
|
||||
import { markdownToSignalTextChunks, type SignalTextStyleRange } from "../../signal/format.js";
|
||||
import { sendMessageSignal } from "../../signal/send.js";
|
||||
import type { sendMessageSlack } from "../../slack/send.js";
|
||||
import type { sendMessageTelegram } from "../../telegram/send.js";
|
||||
import type { sendMessageWhatsApp } from "../../web/outbound.js";
|
||||
@@ -154,6 +156,7 @@ export async function deliverOutboundPayloads(params: {
|
||||
const accountId = params.accountId;
|
||||
const deps = params.deps;
|
||||
const abortSignal = params.abortSignal;
|
||||
const sendSignal = params.deps?.sendSignal ?? sendMessageSignal;
|
||||
const results: OutboundDeliveryResult[] = [];
|
||||
const handler = await createChannelHandler({
|
||||
cfg,
|
||||
@@ -170,6 +173,16 @@ export async function deliverOutboundPayloads(params: {
|
||||
fallbackLimit: handler.textChunkLimit,
|
||||
})
|
||||
: undefined;
|
||||
const isSignalChannel = channel === "signal";
|
||||
const signalMaxBytes = isSignalChannel
|
||||
? resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
cfg.channels?.signal?.mediaMaxMb,
|
||||
accountId,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const sendTextChunks = async (text: string) => {
|
||||
throwIfAborted(abortSignal);
|
||||
@@ -183,13 +196,63 @@ export async function deliverOutboundPayloads(params: {
|
||||
}
|
||||
};
|
||||
|
||||
const sendSignalText = async (text: string, styles: SignalTextStyleRange[]) => {
|
||||
throwIfAborted(abortSignal);
|
||||
return {
|
||||
channel: "signal" as const,
|
||||
...(await sendSignal(to, text, {
|
||||
maxBytes: signalMaxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
textMode: "plain",
|
||||
textStyles: styles,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const sendSignalTextChunks = async (text: string) => {
|
||||
throwIfAborted(abortSignal);
|
||||
let signalChunks =
|
||||
textLimit === undefined
|
||||
? markdownToSignalTextChunks(text, Number.POSITIVE_INFINITY)
|
||||
: markdownToSignalTextChunks(text, textLimit);
|
||||
if (signalChunks.length === 0 && text) {
|
||||
signalChunks = [{ text, styles: [] }];
|
||||
}
|
||||
for (const chunk of signalChunks) {
|
||||
throwIfAborted(abortSignal);
|
||||
results.push(await sendSignalText(chunk.text, chunk.styles));
|
||||
}
|
||||
};
|
||||
|
||||
const sendSignalMedia = async (caption: string, mediaUrl: string) => {
|
||||
throwIfAborted(abortSignal);
|
||||
const formatted = markdownToSignalTextChunks(caption, Number.POSITIVE_INFINITY)[0] ?? {
|
||||
text: caption,
|
||||
styles: [],
|
||||
};
|
||||
return {
|
||||
channel: "signal" as const,
|
||||
...(await sendSignal(to, formatted.text, {
|
||||
mediaUrl,
|
||||
maxBytes: signalMaxBytes,
|
||||
accountId: accountId ?? undefined,
|
||||
textMode: "plain",
|
||||
textStyles: formatted.styles,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizedPayloads = normalizeOutboundPayloads(payloads);
|
||||
for (const payload of normalizedPayloads) {
|
||||
try {
|
||||
throwIfAborted(abortSignal);
|
||||
params.onPayload?.(payload);
|
||||
if (payload.mediaUrls.length === 0) {
|
||||
await sendTextChunks(payload.text);
|
||||
if (isSignalChannel) {
|
||||
await sendSignalTextChunks(payload.text);
|
||||
} else {
|
||||
await sendTextChunks(payload.text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -198,7 +261,11 @@ export async function deliverOutboundPayloads(params: {
|
||||
throwIfAborted(abortSignal);
|
||||
const caption = first ? payload.text : "";
|
||||
first = false;
|
||||
results.push(await handler.sendMedia(caption, url));
|
||||
if (isSignalChannel) {
|
||||
results.push(await sendSignalMedia(caption, url));
|
||||
} else {
|
||||
results.push(await handler.sendMedia(caption, url));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!params.bestEffort) throw err;
|
||||
|
||||
Reference in New Issue
Block a user