fix(telegram): guard voice note sends
This commit is contained in:
committed by
Peter Steinberger
parent
2f036f7173
commit
ce786762db
@@ -60,6 +60,7 @@ import { resolveTelegramAccount } from "./accounts.js";
|
|||||||
import { createTelegramDraftStream } from "./draft-stream.js";
|
import { createTelegramDraftStream } from "./draft-stream.js";
|
||||||
import { resolveTelegramFetch } from "./fetch.js";
|
import { resolveTelegramFetch } from "./fetch.js";
|
||||||
import { markdownToTelegramHtml } from "./format.js";
|
import { markdownToTelegramHtml } from "./format.js";
|
||||||
|
import { isTelegramVoiceCompatible } from "./voice.js";
|
||||||
import {
|
import {
|
||||||
readTelegramAllowFromStore,
|
readTelegramAllowFromStore,
|
||||||
upsertTelegramPairingRequest,
|
upsertTelegramPairingRequest,
|
||||||
@@ -1387,7 +1388,19 @@ async function deliverReplies(params: {
|
|||||||
...mediaParams,
|
...mediaParams,
|
||||||
});
|
});
|
||||||
} else if (kind === "audio") {
|
} 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) {
|
if (useVoice) {
|
||||||
// Voice message - displays as round playable bubble (opt-in via [[audio_as_voice]])
|
// Voice message - displays as round playable bubble (opt-in via [[audio_as_voice]])
|
||||||
await bot.api.sendVoice(chatId, file, {
|
await bot.api.sendVoice(chatId, file, {
|
||||||
|
|||||||
@@ -324,6 +324,40 @@ describe("sendMessageTelegram", () => {
|
|||||||
expect(sendAudio).not.toHaveBeenCalled();
|
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 () => {
|
it("includes message_thread_id for forum topic messages", async () => {
|
||||||
const chatId = "-1001234567890";
|
const chatId = "-1001234567890";
|
||||||
const sendMessage = vi.fn().mockResolvedValue({
|
const sendMessage = vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { formatErrorMessage } from "../infra/errors.js";
|
|||||||
import { recordProviderActivity } from "../infra/provider-activity.js";
|
import { recordProviderActivity } from "../infra/provider-activity.js";
|
||||||
import type { RetryConfig } from "../infra/retry.js";
|
import type { RetryConfig } from "../infra/retry.js";
|
||||||
import { createTelegramRetryRunner } from "../infra/retry-policy.js";
|
import { createTelegramRetryRunner } from "../infra/retry-policy.js";
|
||||||
|
import { logVerbose } from "../globals.js";
|
||||||
import { mediaKindFromMime } from "../media/constants.js";
|
import { mediaKindFromMime } from "../media/constants.js";
|
||||||
import { isGifMedia } from "../media/mime.js";
|
import { isGifMedia } from "../media/mime.js";
|
||||||
import { loadWebMedia } from "../web/media.js";
|
import { loadWebMedia } from "../web/media.js";
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
parseTelegramTarget,
|
parseTelegramTarget,
|
||||||
stripTelegramInternalPrefixes,
|
stripTelegramInternalPrefixes,
|
||||||
} from "./targets.js";
|
} from "./targets.js";
|
||||||
|
import { resolveTelegramVoiceDecision } from "./voice.js";
|
||||||
|
|
||||||
type TelegramSendOpts = {
|
type TelegramSendOpts = {
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -237,7 +239,16 @@ export async function sendMessageTelegram(
|
|||||||
throw wrapChatNotFound(err);
|
throw wrapChatNotFound(err);
|
||||||
});
|
});
|
||||||
} else if (kind === "audio") {
|
} 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) {
|
if (useVoice) {
|
||||||
result = await request(
|
result = await request(
|
||||||
() => api.sendVoice(chatId, file, mediaParams),
|
() => api.sendVoice(chatId, file, mediaParams),
|
||||||
|
|||||||
15
src/telegram/voice.ts
Normal file
15
src/telegram/voice.ts
Normal 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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user