telegram-user: support voice-note media
This commit is contained in:
parent
29d9d875a7
commit
65c3718c96
@ -663,6 +663,7 @@ export function createTelegramUserMessageHandler(params: TelegramUserHandlerPara
|
|||||||
replyToId,
|
replyToId,
|
||||||
threadId,
|
threadId,
|
||||||
mediaUrl,
|
mediaUrl,
|
||||||
|
audioAsVoice: payload.audioAsVoice === true,
|
||||||
maxBytes: mediaMaxMb * 1024 * 1024,
|
maxBytes: mediaMaxMb * 1024 * 1024,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
110
extensions/telegram-user/src/send.test.ts
Normal file
110
extensions/telegram-user/src/send.test.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const loadWebMedia = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("./runtime.js", () => {
|
||||||
|
return {
|
||||||
|
getTelegramUserRuntime: () => ({
|
||||||
|
config: { loadConfig: () => ({}) },
|
||||||
|
media: {
|
||||||
|
loadWebMedia: (...args: unknown[]) => loadWebMedia(...args),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputMediaAuto = vi.fn((file: unknown, params: unknown) => ({
|
||||||
|
type: "auto",
|
||||||
|
file,
|
||||||
|
...(params && typeof params === "object" ? params : {}),
|
||||||
|
}));
|
||||||
|
const inputMediaVoice = vi.fn((file: unknown, params: unknown) => ({
|
||||||
|
type: "voice",
|
||||||
|
file,
|
||||||
|
...(params && typeof params === "object" ? params : {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@mtcute/core", () => {
|
||||||
|
return {
|
||||||
|
InputMedia: {
|
||||||
|
auto: (...args: unknown[]) => inputMediaAuto(...args),
|
||||||
|
voice: (...args: unknown[]) => inputMediaVoice(...args),
|
||||||
|
poll: () => ({ type: "poll" }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("telegram-user send", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
loadWebMedia.mockReset();
|
||||||
|
inputMediaAuto.mockClear();
|
||||||
|
inputMediaVoice.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends audio media as voice note when audioAsVoice is set", async () => {
|
||||||
|
loadWebMedia.mockResolvedValue({
|
||||||
|
buffer: Buffer.from("voice"),
|
||||||
|
contentType: "audio/ogg",
|
||||||
|
fileName: "note.ogg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMedia = vi.fn(async () => ({ id: 123 }));
|
||||||
|
const { sendMediaTelegramUser } = await import("./send.js");
|
||||||
|
await sendMediaTelegramUser("telegram-user:123", "hi", {
|
||||||
|
client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient,
|
||||||
|
mediaUrl: "https://example.com/note.ogg",
|
||||||
|
audioAsVoice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(inputMediaVoice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendMedia).toHaveBeenCalledTimes(1);
|
||||||
|
const [, media] = sendMedia.mock.calls[0] ?? [];
|
||||||
|
expect(media).toMatchObject({ type: "voice" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to normal media when audioAsVoice is set but media is not voice-compatible", async () => {
|
||||||
|
loadWebMedia.mockResolvedValue({
|
||||||
|
buffer: Buffer.from("img"),
|
||||||
|
contentType: "image/png",
|
||||||
|
fileName: "image.png",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMedia = vi.fn(async () => ({ id: 123 }));
|
||||||
|
const { sendMediaTelegramUser } = await import("./send.js");
|
||||||
|
await sendMediaTelegramUser("telegram-user:123", "hi", {
|
||||||
|
client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient,
|
||||||
|
mediaUrl: "https://example.com/image.png",
|
||||||
|
audioAsVoice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(inputMediaVoice).toHaveBeenCalledTimes(0);
|
||||||
|
expect(inputMediaAuto).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to auto when voice messages are forbidden", async () => {
|
||||||
|
loadWebMedia.mockResolvedValue({
|
||||||
|
buffer: Buffer.from("voice"),
|
||||||
|
contentType: "audio/ogg",
|
||||||
|
fileName: "note.ogg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMedia = vi.fn(async (_to: unknown, media: unknown) => {
|
||||||
|
if (media && typeof media === "object" && (media as { type?: string }).type === "voice") {
|
||||||
|
throw new Error("VOICE_MESSAGES_FORBIDDEN");
|
||||||
|
}
|
||||||
|
return { id: 123 };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { sendMediaTelegramUser } = await import("./send.js");
|
||||||
|
await sendMediaTelegramUser("telegram-user:123", "hi", {
|
||||||
|
client: { sendMedia } as unknown as import("@mtcute/node").TelegramClient,
|
||||||
|
mediaUrl: "https://example.com/note.ogg",
|
||||||
|
audioAsVoice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(inputMediaVoice).toHaveBeenCalledTimes(1);
|
||||||
|
expect(inputMediaAuto).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sendMedia).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ export type TelegramUserSendOpts = {
|
|||||||
replyToId?: number;
|
replyToId?: number;
|
||||||
threadId?: string | number | null;
|
threadId?: string | number | null;
|
||||||
mediaUrl?: string;
|
mediaUrl?: string;
|
||||||
|
audioAsVoice?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeTarget(raw: string): string {
|
function normalizeTarget(raw: string): string {
|
||||||
@ -69,6 +70,24 @@ function resolveTargetAndThread(raw: string, threadId?: string | number | null)
|
|||||||
return { target, threadId: parsedThreadId };
|
return { target, threadId: parsedThreadId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVoiceMessagesForbidden(err: unknown): boolean {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return /VOICE_MESSAGES_FORBIDDEN/i.test(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSendAsVoice(params: {
|
||||||
|
wantsVoice: boolean;
|
||||||
|
contentType?: string | null;
|
||||||
|
fileName?: string | null;
|
||||||
|
}): boolean {
|
||||||
|
if (!params.wantsVoice) return false;
|
||||||
|
const contentType = params.contentType?.toLowerCase() ?? "";
|
||||||
|
const fileName = params.fileName?.toLowerCase() ?? "";
|
||||||
|
if (/(^|\/)(ogg|opus)(;|$)/.test(contentType)) return true;
|
||||||
|
if (/\.(ogg|opus|oga)$/.test(fileName)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeTelegramUserMessagingTarget(raw: string): string {
|
export function normalizeTelegramUserMessagingTarget(raw: string): string {
|
||||||
return normalizeTarget(raw);
|
return normalizeTarget(raw);
|
||||||
}
|
}
|
||||||
@ -192,11 +211,24 @@ export async function sendMediaTelegramUser(
|
|||||||
const resolved = resolveTargetAndThread(to, opts.threadId);
|
const resolved = resolveTargetAndThread(to, opts.threadId);
|
||||||
const target = resolveTelegramUserPeer(resolved.target);
|
const target = resolveTelegramUserPeer(resolved.target);
|
||||||
const media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes);
|
const media = await getTelegramUserRuntime().media.loadWebMedia(opts.mediaUrl, opts.maxBytes);
|
||||||
const input = InputMedia.auto(media.buffer, {
|
const wantsVoice = shouldSendAsVoice({
|
||||||
fileName: media.fileName ?? undefined,
|
wantsVoice: opts.audioAsVoice === true,
|
||||||
fileMime: media.contentType,
|
contentType: media.contentType,
|
||||||
caption: text,
|
fileName: media.fileName,
|
||||||
});
|
});
|
||||||
|
const buildAuto = () =>
|
||||||
|
InputMedia.auto(media.buffer, {
|
||||||
|
fileName: media.fileName ?? undefined,
|
||||||
|
fileMime: media.contentType,
|
||||||
|
caption: text,
|
||||||
|
});
|
||||||
|
const buildVoice = () =>
|
||||||
|
InputMedia.voice(media.buffer, {
|
||||||
|
fileName: media.fileName ?? undefined,
|
||||||
|
fileMime: media.contentType,
|
||||||
|
caption: text,
|
||||||
|
});
|
||||||
|
const input = wantsVoice ? buildVoice() : buildAuto();
|
||||||
let message: Awaited<ReturnType<TelegramClient["sendMedia"]>> | null = null;
|
let message: Awaited<ReturnType<TelegramClient["sendMedia"]>> | null = null;
|
||||||
try {
|
try {
|
||||||
message = await client.sendMedia(target, input, {
|
message = await client.sendMedia(target, input, {
|
||||||
@ -204,7 +236,14 @@ export async function sendMediaTelegramUser(
|
|||||||
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
|
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isDestroyedClientError(err)) throw err;
|
if (wantsVoice && isVoiceMessagesForbidden(err)) {
|
||||||
|
message = await client.sendMedia(target, buildAuto(), {
|
||||||
|
...(opts.replyToId ? { replyTo: opts.replyToId } : {}),
|
||||||
|
...(resolved.threadId ? { threadId: resolved.threadId } : {}),
|
||||||
|
});
|
||||||
|
} else if (!isDestroyedClientError(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return { messageId: "", chatId: String(target) };
|
return { messageId: "", chatId: String(target) };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user