feat(channels): add video support for Telegram and WebSocket

Telegram previously sent all video files as documents via send_document,
so users saw a file icon instead of an inline player. WebSocket only
accepted image MIME types, rejecting video uploads entirely.

Telegram:
- Recognize video extensions (mp4/mov/avi/mkv/webm/3gp) in _get_media_type
- Route videos through send_video with supports_streaming=True
- Add VIDEO/VIDEO_NOTE/ANIMATION to inbound message filters
- Add video MIME mappings to _get_extension
- Fix: local file sends now use _call_with_retry (previously no retry)

WebSocket:
- Expand upload MIME whitelist with video/mp4, video/webm, video/quicktime
- Add per-type size limits (_MAX_VIDEO_BYTES=20MB, _MAX_VIDEOS_PER_MESSAGE=1)
- Expand media serving endpoint to serve video with correct Content-Type

Agent:
- Add "video" to message tool media parameter description
- Add .mp4 example to identity.md system prompt

Made-with: Cursor
This commit is contained in:
Xubin Ren
2026-04-25 02:20:13 +08:00
committed by Xubin Ren
parent ee14e2df56
commit be05189f39
5 changed files with 67 additions and 20 deletions
+31 -13
View File
@@ -7,6 +7,7 @@ import re
import time
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
from loguru import logger
@@ -357,10 +358,12 @@ class TelegramChannel(BaseChannel):
)
self._app.add_handler(MessageHandler(filters.Regex(r"^/help(?:@\w+)?$"), self._on_help))
# Add message handler for text, photos, voice, documents, and locations
# Add message handler for text, photos, video, voice, documents, and locations
self._app.add_handler(
MessageHandler(
(filters.TEXT | filters.PHOTO | filters.VOICE | filters.AUDIO | filters.Document.ALL | filters.LOCATION)
(filters.TEXT | filters.PHOTO | filters.VIDEO | filters.VIDEO_NOTE
| filters.ANIMATION | filters.VOICE | filters.AUDIO
| filters.Document.ALL | filters.LOCATION)
& ~filters.COMMAND,
self._on_message
)
@@ -429,6 +432,8 @@ class TelegramChannel(BaseChannel):
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
if ext in ("jpg", "jpeg", "png", "gif", "webp"):
return "photo"
if ext in ("mp4", "mov", "avi", "mkv", "webm", "3gp"):
return "video"
if ext == "ogg":
return "voice"
if ext in ("mp3", "m4a", "wav", "aac"):
@@ -481,10 +486,19 @@ class TelegramChannel(BaseChannel):
media_type = self._get_media_type(media_path)
sender = {
"photo": self._app.bot.send_photo,
"video": self._app.bot.send_video,
"voice": self._app.bot.send_voice,
"audio": self._app.bot.send_audio,
}.get(media_type, self._app.bot.send_document)
param = "photo" if media_type == "photo" else media_type if media_type in ("voice", "audio") else "document"
param = {
"photo": "photo",
"video": "video",
"voice": "voice",
"audio": "audio",
}.get(media_type, "document")
extra: dict[str, Any] = {}
if media_type == "video":
extra["supports_streaming"] = True
# Telegram Bot API accepts HTTP(S) URLs directly for media params.
if self._is_remote_media_url(media_path):
@@ -497,16 +511,19 @@ class TelegramChannel(BaseChannel):
**{param: media_path},
reply_parameters=reply_params,
**thread_kwargs,
**extra,
)
continue
with open(media_path, "rb") as f:
await sender(
chat_id=chat_id,
**{param: f},
reply_parameters=reply_params,
**thread_kwargs,
)
media_bytes = Path(media_path).read_bytes()
await self._call_with_retry(
sender,
chat_id=chat_id,
**{param: media_bytes},
reply_parameters=reply_params,
**thread_kwargs,
**extra,
)
except Exception as e:
filename = media_path.rsplit("/", 1)[-1]
logger.error("Failed to send media {}: {}", media_path, e)
@@ -1184,18 +1201,19 @@ class TelegramChannel(BaseChannel):
if mime_type:
ext_map = {
"image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif",
"image/webp": ".webp",
"audio/ogg": ".ogg", "audio/mpeg": ".mp3", "audio/mp4": ".m4a",
"video/mp4": ".mp4", "video/quicktime": ".mov", "video/webm": ".webm",
"video/x-matroska": ".mkv", "video/3gpp": ".3gp",
}
if mime_type in ext_map:
return ext_map[mime_type]
type_map = {"image": ".jpg", "voice": ".ogg", "audio": ".mp3", "file": ""}
type_map = {"image": ".jpg", "voice": ".ogg", "audio": ".mp3", "video": ".mp4", "file": ""}
if ext := type_map.get(media_type, ""):
return ext
if filename:
from pathlib import Path
return "".join(Path(filename).suffixes)
return ""