From 97ef4797e0ecd0d3e53843f094787bff9a5680d0 Mon Sep 17 00:00:00 2001 From: Anmol Malik Date: Thu, 28 May 2026 23:07:44 +0530 Subject: [PATCH] Send Telegram media attachments natively --- .../_telegram_integration/helpers/handler.py | 6 + .../helpers/telegram_client.py | 93 +++++++++++++++ tests/test_telegram_media_delivery.py | 108 ++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 tests/test_telegram_media_delivery.py diff --git a/plugins/_telegram_integration/helpers/handler.py b/plugins/_telegram_integration/helpers/handler.py index 07c241478..3fd0f8c82 100644 --- a/plugins/_telegram_integration/helpers/handler.py +++ b/plugins/_telegram_integration/helpers/handler.py @@ -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) diff --git a/plugins/_telegram_integration/helpers/telegram_client.py b/plugins/_telegram_integration/helpers/telegram_client.py index 45db75977..2fc9f69f1 100644 --- a/plugins/_telegram_integration/helpers/telegram_client.py +++ b/plugins/_telegram_integration/helpers/telegram_client.py @@ -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] = [] diff --git a/tests/test_telegram_media_delivery.py b/tests/test_telegram_media_delivery.py new file mode 100644 index 000000000..bbdb2a2bc --- /dev/null +++ b/tests/test_telegram_media_delivery.py @@ -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}, + ]