mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:20:45 +00:00
fix(telegram): probe video dimensions through sdk
Fix Telegram portrait video distortion by probing video dimensions through the shared media helper and passing width/height to sendVideo. Validation: - Targeted Telegram/media tests passed locally. - Plugin SDK API baseline check passed locally. - Formatter and git diff whitespace checks passed locally. CI note: current boundary drift observed on prior run came from existing src/plugin-sdk/discord.ts and src/plugin-sdk/telegram-account.ts, not this PR diff.
This commit is contained in:
@@ -10,8 +10,12 @@ import {
|
||||
toPluginMessageSentEvent,
|
||||
} from "openclaw/plugin-sdk/hook-runtime";
|
||||
import type { ReplyPayloadDelivery } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { buildOutboundMediaLoadOptions } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { isGifMedia, kindFromMime } from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
buildOutboundMediaLoadOptions,
|
||||
isGifMedia,
|
||||
kindFromMime,
|
||||
probeVideoDimensions,
|
||||
} from "openclaw/plugin-sdk/media-runtime";
|
||||
import {
|
||||
createOutboundPayloadPlan,
|
||||
projectOutboundPayloadPlanForDelivery,
|
||||
@@ -361,10 +365,12 @@ async function deliverMediaReply(params: {
|
||||
progress: params.progress,
|
||||
});
|
||||
const shouldAttachButtonsToMedia = isFirstMedia && params.replyMarkup && !followUpText;
|
||||
const videoDimensions = kind === "video" ? await probeVideoDimensions(media.buffer) : undefined;
|
||||
const mediaParams: Record<string, unknown> = {
|
||||
caption: htmlCaption,
|
||||
...(htmlCaption ? { parse_mode: "HTML" } : {}),
|
||||
...(shouldAttachButtonsToMedia ? { reply_markup: params.replyMarkup } : {}),
|
||||
...(videoDimensions ? { width: videoDimensions.width, height: videoDimensions.height } : {}),
|
||||
...buildTelegramSendParams({
|
||||
replyToMessageId,
|
||||
replyQuoteMessageId: params.replyQuoteMessageId,
|
||||
|
||||
@@ -4,6 +4,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const { loadWebMedia } = vi.hoisted(() => ({
|
||||
loadWebMedia: vi.fn(),
|
||||
}));
|
||||
const { probeVideoDimensions } = vi.hoisted(() => ({
|
||||
probeVideoDimensions: vi.fn(),
|
||||
}));
|
||||
const triggerInternalHook = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const messageHookRunner = vi.hoisted(() => ({
|
||||
hasHooks: vi.fn<(name: string) => boolean>(() => false),
|
||||
@@ -28,6 +31,14 @@ vi.mock("openclaw/plugin-sdk/web-media", () => ({
|
||||
loadWebMedia: (...args: unknown[]) => loadWebMedia(...args),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/media-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
probeVideoDimensions,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/hook-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/hook-runtime")>();
|
||||
return {
|
||||
@@ -135,6 +146,8 @@ function createVoiceFailureHarness(params: {
|
||||
describe("deliverReplies", () => {
|
||||
beforeEach(() => {
|
||||
loadWebMedia.mockClear();
|
||||
probeVideoDimensions.mockReset();
|
||||
probeVideoDimensions.mockResolvedValue(undefined);
|
||||
triggerInternalHook.mockReset();
|
||||
messageHookRunner.hasHooks.mockReset();
|
||||
messageHookRunner.hasHooks.mockReturnValue(false);
|
||||
@@ -489,6 +502,63 @@ describe("deliverReplies", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes probed dimensions to video reply sends", async () => {
|
||||
const runtime = createRuntime();
|
||||
const sendVideo = vi.fn().mockResolvedValue({
|
||||
message_id: 22,
|
||||
chat: { id: "123" },
|
||||
});
|
||||
const bot = createBot({ sendVideo });
|
||||
probeVideoDimensions.mockResolvedValueOnce({ width: 720, height: 1280 });
|
||||
|
||||
mockMediaLoad("video.mp4", "video/mp4", "video");
|
||||
|
||||
await deliverWith({
|
||||
replies: [{ mediaUrl: "https://example.com/video.mp4", text: "hi **boss**" }],
|
||||
runtime,
|
||||
bot,
|
||||
});
|
||||
|
||||
expect(probeVideoDimensions).toHaveBeenCalledWith(Buffer.from("video"));
|
||||
expect(sendVideo).toHaveBeenCalledWith(
|
||||
"123",
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
caption: "hi <b>boss</b>",
|
||||
parse_mode: "HTML",
|
||||
width: 720,
|
||||
height: 1280,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not probe GIF reply animations", async () => {
|
||||
const runtime = createRuntime();
|
||||
const sendAnimation = vi.fn().mockResolvedValue({
|
||||
message_id: 23,
|
||||
chat: { id: "123" },
|
||||
});
|
||||
const bot = createBot({ sendAnimation });
|
||||
|
||||
mockMediaLoad("fun.gif", "image/gif", "GIF89a");
|
||||
|
||||
await deliverWith({
|
||||
replies: [{ mediaUrl: "https://example.com/fun.gif", text: "gif" }],
|
||||
runtime,
|
||||
bot,
|
||||
});
|
||||
|
||||
expect(probeVideoDimensions).not.toHaveBeenCalled();
|
||||
expect(sendAnimation).toHaveBeenCalledWith(
|
||||
"123",
|
||||
expect.anything(),
|
||||
expect.not.objectContaining({
|
||||
width: expect.any(Number),
|
||||
height: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes mediaLocalRoots to media loading", async () => {
|
||||
const runtime = createRuntime();
|
||||
const sendPhoto = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -8,5 +8,6 @@ export {
|
||||
isGifMedia,
|
||||
kindFromMime,
|
||||
normalizePollInput,
|
||||
probeVideoDimensions,
|
||||
} from "openclaw/plugin-sdk/media-runtime";
|
||||
export { loadWebMedia } from "openclaw/plugin-sdk/web-media";
|
||||
|
||||
@@ -42,6 +42,10 @@ const { imageMetadata } = vi.hoisted(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const { probeVideoDimensions } = vi.hoisted(() => ({
|
||||
probeVideoDimensions: vi.fn(),
|
||||
}));
|
||||
|
||||
const { loadConfig, resolveStorePath } = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
resolveStorePath: vi.fn(
|
||||
@@ -90,6 +94,7 @@ type TelegramSendTestMocks = {
|
||||
loadWebMedia: MockFn;
|
||||
maybePersistResolvedTelegramTarget: MockFn;
|
||||
imageMetadata: { width: number | undefined; height: number | undefined };
|
||||
probeVideoDimensions: MockFn;
|
||||
};
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/web-media", () => ({
|
||||
@@ -153,6 +158,7 @@ vi.mock("./send.runtime.js", () => ({
|
||||
loadConfig,
|
||||
loadWebMedia,
|
||||
normalizePollInput,
|
||||
probeVideoDimensions,
|
||||
requireRuntimeConfig: vi.fn((cfg: unknown) => cfg ?? loadConfig()),
|
||||
resolveMarkdownTableMode,
|
||||
resolveStorePath,
|
||||
@@ -171,6 +177,7 @@ export function getTelegramSendTestMocks(): TelegramSendTestMocks {
|
||||
loadWebMedia,
|
||||
maybePersistResolvedTelegramTarget,
|
||||
imageMetadata,
|
||||
probeVideoDimensions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -179,6 +186,8 @@ export function installTelegramSendTestHooks() {
|
||||
loadConfig.mockReturnValue({});
|
||||
resolveStorePath.mockReturnValue("/tmp/openclaw-telegram-send-tests.json");
|
||||
loadWebMedia.mockReset();
|
||||
probeVideoDimensions.mockReset();
|
||||
probeVideoDimensions.mockResolvedValue(undefined);
|
||||
imageMetadata.width = 1200;
|
||||
imageMetadata.height = 800;
|
||||
maybePersistResolvedTelegramTarget.mockReset();
|
||||
|
||||
@@ -23,6 +23,7 @@ const {
|
||||
loadConfig,
|
||||
loadWebMedia,
|
||||
maybePersistResolvedTelegramTarget,
|
||||
probeVideoDimensions,
|
||||
} = getTelegramSendTestMocks();
|
||||
const {
|
||||
buildInlineKeyboard,
|
||||
@@ -978,6 +979,73 @@ describe("sendMessageTelegram", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("passes probed dimensions to regular video sends", async () => {
|
||||
const chatId = "123";
|
||||
const videoBuffer = Buffer.from("fake-video");
|
||||
const sendVideo = vi.fn().mockResolvedValue({
|
||||
message_id: 201,
|
||||
chat: { id: chatId },
|
||||
});
|
||||
const api = { sendVideo } as unknown as {
|
||||
sendVideo: typeof sendVideo;
|
||||
};
|
||||
probeVideoDimensions.mockResolvedValueOnce({ width: 720, height: 1280 });
|
||||
|
||||
mockLoadedMedia({
|
||||
buffer: videoBuffer,
|
||||
contentType: "video/mp4",
|
||||
fileName: "video.mp4",
|
||||
});
|
||||
|
||||
await sendMessageTelegram(chatId, "my caption", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/video.mp4",
|
||||
});
|
||||
|
||||
expect(probeVideoDimensions).toHaveBeenCalledWith(videoBuffer);
|
||||
expect(sendVideo).toHaveBeenCalledWith(chatId, expect.anything(), {
|
||||
caption: "my caption",
|
||||
parse_mode: "HTML",
|
||||
width: 720,
|
||||
height: 1280,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not probe video dimensions for video notes", async () => {
|
||||
const chatId = "123";
|
||||
const sendVideoNote = vi.fn().mockResolvedValue({
|
||||
message_id: 101,
|
||||
chat: { id: chatId },
|
||||
});
|
||||
const sendMessage = vi.fn().mockResolvedValue({
|
||||
message_id: 102,
|
||||
chat: { id: chatId },
|
||||
});
|
||||
const api = { sendVideoNote, sendMessage } as unknown as {
|
||||
sendVideoNote: typeof sendVideoNote;
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
mockLoadedMedia({
|
||||
buffer: Buffer.from("fake-video"),
|
||||
contentType: "video/mp4",
|
||||
fileName: "video.mp4",
|
||||
});
|
||||
|
||||
await sendMessageTelegram(chatId, "ignored caption context", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/video.mp4",
|
||||
asVideoNote: true,
|
||||
});
|
||||
|
||||
expect(probeVideoDimensions).not.toHaveBeenCalled();
|
||||
expect(sendVideoNote).toHaveBeenCalledWith(chatId, expect.anything(), {});
|
||||
});
|
||||
|
||||
it("applies reply markup and thread options to split video-note sends", async () => {
|
||||
const chatId = "123";
|
||||
const cases: Array<{
|
||||
@@ -1195,6 +1263,7 @@ describe("sendMessageTelegram", () => {
|
||||
caption: "caption",
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
expect(probeVideoDimensions).not.toHaveBeenCalled();
|
||||
expect(res.messageId).toBe("9");
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
loadWebMedia,
|
||||
type MediaKind,
|
||||
normalizePollInput,
|
||||
probeVideoDimensions,
|
||||
type OpenClawConfig,
|
||||
type PollInput,
|
||||
requireRuntimeConfig,
|
||||
@@ -821,10 +822,13 @@ export async function sendMessageTelegram(
|
||||
...(hasThreadParams ? threadParams : {}),
|
||||
...(!needsSeparateText && replyMarkup ? { reply_markup: replyMarkup } : {}),
|
||||
};
|
||||
const videoDimensions =
|
||||
kind === "video" && !isVideoNote ? await probeVideoDimensions(media.buffer) : undefined;
|
||||
const mediaParams = {
|
||||
...(htmlCaption ? { caption: htmlCaption, parse_mode: "HTML" as const } : {}),
|
||||
...baseMediaParams,
|
||||
...(opts.silent === true ? { disable_notification: true } : {}),
|
||||
...(videoDimensions ? { width: videoDimensions.width, height: videoDimensions.height } : {}),
|
||||
};
|
||||
const sendMedia = async (
|
||||
label: string,
|
||||
|
||||
Reference in New Issue
Block a user