Send Telegram media attachments natively

This commit is contained in:
Anmol Malik 2026-05-28 23:07:44 +05:30
parent a17cef5b03
commit 97ef4797e0
3 changed files with 207 additions and 0 deletions

View file

@ -526,6 +526,12 @@ async def send_telegram_reply(
local_path = files.fix_dev_path(path)
if tc.is_image_file(local_path):
await tc.send_photo(reply_bot, chat_id, local_path, reply_to_message_id=reply_to)
elif tc.is_voice_file(local_path):
await tc.send_voice(reply_bot, chat_id, local_path, reply_to_message_id=reply_to)
elif tc.is_audio_file(local_path):
await tc.send_audio(reply_bot, chat_id, local_path, reply_to_message_id=reply_to)
elif tc.is_video_file(local_path):
await tc.send_video(reply_bot, chat_id, local_path, reply_to_message_id=reply_to)
else:
await tc.send_file(reply_bot, chat_id, local_path, reply_to_message_id=reply_to)

View file

@ -115,6 +115,81 @@ async def send_photo(
return None
async def send_voice(
bot: Bot,
chat_id: int,
voice_path: str,
caption: str = "",
reply_to_message_id: int | None = None,
) -> int | None:
"""Send a voice message from local path. Returns message_id or None on error."""
try:
if not os.path.isfile(voice_path):
PrintStyle.error(f"Telegram: voice file not found: {voice_path}")
return None
input_file = FSInputFile(voice_path)
msg = await bot.send_voice(
chat_id=chat_id,
voice=input_file,
caption=caption[:1024] if caption else None,
reply_to_message_id=reply_to_message_id,
)
return msg.message_id
except Exception as e:
PrintStyle.error(f"Telegram send_voice failed: {format_error(e)}")
return None
async def send_audio(
bot: Bot,
chat_id: int,
audio_path: str,
caption: str = "",
reply_to_message_id: int | None = None,
) -> int | None:
"""Send an audio message from local path. Returns message_id or None on error."""
try:
if not os.path.isfile(audio_path):
PrintStyle.error(f"Telegram: audio file not found: {audio_path}")
return None
input_file = FSInputFile(audio_path)
msg = await bot.send_audio(
chat_id=chat_id,
audio=input_file,
caption=caption[:1024] if caption else None,
reply_to_message_id=reply_to_message_id,
)
return msg.message_id
except Exception as e:
PrintStyle.error(f"Telegram send_audio failed: {format_error(e)}")
return None
async def send_video(
bot: Bot,
chat_id: int,
video_path: str,
caption: str = "",
reply_to_message_id: int | None = None,
) -> int | None:
"""Send a video message from local path. Returns message_id or None on error."""
try:
if not os.path.isfile(video_path):
PrintStyle.error(f"Telegram: video file not found: {video_path}")
return None
input_file = FSInputFile(video_path)
msg = await bot.send_video(
chat_id=chat_id,
video=input_file,
caption=caption[:1024] if caption else None,
reply_to_message_id=reply_to_message_id,
)
return msg.message_id
except Exception as e:
PrintStyle.error(f"Telegram send_video failed: {format_error(e)}")
return None
# Inline keyboards
def build_inline_keyboard(
@ -294,6 +369,9 @@ def _split_text(text: str, max_len: int) -> list[str]:
_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
_VOICE_EXTENSIONS = {".ogg", ".oga", ".opus"}
_AUDIO_EXTENSIONS = {".mp3", ".m4a", ".aac", ".wav", ".flac"}
_VIDEO_EXTENSIONS = {".mp4", ".m4v", ".mov", ".webm"}
def is_image_file(path: str) -> bool:
@ -301,6 +379,21 @@ def is_image_file(path: str) -> bool:
return ext in _IMAGE_EXTENSIONS
def is_voice_file(path: str) -> bool:
_, ext = os.path.splitext(path.lower())
return ext in _VOICE_EXTENSIONS
def is_audio_file(path: str) -> bool:
_, ext = os.path.splitext(path.lower())
return ext in _AUDIO_EXTENSIONS
def is_video_file(path: str) -> bool:
_, ext = os.path.splitext(path.lower())
return ext in _VIDEO_EXTENSIONS
def md_to_telegram_html(text: str) -> str:
"""Convert Markdown to Telegram-compatible HTML."""
stash: list[str] = []

View file

@ -0,0 +1,108 @@
import asyncio
from plugins._telegram_integration.helpers import handler
from plugins._telegram_integration.helpers import telegram_client as tc
from plugins._telegram_integration.helpers.constants import (
CTX_TG_BOT,
CTX_TG_CHAT_ID,
CTX_TG_REPLY_TO,
)
class FakeBotInstance:
class Bot:
token = "token"
bot = Bot()
class FakeContext:
def __init__(self):
self.data = {
CTX_TG_BOT: "main",
CTX_TG_CHAT_ID: 123,
CTX_TG_REPLY_TO: 456,
}
class FakeTempBot:
async def __aenter__(self):
return object()
async def __aexit__(self, exc_type, exc, tb):
return False
class FakeMediaBot:
def __init__(self):
self.calls = []
async def send_voice(self, **kwargs):
self.calls.append(("voice", kwargs))
return type("Message", (), {"message_id": 11})()
async def send_audio(self, **kwargs):
self.calls.append(("audio", kwargs))
return type("Message", (), {"message_id": 12})()
async def send_video(self, **kwargs):
self.calls.append(("video", kwargs))
return type("Message", (), {"message_id": 13})()
def test_telegram_client_native_media_helpers(monkeypatch):
bot = FakeMediaBot()
monkeypatch.setattr(tc.os.path, "isfile", lambda path: True)
voice_id = asyncio.run(tc.send_voice(bot, 123, "voice.ogg", reply_to_message_id=456))
audio_id = asyncio.run(tc.send_audio(bot, 123, "song.mp3", reply_to_message_id=456))
video_id = asyncio.run(tc.send_video(bot, 123, "clip.mp4", reply_to_message_id=456))
assert (voice_id, audio_id, video_id) == (11, 12, 13)
assert [kind for kind, _ in bot.calls] == ["voice", "audio", "video"]
assert bot.calls[0][1]["chat_id"] == 123
assert bot.calls[0][1]["reply_to_message_id"] == 456
def test_telegram_reply_routes_native_media_attachments(monkeypatch):
calls = []
def fake_temp_bot(*args, **kwargs):
return FakeTempBot()
async def record(kind, bot, chat_id, path, reply_to_message_id=None, **kwargs):
calls.append(
{
"kind": kind,
"chat_id": chat_id,
"path": path,
"reply_to_message_id": reply_to_message_id,
}
)
return len(calls)
monkeypatch.setattr(handler, "get_bot", lambda name: FakeBotInstance())
monkeypatch.setattr(handler, "_temp_bot", fake_temp_bot)
monkeypatch.setattr(handler.files, "fix_dev_path", lambda path: path)
monkeypatch.setattr(handler.tc, "send_photo", lambda *a, **kw: record("photo", *a, **kw))
monkeypatch.setattr(handler.tc, "send_voice", lambda *a, **kw: record("voice", *a, **kw))
monkeypatch.setattr(handler.tc, "send_audio", lambda *a, **kw: record("audio", *a, **kw))
monkeypatch.setattr(handler.tc, "send_video", lambda *a, **kw: record("video", *a, **kw))
monkeypatch.setattr(handler.tc, "send_file", lambda *a, **kw: record("document", *a, **kw))
error = asyncio.run(
handler.send_telegram_reply(
FakeContext(),
"",
["image.png", "voice.ogg", "song.mp3", "clip.mp4", "archive.zip"],
)
)
assert error is None
assert calls == [
{"kind": "photo", "chat_id": 123, "path": "image.png", "reply_to_message_id": 456},
{"kind": "voice", "chat_id": 123, "path": "voice.ogg", "reply_to_message_id": 456},
{"kind": "audio", "chat_id": 123, "path": "song.mp3", "reply_to_message_id": 456},
{"kind": "video", "chat_id": 123, "path": "clip.mp4", "reply_to_message_id": 456},
{"kind": "document", "chat_id": 123, "path": "archive.zip", "reply_to_message_id": 456},
]