From 4c11fc0c09cf192b7148000253850cef0e528678 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 25 Jan 2026 13:26:25 +0000 Subject: [PATCH] refactor: streamline telegram voice fallback --- src/telegram/bot/delivery.test.ts | 3 ++ src/telegram/bot/delivery.ts | 75 ++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/telegram/bot/delivery.test.ts b/src/telegram/bot/delivery.test.ts index 359ba12b3..404cc2fc2 100644 --- a/src/telegram/bot/delivery.test.ts +++ b/src/telegram/bot/delivery.test.ts @@ -17,6 +17,9 @@ vi.mock("grammy", () => ({ public fileName?: string, ) {} }, + GrammyError: class GrammyError extends Error { + description = ""; + }, })); describe("deliverReplies", () => { diff --git a/src/telegram/bot/delivery.ts b/src/telegram/bot/delivery.ts index c2b1afcf3..4edc91c8a 100644 --- a/src/telegram/bot/delivery.ts +++ b/src/telegram/bot/delivery.ts @@ -1,4 +1,4 @@ -import { type Bot, InputFile } from "grammy"; +import { type Bot, GrammyError, InputFile } from "grammy"; import { markdownToTelegramChunks, markdownToTelegramHtml, @@ -22,6 +22,7 @@ import { buildTelegramThreadParams, resolveTelegramReplyId } from "./helpers.js" import type { TelegramContext } from "./types.js"; const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i; +const VOICE_FORBIDDEN_RE = /VOICE_MESSAGES_FORBIDDEN/; export async function deliverReplies(params: { replies: ReplyPayload[]; @@ -163,31 +164,26 @@ export async function deliverReplies(params: { // Fall back to text if voice messages are forbidden in this chat. // This happens when the recipient has Telegram Premium privacy settings // that block voice messages (Settings > Privacy > Voice Messages). - const errMsg = formatErrorMessage(voiceErr); - if (errMsg.includes("VOICE_MESSAGES_FORBIDDEN")) { - if (!reply.text?.trim()) { + if (isVoiceMessagesForbidden(voiceErr)) { + const fallbackText = reply.text; + if (!fallbackText || !fallbackText.trim()) { throw voiceErr; } logVerbose( "telegram sendVoice forbidden (recipient has voice messages blocked in privacy settings); falling back to text", ); - // Send the text content instead of the voice message. - if (reply.text) { - const chunks = chunkText(reply.text); - for (const chunk of chunks) { - await sendTelegramText(bot, chatId, chunk.html, runtime, { - replyToMessageId: - replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined, - messageThreadId, - textMode: "html", - plainText: chunk.text, - linkPreview, - }); - if (replyToId && !hasReplied) { - hasReplied = true; - } - } - } + hasReplied = await sendTelegramVoiceFallbackText({ + bot, + chatId, + runtime, + text: fallbackText, + chunkText, + replyToId, + replyToMode, + hasReplied, + messageThreadId, + linkPreview, + }); // Skip this media item; continue with next. continue; } @@ -263,6 +259,43 @@ export async function resolveMedia( return { path: saved.path, contentType: saved.contentType, placeholder }; } +function isVoiceMessagesForbidden(err: unknown): boolean { + if (err instanceof GrammyError) { + return VOICE_FORBIDDEN_RE.test(err.description); + } + return VOICE_FORBIDDEN_RE.test(formatErrorMessage(err)); +} + +async function sendTelegramVoiceFallbackText(opts: { + bot: Bot; + chatId: string; + runtime: RuntimeEnv; + text: string; + chunkText: (markdown: string) => ReturnType; + replyToId?: number; + replyToMode: ReplyToMode; + hasReplied: boolean; + messageThreadId?: number; + linkPreview?: boolean; +}): Promise { + const chunks = opts.chunkText(opts.text); + let hasReplied = opts.hasReplied; + for (const chunk of chunks) { + await sendTelegramText(opts.bot, opts.chatId, chunk.html, opts.runtime, { + replyToMessageId: + opts.replyToId && (opts.replyToMode === "all" || !hasReplied) ? opts.replyToId : undefined, + messageThreadId: opts.messageThreadId, + textMode: "html", + plainText: chunk.text, + linkPreview: opts.linkPreview, + }); + if (opts.replyToId && !hasReplied) { + hasReplied = true; + } + } + return hasReplied; +} + function buildTelegramSendParams(opts?: { replyToMessageId?: number; messageThreadId?: number;