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:
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user