feat: Add Line plugin (#1630)
* feat: add LINE plugin (#1630) (thanks @plum-dawg) * feat: complete LINE plugin (#1630) (thanks @plum-dawg) * chore: drop line plugin node_modules (#1630) (thanks @plum-dawg) * test: mock /context report in commands test (#1630) (thanks @plum-dawg) * test: limit macOS CI workers to avoid OOM (#1630) (thanks @plum-dawg) * test: reduce macOS CI vitest workers (#1630) (thanks @plum-dawg) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -311,6 +311,28 @@ describe("deliverOutboundPayloads", () => {
|
||||
expect(results).toEqual([{ channel: "whatsapp", messageId: "w2", toJid: "jid" }]);
|
||||
});
|
||||
|
||||
it("passes normalized payload to onError", async () => {
|
||||
const sendWhatsApp = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
const onError = vi.fn();
|
||||
const cfg: ClawdbotConfig = {};
|
||||
|
||||
await deliverOutboundPayloads({
|
||||
cfg,
|
||||
channel: "whatsapp",
|
||||
to: "+1555",
|
||||
payloads: [{ text: "hi", mediaUrl: "https://x.test/a.jpg" }],
|
||||
deps: { sendWhatsApp },
|
||||
bestEffort: true,
|
||||
onError,
|
||||
});
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
expect.any(Error),
|
||||
expect.objectContaining({ text: "hi", mediaUrls: ["https://x.test/a.jpg"] }),
|
||||
);
|
||||
});
|
||||
|
||||
it("mirrors delivered output when mirror options are provided", async () => {
|
||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "m1", chatId: "c1" });
|
||||
const cfg: ClawdbotConfig = {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
resolveMirroredTranscriptText,
|
||||
} from "../../config/sessions.js";
|
||||
import type { NormalizedOutboundPayload } from "./payloads.js";
|
||||
import { normalizeOutboundPayloads } from "./payloads.js";
|
||||
import { normalizeReplyPayloadsForDelivery } from "./payloads.js";
|
||||
import type { OutboundChannel } from "./targets.js";
|
||||
|
||||
export type { NormalizedOutboundPayload } from "./payloads.js";
|
||||
@@ -69,6 +69,7 @@ type ChannelHandler = {
|
||||
chunker: Chunker | null;
|
||||
chunkerMode?: "text" | "markdown";
|
||||
textChunkLimit?: number;
|
||||
sendPayload?: (payload: ReplyPayload) => Promise<OutboundDeliveryResult>;
|
||||
sendText: (text: string) => Promise<OutboundDeliveryResult>;
|
||||
sendMedia: (caption: string, mediaUrl: string) => Promise<OutboundDeliveryResult>;
|
||||
};
|
||||
@@ -132,6 +133,21 @@ function createPluginHandler(params: {
|
||||
chunker,
|
||||
chunkerMode,
|
||||
textChunkLimit: outbound.textChunkLimit,
|
||||
sendPayload: outbound.sendPayload
|
||||
? async (payload) =>
|
||||
outbound.sendPayload!({
|
||||
cfg: params.cfg,
|
||||
to: params.to,
|
||||
text: payload.text ?? "",
|
||||
mediaUrl: payload.mediaUrl,
|
||||
accountId: params.accountId,
|
||||
replyToId: params.replyToId,
|
||||
threadId: params.threadId,
|
||||
gifPlayback: params.gifPlayback,
|
||||
deps: params.deps,
|
||||
payload,
|
||||
})
|
||||
: undefined,
|
||||
sendText: async (text) =>
|
||||
sendText({
|
||||
cfg: params.cfg,
|
||||
@@ -294,24 +310,33 @@ export async function deliverOutboundPayloads(params: {
|
||||
})),
|
||||
};
|
||||
};
|
||||
const normalizedPayloads = normalizeOutboundPayloads(payloads);
|
||||
const normalizedPayloads = normalizeReplyPayloadsForDelivery(payloads);
|
||||
for (const payload of normalizedPayloads) {
|
||||
const payloadSummary: NormalizedOutboundPayload = {
|
||||
text: payload.text ?? "",
|
||||
mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []),
|
||||
channelData: payload.channelData,
|
||||
};
|
||||
try {
|
||||
throwIfAborted(abortSignal);
|
||||
params.onPayload?.(payload);
|
||||
if (payload.mediaUrls.length === 0) {
|
||||
params.onPayload?.(payloadSummary);
|
||||
if (handler.sendPayload && payload.channelData) {
|
||||
results.push(await handler.sendPayload(payload));
|
||||
continue;
|
||||
}
|
||||
if (payloadSummary.mediaUrls.length === 0) {
|
||||
if (isSignalChannel) {
|
||||
await sendSignalTextChunks(payload.text);
|
||||
await sendSignalTextChunks(payloadSummary.text);
|
||||
} else {
|
||||
await sendTextChunks(payload.text);
|
||||
await sendTextChunks(payloadSummary.text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let first = true;
|
||||
for (const url of payload.mediaUrls) {
|
||||
for (const url of payloadSummary.mediaUrls) {
|
||||
throwIfAborted(abortSignal);
|
||||
const caption = first ? payload.text : "";
|
||||
const caption = first ? payloadSummary.text : "";
|
||||
first = false;
|
||||
if (isSignalChannel) {
|
||||
results.push(await sendSignalMedia(caption, url));
|
||||
@@ -321,7 +346,7 @@ export async function deliverOutboundPayloads(params: {
|
||||
}
|
||||
} catch (err) {
|
||||
if (!params.bestEffort) throw err;
|
||||
params.onError?.(err, payload);
|
||||
params.onError?.(err, payloadSummary);
|
||||
}
|
||||
}
|
||||
if (params.mirror && results.length > 0) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { formatOutboundPayloadLog, normalizeOutboundPayloadsForJson } from "./payloads.js";
|
||||
import {
|
||||
formatOutboundPayloadLog,
|
||||
normalizeOutboundPayloads,
|
||||
normalizeOutboundPayloadsForJson,
|
||||
} from "./payloads.js";
|
||||
|
||||
describe("normalizeOutboundPayloadsForJson", () => {
|
||||
it("normalizes payloads with mediaUrl and mediaUrls", () => {
|
||||
@@ -11,16 +15,18 @@ describe("normalizeOutboundPayloadsForJson", () => {
|
||||
{ text: "multi", mediaUrls: ["https://x.test/1.png"] },
|
||||
]),
|
||||
).toEqual([
|
||||
{ text: "hi", mediaUrl: null, mediaUrls: undefined },
|
||||
{ text: "hi", mediaUrl: null, mediaUrls: undefined, channelData: undefined },
|
||||
{
|
||||
text: "photo",
|
||||
mediaUrl: "https://x.test/a.jpg",
|
||||
mediaUrls: ["https://x.test/a.jpg"],
|
||||
channelData: undefined,
|
||||
},
|
||||
{
|
||||
text: "multi",
|
||||
mediaUrl: null,
|
||||
mediaUrls: ["https://x.test/1.png"],
|
||||
channelData: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -37,11 +43,20 @@ describe("normalizeOutboundPayloadsForJson", () => {
|
||||
text: "",
|
||||
mediaUrl: null,
|
||||
mediaUrls: ["https://x.test/a.png", "https://x.test/b.png"],
|
||||
channelData: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeOutboundPayloads", () => {
|
||||
it("keeps channelData-only payloads", () => {
|
||||
const channelData = { line: { flexMessage: { altText: "Card", contents: {} } } };
|
||||
const normalized = normalizeOutboundPayloads([{ channelData }]);
|
||||
expect(normalized).toEqual([{ text: "", mediaUrls: [], channelData }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatOutboundPayloadLog", () => {
|
||||
it("trims trailing text and appends media lines", () => {
|
||||
expect(
|
||||
|
||||
@@ -5,12 +5,14 @@ import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
export type NormalizedOutboundPayload = {
|
||||
text: string;
|
||||
mediaUrls: string[];
|
||||
channelData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type OutboundPayloadJson = {
|
||||
text: string;
|
||||
mediaUrl: string | null;
|
||||
mediaUrls?: string[];
|
||||
channelData?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function mergeMediaUrls(...lists: Array<Array<string | undefined> | undefined>): string[] {
|
||||
@@ -58,11 +60,23 @@ export function normalizeReplyPayloadsForDelivery(payloads: ReplyPayload[]): Rep
|
||||
|
||||
export function normalizeOutboundPayloads(payloads: ReplyPayload[]): NormalizedOutboundPayload[] {
|
||||
return normalizeReplyPayloadsForDelivery(payloads)
|
||||
.map((payload) => ({
|
||||
text: payload.text ?? "",
|
||||
mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []),
|
||||
}))
|
||||
.filter((payload) => payload.text || payload.mediaUrls.length > 0);
|
||||
.map((payload) => {
|
||||
const channelData = payload.channelData;
|
||||
const normalized: NormalizedOutboundPayload = {
|
||||
text: payload.text ?? "",
|
||||
mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []),
|
||||
};
|
||||
if (channelData && Object.keys(channelData).length > 0) {
|
||||
normalized.channelData = channelData;
|
||||
}
|
||||
return normalized;
|
||||
})
|
||||
.filter(
|
||||
(payload) =>
|
||||
payload.text ||
|
||||
payload.mediaUrls.length > 0 ||
|
||||
Boolean(payload.channelData && Object.keys(payload.channelData).length > 0),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeOutboundPayloadsForJson(payloads: ReplyPayload[]): OutboundPayloadJson[] {
|
||||
@@ -70,6 +84,7 @@ export function normalizeOutboundPayloadsForJson(payloads: ReplyPayload[]): Outb
|
||||
text: payload.text ?? "",
|
||||
mediaUrl: payload.mediaUrl ?? null,
|
||||
mediaUrls: payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : undefined),
|
||||
channelData: payload.channelData,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user