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" });
|
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 () => {
|
it("ignores accounts with missing tokens during duplicate-token checks", async () => {
|
||||||
const cfg = createCfg();
|
const cfg = createCfg();
|
||||||
cfg.channels!.telegram!.accounts!.ops = {} as never;
|
cfg.channels!.telegram!.accounts!.ops = {} as never;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
resolveTelegramAccount,
|
resolveTelegramAccount,
|
||||||
resolveTelegramGroupRequireMention,
|
resolveTelegramGroupRequireMention,
|
||||||
resolveTelegramGroupToolPolicy,
|
resolveTelegramGroupToolPolicy,
|
||||||
|
sendTelegramPayloadMessages,
|
||||||
telegramOnboardingAdapter,
|
telegramOnboardingAdapter,
|
||||||
TelegramConfigSchema,
|
TelegramConfigSchema,
|
||||||
type ChannelMessageActionAdapter,
|
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({
|
const telegramConfigAccessors = createScopedAccountConfigAccessors({
|
||||||
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
|
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
|
||||||
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
|
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 send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
||||||
const replyToMessageId = parseTelegramReplyToMessageId(replyToId);
|
const replyToMessageId = parseTelegramReplyToMessageId(replyToId);
|
||||||
const messageThreadId = parseTelegramThreadId(threadId);
|
const messageThreadId = parseTelegramThreadId(threadId);
|
||||||
const telegramData = payload.channelData?.telegram as
|
const result = await sendTelegramPayloadMessages({
|
||||||
| { buttons?: TelegramInlineButtons; quoteText?: string }
|
send,
|
||||||
| undefined;
|
to,
|
||||||
const quoteText =
|
payload,
|
||||||
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
baseOpts: {
|
||||||
const text = payload.text ?? "";
|
verbose: false,
|
||||||
const mediaUrls = payload.mediaUrls?.length
|
cfg,
|
||||||
? payload.mediaUrls
|
mediaLocalRoots,
|
||||||
: payload.mediaUrl
|
messageThreadId,
|
||||||
? [payload.mediaUrl]
|
replyToMessageId,
|
||||||
: [];
|
accountId: accountId ?? undefined,
|
||||||
const baseOpts = {
|
silent: silent ?? undefined,
|
||||||
verbose: false,
|
},
|
||||||
cfg,
|
});
|
||||||
mediaLocalRoots,
|
return { channel: "telegram", ...result };
|
||||||
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 }) => {
|
sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, silent }) => {
|
||||||
const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram;
|
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 { OutboundSendDeps } from "../../../infra/outbound/deliver.js";
|
||||||
import type { TelegramInlineButtons } from "../../../telegram/button-types.js";
|
import type { TelegramInlineButtons } from "../../../telegram/button-types.js";
|
||||||
import { markdownToTelegramHtmlChunks } from "../../../telegram/format.js";
|
import { markdownToTelegramHtmlChunks } from "../../../telegram/format.js";
|
||||||
@@ -8,16 +9,19 @@ import {
|
|||||||
import { sendMessageTelegram } from "../../../telegram/send.js";
|
import { sendMessageTelegram } from "../../../telegram/send.js";
|
||||||
import type { ChannelOutboundAdapter } from "../types.js";
|
import type { ChannelOutboundAdapter } from "../types.js";
|
||||||
|
|
||||||
|
type TelegramSendFn = typeof sendMessageTelegram;
|
||||||
|
type TelegramSendOpts = Parameters<TelegramSendFn>[2];
|
||||||
|
|
||||||
function resolveTelegramSendContext(params: {
|
function resolveTelegramSendContext(params: {
|
||||||
cfg: NonNullable<Parameters<typeof sendMessageTelegram>[2]>["cfg"];
|
cfg: NonNullable<TelegramSendOpts>["cfg"];
|
||||||
deps?: OutboundSendDeps;
|
deps?: OutboundSendDeps;
|
||||||
accountId?: string | null;
|
accountId?: string | null;
|
||||||
replyToId?: string | null;
|
replyToId?: string | null;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
}): {
|
}): {
|
||||||
send: typeof sendMessageTelegram;
|
send: TelegramSendFn;
|
||||||
baseOpts: {
|
baseOpts: {
|
||||||
cfg: NonNullable<Parameters<typeof sendMessageTelegram>[2]>["cfg"];
|
cfg: NonNullable<TelegramSendOpts>["cfg"];
|
||||||
verbose: false;
|
verbose: false;
|
||||||
textMode: "html";
|
textMode: "html";
|
||||||
messageThreadId?: number;
|
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 = {
|
export const telegramOutbound: ChannelOutboundAdapter = {
|
||||||
deliveryMode: "direct",
|
deliveryMode: "direct",
|
||||||
chunker: markdownToTelegramHtmlChunks,
|
chunker: markdownToTelegramHtmlChunks,
|
||||||
@@ -92,48 +139,22 @@ export const telegramOutbound: ChannelOutboundAdapter = {
|
|||||||
replyToId,
|
replyToId,
|
||||||
threadId,
|
threadId,
|
||||||
}) => {
|
}) => {
|
||||||
const { send, baseOpts: contextOpts } = resolveTelegramSendContext({
|
const { send, baseOpts } = resolveTelegramSendContext({
|
||||||
cfg,
|
cfg,
|
||||||
deps,
|
deps,
|
||||||
accountId,
|
accountId,
|
||||||
replyToId,
|
replyToId,
|
||||||
threadId,
|
threadId,
|
||||||
});
|
});
|
||||||
const telegramData = payload.channelData?.telegram as
|
const result = await sendTelegramPayloadMessages({
|
||||||
| { buttons?: TelegramInlineButtons; quoteText?: string }
|
send,
|
||||||
| undefined;
|
to,
|
||||||
const quoteText =
|
payload,
|
||||||
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
baseOpts: {
|
||||||
const text = payload.text ?? "";
|
...baseOpts,
|
||||||
const mediaUrls = payload.mediaUrls?.length
|
mediaLocalRoots,
|
||||||
? payload.mediaUrls
|
},
|
||||||
: payload.mediaUrl
|
});
|
||||||
? [payload.mediaUrl]
|
return { channel: "telegram", ...result };
|
||||||
: [];
|
|
||||||
const payloadOpts = {
|
|
||||||
...contextOpts,
|
|
||||||
quoteText,
|
|
||||||
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,
|
parseTelegramThreadId,
|
||||||
} from "../telegram/outbound-params.js";
|
} from "../telegram/outbound-params.js";
|
||||||
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js";
|
||||||
|
export { sendTelegramPayloadMessages } from "../channels/plugins/outbound/telegram.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||||
|
|||||||
Reference in New Issue
Block a user