From e4c95a585a0bc267ae8107f5b3a9542aef0830f3 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Tue, 10 Mar 2026 21:07:50 +0530 Subject: [PATCH] fix: unify telegram text chunk sending --- src/telegram/send.test.ts | 25 +++++++ src/telegram/send.ts | 137 +++++++++++++++----------------------- 2 files changed, 77 insertions(+), 85 deletions(-) diff --git a/src/telegram/send.test.ts b/src/telegram/send.test.ts index 25752fc337e..a00d1b2e89e 100644 --- a/src/telegram/send.test.ts +++ b/src/telegram/send.test.ts @@ -1135,6 +1135,31 @@ describe("sendMessageTelegram", () => { }); }); + it("keeps disable_notification on plain-text fallback when silent is true", async () => { + const chatId = "123"; + const parseErr = new Error( + "400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 9", + ); + const sendMessage = vi + .fn() + .mockRejectedValueOnce(parseErr) + .mockResolvedValueOnce({ message_id: 2, chat: { id: chatId } }); + const api = { sendMessage } as unknown as { + sendMessage: typeof sendMessage; + }; + + await sendMessageTelegram(chatId, "_oops_", { + token: "tok", + api, + silent: true, + }); + + expect(sendMessage.mock.calls).toEqual([ + [chatId, "oops", { parse_mode: "HTML", disable_notification: true }], + [chatId, "_oops_", { disable_notification: true }], + ]); + }); + it("parses message_thread_id from recipient string (telegram:group:...:topic:...)", async () => { const chatId = "-1001234567890"; const sendMessage = vi.fn().mockResolvedValue({ diff --git a/src/telegram/send.ts b/src/telegram/send.ts index 4d52d84c80a..fa26df0209a 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -632,28 +632,49 @@ export async function sendMessageTelegram( const linkPreviewEnabled = account.config.linkPreview ?? true; const linkPreviewOptions = linkPreviewEnabled ? undefined : { is_disabled: true }; - const sendTelegramText = async ( - rawText: string, + type TelegramTextChunk = { + plainText: string; + htmlText?: string; + }; + + const sendTelegramTextChunk = async ( + chunk: TelegramTextChunk, params?: Record, - fallbackText?: string, - preRenderedHtml?: string, ) => { return await withTelegramThreadFallback( params, "message", opts.verbose, async (effectiveParams, label) => { - const htmlText = preRenderedHtml ?? renderHtmlText(rawText); const baseParams = effectiveParams ? { ...effectiveParams } : {}; if (linkPreviewOptions) { baseParams.link_preview_options = linkPreviewOptions; } - const hasBaseParams = Object.keys(baseParams).length > 0; - const sendParams = { - parse_mode: "HTML" as const, + const plainParams = { ...baseParams, ...(opts.silent === true ? { disable_notification: true } : {}), }; + const hasPlainParams = Object.keys(plainParams).length > 0; + const requestPlain = (retryLabel: string) => + requestWithChatNotFound( + () => + hasPlainParams + ? api.sendMessage( + chatId, + chunk.plainText, + plainParams as Parameters[2], + ) + : api.sendMessage(chatId, chunk.plainText), + retryLabel, + ); + if (!chunk.htmlText) { + return await requestPlain(label); + } + const htmlText = chunk.htmlText; + const htmlParams = { + parse_mode: "HTML" as const, + ...plainParams, + }; return await withTelegramHtmlParseFallback({ label, verbose: opts.verbose, @@ -663,22 +684,11 @@ export async function sendMessageTelegram( api.sendMessage( chatId, htmlText, - sendParams as Parameters[2], + htmlParams as Parameters[2], ), retryLabel, ), - requestPlain: (retryLabel) => { - const plainParams = hasBaseParams - ? (baseParams as Parameters[2]) - : undefined; - return requestWithChatNotFound( - () => - plainParams - ? api.sendMessage(chatId, fallbackText ?? rawText, plainParams) - : api.sendMessage(chatId, fallbackText ?? rawText), - retryLabel, - ); - }, + requestPlain, }); }, ); @@ -692,11 +702,10 @@ export async function sendMessageTelegram( } : undefined; - const sendPlainChunkedText = async ( - plainText: string, + const sendTelegramTextChunks = async ( + chunks: TelegramTextChunk[], context: string, ): Promise<{ messageId: string; chatId: string }> => { - const chunks = splitTelegramPlainTextChunks(plainText, 4000); let lastMessageId = ""; let lastChatId = chatId; for (let index = 0; index < chunks.length; index += 1) { @@ -704,25 +713,7 @@ export async function sendMessageTelegram( if (!chunk) { continue; } - const res = await withTelegramThreadFallback( - buildTextParams(index === chunks.length - 1), - "message", - opts.verbose, - async (effectiveParams, label) => { - const params = effectiveParams ? { ...effectiveParams } : {}; - if (linkPreviewOptions) { - params.link_preview_options = linkPreviewOptions; - } - const hasParams = Object.keys(params).length > 0; - return await requestWithChatNotFound( - () => - hasParams - ? api.sendMessage(chatId, chunk, params as Parameters[2]) - : api.sendMessage(chatId, chunk), - label, - ); - }, - ); + const res = await sendTelegramTextChunk(chunk, buildTextParams(index === chunks.length - 1)); const messageId = resolveTelegramMessageIdOrThrow(res, context); recordSentMessage(chatId, messageId); lastMessageId = String(messageId); @@ -731,10 +722,7 @@ export async function sendMessageTelegram( return { messageId: lastMessageId, chatId: lastChatId }; }; - const sendChunkedText = async ( - rawText: string, - context: string, - ): Promise<{ messageId: string; chatId: string }> => { + const buildChunkedTextPlan = (rawText: string, context: string): TelegramTextChunk[] => { const fallbackText = opts.plainText ?? rawText; let htmlChunks: string[]; try { @@ -745,45 +733,25 @@ export async function sendMessageTelegram( error, )}`, ); - return await sendPlainChunkedText(fallbackText, context); + return splitTelegramPlainTextChunks(fallbackText, 4000).map((plainText) => ({ plainText })); } const fixedPlainTextChunks = splitTelegramPlainTextChunks(fallbackText, 4000); if (fixedPlainTextChunks.length > htmlChunks.length) { logVerbose( `telegram ${context} plain-text fallback needs more chunks than HTML; sending plain text`, ); - return await sendPlainChunkedText(fallbackText, context); + return fixedPlainTextChunks.map((plainText) => ({ plainText })); } const plainTextChunks = splitTelegramPlainTextFallback(fallbackText, htmlChunks.length, 4000); - const chunks = htmlChunks.map((chunk, index) => ({ - rawText: chunk, - htmlText: chunk, - plainText: plainTextChunks[index], + return htmlChunks.map((htmlText, index) => ({ + htmlText, + plainText: plainTextChunks[index] ?? htmlText, })); - - let lastMessageId = ""; - let lastChatId = chatId; - for (let index = 0; index < chunks.length; index += 1) { - const chunk = chunks[index]; - if (!chunk) { - continue; - } - const isLastChunk = index === chunks.length - 1; - const res = await sendTelegramText( - chunk.rawText, - buildTextParams(isLastChunk), - chunk.plainText, - chunk.htmlText, - ); - const messageId = resolveTelegramMessageIdOrThrow(res, context); - recordSentMessage(chatId, messageId); - lastMessageId = String(messageId); - lastChatId = String(res?.chat?.id ?? chatId); - } - - return { messageId: lastMessageId, chatId: lastChatId }; }; + const sendChunkedText = async (rawText: string, context: string) => + await sendTelegramTextChunks(buildChunkedTextPlan(rawText, context), context); + if (mediaUrl) { const media = await loadWebMedia( mediaUrl, @@ -942,11 +910,11 @@ export async function sendMessageTelegram( const textResult = await sendChunkedText(followUpText, "text follow-up send"); return { messageId: textResult.messageId, chatId: resolvedChatId }; } - const textParams = buildTextParams(true); - const textRes = await sendTelegramText(followUpText, textParams); - const textMessageId = resolveTelegramMessageIdOrThrow(textRes, "text follow-up send"); - recordSentMessage(chatId, textMessageId); - return { messageId: String(textMessageId), chatId: resolvedChatId }; + const textResult = await sendTelegramTextChunks( + [{ plainText: followUpText, htmlText: renderHtmlText(followUpText) }], + "text follow-up send", + ); + return { messageId: textResult.messageId, chatId: resolvedChatId }; } return { messageId: String(mediaMessageId), chatId: resolvedChatId }; @@ -959,11 +927,10 @@ export async function sendMessageTelegram( if (textMode === "html") { textResult = await sendChunkedText(text, "text send"); } else { - const textParams = buildTextParams(true); - const res = await sendTelegramText(text, textParams, opts.plainText); - const messageId = resolveTelegramMessageIdOrThrow(res, "text send"); - recordSentMessage(chatId, messageId); - textResult = { messageId: String(messageId), chatId: String(res?.chat?.id ?? chatId) }; + textResult = await sendTelegramTextChunks( + [{ plainText: opts.plainText ?? text, htmlText: renderHtmlText(text) }], + "text send", + ); } recordChannelActivity({ channel: "telegram",