refactor: streamline telegram voice fallback

This commit is contained in:
Peter Steinberger
2026-01-25 13:26:25 +00:00
parent 0130ecd800
commit 4c11fc0c09
2 changed files with 57 additions and 21 deletions

View File

@@ -17,6 +17,9 @@ vi.mock("grammy", () => ({
public fileName?: string, public fileName?: string,
) {} ) {}
}, },
GrammyError: class GrammyError extends Error {
description = "";
},
})); }));
describe("deliverReplies", () => { describe("deliverReplies", () => {

View File

@@ -1,4 +1,4 @@
import { type Bot, InputFile } from "grammy"; import { type Bot, GrammyError, InputFile } from "grammy";
import { import {
markdownToTelegramChunks, markdownToTelegramChunks,
markdownToTelegramHtml, markdownToTelegramHtml,
@@ -22,6 +22,7 @@ import { buildTelegramThreadParams, resolveTelegramReplyId } from "./helpers.js"
import type { TelegramContext } from "./types.js"; import type { TelegramContext } from "./types.js";
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i; 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: { export async function deliverReplies(params: {
replies: ReplyPayload[]; replies: ReplyPayload[];
@@ -163,31 +164,26 @@ export async function deliverReplies(params: {
// Fall back to text if voice messages are forbidden in this chat. // Fall back to text if voice messages are forbidden in this chat.
// This happens when the recipient has Telegram Premium privacy settings // This happens when the recipient has Telegram Premium privacy settings
// that block voice messages (Settings > Privacy > Voice Messages). // that block voice messages (Settings > Privacy > Voice Messages).
const errMsg = formatErrorMessage(voiceErr); if (isVoiceMessagesForbidden(voiceErr)) {
if (errMsg.includes("VOICE_MESSAGES_FORBIDDEN")) { const fallbackText = reply.text;
if (!reply.text?.trim()) { if (!fallbackText || !fallbackText.trim()) {
throw voiceErr; throw voiceErr;
} }
logVerbose( logVerbose(
"telegram sendVoice forbidden (recipient has voice messages blocked in privacy settings); falling back to text", "telegram sendVoice forbidden (recipient has voice messages blocked in privacy settings); falling back to text",
); );
// Send the text content instead of the voice message. hasReplied = await sendTelegramVoiceFallbackText({
if (reply.text) { bot,
const chunks = chunkText(reply.text); chatId,
for (const chunk of chunks) { runtime,
await sendTelegramText(bot, chatId, chunk.html, runtime, { text: fallbackText,
replyToMessageId: chunkText,
replyToId && (replyToMode === "all" || !hasReplied) ? replyToId : undefined, replyToId,
messageThreadId, replyToMode,
textMode: "html", hasReplied,
plainText: chunk.text, messageThreadId,
linkPreview, linkPreview,
}); });
if (replyToId && !hasReplied) {
hasReplied = true;
}
}
}
// Skip this media item; continue with next. // Skip this media item; continue with next.
continue; continue;
} }
@@ -263,6 +259,43 @@ export async function resolveMedia(
return { path: saved.path, contentType: saved.contentType, placeholder }; 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<typeof markdownToTelegramChunks>;
replyToId?: number;
replyToMode: ReplyToMode;
hasReplied: boolean;
messageThreadId?: number;
linkPreview?: boolean;
}): Promise<boolean> {
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?: { function buildTelegramSendParams(opts?: {
replyToMessageId?: number; replyToMessageId?: number;
messageThreadId?: number; messageThreadId?: number;