Repository: python-telegram-bot/python-telegram-bot

Issue:
[ENHANCEMENT] filename parameter of send_* functions
T[he send_document function has a filename parameter](https://python-telegram-bot.readthedocs.io/en/stable/telegram.bot.html#telegram.Bot.send_document), which sets the file_name for the document to send when the actual document input is a file_like object. Since this is a feature of our library (and sadly not an undocumented one of telegrams API), we can evaluate  which other file sending function would benefit from exposing this parameter.


Discussion:
Can you brief it a bit...
Hey, what do you mean with brief?
Like you said could benefit from.. means how do you say we work it on..
by adding a `filename` parameter to `send_*` methods that can use it and passing that argument to `parse_file_input` internally
this means we need to evaluate which functions end up showing the filename in the end/if telegrams servers store it. Latter could be achieved by checking what other bots receive as a filename for each type of media. First would be to take as many clients as possible and see what they do with the media: Do they or do they not show the filename? E.g. a pictures filename is not shown, an audio ones is.
`InputMedia*` should be considered, too.

Resolving patch:
diff --git a/telegram/bot.py b/telegram/bot.py
--- a/telegram/bot.py
+++ b/telegram/bot.py
@@ -575,6 +575,7 @@ def send_photo(
         api_kwargs: JSONDict = None,
         allow_sending_without_reply: bool = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ) -> Optional[Message]:
         """Use this method to send photos.
 
@@ -591,6 +592,9 @@ def send_photo(
                 (recommended), pass an HTTP URL as a String for Telegram to get a photo from the
                 Internet, or upload a new photo using multipart/form-data. Lastly you can pass
                 an existing :class:`telegram.PhotoSize` object to send.
+            filename (:obj:`str`, optional): Custom file name for the photo, when uploading a
+                new file. Convenience parameter, useful e.g. when sending files generated by the
+                :obj:`tempfile` module.
             caption (:obj:`str`, optional): Photo caption (may also be used when resending photos
                 by file_id), 0-1024 characters after entities parsing.
             parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
@@ -619,7 +623,10 @@ def send_photo(
             :class:`telegram.TelegramError`
 
         """
-        data: JSONDict = {'chat_id': chat_id, 'photo': parse_file_input(photo, PhotoSize)}
+        data: JSONDict = {
+            'chat_id': chat_id,
+            'photo': parse_file_input(photo, PhotoSize, filename=filename),
+        }
 
         if caption:
             data['caption'] = caption
@@ -657,6 +664,7 @@ def send_audio(
         api_kwargs: JSONDict = None,
         allow_sending_without_reply: bool = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ) -> Optional[Message]:
         """
         Use this method to send audio files, if you want Telegram clients to display them in the
@@ -680,6 +688,9 @@ def send_audio(
                 (recommended), pass an HTTP URL as a String for Telegram to get an audio file from
                 the Internet, or upload a new one using multipart/form-data. Lastly you can pass
                 an existing :class:`telegram.Audio` object to send.
+            filename (:obj:`str`, optional): Custom file name for the audio, when uploading a
+                new file. Convenience parameter, useful e.g. when sending files generated by the
+                :obj:`tempfile` module.
             caption (:obj:`str`, optional): Audio caption, 0-1024 characters after entities
                 parsing.
             parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
@@ -717,7 +728,10 @@ def send_audio(
             :class:`telegram.TelegramError`
 
         """
-        data: JSONDict = {'chat_id': chat_id, 'audio': parse_file_input(audio, Audio)}
+        data: JSONDict = {
+            'chat_id': chat_id,
+            'audio': parse_file_input(audio, Audio, filename=filename),
+        }
 
         if duration:
             data['duration'] = duration
@@ -782,8 +796,9 @@ def send_document(
                 (recommended), pass an HTTP URL as a String for Telegram to get a file from the
                 Internet, or upload a new one using multipart/form-data. Lastly you can pass
                 an existing :class:`telegram.Document` object to send.
-            filename (:obj:`str`, optional): File name that shows in telegram message (it is useful
-                when you send file generated by temp module, for example). Undocumented.
+            filename (:obj:`str`, optional): Custom file name for the document, when uploading a
+                new file. Convenience parameter, useful e.g. when sending files generated by the
+                :obj:`tempfile` module.
             caption (:obj:`str`, optional): Document caption (may also be used when resending
                 documents by file_id), 0-1024 characters after entities parsing.
             disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side
@@ -927,6 +942,7 @@ def send_video(
         api_kwargs: JSONDict = None,
         allow_sending_without_reply: bool = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ) -> Optional[Message]:
         """
         Use this method to send video files, Telegram clients support mp4 videos
@@ -951,6 +967,9 @@ def send_video(
                 (recommended), pass an HTTP URL as a String for Telegram to get an video file from
                 the Internet, or upload a new one using multipart/form-data. Lastly you can pass
                 an existing :class:`telegram.Video` object to send.
+            filename (:obj:`str`, optional): Custom file name for the video, when uploading a
+                new file. Convenience parameter, useful e.g. when sending files generated by the
+                :obj:`tempfile` module.
             duration (:obj:`int`, optional): Duration of sent video in seconds.
             width (:obj:`int`, optional): Video width.
             height (:obj:`int`, optional): Video height.
@@ -990,7 +1009,10 @@ def send_video(
             :class:`telegram.TelegramError`
 
         """
-        data: JSONDict = {'chat_id': chat_id, 'video': parse_file_input(video, Video)}
+        data: JSONDict = {
+            'chat_id': chat_id,
+            'video': parse_file_input(video, Video, filename=filename),
+        }
 
         if duration:
             data['duration'] = duration
@@ -1034,6 +1056,7 @@ def send_video_note(
         thumb: FileInput = None,
         api_kwargs: JSONDict = None,
         allow_sending_without_reply: bool = None,
+        filename: str = None,
     ) -> Optional[Message]:
         """
         As of v.4.0, Telegram clients support rounded square mp4 videos of up to 1 minute long.
@@ -1055,6 +1078,9 @@ def send_video_note(
                 servers (recommended) or upload a new video using multipart/form-data. Or you can
                 pass an existing :class:`telegram.VideoNote` object to send. Sending video notes by
                 a URL is currently unsupported.
+            filename (:obj:`str`, optional): Custom file name for the video note, when uploading a
+                new file. Convenience parameter, useful e.g. when sending files generated by the
+                :obj:`tempfile` module.
             duration (:obj:`int`, optional): Duration of sent video in seconds.
             length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
                 message.
@@ -1086,7 +1112,7 @@ def send_video_note(
         """
         data: JSONDict = {
             'chat_id': chat_id,
-            'video_note': parse_file_input(video_note, VideoNote),
+            'video_note': parse_file_input(video_note, VideoNote, filename=filename),
         }
 
         if duration is not None:
@@ -1125,6 +1151,7 @@ def send_animation(
         api_kwargs: JSONDict = None,
         allow_sending_without_reply: bool = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ) -> Optional[Message]:
         """
         Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
@@ -1145,6 +1172,9 @@ def send_animation(
                 servers (recommended), pass an HTTP URL as a String for Telegram to get an
                 animation from the Internet, or upload a new animation using multipart/form-data.
                 Lastly you can pass an existing :class:`telegram.Animation` object to send.
+            filename (:obj:`str`, optional): Custom file name for the animation, when uploading a
+                new file. Convenience parameter, useful e.g. when sending files generated by the
+                :obj:`tempfile` module.
             duration (:obj:`int`, optional): Duration of sent animation in seconds.
             width (:obj:`int`, optional): Animation width.
             height (:obj:`int`, optional): Animation height.
@@ -1182,7 +1212,10 @@ def send_animation(
             :class:`telegram.TelegramError`
 
         """
-        data: JSONDict = {'chat_id': chat_id, 'animation': parse_file_input(animation, Animation)}
+        data: JSONDict = {
+            'chat_id': chat_id,
+            'animation': parse_file_input(animation, Animation, filename=filename),
+        }
 
         if duration:
             data['duration'] = duration
@@ -1225,6 +1258,7 @@ def send_voice(
         api_kwargs: JSONDict = None,
         allow_sending_without_reply: bool = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ) -> Optional[Message]:
         """
         Use this method to send audio files, if you want Telegram clients to display the file
@@ -1245,6 +1279,9 @@ def send_voice(
                 (recommended), pass an HTTP URL as a String for Telegram to get an voice file from
                 the Internet, or upload a new one using multipart/form-data. Lastly you can pass
                 an existing :class:`telegram.Voice` object to send.
+            filename (:obj:`str`, optional): Custom file name for the voice, when uploading a
+                new file. Convenience parameter, useful e.g. when sending files generated by the
+                :obj:`tempfile` module.
             caption (:obj:`str`, optional): Voice message caption, 0-1024 characters after entities
                 parsing.
             parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
@@ -1274,7 +1311,10 @@ def send_voice(
             :class:`telegram.TelegramError`
 
         """
-        data: JSONDict = {'chat_id': chat_id, 'voice': parse_file_input(voice, Voice)}
+        data: JSONDict = {
+            'chat_id': chat_id,
+            'voice': parse_file_input(voice, Voice, filename=filename),
+        }
 
         if duration:
             data['duration'] = duration
diff --git a/telegram/files/inputmedia.py b/telegram/files/inputmedia.py
--- a/telegram/files/inputmedia.py
+++ b/telegram/files/inputmedia.py
@@ -78,6 +78,9 @@ class InputMediaAnimation(InputMedia):
             file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
             URL for Telegram to get a file from the Internet. Lastly you can pass an existing
             :class:`telegram.Animation` object to send.
+        filename (:obj:`str`, optional): Custom file name for the animation, when uploading a
+            new file. Convenience parameter, useful e.g. when sending files generated by the
+            :obj:`tempfile` module.
         thumb (`filelike object` | :class:`pathlib.Path`, optional): Thumbnail of the file sent;
             can be ignored if
             thumbnail generation for the file is supported server-side. The thumbnail should be
@@ -111,6 +114,7 @@ def __init__(
         height: int = None,
         duration: int = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ):
         self.type = 'animation'
 
@@ -120,7 +124,7 @@ def __init__(
             self.height = media.height
             self.duration = media.duration
         else:
-            self.media = parse_file_input(media, attach=True)
+            self.media = parse_file_input(media, attach=True, filename=filename)
 
         if thumb:
             self.thumb = parse_file_input(thumb, attach=True)
@@ -154,6 +158,9 @@ class InputMediaPhoto(InputMedia):
             file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
             URL for Telegram to get a file from the Internet. Lastly you can pass an existing
             :class:`telegram.PhotoSize` object to send.
+        filename (:obj:`str`, optional): Custom file name for the photo, when uploading a
+            new file. Convenience parameter, useful e.g. when sending files generated by the
+            :obj:`tempfile` module.
         caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-1024 characters after
             entities parsing.
         parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -169,9 +176,10 @@ def __init__(
         caption: str = None,
         parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ):
         self.type = 'photo'
-        self.media = parse_file_input(media, PhotoSize, attach=True)
+        self.media = parse_file_input(media, PhotoSize, attach=True, filename=filename)
 
         if caption:
             self.caption = caption
@@ -202,6 +210,9 @@ class InputMediaVideo(InputMedia):
             file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
             URL for Telegram to get a file from the Internet. Lastly you can pass an existing
             :class:`telegram.Video` object to send.
+        filename (:obj:`str`, optional): Custom file name for the video, when uploading a
+            new file. Convenience parameter, useful e.g. when sending files generated by the
+            :obj:`tempfile` module.
         caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after
             entities parsing.
         parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -241,6 +252,7 @@ def __init__(
         parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
         thumb: FileInput = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ):
         self.type = 'video'
 
@@ -250,7 +262,7 @@ def __init__(
             self.height = media.height
             self.duration = media.duration
         else:
-            self.media = parse_file_input(media, attach=True)
+            self.media = parse_file_input(media, attach=True, filename=filename)
 
         if thumb:
             self.thumb = parse_file_input(thumb, attach=True)
@@ -291,6 +303,9 @@ class InputMediaAudio(InputMedia):
             file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
             URL for Telegram to get a file from the Internet. Lastly you can pass an existing
             :class:`telegram.Audio` object to send.
+        filename (:obj:`str`, optional): Custom file name for the audio, when uploading a
+            new file. Convenience parameter, useful e.g. when sending files generated by the
+            :obj:`tempfile` module.
         caption (:obj:`str`, optional): Caption of the audio to be sent, 0-1024 characters after
             entities parsing.
         parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -325,6 +340,7 @@ def __init__(
         performer: str = None,
         title: str = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ):
         self.type = 'audio'
 
@@ -334,7 +350,7 @@ def __init__(
             self.performer = media.performer
             self.title = media.title
         else:
-            self.media = parse_file_input(media, attach=True)
+            self.media = parse_file_input(media, attach=True, filename=filename)
 
         if thumb:
             self.thumb = parse_file_input(thumb, attach=True)
@@ -372,6 +388,9 @@ class InputMediaDocument(InputMedia):
             file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
             URL for Telegram to get a file from the Internet. Lastly you can pass an existing
             :class:`telegram.Document` object to send.
+        filename (:obj:`str`, optional): Custom file name for the document, when uploading a
+            new file. Convenience parameter, useful e.g. when sending files generated by the
+            :obj:`tempfile` module.
         caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters after
             entities parsing.
         parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -398,9 +417,10 @@ def __init__(
         parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
         disable_content_type_detection: bool = None,
         caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
+        filename: str = None,
     ):
         self.type = 'document'
-        self.media = parse_file_input(media, Document, attach=True)
+        self.media = parse_file_input(media, Document, attach=True, filename=filename)
 
         if thumb:
             self.thumb = parse_file_input(thumb, attach=True)


Test patch:
diff --git a/tests/test_animation.py b/tests/test_animation.py
--- a/tests/test_animation.py
+++ b/tests/test_animation.py
@@ -96,6 +96,16 @@ def test_send_all_args(self, bot, chat_id, animation_file, animation, thumb_file
         assert message.animation.thumb.width == self.width
         assert message.animation.thumb.height == self.height
 
+    @flaky(3, 1)
+    @pytest.mark.timeout(10)
+    def test_send_animation_custom_filename(self, bot, chat_id, animation_file, monkeypatch):
+        def make_assertion(url, data, **kwargs):
+            return data['animation'].filename == 'custom_filename'
+
+        monkeypatch.setattr(bot.request, 'post', make_assertion)
+
+        assert bot.send_animation(chat_id, animation_file, filename='custom_filename')
+
     @flaky(3, 1)
     @pytest.mark.timeout(10)
     def test_get_and_download(self, bot, animation):
diff --git a/tests/test_audio.py b/tests/test_audio.py
--- a/tests/test_audio.py
+++ b/tests/test_audio.py
@@ -108,6 +108,16 @@ def test_send_all_args(self, bot, chat_id, audio_file, thumb_file):
         assert message.audio.thumb.width == self.thumb_width
         assert message.audio.thumb.height == self.thumb_height
 
+    @flaky(3, 1)
+    @pytest.mark.timeout(10)
+    def test_send_audio_custom_filename(self, bot, chat_id, audio_file, monkeypatch):
+        def make_assertion(url, data, **kwargs):
+            return data['audio'].filename == 'custom_filename'
+
+        monkeypatch.setattr(bot.request, 'post', make_assertion)
+
+        assert bot.send_audio(chat_id, audio_file, filename='custom_filename')
+
     @flaky(3, 1)
     @pytest.mark.timeout(10)
     def test_get_and_download(self, bot, audio):
diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py
--- a/tests/test_inputmedia.py
+++ b/tests/test_inputmedia.py
@@ -437,6 +437,34 @@ def test_send_media_group_all_args(self, bot, chat_id, media_group):
             mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages
         )
 
+    @flaky(3, 1)
+    @pytest.mark.timeout(10)
+    def test_send_media_group_custom_filename(
+        self,
+        bot,
+        chat_id,
+        photo_file,  # noqa: F811
+        animation_file,  # noqa: F811
+        audio_file,  # noqa: F811
+        video_file,  # noqa: F811
+        monkeypatch,
+    ):
+        def make_assertion(url, data, **kwargs):
+            result = all(im.media.filename == 'custom_filename' for im in data['media'])
+            # We are a bit hacky here b/c Bot.send_media_group expects a list of Message-dicts
+            return [Message(0, None, None, text=result).to_dict()]
+
+        monkeypatch.setattr(bot.request, 'post', make_assertion)
+
+        media = [
+            InputMediaAnimation(animation_file, filename='custom_filename'),
+            InputMediaAudio(audio_file, filename='custom_filename'),
+            InputMediaPhoto(photo_file, filename='custom_filename'),
+            InputMediaVideo(video_file, filename='custom_filename'),
+        ]
+
+        assert bot.send_media_group(chat_id, media)[0].text is True
+
     def test_send_media_group_with_thumbs(
         self, bot, chat_id, video_file, photo_file, monkeypatch  # noqa: F811
     ):
diff --git a/tests/test_official.py b/tests/test_official.py
--- a/tests/test_official.py
+++ b/tests/test_official.py
@@ -76,8 +76,19 @@ def check_method(h4):
     ignored = IGNORED_PARAMETERS.copy()
     if name == 'getUpdates':
         ignored -= {'timeout'}  # Has it's own timeout parameter that we do wanna check for
-    elif name == 'sendDocument':
-        ignored |= {'filename'}  # Undocumented
+    elif name in (
+        f'send{media_type}'
+        for media_type in [
+            'Animation',
+            'Audio',
+            'Document',
+            'Photo',
+            'Video',
+            'VideoNote',
+            'Voice',
+        ]
+    ):
+        ignored |= {'filename'}  # Convenience parameter
     elif name == 'setGameScore':
         ignored |= {'edit_message'}  # TODO: Now deprecated, so no longer in telegrams docs
     elif name == 'sendContact':
@@ -131,8 +142,8 @@ def check_object(h4):
         ignored |= {'credentials'}
     elif name == 'PassportElementError':
         ignored |= {'message', 'type', 'source'}
-    elif name == 'Message':
-        ignored |= {'default_quote'}
+    elif name.startswith('InputMedia'):
+        ignored |= {'filename'}  # Convenience parameter
 
     assert (sig.parameters.keys() ^ checked) - ignored == set()
 
diff --git a/tests/test_photo.py b/tests/test_photo.py
--- a/tests/test_photo.py
+++ b/tests/test_photo.py
@@ -115,6 +115,16 @@ def test_send_photo_all_args(self, bot, chat_id, photo_file, thumb, photo):
 
         assert message.caption == TestPhoto.caption.replace('*', '')
 
+    @flaky(3, 1)
+    @pytest.mark.timeout(10)
+    def test_send_photo_custom_filename(self, bot, chat_id, photo_file, monkeypatch):
+        def make_assertion(url, data, **kwargs):
+            return data['photo'].filename == 'custom_filename'
+
+        monkeypatch.setattr(bot.request, 'post', make_assertion)
+
+        assert bot.send_photo(chat_id, photo_file, filename='custom_filename')
+
     @flaky(3, 1)
     @pytest.mark.timeout(10)
     def test_send_photo_parse_mode_markdown(self, bot, chat_id, photo_file, thumb, photo):
diff --git a/tests/test_video.py b/tests/test_video.py
--- a/tests/test_video.py
+++ b/tests/test_video.py
@@ -114,6 +114,16 @@ def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file):
 
         assert message.video.file_name == self.file_name
 
+    @flaky(3, 1)
+    @pytest.mark.timeout(10)
+    def test_send_video_custom_filename(self, bot, chat_id, video_file, monkeypatch):
+        def make_assertion(url, data, **kwargs):
+            return data['video'].filename == 'custom_filename'
+
+        monkeypatch.setattr(bot.request, 'post', make_assertion)
+
+        assert bot.send_video(chat_id, video_file, filename='custom_filename')
+
     @flaky(3, 1)
     @pytest.mark.timeout(10)
     def test_get_and_download(self, bot, video):
diff --git a/tests/test_videonote.py b/tests/test_videonote.py
--- a/tests/test_videonote.py
+++ b/tests/test_videonote.py
@@ -96,6 +96,16 @@ def test_send_all_args(self, bot, chat_id, video_note_file, video_note, thumb_fi
         assert message.video_note.thumb.width == self.thumb_width
         assert message.video_note.thumb.height == self.thumb_height
 
+    @flaky(3, 1)
+    @pytest.mark.timeout(10)
+    def test_send_video_note_custom_filename(self, bot, chat_id, video_note_file, monkeypatch):
+        def make_assertion(url, data, **kwargs):
+            return data['video_note'].filename == 'custom_filename'
+
+        monkeypatch.setattr(bot.request, 'post', make_assertion)
+
+        assert bot.send_video_note(chat_id, video_note_file, filename='custom_filename')
+
     @flaky(3, 1)
     @pytest.mark.timeout(10)
     def test_get_and_download(self, bot, video_note):
diff --git a/tests/test_voice.py b/tests/test_voice.py
--- a/tests/test_voice.py
+++ b/tests/test_voice.py
@@ -86,6 +86,16 @@ def test_send_all_args(self, bot, chat_id, voice_file, voice):
         assert message.voice.file_size == voice.file_size
         assert message.caption == self.caption.replace('*', '')
 
+    @flaky(3, 1)
+    @pytest.mark.timeout(10)
+    def test_send_voice_custom_filename(self, bot, chat_id, voice_file, monkeypatch):
+        def make_assertion(url, data, **kwargs):
+            return data['voice'].filename == 'custom_filename'
+
+        monkeypatch.setattr(bot.request, 'post', make_assertion)
+
+        assert bot.send_voice(chat_id, voice_file, filename='custom_filename')
+
     @flaky(3, 1)
     @pytest.mark.timeout(10)
     def test_get_and_download(self, bot, voice):