diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a280440614..185fa098765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - Doctor/GitHub CLI: surface a `GH_CONFIG_DIR` hint when the GitHub skill is usable but `gh` auth lives under a different operator HOME than the agent process, without warning for disabled or filtered skills. Fixes #78063. (#78095) Thanks @tmimmanuel. - Gateway: dedupe concurrent `send`, `poll`, and `message.action` requests while delivery is still in flight, preventing duplicate outbound work for the same idempotency key. (#68341) Thanks @thesomewhatyou. - Cron: keep main-session `systemEvent` heartbeat wakes on their bound session route for both direct and queued wake paths by dropping inherited explicit heartbeat destinations when forcing `target: "last"`. Fixes #73900. Thanks @richardmqq. +- Telegram: honor forced document delivery for video media so `--force-document` sends MP4s as documents instead of typed videos. Fixes #80389. (#80405) Thanks @jbetala7. - Gateway: clear speculative node wake state when APNs registration is missing, preventing unregistered or mistyped node IDs from retaining wake throttle entries. Fixes #68847. (#68848) Thanks @Feelw00. - Auto-reply: keep late follow-up queue drain finalizers from deleting a replacement queue registered after `/stop`, preventing immediate follow-up messages from being orphaned. Fixes #68838. (#68839) Thanks @Feelw00. - Feishu: make manual App ID/App Secret setup the default channel-binding path while keeping QR scan-to-create as an optional best-effort flow, and document the manual fallback for domestic Feishu mobile clients that do not react to the QR code. Fixes #80591. Thanks @wei-wei-zhao. diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index d6d31cd1436..1beea5c05b6 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -840,7 +840,7 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \ - `--presentation` with `buttons` blocks for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it - `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat - - `--force-document` to send outbound images and GIFs as documents instead of compressed photo or animated-media uploads + - `--force-document` to send outbound images, GIFs, and videos as documents instead of compressed photo, animated-media, or video uploads Action gating: diff --git a/docs/cli/message.md b/docs/cli/message.md index d19dd50bdd2..873b16330a0 100644 --- a/docs/cli/message.md +++ b/docs/cli/message.md @@ -72,7 +72,7 @@ Name lookup: - Optional: `--media`, `--presentation`, `--delivery`, `--pin`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent` - Shared presentation payloads: `--presentation` sends semantic blocks (`text`, `context`, `divider`, `buttons`, `select`) that core renders through the selected channel's declared capabilities. See [Message Presentation](/plugins/message-presentation). - Generic delivery preferences: `--delivery` accepts delivery hints such as `{ "pin": true }`; `--pin` is shorthand for pinned delivery when the channel supports it. - - Telegram only: `--force-document` (send images and GIFs as documents to avoid Telegram compression) + - Telegram only: `--force-document` (send images, GIFs, and videos as documents to avoid Telegram compression) - Telegram only: `--thread-id` (forum topic id) - Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field) - Telegram + Discord: `--silent` diff --git a/extensions/telegram/src/send.test.ts b/extensions/telegram/src/send.test.ts index baf852f2fb3..ab2acb19be9 100644 --- a/extensions/telegram/src/send.test.ts +++ b/extensions/telegram/src/send.test.ts @@ -1385,6 +1385,13 @@ describe("sendMessageTelegram", () => { fileName: "fun.gif", mediaUrl: "https://example.com/fun.gif", }, + { + name: "videos", + buffer: Buffer.from("fake-video"), + contentType: "video/mp4", + fileName: "clip.mp4", + mediaUrl: "https://example.com/clip.mp4", + }, ])("sends $name as documents when forceDocument is true", async (testCase) => { const chatId = "123"; const sendAnimation = vi.fn(); @@ -1393,10 +1400,12 @@ describe("sendMessageTelegram", () => { chat: { id: chatId }, }); const sendPhoto = vi.fn(); - const api = { sendAnimation, sendDocument, sendPhoto } as unknown as { + const sendVideo = vi.fn(); + const api = { sendAnimation, sendDocument, sendPhoto, sendVideo } as unknown as { sendAnimation: typeof sendAnimation; sendDocument: typeof sendDocument; sendPhoto: typeof sendPhoto; + sendVideo: typeof sendVideo; }; mockLoadedMedia({ @@ -1420,6 +1429,8 @@ describe("sendMessageTelegram", () => { }); expect(sendPhoto, testCase.name).not.toHaveBeenCalled(); expect(sendAnimation, testCase.name).not.toHaveBeenCalled(); + expect(sendVideo, testCase.name).not.toHaveBeenCalled(); + expect(probeVideoDimensions, testCase.name).not.toHaveBeenCalled(); expect(res.messageId).toBe("10"); }); diff --git a/extensions/telegram/src/send.ts b/extensions/telegram/src/send.ts index 2efecb01af2..dbd1663e31f 100644 --- a/extensions/telegram/src/send.ts +++ b/extensions/telegram/src/send.ts @@ -814,12 +814,13 @@ export async function sendMessageTelegram( fileName: media.fileName, }); - // Validate photo dimensions before attempting sendPhoto let sendImageAsPhoto = true; - if (kind === "image" && !isGif && !opts.forceDocument) { + const deliveryKind = + opts.forceDocument === true && (kind === "image" || kind === "video") ? "document" : kind; + if (deliveryKind === "image" && !isGif) { sendImageAsPhoto = await shouldSendTelegramImageAsPhoto(media.buffer); } - const isVideoNote = kind === "video" && opts.asVideoNote === true; + const isVideoNote = deliveryKind === "video" && opts.asVideoNote === true; const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind ?? "document")) ?? "file"; const file = new InputFileCtor(media.buffer, fileName); @@ -845,7 +846,9 @@ export async function sendMessageTelegram( ...(!needsSeparateText && replyMarkup ? { reply_markup: replyMarkup } : {}), }; const videoDimensions = - kind === "video" && !isVideoNote ? await probeVideoDimensions(media.buffer) : undefined; + deliveryKind === "video" && !isVideoNote + ? await probeVideoDimensions(media.buffer) + : undefined; const mediaParams = { ...(htmlCaption ? { caption: htmlCaption, parse_mode: "HTML" as const } : {}), ...baseMediaParams, @@ -868,7 +871,7 @@ export async function sendMessageTelegram( ); const mediaSender = (() => { - if (isGif && !opts.forceDocument) { + if (isGif && deliveryKind !== "document") { return { label: "animation", sender: (effectiveParams: TelegramThreadScopedParams | undefined) => @@ -879,7 +882,7 @@ export async function sendMessageTelegram( ) as Promise, }; } - if (kind === "image" && !opts.forceDocument && sendImageAsPhoto) { + if (deliveryKind === "image" && !isGif && sendImageAsPhoto) { return { label: "photo", sender: (effectiveParams: TelegramThreadScopedParams | undefined) => @@ -890,7 +893,7 @@ export async function sendMessageTelegram( ) as Promise, }; } - if (kind === "video") { + if (deliveryKind === "video") { if (isVideoNote) { return { label: "video_note", @@ -946,8 +949,6 @@ export async function sendMessageTelegram( api.sendDocument( chatId, file, - // Only force Telegram to keep the uploaded media type when callers explicitly - // opt into document delivery for image/GIF uploads. (opts.forceDocument ? { ...effectiveParams, disable_content_type_detection: true } : effectiveParams) as Parameters[2],