fix(telegram): guard voice note sends

This commit is contained in:
Jarvis
2026-01-08 13:55:36 +00:00
committed by Peter Steinberger
parent 2f036f7173
commit ce786762db
4 changed files with 75 additions and 2 deletions

View File

@@ -60,6 +60,7 @@ import { resolveTelegramAccount } from "./accounts.js";
import { createTelegramDraftStream } from "./draft-stream.js";
import { resolveTelegramFetch } from "./fetch.js";
import { markdownToTelegramHtml } from "./format.js";
import { isTelegramVoiceCompatible } from "./voice.js";
import {
readTelegramAllowFromStore,
upsertTelegramPairingRequest,
@@ -1387,7 +1388,19 @@ async function deliverReplies(params: {
...mediaParams,
});
} else if (kind === "audio") {
const useVoice = reply.audioAsVoice === true; // default false (backward compatible)
const wantsVoice = reply.audioAsVoice === true; // default false (backward compatible)
const canVoice = wantsVoice
? isTelegramVoiceCompatible({
contentType: media.contentType,
fileName,
})
: false;
const useVoice = wantsVoice && canVoice;
if (wantsVoice && !canVoice) {
logVerbose(
`Telegram voice requested but media is ${media.contentType ?? "unknown"} (${fileName}); sending as audio file instead.`,
);
}
if (useVoice) {
// Voice message - displays as round playable bubble (opt-in via [[audio_as_voice]])
await bot.api.sendVoice(chatId, file, {

View File

@@ -324,6 +324,40 @@ describe("sendMessageTelegram", () => {
expect(sendAudio).not.toHaveBeenCalled();
});
it("falls back to audio when asVoice is true but media is not voice compatible", async () => {
const chatId = "123";
const sendAudio = vi.fn().mockResolvedValue({
message_id: 14,
chat: { id: chatId },
});
const sendVoice = vi.fn().mockResolvedValue({
message_id: 15,
chat: { id: chatId },
});
const api = { sendAudio, sendVoice } as unknown as {
sendAudio: typeof sendAudio;
sendVoice: typeof sendVoice;
};
loadWebMedia.mockResolvedValueOnce({
buffer: Buffer.from("audio"),
contentType: "audio/mpeg",
fileName: "clip.mp3",
});
await sendMessageTelegram(chatId, "caption", {
token: "tok",
api,
mediaUrl: "https://example.com/clip.mp3",
asVoice: true,
});
expect(sendAudio).toHaveBeenCalledWith(chatId, expect.anything(), {
caption: "caption",
});
expect(sendVoice).not.toHaveBeenCalled();
});
it("includes message_thread_id for forum topic messages", async () => {
const chatId = "-1001234567890";
const sendMessage = vi.fn().mockResolvedValue({

View File

@@ -10,6 +10,7 @@ import { formatErrorMessage } from "../infra/errors.js";
import { recordProviderActivity } from "../infra/provider-activity.js";
import type { RetryConfig } from "../infra/retry.js";
import { createTelegramRetryRunner } from "../infra/retry-policy.js";
import { logVerbose } from "../globals.js";
import { mediaKindFromMime } from "../media/constants.js";
import { isGifMedia } from "../media/mime.js";
import { loadWebMedia } from "../web/media.js";
@@ -20,6 +21,7 @@ import {
parseTelegramTarget,
stripTelegramInternalPrefixes,
} from "./targets.js";
import { resolveTelegramVoiceDecision } from "./voice.js";
type TelegramSendOpts = {
token?: string;
@@ -237,7 +239,16 @@ export async function sendMessageTelegram(
throw wrapChatNotFound(err);
});
} else if (kind === "audio") {
const useVoice = opts.asVoice === true; // default false (backward compatible)
const { useVoice, reason } = resolveTelegramVoiceDecision({
wantsVoice: opts.asVoice === true, // default false (backward compatible)
contentType: media.contentType,
fileName,
});
if (reason) {
logVerbose(
`Telegram voice requested but ${reason}; sending as audio file instead.`,
);
}
if (useVoice) {
result = await request(
() => api.sendVoice(chatId, file, mediaParams),

15
src/telegram/voice.ts Normal file
View File

@@ -0,0 +1,15 @@
import path from "node:path";
export function isTelegramVoiceCompatible(opts: {
contentType?: string | null;
fileName?: string | null;
}): boolean {
const mime = opts.contentType?.toLowerCase();
if (mime && (mime.includes("ogg") || mime.includes("opus"))) {
return true;
}
const fileName = opts.fileName?.trim();
if (!fileName) return false;
const ext = path.extname(fileName).toLowerCase();
return ext === ".ogg" || ext === ".opus" || ext === ".oga";
}