From f215675e86c11e49a7cd23425941255170054572 Mon Sep 17 00:00:00 2001 From: cristianchiriac <80774326+cristianchiriac@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:33:26 +0300 Subject: [PATCH] Upload media files of rich messages InputRichMessage was sent with to_json() only, so a media object built from an InputFile (in a photo, video, animation, audio, voice note or document block, or in InputRichMessage.media) was sent as an attach:// reference without the file. Add InputRichMessage.convert_input_rich_message(), which returns the JSON and the files of every InputMedia in the message, including blocks nested in lists, collages, slideshows, details and quotations, and send those files with send_rich_message, send_rich_message_draft and edit_message_text, in apihelper and asyncio_helper. Items 1 and 2 of #2624. --- telebot/apihelper.py | 15 +++--- telebot/asyncio_helper.py | 15 +++--- telebot/types.py | 22 +++++++++ tests/test_rich_message_uploads.py | 79 ++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 tests/test_rich_message_uploads.py diff --git a/telebot/apihelper.py b/telebot/apihelper.py index 33545d4d2..c1f15f666 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -306,7 +306,8 @@ def send_rich_message( direct_messages_topic_id=None, suggested_post_parameters=None, allow_paid_broadcast=None, message_thread_id=None, ephemeral_message_parameters=None): method_url = r'sendRichMessage' - payload = {'chat_id': chat_id, 'rich_message': rich_message.to_json()} + rich_message_json, files = rich_message.convert_input_rich_message() + payload = {'chat_id': chat_id, 'rich_message': rich_message_json} if disable_notification is not None: payload['disable_notification'] = disable_notification if protect_content is not None: @@ -329,18 +330,19 @@ def send_rich_message( payload['message_thread_id'] = message_thread_id if ephemeral_message_parameters is not None: payload['ephemeral_message_parameters'] = ephemeral_message_parameters.to_json() - return _make_request(token, method_url, params=payload, method='post') + return _make_request(token, method_url, params=payload, files=files, method='post') def send_rich_message_draft(token, chat_id, draft_id, rich_message, message_thread_id=None, can_stop=None, keep_on_stop=None): method_url = r'sendRichMessageDraft' - payload = {'chat_id': str(chat_id), 'draft_id': draft_id, 'rich_message': rich_message.to_json()} + rich_message_json, files = rich_message.convert_input_rich_message() + payload = {'chat_id': str(chat_id), 'draft_id': draft_id, 'rich_message': rich_message_json} if message_thread_id is not None: payload['message_thread_id'] = message_thread_id if can_stop is not None: payload['can_stop'] = can_stop if keep_on_stop is not None: payload['keep_on_stop'] = keep_on_stop - return _make_request(token, method_url, params=payload, method='post') + return _make_request(token, method_url, params=payload, files=files, method='post') def set_webhook(token, url=None, certificate=None, max_connections=None, allowed_updates=None, ip_address=None, @@ -1875,9 +1877,10 @@ def edit_message_text( payload['business_connection_id'] = business_connection_id if timeout: payload['timeout'] = timeout + files = None if rich_message: - payload['rich_message'] = rich_message.to_json() - return _make_request(token, method_url, params=payload, method='post') + payload['rich_message'], files = rich_message.convert_input_rich_message() + return _make_request(token, method_url, params=payload, files=files, method='post') def edit_message_caption( diff --git a/telebot/asyncio_helper.py b/telebot/asyncio_helper.py index a90935864..6dd5820a4 100644 --- a/telebot/asyncio_helper.py +++ b/telebot/asyncio_helper.py @@ -338,7 +338,8 @@ async def send_rich_message( reply_parameters=None, reply_markup=None, business_connection_id=None, allow_paid_broadcast=None, direct_messages_topic_id=None, suggested_post_parameters=None, message_thread_id=None, ephemeral_message_parameters=None): method_url = r'sendRichMessage' - payload = {'chat_id': str(chat_id), 'rich_message': rich_message.to_json()} + rich_message_json, files = rich_message.convert_input_rich_message() + payload = {'chat_id': str(chat_id), 'rich_message': rich_message_json} if disable_notification is not None: payload['disable_notification'] = disable_notification if protect_content is not None: @@ -362,19 +363,20 @@ async def send_rich_message( if ephemeral_message_parameters is not None: payload['ephemeral_message_parameters'] = ephemeral_message_parameters.to_json() - return await _process_request(token, method_url, params=payload, method='post') + return await _process_request(token, method_url, params=payload, files=files, method='post') async def send_rich_message_draft(token, chat_id, draft_id, rich_message, message_thread_id=None, can_stop=None, keep_on_stop=None): method_url = r'sendRichMessageDraft' - payload = {'chat_id': chat_id, 'draft_id': draft_id, 'rich_message': rich_message.to_json()} + rich_message_json, files = rich_message.convert_input_rich_message() + payload = {'chat_id': chat_id, 'draft_id': draft_id, 'rich_message': rich_message_json} if message_thread_id is not None: payload['message_thread_id'] = message_thread_id if can_stop is not None: payload['can_stop'] = can_stop if keep_on_stop is not None: payload['keep_on_stop'] = keep_on_stop - return await _process_request(token, method_url, params=payload, method='post') + return await _process_request(token, method_url, params=payload, files=files, method='post') async def get_user_profile_photos(token, user_id, offset=None, limit=None): @@ -1872,9 +1874,10 @@ async def edit_message_text( payload['business_connection_id'] = business_connection_id if timeout: payload['timeout'] = timeout + files = None if rich_message: - payload['rich_message'] = rich_message.to_json() - return await _process_request(token, method_url, params=payload, method='post') + payload['rich_message'], files = rich_message.convert_input_rich_message() + return await _process_request(token, method_url, params=payload, files=files, method='post') async def edit_message_caption( diff --git a/telebot/types.py b/telebot/types.py index 1b3388c01..0fd74bbf1 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -16637,6 +16637,28 @@ def to_dict(self) -> dict: def to_json(self) -> str: return json.dumps(self.to_dict()) + def convert_input_rich_message(self): + """ + :meta private: + """ + files = {} + + # Media can sit in self.media or in any block, including blocks nested + # in lists, collages, slideshows, details and quotations. + def collect(value): + if isinstance(value, InputMedia): + files.update(value.convert_input_media()[1]) + elif isinstance(value, list): + for item in value: + collect(item) + elif isinstance(value, (InputRichBlock, InputRichBlockListItem, InputRichMessageMedia)): + for item in vars(value).values(): + collect(item) + + collect(self.media) + collect(self.blocks) + return self.to_json(), files + class Link(JsonDeserializable): """ diff --git a/tests/test_rich_message_uploads.py b/tests/test_rich_message_uploads.py new file mode 100644 index 000000000..a360e7b64 --- /dev/null +++ b/tests/test_rich_message_uploads.py @@ -0,0 +1,79 @@ +import asyncio +import io +import json + +from telebot import apihelper, asyncio_helper, types + + +def _rich_message(): + photo_file = types.InputFile(io.BytesIO(b'photo'), file_name='photo.jpg') + video_file = types.InputFile(io.BytesIO(b'video'), file_name='video.mp4') + # A photo block nested in a list item, and a media entry for tg:// links + nested_photo = types.InputRichBlockList(items=[ + types.InputRichBlockListItem(blocks=[types.InputRichBlockPhoto(photo=types.InputMediaPhoto(photo_file))]), + ]) + media = types.InputRichMessageMedia(id='clip', media=types.InputMediaVideo(video_file)) + return types.InputRichMessage(blocks=[nested_photo], media=[media]), photo_file, video_file + + +def _check_uploads(params, files, photo_file, video_file): + sent = json.loads(params['rich_message']) + photo_ref = sent['blocks'][0]['items'][0]['blocks'][0]['photo']['media'] + video_ref = sent['media'][0]['media']['media'] + assert files == { + photo_ref[len('attach://'):]: photo_file, + video_ref[len('attach://'):]: video_file, + } + + +def _capture(captured): + def fake_request(token, method_url, params=None, files=None, method=None, **kwargs): + captured.update(method_url=method_url, params=params, files=files, method=method) + return {} + return fake_request + + +def test_rich_message_without_uploads_has_no_files(): + rich_message = types.InputRichMessage(html='Hello') + assert rich_message.convert_input_rich_message() == (rich_message.to_json(), {}) + + +def test_rich_message_methods_upload_media_files(monkeypatch): + captured = {} + monkeypatch.setattr(apihelper, '_make_request', _capture(captured)) + + rich_message, photo_file, video_file = _rich_message() + apihelper.send_rich_message('token', 1, rich_message) + assert captured['method'] == 'post' + _check_uploads(captured['params'], captured['files'], photo_file, video_file) + + rich_message, photo_file, video_file = _rich_message() + apihelper.send_rich_message_draft('token', 1, 2, rich_message) + _check_uploads(captured['params'], captured['files'], photo_file, video_file) + + rich_message, photo_file, video_file = _rich_message() + apihelper.edit_message_text('token', chat_id=1, message_id=2, rich_message=rich_message) + _check_uploads(captured['params'], captured['files'], photo_file, video_file) + + +def test_async_rich_message_methods_upload_media_files(monkeypatch): + captured = {} + + async def fake_process_request(token, url, method='get', params=None, files=None, **kwargs): + captured.update(url=url, params=params, files=files, method=method) + return {} + + monkeypatch.setattr(asyncio_helper, '_process_request', fake_process_request) + + rich_message, photo_file, video_file = _rich_message() + asyncio.run(asyncio_helper.send_rich_message('token', 1, rich_message)) + assert captured['method'] == 'post' + _check_uploads(captured['params'], captured['files'], photo_file, video_file) + + rich_message, photo_file, video_file = _rich_message() + asyncio.run(asyncio_helper.send_rich_message_draft('token', 1, 2, rich_message)) + _check_uploads(captured['params'], captured['files'], photo_file, video_file) + + rich_message, photo_file, video_file = _rich_message() + asyncio.run(asyncio_helper.edit_message_text('token', chat_id=1, message_id=2, rich_message=rich_message)) + _check_uploads(captured['params'], captured['files'], photo_file, video_file)