From e7e6d06a2ea01a63eb754409f0d75aee4143a421 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Mon, 16 Feb 2026 12:01:01 +0530 Subject: [PATCH] refactor(telegram): unify send parse fallback and thread params --- src/telegram/send.ts | 213 ++++++++++++++++++++++++------------------- 1 file changed, 121 insertions(+), 92 deletions(-) diff --git a/src/telegram/send.ts b/src/telegram/send.ts index 414d080bfc8..72cf2eb5037 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -219,6 +219,60 @@ function removeMessageThreadIdParam( return Object.keys(next).length > 0 ? next : undefined; } +function isTelegramHtmlParseError(err: unknown): boolean { + return PARSE_ERR_RE.test(formatErrorMessage(err)); +} + +function buildTelegramThreadReplyParams(params: { + targetMessageThreadId?: number; + messageThreadId?: number; + replyToMessageId?: number; + quoteText?: string; +}): Record { + const messageThreadId = + params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId; + const threadSpec = + messageThreadId != null ? { id: messageThreadId, scope: "forum" as const } : undefined; + const threadIdParams = buildTelegramThreadParams(threadSpec); + const threadParams: Record = threadIdParams ? { ...threadIdParams } : {}; + + if (params.replyToMessageId != null) { + const replyToMessageId = Math.trunc(params.replyToMessageId); + if (params.quoteText?.trim()) { + threadParams.reply_parameters = { + message_id: replyToMessageId, + quote: params.quoteText.trim(), + }; + } else { + threadParams.reply_to_message_id = replyToMessageId; + } + } + return threadParams; +} + +async function withTelegramHtmlParseFallback(params: { + label: string; + verbose?: boolean; + requestHtml: (label: string) => Promise; + requestPlain: (label: string) => Promise; +}): Promise { + try { + return await params.requestHtml(params.label); + } catch (err) { + if (!isTelegramHtmlParseError(err)) { + throw err; + } + if (params.verbose) { + console.warn( + `telegram ${params.label} failed with HTML parse error, retrying as plain text: ${formatErrorMessage( + err, + )}`, + ); + } + return await params.requestPlain(`${params.label}-plain`); + } +} + type TelegramApiContext = { cfg: ReturnType; account: ResolvedTelegramAccount; @@ -371,25 +425,12 @@ export async function sendMessageTelegram( const mediaUrl = opts.mediaUrl?.trim(); const replyMarkup = buildInlineKeyboard(opts.buttons); - // Build optional params for forum topics and reply threading. - // Only include these if actually provided to keep API calls clean. - const messageThreadId = - opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId; - const threadSpec = - messageThreadId != null ? { id: messageThreadId, scope: "forum" as const } : undefined; - const threadIdParams = buildTelegramThreadParams(threadSpec); - const threadParams: Record = threadIdParams ? { ...threadIdParams } : {}; - const quoteText = opts.quoteText?.trim(); - if (opts.replyToMessageId != null) { - if (quoteText) { - threadParams.reply_parameters = { - message_id: Math.trunc(opts.replyToMessageId), - quote: quoteText, - }; - } else { - threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId); - } - } + const threadParams = buildTelegramThreadReplyParams({ + targetMessageThreadId: target.messageThreadId, + messageThreadId: opts.messageThreadId, + replyToMessageId: opts.replyToMessageId, + quoteText: opts.quoteText, + }); const hasThreadParams = Object.keys(threadParams).length > 0; const requestWithDiag = createTelegramRequestWithDiag({ cfg, @@ -437,33 +478,32 @@ export async function sendMessageTelegram( ...baseParams, ...(opts.silent === true ? { disable_notification: true } : {}), }; - const res = await requestWithChatNotFound( - () => - api.sendMessage(chatId, htmlText, sendParams as Parameters[2]), + return await withTelegramHtmlParseFallback({ label, - ).catch(async (err) => { - // Telegram rejects malformed HTML (e.g., unsupported tags or entities). - // When that happens, fall back to plain text so the message still delivers. - const errText = formatErrorMessage(err); - if (PARSE_ERR_RE.test(errText)) { - if (opts.verbose) { - console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`); - } - const fallback = fallbackText ?? rawText; + verbose: opts.verbose, + requestHtml: (retryLabel) => + requestWithChatNotFound( + () => + api.sendMessage( + chatId, + htmlText, + sendParams as Parameters[2], + ), + retryLabel, + ), + requestPlain: (retryLabel) => { const plainParams = hasBaseParams ? (baseParams as Parameters[2]) : undefined; - return await requestWithChatNotFound( + return requestWithChatNotFound( () => plainParams - ? api.sendMessage(chatId, fallback, plainParams) - : api.sendMessage(chatId, fallback), - `${label}-plain`, + ? api.sendMessage(chatId, fallbackText ?? rawText, plainParams) + : api.sendMessage(chatId, fallbackText ?? rawText), + retryLabel, ); - } - throw err; + }, }); - return res; }, ); }; @@ -792,45 +832,41 @@ export async function editMessageTelegram( if (replyMarkup !== undefined) { editParams.reply_markup = replyMarkup; } + const plainParams: Record = {}; + if (opts.linkPreview === false) { + plainParams.link_preview_options = { is_disabled: true }; + } + if (replyMarkup !== undefined) { + plainParams.reply_markup = replyMarkup; + } - await requestWithEditShouldLog( - () => api.editMessageText(chatId, messageId, htmlText, editParams), - "editMessage", - (err) => !isTelegramMessageNotModifiedError(err), - ).catch(async (err) => { + try { + await withTelegramHtmlParseFallback({ + label: "editMessage", + verbose: opts.verbose, + requestHtml: (retryLabel) => + requestWithEditShouldLog( + () => api.editMessageText(chatId, messageId, htmlText, editParams), + retryLabel, + (err) => !isTelegramMessageNotModifiedError(err), + ), + requestPlain: (retryLabel) => + requestWithEditShouldLog( + () => + Object.keys(plainParams).length > 0 + ? api.editMessageText(chatId, messageId, text, plainParams) + : api.editMessageText(chatId, messageId, text), + retryLabel, + (plainErr) => !isTelegramMessageNotModifiedError(plainErr), + ), + }); + } catch (err) { if (isTelegramMessageNotModifiedError(err)) { - return; + // no-op: Telegram reports message content unchanged, treat as success + } else { + throw err; } - - // Telegram rejects malformed HTML. Fall back to plain text. - const errText = formatErrorMessage(err); - if (PARSE_ERR_RE.test(errText)) { - if (opts.verbose) { - console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`); - } - const plainParams: Record = {}; - if (opts.linkPreview === false) { - plainParams.link_preview_options = { is_disabled: true }; - } - if (replyMarkup !== undefined) { - plainParams.reply_markup = replyMarkup; - } - return await requestWithEditShouldLog( - () => - Object.keys(plainParams).length > 0 - ? api.editMessageText(chatId, messageId, text, plainParams) - : api.editMessageText(chatId, messageId, text), - "editMessage-plain", - (plainErr) => !isTelegramMessageNotModifiedError(plainErr), - ).catch((plainErr) => { - if (isTelegramMessageNotModifiedError(plainErr)) { - return; - } - throw plainErr; - }); - } - throw err; - }); + } logVerbose(`[telegram] Edited message ${messageId} in chat ${chatId}`); return { ok: true, messageId: String(messageId), chatId }; @@ -880,15 +916,11 @@ export async function sendStickerTelegram( const target = parseTelegramTarget(to); const chatId = normalizeChatId(target.chatId); - const messageThreadId = - opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId; - const threadSpec = - messageThreadId != null ? { id: messageThreadId, scope: "forum" as const } : undefined; - const threadIdParams = buildTelegramThreadParams(threadSpec); - const threadParams: Record = threadIdParams ? { ...threadIdParams } : {}; - if (opts.replyToMessageId != null) { - threadParams.reply_to_message_id = Math.trunc(opts.replyToMessageId); - } + const threadParams = buildTelegramThreadReplyParams({ + targetMessageThreadId: target.messageThreadId, + messageThreadId: opts.messageThreadId, + replyToMessageId: opts.replyToMessageId, + }); const hasThreadParams = Object.keys(threadParams).length > 0; const requestWithDiag = createTelegramRequestWithDiag({ @@ -962,11 +994,11 @@ export async function sendPollTelegram( // Normalize the poll input (validates question, options, maxSelections) const normalizedPoll = normalizePollInput(poll, { maxOptions: 10 }); - const messageThreadId = - opts.messageThreadId != null ? opts.messageThreadId : target.messageThreadId; - const threadSpec = - messageThreadId != null ? { id: messageThreadId, scope: "forum" as const } : undefined; - const threadIdParams = buildTelegramThreadParams(threadSpec); + const threadParams = buildTelegramThreadReplyParams({ + targetMessageThreadId: target.messageThreadId, + messageThreadId: opts.messageThreadId, + replyToMessageId: opts.replyToMessageId, + }); // Build poll options as simple strings (Grammy accepts string[] or InputPollOption[]) const pollOptions = normalizedPoll.options; @@ -1000,10 +1032,7 @@ export async function sendPollTelegram( allows_multiple_answers: normalizedPoll.maxSelections > 1, is_anonymous: opts.isAnonymous ?? true, ...(durationSeconds !== undefined ? { open_period: durationSeconds } : {}), - ...(threadIdParams ? threadIdParams : {}), - ...(opts.replyToMessageId != null - ? { reply_to_message_id: Math.trunc(opts.replyToMessageId) } - : {}), + ...(Object.keys(threadParams).length > 0 ? threadParams : {}), ...(opts.silent === true ? { disable_notification: true } : {}), };