mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor: share telegram payload send flow
This commit is contained in:
@@ -313,6 +313,68 @@ describe("telegramPlugin duplicate token guard", () => {
|
||||
expect(result).toMatchObject({ channel: "telegram", messageId: "tg-2" });
|
||||
});
|
||||
|
||||
it("sends outbound payload media lists and keeps buttons on the first message only", async () => {
|
||||
const sendMessageTelegram = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ messageId: "tg-3", chatId: "12345" })
|
||||
.mockResolvedValueOnce({ messageId: "tg-4", chatId: "12345" });
|
||||
setTelegramRuntime({
|
||||
channel: {
|
||||
telegram: {
|
||||
sendMessageTelegram,
|
||||
},
|
||||
},
|
||||
} as unknown as PluginRuntime);
|
||||
|
||||
const result = await telegramPlugin.outbound!.sendPayload!({
|
||||
cfg: createCfg(),
|
||||
to: "12345",
|
||||
text: "",
|
||||
payload: {
|
||||
text: "Approval required",
|
||||
mediaUrls: ["https://example.com/1.jpg", "https://example.com/2.jpg"],
|
||||
channelData: {
|
||||
telegram: {
|
||||
quoteText: "quoted",
|
||||
buttons: [[{ text: "Allow Once", callback_data: "/approve abc allow-once" }]],
|
||||
},
|
||||
},
|
||||
},
|
||||
mediaLocalRoots: ["/tmp/media"],
|
||||
accountId: "ops",
|
||||
silent: true,
|
||||
});
|
||||
|
||||
expect(sendMessageTelegram).toHaveBeenCalledTimes(2);
|
||||
expect(sendMessageTelegram).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"12345",
|
||||
"Approval required",
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://example.com/1.jpg",
|
||||
mediaLocalRoots: ["/tmp/media"],
|
||||
quoteText: "quoted",
|
||||
silent: true,
|
||||
buttons: [[{ text: "Allow Once", callback_data: "/approve abc allow-once" }]],
|
||||
}),
|
||||
);
|
||||
expect(sendMessageTelegram).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"12345",
|
||||
"",
|
||||
expect.objectContaining({
|
||||
mediaUrl: "https://example.com/2.jpg",
|
||||
mediaLocalRoots: ["/tmp/media"],
|
||||
quoteText: "quoted",
|
||||
silent: true,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(sendMessageTelegram.mock.calls[1]?.[2] as Record<string, unknown>)?.buttons,
|
||||
).toBeUndefined();
|
||||
expect(result).toMatchObject({ channel: "telegram", messageId: "tg-4" });
|
||||
});
|
||||
|
||||
it("ignores accounts with missing tokens during duplicate-token checks", async () => {
|
||||
const cfg = createCfg();
|
||||
cfg.channels!.telegram!.accounts!.ops = {} as never;
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
resolveTelegramAccount,
|
||||
resolveTelegramGroupRequireMention,
|
||||
resolveTelegramGroupToolPolicy,
|
||||
sendTelegramPayloadMessages,
|
||||
telegramOnboardingAdapter,
|
||||
TelegramConfigSchema,
|
||||
type ChannelMessageActionAdapter,
|
||||
@@ -91,10 +92,6 @@ const telegramMessageActions: ChannelMessageActionAdapter = {
|
||||
},
|
||||
};
|
||||
|
||||
type TelegramInlineButtons = ReadonlyArray<
|
||||
ReadonlyArray<{ text: string; callback_data: string; style?: "danger" | "success" | "primary" }>
|
||||
>;
|
||||
|
||||
const telegramConfigAccessors = createScopedAccountConfigAccessors({
|
||||
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
|
||||
@@ -332,47 +329,21 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProb
|
||||
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
const replyToMessageId = parseTelegramReplyToMessageId(replyToId);
|
||||
const messageThreadId = parseTelegramThreadId(threadId);
|
||||
const telegramData = payload.channelData?.telegram as
|
||||
| { buttons?: TelegramInlineButtons; quoteText?: string }
|
||||
| undefined;
|
||||
const quoteText =
|
||||
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
||||
const text = payload.text ?? "";
|
||||
const mediaUrls = payload.mediaUrls?.length
|
||||
? payload.mediaUrls
|
||||
: payload.mediaUrl
|
||||
? [payload.mediaUrl]
|
||||
: [];
|
||||
const baseOpts = {
|
||||
const result = await sendTelegramPayloadMessages({
|
||||
send,
|
||||
to,
|
||||
payload,
|
||||
baseOpts: {
|
||||
verbose: false,
|
||||
cfg,
|
||||
mediaLocalRoots,
|
||||
messageThreadId,
|
||||
replyToMessageId,
|
||||
quoteText,
|
||||
accountId: accountId ?? undefined,
|
||||
silent: silent ?? undefined,
|
||||
};
|
||||
|
||||
if (mediaUrls.length === 0) {
|
||||
const result = await send(to, text, {
|
||||
...baseOpts,
|
||||
buttons: telegramData?.buttons,
|
||||
},
|
||||
});
|
||||
return { channel: "telegram", ...result };
|
||||
}
|
||||
|
||||
let finalResult: Awaited<ReturnType<typeof send>> | undefined;
|
||||
for (let i = 0; i < mediaUrls.length; i += 1) {
|
||||
const mediaUrl = mediaUrls[i];
|
||||
const isFirst = i === 0;
|
||||
finalResult = await send(to, isFirst ? text : "", {
|
||||
...baseOpts,
|
||||
mediaUrl,
|
||||
...(isFirst ? { buttons: telegramData?.buttons } : {}),
|
||||
});
|
||||
}
|
||||
return { channel: "telegram", ...(finalResult ?? { messageId: "unknown", chatId: to }) };
|
||||
},
|
||||
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, silent }) => {
|
||||
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReplyPayload } from "../../../auto-reply/types.js";
|
||||
import type { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||
import type { TelegramInlineButtons } from "../../../telegram/button-types.js";
|
||||
import { markdownToTelegramHtmlChunks } from "../../../telegram/format.js";
|
||||
@@ -8,16 +9,19 @@ import {
|
||||
import { sendMessageTelegram } from "../../../telegram/send.js";
|
||||
import type { ChannelOutboundAdapter } from "../types.js";
|
||||
|
||||
type TelegramSendFn = typeof sendMessageTelegram;
|
||||
type TelegramSendOpts = Parameters<TelegramSendFn>[2];
|
||||
|
||||
function resolveTelegramSendContext(params: {
|
||||
cfg: NonNullable<Parameters<typeof sendMessageTelegram>[2]>["cfg"];
|
||||
cfg: NonNullable<TelegramSendOpts>["cfg"];
|
||||
deps?: OutboundSendDeps;
|
||||
accountId?: string | null;
|
||||
replyToId?: string | null;
|
||||
threadId?: string | number | null;
|
||||
}): {
|
||||
send: typeof sendMessageTelegram;
|
||||
send: TelegramSendFn;
|
||||
baseOpts: {
|
||||
cfg: NonNullable<Parameters<typeof sendMessageTelegram>[2]>["cfg"];
|
||||
cfg: NonNullable<TelegramSendOpts>["cfg"];
|
||||
verbose: false;
|
||||
textMode: "html";
|
||||
messageThreadId?: number;
|
||||
@@ -39,6 +43,49 @@ function resolveTelegramSendContext(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendTelegramPayloadMessages(params: {
|
||||
send: TelegramSendFn;
|
||||
to: string;
|
||||
payload: ReplyPayload;
|
||||
baseOpts: Omit<NonNullable<TelegramSendOpts>, "buttons" | "mediaUrl" | "quoteText">;
|
||||
}): Promise<Awaited<ReturnType<TelegramSendFn>>> {
|
||||
const telegramData = params.payload.channelData?.telegram as
|
||||
| { buttons?: TelegramInlineButtons; quoteText?: string }
|
||||
| undefined;
|
||||
const quoteText =
|
||||
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
||||
const text = params.payload.text ?? "";
|
||||
const mediaUrls = params.payload.mediaUrls?.length
|
||||
? params.payload.mediaUrls
|
||||
: params.payload.mediaUrl
|
||||
? [params.payload.mediaUrl]
|
||||
: [];
|
||||
const payloadOpts = {
|
||||
...params.baseOpts,
|
||||
quoteText,
|
||||
};
|
||||
|
||||
if (mediaUrls.length === 0) {
|
||||
return await params.send(params.to, text, {
|
||||
...payloadOpts,
|
||||
buttons: telegramData?.buttons,
|
||||
});
|
||||
}
|
||||
|
||||
// Telegram allows reply_markup on media; attach buttons only to the first send.
|
||||
let finalResult: Awaited<ReturnType<TelegramSendFn>> | undefined;
|
||||
for (let i = 0; i < mediaUrls.length; i += 1) {
|
||||
const mediaUrl = mediaUrls[i];
|
||||
const isFirst = i === 0;
|
||||
finalResult = await params.send(params.to, isFirst ? text : "", {
|
||||
...payloadOpts,
|
||||
mediaUrl,
|
||||
...(isFirst ? { buttons: telegramData?.buttons } : {}),
|
||||
});
|
||||
}
|
||||
return finalResult ?? { messageId: "unknown", chatId: params.to };
|
||||
}
|
||||
|
||||
export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
chunker: markdownToTelegramHtmlChunks,
|
||||
@@ -92,48 +139,22 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
||||
replyToId,
|
||||
threadId,
|
||||
}) => {
|
||||
const { send, baseOpts: contextOpts } = resolveTelegramSendContext({
|
||||
const { send, baseOpts } = resolveTelegramSendContext({
|
||||
cfg,
|
||||
deps,
|
||||
accountId,
|
||||
replyToId,
|
||||
threadId,
|
||||
});
|
||||
const telegramData = payload.channelData?.telegram as
|
||||
| { buttons?: TelegramInlineButtons; quoteText?: string }
|
||||
| undefined;
|
||||
const quoteText =
|
||||
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
||||
const text = payload.text ?? "";
|
||||
const mediaUrls = payload.mediaUrls?.length
|
||||
? payload.mediaUrls
|
||||
: payload.mediaUrl
|
||||
? [payload.mediaUrl]
|
||||
: [];
|
||||
const payloadOpts = {
|
||||
...contextOpts,
|
||||
quoteText,
|
||||
const result = await sendTelegramPayloadMessages({
|
||||
send,
|
||||
to,
|
||||
payload,
|
||||
baseOpts: {
|
||||
...baseOpts,
|
||||
mediaLocalRoots,
|
||||
};
|
||||
if (mediaUrls.length === 0) {
|
||||
const result = await send(to, text, {
|
||||
...payloadOpts,
|
||||
buttons: telegramData?.buttons,
|
||||
},
|
||||
});
|
||||
return { channel: "telegram", ...result };
|
||||
}
|
||||
|
||||
// Telegram allows reply_markup on media; attach buttons only to first send.
|
||||
let finalResult: Awaited<ReturnType<typeof send>> | undefined;
|
||||
for (let i = 0; i < mediaUrls.length; i += 1) {
|
||||
const mediaUrl = mediaUrls[i];
|
||||
const isFirst = i === 0;
|
||||
finalResult = await send(to, isFirst ? text : "", {
|
||||
...payloadOpts,
|
||||
mediaUrl,
|
||||
...(isFirst ? { buttons: telegramData?.buttons } : {}),
|
||||
});
|
||||
}
|
||||
return { channel: "telegram", ...(finalResult ?? { messageId: "unknown", chatId: to }) };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@ export {
|
||||
parseTelegramThreadId,
|
||||
} from "../telegram/outbound-params.js";
|
||||
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
||||
export { sendTelegramPayloadMessages } from "../channels/plugins/outbound/telegram.js";
|
||||
|
||||
export {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
|
||||
Reference in New Issue
Block a user