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,
) {}
},
GrammyError: class GrammyError extends Error {
description = "";
},
}));
describe("deliverReplies", () => {

View File

@@ -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<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?: {
replyToMessageId?: number;
messageThreadId?: number;