Explicit local_mode Setting (#3154)

This commit is contained in:
Bibo-Joshi 2022-09-19 22:31:23 +02:00 committed by GitHub
parent aed8e68fca
commit fdfbcdf51e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 801 additions and 485 deletions

View file

@ -64,6 +64,9 @@ source_suffix = ".rst"
# The master toctree document. # The master toctree document.
master_doc = "index" master_doc = "index"
# Global substitutions
rst_prolog = (Path.cwd() / "../substitutions/global.rst").read_text(encoding="utf-8")
# -- Extension settings ------------------------------------------------ # -- Extension settings ------------------------------------------------
napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_examples = True

View file

@ -304,6 +304,8 @@
- The first name of the bot - The first name of the bot
* - :attr:`~telegram.Bot.last_name` * - :attr:`~telegram.Bot.last_name`
- The last name of the bot - The last name of the bot
* - :attr:`~telegram.Bot.local_mode`
- Whether the bot is running in local mode
* - :attr:`~telegram.Bot.username` * - :attr:`~telegram.Bot.username`
- The username of the bot, without leading ``@`` - The username of the bot, without leading ``@``
* - :attr:`~telegram.Bot.link` * - :attr:`~telegram.Bot.link`

View file

@ -0,0 +1,17 @@
.. |uploadinput| replace:: To upload a file, you can either pass a :term:`file object` (e.g. ``open("filename", "rb")``), the file contents as bytes or the path of the file (as string or :class:`pathlib.Path` object). In the latter case, the file contents will either be read as bytes or the file path will be passed to Telegram, depending on the :paramref:`~telegram.Bot.local_mode` setting.
.. |uploadinputnopath| replace:: To upload a file, you can either pass a :term:`file object` (e.g. ``open("filename", "rb")``) or the file contents as bytes. If the bot is running in :paramref:`~telegram.Bot.local_mode`, passing the path of the file (as string or :class:`pathlib.Path` object) is supported as well.
.. |fileinputbase| replace:: Pass a ``file_id`` as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one.
.. |fileinput| replace:: |fileinputbase| |uploadinput|
.. |fileinputnopath| replace:: |fileinputbase| |uploadinputnopath|
.. |thumbdocstringbase| replace:: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file.
.. |thumbdocstring| replace:: |thumbdocstringbase| |uploadinput|
.. |thumbdocstringnopath| replace:: |thumbdocstringbase| |uploadinputnopath|
.. |editreplymarkup| replace:: It is currently only possible to edit messages without :attr:`telegram.Message.reply_markup` or with inline keyboards.

View file

@ -99,6 +99,7 @@ from telegram.request._requestparameter import RequestParameter
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram import ( from telegram import (
InlineQueryResult, InlineQueryResult,
InputFile,
InputMediaAudio, InputMediaAudio,
InputMediaDocument, InputMediaDocument,
InputMediaPhoto, InputMediaPhoto,
@ -160,6 +161,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
``location``, ``filename``, ``venue``, ``contact``, ``location``, ``filename``, ``venue``, ``contact``,
``{read, write, connect, pool}_timeout``, ``api_kwargs``. Use a named argument for those, ``{read, write, connect, pool}_timeout``, ``api_kwargs``. Use a named argument for those,
and notice that some positional arguments changed position as a result. and notice that some positional arguments changed position as a result.
* For uploading files, file paths are now always accepted. If :paramref:`local_mode` is
:obj:`False`, the file contents will be read in binary mode and uploaded. Otherwise,
the file path will be passed in the
`file URI scheme <https://en.wikipedia.org/wiki/File_URI_scheme>`_.
Args: Args:
token (:obj:`str`): Bot's unique authentication token. token (:obj:`str`): Bot's unique authentication token.
@ -175,6 +180,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`telegram.request.HTTPXRequest` will be used. :class:`telegram.request.HTTPXRequest` will be used.
private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data. private_key (:obj:`bytes`, optional): Private key for decryption of telegram passport data.
private_key_password (:obj:`bytes`, optional): Password for above private key. private_key_password (:obj:`bytes`, optional): Password for above private key.
local_mode (:obj:`bool`, optional): Set to :obj:`True`, if the :paramref:`base_url` is
the URI of a `Local Bot API Server <https://core.telegram.org/bots/api#using-a-local\
-bot-api-server>`_ that runs with the ``--local`` flag. Currently, the only effect of
this is that files are uploaded using their local path in the
`file URI scheme <https://en.wikipedia.org/wiki/File_URI_scheme>`_.
Defaults to :obj:`False`.
.. versionadded:: 20.0.
.. include:: inclusions/bot_methods.rst .. include:: inclusions/bot_methods.rst
@ -189,6 +202,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"_request", "_request",
"_logger", "_logger",
"_initialized", "_initialized",
"_local_mode",
) )
def __init__( def __init__(
@ -200,6 +214,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
get_updates_request: BaseRequest = None, get_updates_request: BaseRequest = None,
private_key: bytes = None, private_key: bytes = None,
private_key_password: bytes = None, private_key_password: bytes = None,
local_mode: bool = False,
): ):
if not token: if not token:
raise InvalidToken("You must pass the token you received from https://t.me/Botfather!") raise InvalidToken("You must pass the token you received from https://t.me/Botfather!")
@ -207,6 +222,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
self._base_url = base_url + self._token self._base_url = base_url + self._token
self._base_file_url = base_file_url + self._token self._base_file_url = base_file_url + self._token
self._local_mode = local_mode
self._bot_user: Optional[User] = None self._bot_user: Optional[User] = None
self._private_key = None self._private_key = None
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
@ -253,6 +269,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
return self._base_file_url return self._base_file_url
@property
def local_mode(self) -> bool:
""":obj:`bool`: Whether this bot is running in local mode.
.. versionadded:: 20.0
"""
return self._local_mode
# Proper type hints are difficult because: # Proper type hints are difficult because:
# 1. cryptography doesn't have a nice base class, so it would get lengthy # 1. cryptography doesn't have a nice base class, so it would get lengthy
# 2. we can't import cryptography if it's not installed # 2. we can't import cryptography if it's not installed
@ -283,6 +307,21 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
return decorator return decorator
def _parse_file_input(
self,
file_input: Union[FileInput, "TelegramObject"],
tg_type: Type["TelegramObject"] = None,
filename: str = None,
attach: bool = False,
) -> Union[str, "InputFile", Any]:
return parse_file_input(
file_input=file_input,
tg_type=tg_type,
filename=filename,
attach=attach,
local_mode=self._local_mode,
)
def _insert_defaults(self, data: Dict[str, object]) -> None: # skipcq: PYL-R0201 def _insert_defaults(self, data: Dict[str, object]) -> None: # skipcq: PYL-R0201
"""This method is here to make ext.Defaults work. Because we need to be able to tell """This method is here to make ext.Defaults work. Because we need to be able to tell
e.g. `send_message(chat_id, text)` from `send_message(chat_id, text, parse_mode=None)`, the e.g. `send_message(chat_id, text)` from `send_message(chat_id, text, parse_mode=None)`, the
@ -888,10 +927,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> Message: ) -> Message:
"""Use this method to send photos. """Use this method to send photos.
Note:
The photo argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
.. seealso:: :attr:`telegram.Message.reply_photo`, :attr:`telegram.Chat.send_photo`, .. seealso:: :attr:`telegram.Message.reply_photo`, :attr:`telegram.Chat.send_photo`,
:attr:`telegram.User.send_photo` :attr:`telegram.User.send_photo`
@ -900,13 +935,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
photo (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ photo (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.PhotoSize`): Photo to send. :class:`telegram.PhotoSize`): Photo to send.
Pass a file_id as String to send a photo that exists on the Telegram servers |fileinput|
(recommended), pass an HTTP URL as a String for Telegram to get a photo from the Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
Internet, or upload a new photo using multipart/form-data. Lastly you can pass
an existing :class:`telegram.PhotoSize` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
caption (:obj:`str`, optional): Photo caption (may also be used when resending photos caption (:obj:`str`, optional): Photo caption (may also be used when resending photos
by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH`
characters after entities parsing. characters after entities parsing.
@ -961,7 +998,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
data: JSONDict = { data: JSONDict = {
"chat_id": chat_id, "chat_id": chat_id,
"photo": parse_file_input(photo, PhotoSize, filename=filename), "photo": self._parse_file_input(photo, PhotoSize, filename=filename),
"parse_mode": parse_mode, "parse_mode": parse_mode,
} }
@ -1021,10 +1058,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
For sending voice messages, use the :meth:`send_voice` method instead. For sending voice messages, use the :meth:`send_voice` method instead.
Note:
The audio argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
.. seealso:: :attr:`telegram.Message.reply_audio`, :attr:`telegram.Chat.send_audio`, .. seealso:: :attr:`telegram.Message.reply_audio`, :attr:`telegram.Chat.send_audio`,
:attr:`telegram.User.send_audio` :attr:`telegram.User.send_audio`
@ -1033,13 +1066,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
audio (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ audio (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Audio`): Audio file to send. :class:`telegram.Audio`): Audio file to send.
Pass a file_id as String to send an audio file that exists on the Telegram servers |fileinput|
(recommended), pass an HTTP URL as a String for Telegram to get an audio file from Lastly you can pass an existing :class:`telegram.Audio` object to send.
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Audio` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
caption (:obj:`str`, optional): Audio caption, caption (:obj:`str`, optional): Audio caption,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. entities parsing.
@ -1067,16 +1102,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user. keyboard, instructions to remove reply keyboard or to force a reply from the user.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
of the file sent; can be ignored if optional): |thumbdocstring|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
Keyword Args: Keyword Args:
filename (:obj:`str`, optional): Custom file name for the audio, when uploading a 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 new file. Convenience parameter, useful e.g. when sending files generated by the
@ -1106,7 +1141,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
data: JSONDict = { data: JSONDict = {
"chat_id": chat_id, "chat_id": chat_id,
"audio": parse_file_input(audio, Audio, filename=filename), "audio": self._parse_file_input(audio, Audio, filename=filename),
"parse_mode": parse_mode, "parse_mode": parse_mode,
} }
@ -1122,7 +1157,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
if caption_entities: if caption_entities:
data["caption_entities"] = caption_entities data["caption_entities"] = caption_entities
if thumb: if thumb:
data["thumb"] = parse_file_input(thumb, attach=True) data["thumb"] = self._parse_file_input(thumb, attach=True)
return await self._send_message( # type: ignore[return-value] return await self._send_message( # type: ignore[return-value]
"sendAudio", "sendAudio",
@ -1169,12 +1204,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, this limit may be :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size, this limit may be
changed in the future. changed in the future.
Note:
* The document argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``.
* Sending by URL will currently only work ``GIF``, ``PDF`` & ``ZIP`` files.
.. seealso:: :attr:`telegram.Message.reply_document`, :attr:`telegram.Chat.send_document`, .. seealso:: :attr:`telegram.Message.reply_document`, :attr:`telegram.Chat.send_document`,
:attr:`telegram.User.send_document` :attr:`telegram.User.send_document`
@ -1183,13 +1212,18 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
document (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ document (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Document`): File to send. :class:`telegram.Document`): File to send.
Pass a file_id as String to send a file that exists on the Telegram servers |fileinput|
(recommended), pass an HTTP URL as a String for Telegram to get a file from the Lastly you can pass an existing :class:`telegram.Document` object to send.
Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Document` object to send. Note:
Sending by URL will currently only work ``GIF``, ``PDF`` & ``ZIP`` files.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
caption (:obj:`str`, optional): Document caption (may also be used when resending caption (:obj:`str`, optional): Document caption (may also be used when resending
documents by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` documents by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH`
characters after entities parsing. characters after entities parsing.
@ -1216,16 +1250,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user. keyboard, instructions to remove reply keyboard or to force a reply from the user.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
of the file sent; can be ignored if optional): |thumbdocstring|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
Keyword Args: Keyword Args:
filename (:obj:`str`, optional): Custom file name for the document, when uploading a 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 new file. Convenience parameter, useful e.g. when sending files generated by the
@ -1253,7 +1287,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
data: JSONDict = { data: JSONDict = {
"chat_id": chat_id, "chat_id": chat_id,
"document": parse_file_input(document, Document, filename=filename), "document": self._parse_file_input(document, Document, filename=filename),
"parse_mode": parse_mode, "parse_mode": parse_mode,
} }
@ -1265,7 +1299,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
if disable_content_type_detection is not None: if disable_content_type_detection is not None:
data["disable_content_type_detection"] = disable_content_type_detection data["disable_content_type_detection"] = disable_content_type_detection
if thumb: if thumb:
data["thumb"] = parse_file_input(thumb, attach=True) data["thumb"] = self._parse_file_input(thumb, attach=True)
return await self._send_message( # type: ignore[return-value] return await self._send_message( # type: ignore[return-value]
"sendDocument", "sendDocument",
@ -1302,10 +1336,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers. Use this method to send static ``.WEBP``, animated ``.TGS``, or video ``.WEBM`` stickers.
Note:
The :paramref:`sticker` argument can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
.. seealso:: :attr:`telegram.Message.reply_sticker`, :attr:`telegram.Chat.send_sticker`, .. seealso:: :attr:`telegram.Message.reply_sticker`, :attr:`telegram.Chat.send_sticker`,
:attr:`telegram.User.send_sticker` :attr:`telegram.User.send_sticker`
@ -1314,13 +1344,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Sticker`): Sticker to send. :class:`telegram.Sticker`): Sticker to send.
Pass a file_id as String to send a file that exists on the Telegram servers |fileinput|
(recommended), pass an HTTP URL as a String for Telegram to get a .webp file from Lastly you can pass an existing :class:`telegram.Sticker` object to send.
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Sticker` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound. receive a notification with no sound.
protect_content (:obj:`bool`, optional): Protects the contents of the sent message from protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
@ -1359,7 +1391,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"chat_id": chat_id, "sticker": parse_file_input(sticker, Sticker)} data: JSONDict = {"chat_id": chat_id, "sticker": self._parse_file_input(sticker, Sticker)}
return await self._send_message( # type: ignore[return-value] return await self._send_message( # type: ignore[return-value]
"sendSticker", "sendSticker",
data, data,
@ -1410,11 +1442,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
changed in the future. changed in the future.
Note: Note:
* The :paramref:`video` argument can be either a file_id, an URL or a file from disk :paramref:`thumb` will be ignored for small video files, for which Telegram can
``open(filename, 'rb')`` easily generate thumbnails. However, this behaviour is undocumented and might be
* :paramref:`thumb` will be ignored for small video files, for which Telegram can changed by Telegram.
easily generate thumbnails. However, this behaviour is undocumented and might be
changed by Telegram.
.. seealso:: :attr:`telegram.Message.reply_video`, :attr:`telegram.Chat.send_video`, .. seealso:: :attr:`telegram.Message.reply_video`, :attr:`telegram.Chat.send_video`,
:attr:`telegram.User.send_video` :attr:`telegram.User.send_video`
@ -1424,13 +1454,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
video (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ video (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Video`): Video file to send. :class:`telegram.Video`): Video file to send.
Pass a file_id as String to send an video file that exists on the Telegram servers |fileinput|
(recommended), pass an HTTP URL as a String for Telegram to get an video file from Lastly you can pass an existing :class:`telegram.Video` object to send.
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Video` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
duration (:obj:`int`, optional): Duration of sent video in seconds. duration (:obj:`int`, optional): Duration of sent video in seconds.
width (:obj:`int`, optional): Video width. width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height. height (:obj:`int`, optional): Video height.
@ -1460,16 +1492,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user. keyboard, instructions to remove reply keyboard or to force a reply from the user.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
of the file sent; can be ignored if optional): |thumbdocstring|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
Keyword Args: Keyword Args:
filename (:obj:`str`, optional): Custom file name for the video, when uploading a 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 new file. Convenience parameter, useful e.g. when sending files generated by the
@ -1499,7 +1531,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
data: JSONDict = { data: JSONDict = {
"chat_id": chat_id, "chat_id": chat_id,
"video": parse_file_input(video, Video, filename=filename), "video": self._parse_file_input(video, Video, filename=filename),
"parse_mode": parse_mode, "parse_mode": parse_mode,
} }
@ -1516,7 +1548,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
if height: if height:
data["height"] = height data["height"] = height
if thumb: if thumb:
data["thumb"] = parse_file_input(thumb, attach=True) data["thumb"] = self._parse_file_input(thumb, attach=True)
return await self._send_message( # type: ignore[return-value] return await self._send_message( # type: ignore[return-value]
"sendVideo", "sendVideo",
@ -1559,11 +1591,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Use this method to send video messages. Use this method to send video messages.
Note: Note:
* The :paramref:`video_note` argument can be either a file_id or a file from disk :paramref:`thumb` will be ignored for small video files, for which Telegram can
``open(filename, 'rb')`` easily generate thumbnails. However, this behaviour is undocumented and might be
* :paramref:`thumb` will be ignored for small video files, for which Telegram can changed by Telegram.
easily generate thumbnails. However, this behaviour is undocumented and might be
changed by Telegram.
.. seealso:: :attr:`telegram.Message.reply_video_note`, .. seealso:: :attr:`telegram.Message.reply_video_note`,
:attr:`telegram.Chat.send_video_note`, :attr:`telegram.Chat.send_video_note`,
@ -1573,14 +1603,19 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
video_note (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ video_note (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.VideoNote`): Video note :class:`telegram.VideoNote`): Video note to send.
to send. Pass a file_id as String to send a video note that exists on the Telegram Pass a file_id as String to send a video note that exists on the Telegram
servers (recommended) or upload a new video using multipart/form-data. Or you can servers (recommended) or upload a new video using multipart/form-data.
pass an existing :class:`telegram.VideoNote` object to send. Sending video notes by |uploadinput|
a URL is currently unsupported. Lastly you can pass an existing :class:`telegram.VideoNote` object to send.
Sending video notes by a URL is currently unsupported.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
duration (:obj:`int`, optional): Duration of sent video in seconds. duration (:obj:`int`, optional): Duration of sent video in seconds.
length (:obj:`int`, optional): Video width and height, i.e. diameter of the video length (:obj:`int`, optional): Video width and height, i.e. diameter of the video
message. message.
@ -1599,16 +1634,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user. keyboard, instructions to remove reply keyboard or to force a reply from the user.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
of the file sent; can be ignored if optional): |thumbdocstring|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
Keyword Args: Keyword Args:
filename (:obj:`str`, optional): Custom file name for the video note, when uploading a 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 new file. Convenience parameter, useful e.g. when sending files generated by the
@ -1638,7 +1673,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
data: JSONDict = { data: JSONDict = {
"chat_id": chat_id, "chat_id": chat_id,
"video_note": parse_file_input(video_note, VideoNote, filename=filename), "video_note": self._parse_file_input(video_note, VideoNote, filename=filename),
} }
if duration is not None: if duration is not None:
@ -1646,7 +1681,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
if length is not None: if length is not None:
data["length"] = length data["length"] = length
if thumb: if thumb:
data["thumb"] = parse_file_input(thumb, attach=True) data["thumb"] = self._parse_file_input(thumb, attach=True)
return await self._send_message( # type: ignore[return-value] return await self._send_message( # type: ignore[return-value]
"sendVideoNote", "sendVideoNote",
@ -1696,7 +1731,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Note: Note:
:paramref:`thumb` will be ignored for small files, for which Telegram can easily :paramref:`thumb` will be ignored for small files, for which Telegram can easily
generate thumb nails. However, this behaviour is undocumented and might be changed generate thumbnails. However, this behaviour is undocumented and might be changed
by Telegram. by Telegram.
.. seealso:: :attr:`telegram.Message.reply_animation`, .. seealso:: :attr:`telegram.Message.reply_animation`,
@ -1707,10 +1742,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
animation (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ animation (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Animation`): Animation to :class:`telegram.Animation`): Animation to send.
send. Pass a file_id as String to send an animation that exists on the Telegram |fileinput|
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. Lastly you can pass an existing :class:`telegram.Animation` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -1718,15 +1751,16 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
duration (:obj:`int`, optional): Duration of sent animation in seconds. duration (:obj:`int`, optional): Duration of sent animation in seconds.
width (:obj:`int`, optional): Animation width. width (:obj:`int`, optional): Animation width.
height (:obj:`int`, optional): Animation height. height (:obj:`int`, optional): Animation height.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
of the file sent; can be ignored if optional): |thumbdocstring|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
caption (:obj:`str`, optional): Animation caption (may also be used when resending caption (:obj:`str`, optional): Animation caption (may also be used when resending
animations by file_id), animations by file_id),
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
@ -1782,7 +1816,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
data: JSONDict = { data: JSONDict = {
"chat_id": chat_id, "chat_id": chat_id,
"animation": parse_file_input(animation, Animation, filename=filename), "animation": self._parse_file_input(animation, Animation, filename=filename),
"parse_mode": parse_mode, "parse_mode": parse_mode,
} }
@ -1793,7 +1827,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
if height: if height:
data["height"] = height data["height"] = height
if thumb: if thumb:
data["thumb"] = parse_file_input(thumb, attach=True) data["thumb"] = self._parse_file_input(thumb, attach=True)
if caption: if caption:
data["caption"] = caption data["caption"] = caption
if caption_entities: if caption_entities:
@ -1844,11 +1878,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
in size, this limit may be changed in the future. in size, this limit may be changed in the future.
Note: Note:
* The :paramref:`voice` argument can be either a file_id, an URL or a file from disk To use this method, the file must have the type :mimetype:`audio/ogg` and be no more
``open(filename, 'rb')``. than ``1MB`` in size. ``1-20MB`` voice notes will be sent as files.
* To use this method, the file must have the type :mimetype:`audio/ogg` and be no more
than ``1MB`` in size. ``1-20MB`` voice notes will be sent as files.
.. seealso:: :attr:`telegram.Message.reply_voice`, :attr:`telegram.Chat.send_voice`, .. seealso:: :attr:`telegram.Message.reply_voice`, :attr:`telegram.Chat.send_voice`,
:attr:`telegram.User.send_voice` :attr:`telegram.User.send_voice`
@ -1858,13 +1889,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
voice (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ voice (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Voice`): Voice file to send. :class:`telegram.Voice`): Voice file to send.
Pass a file_id as String to send an voice file that exists on the Telegram servers |fileinput|
(recommended), pass an HTTP URL as a String for Telegram to get an voice file from Lastly you can pass an existing :class:`telegram.Voice` object to send.
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Voice` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
caption (:obj:`str`, optional): Voice message caption, caption (:obj:`str`, optional): Voice message caption,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. entities parsing.
@ -1920,7 +1953,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
""" """
data: JSONDict = { data: JSONDict = {
"chat_id": chat_id, "chat_id": chat_id,
"voice": parse_file_input(voice, Voice, filename=filename), "voice": self._parse_file_input(voice, Voice, filename=filename),
"parse_mode": parse_mode, "parse_mode": parse_mode,
} }
@ -3502,8 +3535,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Use this method to edit text and game messages. Use this method to edit text and game messages.
Note: Note:
It is currently only possible to edit messages without |editreplymarkup|.
:attr:`telegram.Message.reply_markup` or with inline keyboards.
.. seealso:: :attr:`telegram.Message.edit_text`, .. seealso:: :attr:`telegram.Message.edit_text`,
:attr:`telegram.CallbackQuery.edit_message_text` :attr:`telegram.CallbackQuery.edit_message_text`
@ -3601,8 +3633,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Use this method to edit captions of messages. Use this method to edit captions of messages.
Note: Note:
It is currently only possible to edit messages without |editreplymarkup|
:attr:`telegram.Message.reply_markup` or with inline keyboards
.. seealso:: :attr:`telegram.Message.edit_caption`, .. seealso:: :attr:`telegram.Message.edit_caption`,
:attr:`telegram.CallbackQuery.edit_message_caption` :attr:`telegram.CallbackQuery.edit_message_caption`
@ -3698,8 +3729,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:attr:`~telegram.File.file_id` or specify a URL. :attr:`~telegram.File.file_id` or specify a URL.
Note: Note:
It is currently only possible to edit messages without |editreplymarkup|
:attr:`telegram.Message.reply_markup` or with inline keyboards
.. seealso:: :attr:`telegram.Message.edit_media`, .. seealso:: :attr:`telegram.Message.edit_media`,
:attr:`telegram.CallbackQuery.edit_message_media` :attr:`telegram.CallbackQuery.edit_message_media`
@ -3779,8 +3809,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
(for inline bots). (for inline bots).
Note: Note:
It is currently only possible to edit messages without |editreplymarkup|
:attr:`telegram.Message.reply_markup` or with inline keyboards
.. seealso:: :attr:`telegram.Message.edit_reply_markup`, .. seealso:: :attr:`telegram.Message.edit_reply_markup`,
:attr:`telegram.CallbackQuery.edit_message_reply_markup` :attr:`telegram.CallbackQuery.edit_message_reply_markup`
@ -3856,6 +3885,12 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) -> List[Update]: ) -> List[Update]:
"""Use this method to receive incoming updates using long polling. """Use this method to receive incoming updates using long polling.
Note:
1. This method will not work if an outgoing webhook is set up.
2. In order to avoid getting duplicate updates, recalculate offset after each
server response.
3. To take full advantage of this library take a look at :class:`telegram.ext.Updater`
Args: Args:
offset (:obj:`int`, optional): Identifier of the first update to be returned. Must be offset (:obj:`int`, optional): Identifier of the first update to be returned. Must be
greater by one than the highest among the identifiers of previously received greater by one than the highest among the identifiers of previously received
@ -3895,12 +3930,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API. Telegram API.
Note:
1. This method will not work if an outgoing webhook is set up.
2. In order to avoid getting duplicate updates, recalculate offset after each
server response.
3. To take full advantage of this library take a look at :class:`telegram.ext.Updater`
Returns: Returns:
List[:class:`telegram.Update`] List[:class:`telegram.Update`]
@ -3970,15 +3999,25 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
``X-Telegram-Bot-Api-Secret-Token`` with the secret token as content. ``X-Telegram-Bot-Api-Secret-Token`` with the secret token as content.
Note: Note:
The certificate argument should be a file from disk ``open(filename, 'rb')``. 1. You will not be able to receive updates using :meth:`get_updates` for long as an
outgoing webhook is set up.
2. To use a self-signed certificate, you need to upload your public key certificate
using :paramref:`certificate` parameter. Please upload as
:class:`~telegram.InputFile`, sending a String will not work.
3. Ports currently supported for Webhooks:
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`.
If you're having any trouble setting up webhooks, please check out this `guide to
Webhooks`_.
Args: Args:
url (:obj:`str`): HTTPS url to send updates to. Use an empty string to remove webhook url (:obj:`str`): HTTPS url to send updates to. Use an empty string to remove webhook
integration. integration.
certificate (:term:`file object`): Upload your public key certificate so that the root certificate (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`):
certificate in use can be checked. See our self-signed guide for details. Upload your public key certificate so that the root
(https://github.com/python-telegram-bot/python-telegram-bot/wiki/Webhooks#\ certificate in use can be checked. See our `self-signed guide <https://github.com/\
creating-a-self-signed-certificate-using-openssl) python-telegram-bot/python-telegram-bot/wiki/Webhooks#creating-a-self-signed-\
certificate-using-openssl>`_ for details. |uploadinputnopath|
ip_address (:obj:`str`, optional): The fixed IP address which will be used to send ip_address (:obj:`str`, optional): The fixed IP address which will be used to send
webhook requests instead of the IP address resolved through DNS. webhook requests instead of the IP address resolved through DNS.
max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS max_connections (:obj:`int`, optional): Maximum allowed number of simultaneous HTTPS
@ -4021,18 +4060,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API. Telegram API.
Note:
1. You will not be able to receive updates using :meth:`get_updates` for long as an
outgoing webhook is set up.
2. To use a self-signed certificate, you need to upload your public key certificate
using certificate parameter. Please upload as InputFile, sending a String will not
work.
3. Ports currently supported for Webhooks:
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS`.
If you're having any trouble setting up webhooks, please check out this `guide to
Webhooks`_.
Returns: Returns:
:obj:`bool` On success, :obj:`True` is returned. :obj:`bool` On success, :obj:`True` is returned.
@ -4045,7 +4072,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
data: JSONDict = {"url": url} data: JSONDict = {"url": url}
if certificate: if certificate:
data["certificate"] = parse_file_input(certificate) data["certificate"] = self._parse_file_input(certificate)
if max_connections is not None: if max_connections is not None:
data["max_connections"] = max_connections data["max_connections"] = max_connections
if allowed_updates is not None: if allowed_updates is not None:
@ -5492,6 +5519,13 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
link is revoked. The bot must be an administrator in the chat for this to work and must link is revoked. The bot must be an administrator in the chat for this to work and must
have the appropriate admin rights. have the appropriate admin rights.
Note:
Each administrator in a chat generates their own invite links. Bots can't use invite
links generated by other administrators. If you want your bot to work with invite
links, it will need to generate its own link using :meth:`export_chat_invite_link` or
by calling the :meth:`get_chat` method. If your bot needs to generate a new primary
invite link replacing its previous one, use :attr:`export_chat_invite_link` again.
.. seealso:: :attr:`telegram.Chat.export_invite_link` .. seealso:: :attr:`telegram.Chat.export_invite_link`
Args: Args:
@ -5514,13 +5548,6 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API. Telegram API.
Note:
Each administrator in a chat generates their own invite links. Bots can't use invite
links generated by other administrators. If you want your bot to work with invite
links, it will need to generate its own link using :meth:`export_chat_invite_link` or
by calling the :meth:`get_chat` method. If your bot needs to generate a new primary
invite link replacing its previous one, use :attr:`export_chat_invite_link` again.
Returns: Returns:
:obj:`str`: New invite link on success. :obj:`str`: New invite link on success.
@ -5955,10 +5982,15 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target channel (in the format ``@channelusername``). of the target channel (in the format ``@channelusername``).
photo (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): New chat photo. photo (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): New chat photo.
|uploadinput|
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
Keyword Args: Keyword Args:
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
@ -5982,7 +6014,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"chat_id": chat_id, "photo": parse_file_input(photo)} data: JSONDict = {"chat_id": chat_id, "photo": self._parse_file_input(photo)}
result = await self._post( result = await self._post(
"setChatPhoto", "setChatPhoto",
data, data,
@ -6488,19 +6520,20 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:meth:`create_new_sticker_set` and :meth:`add_sticker_to_set` methods (can be used multiple :meth:`create_new_sticker_set` and :meth:`add_sticker_to_set` methods (can be used multiple
times). times).
Note:
The :paramref:`png_sticker` argument can be either a file_id, an URL or a file from
disk ``open(filename, 'rb')``
Args: Args:
user_id (:obj:`int`): User identifier of sticker file owner. user_id (:obj:`int`): User identifier of sticker file owner.
png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`):
**PNG** image with the sticker, must be up to 512 kilobytes in size, **PNG** image with the sticker, must be up to 512 kilobytes in size,
dimensions must not exceed 512px, and either width or height must be exactly 512px. dimensions must not exceed 512px, and either width or height must be exactly 512px.
|uploadinput|
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
Keyword Args: Keyword Args:
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to :paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
@ -6524,7 +6557,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:class:`telegram.error.TelegramError` :class:`telegram.error.TelegramError`
""" """
data: JSONDict = {"user_id": user_id, "png_sticker": parse_file_input(png_sticker)} data: JSONDict = {"user_id": user_id, "png_sticker": self._parse_file_input(png_sticker)}
result = await self._post( result = await self._post(
"uploadStickerFile", "uploadStickerFile",
data, data,
@ -6566,10 +6599,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
of the arguments had to be changed. Use keyword arguments to make sure that the of the arguments had to be changed. Use keyword arguments to make sure that the
arguments are passed correctly. arguments are passed correctly.
Note:
The :paramref:`png_sticker` and :paramref:`tgs_sticker` argument can be either a
file_id, an URL or a file from disk ``open(filename, 'rb')``
.. versionchanged:: 20.0 .. versionchanged:: 20.0
The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type`
instead. instead.
@ -6585,27 +6614,37 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \
optional): **PNG** image with the sticker, optional): **PNG** image with the sticker,
must be up to 512 kilobytes in size, dimensions must not exceed 512px, must be up to 512 kilobytes in size, dimensions must not exceed 512px,
and either width or height must be exactly 512px. Pass a file_id as a String to and either width or height must be exactly 512px.
send a file that already exists on the Telegram servers, pass an HTTP URL as a |fileinput|
String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
tgs_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ tgs_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \
optional): **TGS** animation with the sticker, uploaded using multipart/form-data. optional): **TGS** animation with the sticker. |uploadinput|
See https://core.telegram.org/stickers#animated-sticker-requirements for technical See https://core.telegram.org/stickers#animated-sticker-requirements for technical
requirements. requirements.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\
optional): **WEBM** video with the sticker, uploaded using multipart/form-data. optional): **WEBM** video with the sticker. |uploadinput|
See https://core.telegram.org/stickers#video-sticker-requirements for See https://core.telegram.org/stickers#video-sticker-requirements for
technical requirements. technical requirements.
.. versionadded:: 13.11 .. versionadded:: 13.11
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
emojis (:obj:`str`): One or more emoji corresponding to the sticker. emojis (:obj:`str`): One or more emoji corresponding to the sticker.
mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask
should be placed on faces. should be placed on faces.
@ -6642,11 +6681,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
data: JSONDict = {"user_id": user_id, "name": name, "title": title, "emojis": emojis} data: JSONDict = {"user_id": user_id, "name": name, "title": title, "emojis": emojis}
if png_sticker is not None: if png_sticker is not None:
data["png_sticker"] = parse_file_input(png_sticker) data["png_sticker"] = self._parse_file_input(png_sticker)
if tgs_sticker is not None: if tgs_sticker is not None:
data["tgs_sticker"] = parse_file_input(tgs_sticker) data["tgs_sticker"] = self._parse_file_input(tgs_sticker)
if webm_sticker is not None: if webm_sticker is not None:
data["webm_sticker"] = parse_file_input(webm_sticker) data["webm_sticker"] = self._parse_file_input(webm_sticker)
if mask_position is not None: if mask_position is not None:
data["mask_position"] = mask_position data["mask_position"] = mask_position
if sticker_type is not None: if sticker_type is not None:
@ -6693,10 +6732,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
of the arguments had to be changed. Use keyword arguments to make sure that the of the arguments had to be changed. Use keyword arguments to make sure that the
arguments are passed correctly. arguments are passed correctly.
Note:
The :paramref:`png_sticker` and :paramref:`tgs_sticker` argument can be either a
file_id, an URL or a file from disk ``open(filename, 'rb')``
Args: Args:
user_id (:obj:`int`): User identifier of created sticker set owner. user_id (:obj:`int`): User identifier of created sticker set owner.
@ -6704,26 +6739,36 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ png_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \
optional): **PNG** image with the sticker, optional): **PNG** image with the sticker,
must be up to 512 kilobytes in size, dimensions must not exceed 512px, must be up to 512 kilobytes in size, dimensions must not exceed 512px,
and either width or height must be exactly 512px. Pass a file_id as a String to and either width or height must be exactly 512px.
send a file that already exists on the Telegram servers, pass an HTTP URL as a |fileinput|
String for Telegram to get a file from the Internet, or upload a new one
using multipart/form-data.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
tgs_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \ tgs_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, \
optional): **TGS** animation with the sticker, uploaded using multipart/form-data. optional): **TGS** animation with the sticker. |uploadinput|
See https://core.telegram.org/stickers#animated-sticker-requirements for technical See https://core.telegram.org/stickers#animated-sticker-requirements for technical
requirements. requirements.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\ webm_sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`,\
optional): **WEBM** video with the sticker, uploaded using multipart/form-data. optional): **WEBM** video with the sticker. |uploadinput|
See https://core.telegram.org/stickers#video-sticker-requirements for See https://core.telegram.org/stickers#video-sticker-requirements for
technical requirements. technical requirements.
.. versionadded:: 13.11 .. versionadded:: 13.11
.. versionchanged:: 20.0
File paths as input is also accepted for bots *not* running in
:paramref:`~telegram.Bot.local_mode`.
emojis (:obj:`str`): One or more emoji corresponding to the sticker. emojis (:obj:`str`): One or more emoji corresponding to the sticker.
mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask
should be placed on faces. should be placed on faces.
@ -6754,11 +6799,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
data: JSONDict = {"user_id": user_id, "name": name, "emojis": emojis} data: JSONDict = {"user_id": user_id, "name": name, "emojis": emojis}
if png_sticker is not None: if png_sticker is not None:
data["png_sticker"] = parse_file_input(png_sticker) data["png_sticker"] = self._parse_file_input(png_sticker)
if tgs_sticker is not None: if tgs_sticker is not None:
data["tgs_sticker"] = parse_file_input(tgs_sticker) data["tgs_sticker"] = self._parse_file_input(tgs_sticker)
if webm_sticker is not None: if webm_sticker is not None:
data["webm_sticker"] = parse_file_input(webm_sticker) data["webm_sticker"] = self._parse_file_input(webm_sticker)
if mask_position is not None: if mask_position is not None:
data["mask_position"] = mask_position data["mask_position"] = mask_position
@ -6895,10 +6940,6 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
for animated sticker sets only. Video thumbnails can be set only for video sticker sets for animated sticker sets only. Video thumbnails can be set only for video sticker sets
only. only.
Note:
The :paramref:`thumb` can be either a file_id, an URL or a file from disk
``open(filename, 'rb')``
Args: Args:
name (:obj:`str`): Sticker set name name (:obj:`str`): Sticker set name
user_id (:obj:`int`): User identifier of created sticker set owner. user_id (:obj:`int`): User identifier of created sticker set owner.
@ -6910,9 +6951,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 sticker technical requirements, or a **WEBM** video with the thumbnail up to 32
kilobytes in size; see kilobytes in size; see
https://core.telegram.org/stickers#video-sticker-requirements for video sticker https://core.telegram.org/stickers#video-sticker-requirements for video sticker
technical requirements. Pass a file_id as a String to send a file that technical requirements.
already exists on the Telegram servers, pass an HTTP URL as a String for Telegram |fileinput|
to get a file from the Internet, or upload a new one using multipart/form-data.
Animated sticker set thumbnails can't be uploaded via HTTP URL. Animated sticker set thumbnails can't be uploaded via HTTP URL.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
@ -6943,7 +6983,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
""" """
data: JSONDict = {"name": name, "user_id": user_id} data: JSONDict = {"name": name, "user_id": user_id}
if thumb is not None: if thumb is not None:
data["thumb"] = parse_file_input(thumb) data["thumb"] = self._parse_file_input(thumb)
result = await self._post( result = await self._post(
"setStickerSetThumb", "setStickerSetThumb",

View file

@ -20,10 +20,10 @@
import logging import logging
import mimetypes import mimetypes
from pathlib import Path
from typing import IO, Optional, Union from typing import IO, Optional, Union
from uuid import uuid4 from uuid import uuid4
from telegram._utils.files import load_file
from telegram._utils.types import FieldTuple from telegram._utils.types import FieldTuple
_DEFAULT_MIME_TYPE = "application/octet-stream" _DEFAULT_MIME_TYPE = "application/octet-stream"
@ -75,15 +75,10 @@ class InputFile:
elif isinstance(obj, str): elif isinstance(obj, str):
self.input_file_content = obj.encode("utf-8") self.input_file_content = obj.encode("utf-8")
else: else:
self.input_file_content = obj.read() reported_filename, self.input_file_content = load_file(obj)
self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None filename = filename or reported_filename
if ( self.attach_name: Optional[str] = "attached" + uuid4().hex if attach else None
not filename
and hasattr(obj, "name")
and not isinstance(obj.name, int) # type: ignore[union-attr]
):
filename = Path(obj.name).name # type: ignore[union-attr]
if filename: if filename:
self.mimetype = mimetypes.guess_type(filename, strict=False)[0] or _DEFAULT_MIME_TYPE self.mimetype = mimetypes.guess_type(filename, strict=False)[0] or _DEFAULT_MIME_TYPE

View file

@ -48,9 +48,8 @@ class InputMedia(TelegramObject):
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Animation` | :class:`telegram.Audio` | \ :class:`telegram.Animation` | :class:`telegram.Audio` | \
:class:`telegram.Document` | :class:`telegram.PhotoSize` | \ :class:`telegram.Document` | :class:`telegram.PhotoSize` | \
:class:`telegram.Video`): :class:`telegram.Video`): File to send.
File to send. Pass a file_id to send a file that exists on the Telegram servers |fileinputnopath|
(recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing telegram media object of the corresponding type Lastly you can pass an existing telegram media object of the corresponding type
to send. to send.
caption (:obj:`str`, optional): Caption of the media to be sent, caption (:obj:`str`, optional): Caption of the media to be sent,
@ -99,7 +98,11 @@ class InputMedia(TelegramObject):
@staticmethod @staticmethod
def _parse_thumb_input(thumb: Optional[FileInput]) -> Optional[Union[str, InputFile]]: def _parse_thumb_input(thumb: Optional[FileInput]) -> Optional[Union[str, InputFile]]:
return parse_file_input(thumb, attach=True) if thumb is not None else thumb # We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
return (
parse_file_input(thumb, attach=True, local_mode=True) if thumb is not None else thumb
)
class InputMediaAnimation(InputMedia): class InputMediaAnimation(InputMedia):
@ -112,10 +115,8 @@ class InputMediaAnimation(InputMedia):
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Animation`): File to send. Pass a :class:`telegram.Animation`): File to send. |fileinputnopath|
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP Lastly you can pass an existing :class:`telegram.Animation` object to send.
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Animation` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -123,13 +124,9 @@ class InputMediaAnimation(InputMedia):
new file. Convenience parameter, useful e.g. when sending files generated by the new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module. :obj:`tempfile` module.
.. versionadded:: 13.1 .. versionadded:: 13.1
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
the file sent; can be ignored if optional): |thumbdocstringnopath|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should
not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -180,7 +177,9 @@ class InputMediaAnimation(InputMedia):
duration = media.duration if duration is None else duration duration = media.duration if duration is None else duration
media = media.file_id media = media.file_id
else: else:
media = parse_file_input(media, filename=filename, attach=True) # We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, filename=filename, attach=True, local_mode=True)
super().__init__(InputMediaType.ANIMATION, media, caption, caption_entities, parse_mode) super().__init__(InputMediaType.ANIMATION, media, caption, caption_entities, parse_mode)
self.thumb = self._parse_thumb_input(thumb) self.thumb = self._parse_thumb_input(thumb)
@ -194,10 +193,8 @@ class InputMediaPhoto(InputMedia):
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.PhotoSize`): File to send. Pass a :class:`telegram.PhotoSize`): File to send. |fileinputnopath|
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.PhotoSize` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -205,7 +202,7 @@ class InputMediaPhoto(InputMedia):
new file. Convenience parameter, useful e.g. when sending files generated by the new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module. :obj:`tempfile` module.
.. versionadded:: 13.1 .. versionadded:: 13.1
caption (:obj:`str`, optional ): Caption of the photo to be sent, caption (:obj:`str`, optional ): Caption of the photo to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. entities parsing.
@ -236,7 +233,9 @@ class InputMediaPhoto(InputMedia):
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
filename: str = None, filename: str = None,
): ):
media = parse_file_input(media, PhotoSize, filename=filename, attach=True) # We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, PhotoSize, filename=filename, attach=True, local_mode=True)
super().__init__(InputMediaType.PHOTO, media, caption, caption_entities, parse_mode) super().__init__(InputMediaType.PHOTO, media, caption, caption_entities, parse_mode)
@ -253,10 +252,8 @@ class InputMediaVideo(InputMedia):
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Video`): File to send. Pass a :class:`telegram.Video`): File to send. |fileinputnopath|
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP Lastly you can pass an existing :class:`telegram.Video` object to send.
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Video` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -264,7 +261,7 @@ class InputMediaVideo(InputMedia):
new file. Convenience parameter, useful e.g. when sending files generated by the new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module. :obj:`tempfile` module.
.. versionadded:: 13.1 .. versionadded:: 13.1
caption (:obj:`str`, optional): Caption of the video to be sent, caption (:obj:`str`, optional): Caption of the video to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. entities parsing.
@ -279,12 +276,8 @@ class InputMediaVideo(InputMedia):
duration (:obj:`int`, optional): Video duration in seconds. duration (:obj:`int`, optional): Video duration in seconds.
supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is
suitable for streaming. suitable for streaming.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
the file sent; can be ignored if optional): |thumbdocstringnopath|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should
not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -327,7 +320,9 @@ class InputMediaVideo(InputMedia):
duration = duration if duration is not None else media.duration duration = duration if duration is not None else media.duration
media = media.file_id media = media.file_id
else: else:
media = parse_file_input(media, filename=filename, attach=True) # We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, filename=filename, attach=True, local_mode=True)
super().__init__(InputMediaType.VIDEO, media, caption, caption_entities, parse_mode) super().__init__(InputMediaType.VIDEO, media, caption, caption_entities, parse_mode)
self.width = width self.width = width
@ -347,11 +342,8 @@ class InputMediaAudio(InputMedia):
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Audio`): :class:`telegram.Audio`): File to send. |fileinputnopath|
File to send. Pass a Lastly you can pass an existing :class:`telegram.Audio` object to send.
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.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -359,7 +351,7 @@ class InputMediaAudio(InputMedia):
new file. Convenience parameter, useful e.g. when sending files generated by the new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module. :obj:`tempfile` module.
.. versionadded:: 13.1 .. versionadded:: 13.1
caption (:obj:`str`, optional): Caption of the audio to be sent, caption (:obj:`str`, optional): Caption of the audio to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. entities parsing.
@ -373,12 +365,8 @@ class InputMediaAudio(InputMedia):
performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio
tags. tags.
title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags. title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
the file sent; can be ignored if optional): |thumbdocstringnopath|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should
not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -418,7 +406,9 @@ class InputMediaAudio(InputMedia):
title = media.title if title is None else title title = media.title if title is None else title
media = media.file_id media = media.file_id
else: else:
media = parse_file_input(media, filename=filename, attach=True) # We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, filename=filename, attach=True, local_mode=True)
super().__init__(InputMediaType.AUDIO, media, caption, caption_entities, parse_mode) super().__init__(InputMediaType.AUDIO, media, caption, caption_entities, parse_mode)
self.thumb = self._parse_thumb_input(thumb) self.thumb = self._parse_thumb_input(thumb)
@ -432,10 +422,8 @@ class InputMediaDocument(InputMedia):
Args: Args:
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \ media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
:class:`telegram.Document`): File to send. Pass a :class:`telegram.Document`): File to send. |fileinputnopath|
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP Lastly you can pass an existing :class:`telegram.Document` object to send.
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Document` object to send.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -443,7 +431,7 @@ class InputMediaDocument(InputMedia):
new file. Convenience parameter, useful e.g. when sending files generated by the new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module. :obj:`tempfile` module.
.. versionadded:: 13.1 .. versionadded:: 13.1
caption (:obj:`str`, optional): Caption of the document to be sent, caption (:obj:`str`, optional): Caption of the document to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. entities parsing.
@ -453,12 +441,8 @@ class InputMediaDocument(InputMedia):
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of entities that appear in the caption, which can be specified instead of
:paramref:`parse_mode`. :paramref:`parse_mode`.
thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path`, optional): Thumbnail of thumb (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
the file sent; can be ignored if optional): |thumbdocstringnopath|
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than ``200`` kB in size. A thumbnail's width and height should
not exceed ``320``. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
.. versionchanged:: 13.2 .. versionchanged:: 13.2
Accept :obj:`bytes` as input. Accept :obj:`bytes` as input.
@ -492,7 +476,9 @@ class InputMediaDocument(InputMedia):
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None, caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
filename: str = None, filename: str = None,
): ):
media = parse_file_input(media, Document, filename=filename, attach=True) # We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, Document, filename=filename, attach=True, local_mode=True)
super().__init__(InputMediaType.DOCUMENT, media, caption, caption_entities, parse_mode) super().__init__(InputMediaType.DOCUMENT, media, caption, caption_entities, parse_mode)
self.thumb = self._parse_thumb_input(thumb) self.thumb = self._parse_thumb_input(thumb)
self.disable_content_type_detection = disable_content_type_detection self.disable_content_type_detection = disable_content_type_detection

View file

@ -29,13 +29,47 @@ Warning:
""" """
from pathlib import Path from pathlib import Path
from typing import IO, TYPE_CHECKING, Any, Optional, Type, Union, cast from typing import IO, TYPE_CHECKING, Any, Optional, Tuple, Type, TypeVar, Union, cast, overload
from telegram._utils.types import FileInput, FilePathInput from telegram._utils.types import FileInput, FilePathInput
if TYPE_CHECKING: if TYPE_CHECKING:
from telegram import InputFile, TelegramObject from telegram import InputFile, TelegramObject
_T = TypeVar("_T", bound=Union[bytes, "InputFile", str, Path, None])
@overload
def load_file(obj: IO[bytes]) -> Tuple[Optional[str], bytes]:
...
@overload
def load_file(obj: _T) -> Tuple[None, _T]:
...
def load_file(
obj: Optional[FileInput],
) -> Tuple[Optional[str], Union[bytes, "InputFile", str, Path, None]]:
"""If the input is a file handle, read the data and name and return it. Otherwise, return
the input unchanged.
"""
if obj is None:
return None, None
try:
contents = obj.read() # type: ignore[union-attr]
except AttributeError:
return None, cast(Union[bytes, "InputFile", str, Path], obj)
if hasattr(obj, "name") and not isinstance(obj.name, int): # type: ignore[union-attr]
filename = Path(obj.name).name # type: ignore[union-attr]
else:
filename = None
return filename, contents
def is_local_file(obj: Optional[FilePathInput]) -> bool: def is_local_file(obj: Optional[FilePathInput]) -> bool:
""" """
@ -54,18 +88,24 @@ def is_local_file(obj: Optional[FilePathInput]) -> bool:
return False return False
def parse_file_input( def parse_file_input( # pylint: disable=too-many-return-statements
file_input: Union[FileInput, "TelegramObject"], file_input: Union[FileInput, "TelegramObject"],
tg_type: Type["TelegramObject"] = None, tg_type: Type["TelegramObject"] = None,
filename: str = None, filename: str = None,
attach: bool = False, attach: bool = False,
local_mode: bool = False,
) -> Union[str, "InputFile", Any]: ) -> Union[str, "InputFile", Any]:
""" """
Parses input for sending files: Parses input for sending files:
* For string input, if the input is an absolute path of a local file, * For string input, if the input is an absolute path of a local file:
adds the ``file://`` prefix. If the input is a relative path of a local file, computes the
absolute path and adds the ``file://`` prefix. Returns the input unchanged, otherwise. * if ``local_mode`` is ``True``, adds the ``file://`` prefix. If the input is a relative
path of a local file, computes the absolute path and adds the ``file://`` prefix.
* if ``local_mode`` is ``False``, loads the file as binary data and builds an
:class:`InputFile` from that
Returns the input unchanged, otherwise.
* :class:`pathlib.Path` objects are treated the same way as strings. * :class:`pathlib.Path` objects are treated the same way as strings.
* For IO and bytes input, returns an :class:`telegram.InputFile`. * For IO and bytes input, returns an :class:`telegram.InputFile`.
* If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id`` * If :attr:`tg_type` is specified and the input is of that type, returns the ``file_id``
@ -81,6 +121,8 @@ def parse_file_input(
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
the request to Telegram should point to the multipart data via an ``attach://`` URI. the request to Telegram should point to the multipart data via an ``attach://`` URI.
Defaults to `False`. Only relevant if an :class:`telegram.InputFile` is returned. Defaults to `False`. Only relevant if an :class:`telegram.InputFile` is returned.
local_mode (:obj:`bool`, optional): Pass :obj:`True` if the bot is running an api server
in ``--local`` mode.
Returns: Returns:
:obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched :obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched
@ -90,13 +132,17 @@ def parse_file_input(
from telegram import InputFile # pylint: disable=import-outside-toplevel from telegram import InputFile # pylint: disable=import-outside-toplevel
if isinstance(file_input, str) and file_input.startswith("file://"): if isinstance(file_input, str) and file_input.startswith("file://"):
if not local_mode:
raise ValueError("Specified file input is a file URI, but local mode is not enabled.")
return file_input return file_input
if isinstance(file_input, (str, Path)): if isinstance(file_input, (str, Path)):
if is_local_file(file_input): if is_local_file(file_input):
out = Path(file_input).absolute().as_uri() path = Path(file_input)
else: if local_mode:
out = file_input # type: ignore[assignment] return path.absolute().as_uri()
return out return InputFile(path.open(mode="rb"), filename=filename, attach=attach)
return file_input
if isinstance(file_input, bytes): if isinstance(file_input, bytes):
return InputFile(file_input, filename=filename, attach=attach) return InputFile(file_input, filename=filename, attach=attach)
if hasattr(file_input, "read"): if hasattr(file_input, "read"):

View file

@ -44,8 +44,7 @@ FilePathInput = Union[str, Path]
FileInput = Union[FilePathInput, FileLike, bytes, str] FileInput = Union[FilePathInput, FileLike, bytes, str]
"""Valid input for passing files to Telegram. Either a file id as string, a file like object, """Valid input for passing files to Telegram. Either a file id as string, a file like object,
a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes` or a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`."""
:obj:`str`."""
JSONDict = Dict[str, Any] JSONDict = Dict[str, Any]
"""Dictionary containing response from Telegram or data to send to the API.""" """Dictionary containing response from Telegram or data to send to the API."""

View file

@ -83,6 +83,7 @@ _BOT_CHECKS = [
("arbitrary_callback_data", "arbitrary_callback_data"), ("arbitrary_callback_data", "arbitrary_callback_data"),
("private_key", "private_key"), ("private_key", "private_key"),
("rate_limiter", "rate_limiter instance"), ("rate_limiter", "rate_limiter instance"),
("local_mode", "local_mode setting"),
] ]
_TWO_ARGS_REQ = "The parameter `{}` may only be set, if no {} was set." _TWO_ARGS_REQ = "The parameter `{}` may only be set, if no {} was set."
@ -148,6 +149,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
"_update_queue", "_update_queue",
"_updater", "_updater",
"_write_timeout", "_write_timeout",
"_local_mode",
) )
def __init__(self: "InitApplicationBuilder"): def __init__(self: "InitApplicationBuilder"):
@ -172,6 +174,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
self._private_key_password: ODVInput[bytes] = DEFAULT_NONE self._private_key_password: ODVInput[bytes] = DEFAULT_NONE
self._defaults: ODVInput["Defaults"] = DEFAULT_NONE self._defaults: ODVInput["Defaults"] = DEFAULT_NONE
self._arbitrary_callback_data: DVInput[Union[bool, int]] = DEFAULT_FALSE self._arbitrary_callback_data: DVInput[Union[bool, int]] = DEFAULT_FALSE
self._local_mode: DVInput[bool] = DEFAULT_FALSE
self._bot: DVInput[Bot] = DEFAULT_NONE self._bot: DVInput[Bot] = DEFAULT_NONE
self._update_queue: DVInput[Queue] = DefaultValue(Queue()) self._update_queue: DVInput[Queue] = DefaultValue(Queue())
self._job_queue: ODVInput["JobQueue"] = DefaultValue(JobQueue()) self._job_queue: ODVInput["JobQueue"] = DefaultValue(JobQueue())
@ -232,8 +235,17 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
request=self._build_request(get_updates=False), request=self._build_request(get_updates=False),
get_updates_request=self._build_request(get_updates=True), get_updates_request=self._build_request(get_updates=True),
rate_limiter=DefaultValue.get_value(self._rate_limiter), rate_limiter=DefaultValue.get_value(self._rate_limiter),
local_mode=DefaultValue.get_value(self._local_mode),
) )
def _bot_check(self, name: str) -> None:
if self._bot is not DEFAULT_NONE:
raise RuntimeError(_TWO_ARGS_REQ.format(name, "bot instance"))
def _updater_check(self, name: str) -> None:
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater"))
def build( def build(
self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]",
) -> Application[BT, CCT, UD, CD, BD, JQ]: ) -> Application[BT, CCT, UD, CD, BD, JQ]:
@ -326,10 +338,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._bot is not DEFAULT_NONE: self._bot_check("token")
raise RuntimeError(_TWO_ARGS_REQ.format("token", "bot instance")) self._updater_check("token")
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("token", "updater"))
self._token = token self._token = token
return self return self
@ -347,10 +357,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._bot is not DEFAULT_NONE: self._bot_check("base_url")
raise RuntimeError(_TWO_ARGS_REQ.format("base_url", "bot instance")) self._updater_check("base_url")
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("base_url", "updater"))
self._base_url = base_url self._base_url = base_url
return self return self
@ -368,10 +376,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._bot is not DEFAULT_NONE: self._bot_check("base_file_url")
raise RuntimeError(_TWO_ARGS_REQ.format("base_file_url", "bot instance")) self._updater_check("base_file_url")
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("base_file_url", "updater"))
self._base_file_url = base_file_url self._base_file_url = base_file_url
return self return self
@ -388,8 +394,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "connection_pool_size")) raise RuntimeError(_TWO_ARGS_REQ.format(name, "connection_pool_size"))
if not isinstance(getattr(self, f"_{prefix}proxy_url"), DefaultValue): if not isinstance(getattr(self, f"_{prefix}proxy_url"), DefaultValue):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy_url")) raise RuntimeError(_TWO_ARGS_REQ.format(name, "proxy_url"))
if self._bot is not DEFAULT_NONE: self._bot_check(name)
raise RuntimeError(_TWO_ARGS_REQ.format(name, "bot instance"))
if self._updater not in (DEFAULT_NONE, None): if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater instance")) raise RuntimeError(_TWO_ARGS_REQ.format(name, "updater instance"))
@ -670,10 +675,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._bot is not DEFAULT_NONE: self._bot_check("private_key")
raise RuntimeError(_TWO_ARGS_REQ.format("private_key", "bot instance")) self._updater_check("private_key")
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("private_key", "updater"))
self._private_key = ( self._private_key = (
private_key if isinstance(private_key, bytes) else Path(private_key).read_bytes() private_key if isinstance(private_key, bytes) else Path(private_key).read_bytes()
@ -698,10 +701,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._bot is not DEFAULT_NONE: self._bot_check("defaults")
raise RuntimeError(_TWO_ARGS_REQ.format("defaults", "bot instance")) self._updater_check("defaults")
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("defaults", "updater"))
self._defaults = defaults self._defaults = defaults
return self return self
@ -725,13 +726,30 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._bot is not DEFAULT_NONE: self._bot_check("arbitrary_callback_data")
raise RuntimeError(_TWO_ARGS_REQ.format("arbitrary_callback_data", "bot instance")) self._updater_check("arbitrary_callback_data")
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("arbitrary_callback_data", "updater"))
self._arbitrary_callback_data = arbitrary_callback_data self._arbitrary_callback_data = arbitrary_callback_data
return self return self
def local_mode(self: BuilderType, local_mode: bool) -> BuilderType:
"""Specifies the value for :paramref:`~telegram.Bot.local_mode` for the
:attr:`telegram.ext.Application.bot`.
If not called, will default to :obj:`False`.
.. seealso:: `Local Bot API Server <https://github.com/python-telegram-bot/\
python-telegram-bot/wiki/Local-Bot-API-Server>`_,
Args:
local_mode (:obj:`bool`): Whether the bot should run in local mode.
Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument.
"""
self._bot_check("local_mode")
self._updater_check("local_mode")
self._local_mode = local_mode
return self
def bot( def bot(
self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]", self: "ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]",
bot: InBT, bot: InBT,
@ -746,8 +764,7 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._updater not in (DEFAULT_NONE, None): self._updater_check("bot")
raise RuntimeError(_TWO_ARGS_REQ.format("bot", "updater"))
for attr, error in _BOT_CHECKS: for attr, error in _BOT_CHECKS:
if not isinstance(getattr(self, f"_{attr}"), DefaultValue): if not isinstance(getattr(self, f"_{attr}"), DefaultValue):
raise RuntimeError(_TWO_ARGS_REQ.format("bot", error)) raise RuntimeError(_TWO_ARGS_REQ.format("bot", error))
@ -992,10 +1009,8 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
Returns: Returns:
:class:`ApplicationBuilder`: The same builder with the updated argument. :class:`ApplicationBuilder`: The same builder with the updated argument.
""" """
if self._bot is not DEFAULT_NONE: self._bot_check("rate_limiter")
raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "bot instance")) self._updater_check("rate_limiter")
if self._updater not in (DEFAULT_NONE, None):
raise RuntimeError(_TWO_ARGS_REQ.format("rate_limiter", "updater"))
self._rate_limiter = rate_limiter self._rate_limiter = rate_limiter
return self # type: ignore[return-value] return self # type: ignore[return-value]

View file

@ -163,6 +163,7 @@ class ExtBot(Bot, Generic[RLARGS]):
private_key_password: bytes = None, private_key_password: bytes = None,
defaults: "Defaults" = None, defaults: "Defaults" = None,
arbitrary_callback_data: Union[bool, int] = False, arbitrary_callback_data: Union[bool, int] = False,
local_mode: bool = False,
): ):
... ...
@ -178,6 +179,7 @@ class ExtBot(Bot, Generic[RLARGS]):
private_key_password: bytes = None, private_key_password: bytes = None,
defaults: "Defaults" = None, defaults: "Defaults" = None,
arbitrary_callback_data: Union[bool, int] = False, arbitrary_callback_data: Union[bool, int] = False,
local_mode: bool = False,
rate_limiter: "BaseRateLimiter[RLARGS]" = None, rate_limiter: "BaseRateLimiter[RLARGS]" = None,
): ):
... ...
@ -193,6 +195,7 @@ class ExtBot(Bot, Generic[RLARGS]):
private_key_password: bytes = None, private_key_password: bytes = None,
defaults: "Defaults" = None, defaults: "Defaults" = None,
arbitrary_callback_data: Union[bool, int] = False, arbitrary_callback_data: Union[bool, int] = False,
local_mode: bool = False,
rate_limiter: "BaseRateLimiter" = None, rate_limiter: "BaseRateLimiter" = None,
): ):
super().__init__( super().__init__(
@ -203,6 +206,7 @@ class ExtBot(Bot, Generic[RLARGS]):
get_updates_request=get_updates_request, get_updates_request=get_updates_request,
private_key=private_key, private_key=private_key,
private_key_password=private_key_password, private_key_password=private_key_password,
local_mode=local_mode,
) )
self._defaults = defaults self._defaults = defaults
self._rate_limiter = rate_limiter self._rate_limiter = rate_limiter

View file

@ -22,7 +22,7 @@ from pathlib import Path
import pytest import pytest
from flaky import flaky from flaky import flaky
from telegram import Animation, Bot, MessageEntity, PhotoSize, Voice from telegram import Animation, Bot, InputFile, MessageEntity, PhotoSize, Voice
from telegram.error import BadRequest, TelegramError from telegram.error import BadRequest, TelegramError
from telegram.helpers import escape_markdown from telegram.helpers import escape_markdown
from telegram.request import RequestData from telegram.request import RequestData
@ -202,20 +202,30 @@ class TestAnimation:
assert message.caption == test_markdown_string assert message.caption == test_markdown_string
assert message.caption_markdown == escape_markdown(test_markdown_string) assert message.caption_markdown == escape_markdown(test_markdown_string)
async def test_send_animation_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_animation_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("animation") == expected and data.get("thumb") == expected if local_mode:
test_flag = data.get("animation") == expected and data.get("thumb") == expected
else:
test_flag = isinstance(data.get("animation"), InputFile) and isinstance(
data.get("thumb"), InputFile
)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_animation(chat_id, file, thumb=file) await bot.send_animation(chat_id, file, thumb=file)
assert test_flag assert test_flag
monkeypatch.delattr(bot, "_post") monkeypatch.delattr(bot, "_post")
finally:
bot._local_mode = False
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -84,6 +84,7 @@ class TestApplicationBuilder:
assert app.bot.arbitrary_callback_data is False assert app.bot.arbitrary_callback_data is False
assert app.bot.defaults is None assert app.bot.defaults is None
assert app.bot.rate_limiter is None assert app.bot.rate_limiter is None
assert app.bot.local_mode is False
get_updates_client = app.bot._request[0]._client get_updates_client = app.bot._request[0]._client
assert get_updates_client.limits == httpx.Limits( assert get_updates_client.limits == httpx.Limits(
@ -257,6 +258,8 @@ class TestApplicationBuilder:
get_updates_request get_updates_request
).rate_limiter( ).rate_limiter(
rate_limiter rate_limiter
).local_mode(
True
) )
built_bot = builder.build().bot built_bot = builder.build().bot
@ -273,6 +276,7 @@ class TestApplicationBuilder:
assert built_bot.callback_data_cache.maxsize == 42 assert built_bot.callback_data_cache.maxsize == 42
assert built_bot.private_key assert built_bot.private_key
assert built_bot.rate_limiter is rate_limiter assert built_bot.rate_limiter is rate_limiter
assert built_bot.local_mode is True
@dataclass @dataclass
class Client: class Client:

View file

@ -22,7 +22,7 @@ from pathlib import Path
import pytest import pytest
from flaky import flaky from flaky import flaky
from telegram import Audio, Bot, MessageEntity, Voice from telegram import Audio, Bot, InputFile, MessageEntity, Voice
from telegram.error import TelegramError from telegram.error import TelegramError
from telegram.helpers import escape_markdown from telegram.helpers import escape_markdown
from telegram.request import RequestData from telegram.request import RequestData
@ -234,20 +234,30 @@ class TestAudio:
unprotected = await default_bot.send_audio(chat_id, audio, protect_content=False) unprotected = await default_bot.send_audio(chat_id, audio, protect_content=False)
assert not unprotected.has_protected_content assert not unprotected.has_protected_content
async def test_send_audio_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_audio_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("audio") == expected and data.get("thumb") == expected if local_mode:
test_flag = data.get("audio") == expected and data.get("thumb") == expected
else:
test_flag = isinstance(data.get("audio"), InputFile) and isinstance(
data.get("thumb"), InputFile
)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_audio(chat_id, file, thumb=file) await bot.send_audio(chat_id, file, thumb=file)
assert test_flag assert test_flag
monkeypatch.delattr(bot, "_post") monkeypatch.delattr(bot, "_post")
finally:
bot._local_mode = False
def test_de_json(self, bot, audio): def test_de_json(self, bot, audio):
json_dict = { json_dict = {

View file

@ -44,6 +44,7 @@ from telegram import (
InlineQueryResultArticle, InlineQueryResultArticle,
InlineQueryResultDocument, InlineQueryResultDocument,
InlineQueryResultVoice, InlineQueryResultVoice,
InputFile,
InputMedia, InputMedia,
InputTextMessageContent, InputTextMessageContent,
LabeledPrice, LabeledPrice,
@ -1578,18 +1579,25 @@ class TestBot:
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize("use_ip", [True, False]) @pytest.mark.parametrize("use_ip", [True, False])
async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip): # local file path as file_input is tested below in test_set_webhook_params
@pytest.mark.parametrize("file_input", ["bytes", "file_handle"])
async def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot, use_ip, file_input):
url = "https://python-telegram-bot.org/test/webhook" url = "https://python-telegram-bot.org/test/webhook"
# Get the ip address of the website - dynamically just in case it ever changes # Get the ip address of the website - dynamically just in case it ever changes
ip = socket.gethostbyname("python-telegram-bot.org") ip = socket.gethostbyname("python-telegram-bot.org")
max_connections = 7 max_connections = 7
allowed_updates = ["message"] allowed_updates = ["message"]
file_input = (
data_file("sslcert.pem").read_bytes()
if file_input == "bytes"
else data_file("sslcert.pem").open("rb")
)
await bot.set_webhook( await bot.set_webhook(
url, url,
max_connections=max_connections, max_connections=max_connections,
allowed_updates=allowed_updates, allowed_updates=allowed_updates,
ip_address=ip if use_ip else None, ip_address=ip if use_ip else None,
certificate=data_file("sslcert.pem").read_bytes() if use_ip else None, certificate=file_input if use_ip else None,
) )
await asyncio.sleep(1) await asyncio.sleep(1)
@ -1620,16 +1628,25 @@ class TestBot:
assert await bot.set_webhook("", drop_pending_updates=drop_pending_updates) assert await bot.set_webhook("", drop_pending_updates=drop_pending_updates)
assert await bot.delete_webhook(drop_pending_updates=drop_pending_updates) assert await bot.delete_webhook(drop_pending_updates=drop_pending_updates)
async def test_set_webhook_params(self, bot, monkeypatch): @pytest.mark.parametrize("local_file", ["str", "Path", False])
async def test_set_webhook_params(self, bot, monkeypatch, local_file):
# actually making calls to TG is done in # actually making calls to TG is done in
# test_set_webhook_get_webhook_info_and_delete_webhook. Sadly secret_token can't be tested # test_set_webhook_get_webhook_info_and_delete_webhook. Sadly secret_token can't be tested
# there so we have this function \o/ # there so we have this function \o/
async def make_assertion(*args, **_): async def make_assertion(*args, **_):
kwargs = args[1] kwargs = args[1]
if local_file is False:
cert_assertion = (
kwargs["certificate"].input_file_content
== data_file("sslcert.pem").read_bytes()
)
else:
cert_assertion = data_file("sslcert.pem").as_uri()
return ( return (
kwargs["url"] == "example.com" kwargs["url"] == "example.com"
and kwargs["certificate"].input_file_content and cert_assertion
== data_file("sslcert.pem").read_bytes()
and kwargs["max_connections"] == 7 and kwargs["max_connections"] == 7
and kwargs["allowed_updates"] == ["messages"] and kwargs["allowed_updates"] == ["messages"]
and kwargs["ip_address"] == "127.0.0.1" and kwargs["ip_address"] == "127.0.0.1"
@ -1639,9 +1656,17 @@ class TestBot:
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
cert_path = data_file("sslcert.pem")
if local_file == "str":
certificate = str(cert_path)
elif local_file == "Path":
certificate = cert_path
else:
certificate = cert_path.read_bytes()
assert await bot.set_webhook( assert await bot.set_webhook(
"example.com", "example.com",
data_file("sslcert.pem").read_bytes(), certificate,
7, 7,
["messages"], ["messages"],
"127.0.0.1", "127.0.0.1",
@ -2163,19 +2188,27 @@ class TestBot:
func, "Type of file mismatch", "Telegram did not accept the file." func, "Type of file mismatch", "Telegram did not accept the file."
) )
async def test_set_chat_photo_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_set_chat_photo_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("photo") == expected if local_mode:
test_flag = data.get("photo") == expected
else:
test_flag = isinstance(data.get("photo"), InputFile)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.set_chat_photo(chat_id, file) await bot.set_chat_photo(chat_id, file)
assert test_flag assert test_flag
finally:
bot._local_mode = False
@flaky(3, 1) @flaky(3, 1)
async def test_delete_chat_photo(self, bot, channel_id): async def test_delete_chat_photo(self, bot, channel_id):

View file

@ -22,7 +22,7 @@ from pathlib import Path
import pytest import pytest
from flaky import flaky from flaky import flaky
from telegram import Bot, Document, MessageEntity, PhotoSize, Voice from telegram import Bot, Document, InputFile, MessageEntity, PhotoSize, Voice
from telegram.error import BadRequest, TelegramError from telegram.error import BadRequest, TelegramError
from telegram.helpers import escape_markdown from telegram.helpers import escape_markdown
from telegram.request import RequestData from telegram.request import RequestData
@ -255,19 +255,29 @@ class TestDocument:
unprotected = await default_bot.send_document(chat_id, document, protect_content=False) unprotected = await default_bot.send_document(chat_id, document, protect_content=False)
assert not unprotected.has_protected_content assert not unprotected.has_protected_content
async def test_send_document_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_document_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("document") == expected and data.get("thumb") == expected if local_mode:
test_flag = data.get("document") == expected and data.get("thumb") == expected
else:
test_flag = isinstance(data.get("document"), InputFile) and isinstance(
data.get("thumb"), InputFile
)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_document(chat_id, file, thumb=file) await bot.send_document(chat_id, file, thumb=file)
assert test_flag assert test_flag
finally:
bot._local_mode = False
def test_de_json(self, bot, document): def test_de_json(self, bot, document):
json_dict = { json_dict = {

View file

@ -16,6 +16,9 @@
# #
# You should have received a copy of the GNU Lesser Public License # You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]. # along with this program. If not, see [http://www.gnu.org/licenses/].
import subprocess
import sys
from pathlib import Path
import pytest import pytest
@ -43,23 +46,43 @@ class TestFiles:
assert telegram._utils.files.is_local_file(string) == expected assert telegram._utils.files.is_local_file(string) == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
"string,expected", "string,expected_local,expected_non_local",
[ [
(data_file("game.gif"), data_file("game.gif").as_uri()), (data_file("game.gif"), data_file("game.gif").as_uri(), InputFile),
(TEST_DATA_PATH, TEST_DATA_PATH), (TEST_DATA_PATH, TEST_DATA_PATH, TEST_DATA_PATH),
("file://foobar", "file://foobar"), ("file://foobar", "file://foobar", ValueError),
(str(data_file("game.gif")), data_file("game.gif").as_uri()), (str(data_file("game.gif")), data_file("game.gif").as_uri(), InputFile),
(str(TEST_DATA_PATH), str(TEST_DATA_PATH)), (str(TEST_DATA_PATH), str(TEST_DATA_PATH), str(TEST_DATA_PATH)),
(data_file("game.gif"), data_file("game.gif").as_uri()),
(TEST_DATA_PATH, TEST_DATA_PATH),
( (
"https:/api.org/file/botTOKEN/document/file_3", "https:/api.org/file/botTOKEN/document/file_3",
"https:/api.org/file/botTOKEN/document/file_3", "https:/api.org/file/botTOKEN/document/file_3",
"https:/api.org/file/botTOKEN/document/file_3",
), ),
], ],
ids=[
"Path(local_file)",
"Path(directory)",
"file_uri",
"str-path local file",
"str-path directory",
"URL",
],
) )
def test_parse_file_input_string(self, string, expected): def test_parse_file_input_string(self, string, expected_local, expected_non_local):
assert telegram._utils.files.parse_file_input(string) == expected assert telegram._utils.files.parse_file_input(string, local_mode=True) == expected_local
if expected_non_local is InputFile:
assert isinstance(
telegram._utils.files.parse_file_input(string, local_mode=False), InputFile
)
elif expected_non_local is ValueError:
with pytest.raises(ValueError):
telegram._utils.files.parse_file_input(string, local_mode=False)
else:
assert (
telegram._utils.files.parse_file_input(string, local_mode=False)
== expected_non_local
)
def test_parse_file_input_file_like(self): def test_parse_file_input_file_like(self):
source_file = data_file("game.gif") source_file = data_file("game.gif")
@ -105,3 +128,34 @@ class TestFiles:
assert isinstance(parsed, InputFile) assert isinstance(parsed, InputFile)
assert bool(parsed.attach_name) is attach assert bool(parsed.attach_name) is attach
def test_load_file_none(self):
assert telegram._utils.files.load_file(None) == (None, None)
@pytest.mark.parametrize("arg", [b"bytes", "string", InputFile(b"content"), Path("file/path")])
def test_load_file_no_file(self, arg):
out = telegram._utils.files.load_file(arg)
assert out[0] is None
assert out[1] is arg
def test_load_file_file_handle(self):
out = telegram._utils.files.load_file(data_file("telegram.gif").open("rb"))
assert out[0] == "telegram.gif"
assert out[1] == data_file("telegram.gif").read_bytes()
def test_load_file_subprocess_pipe(self):
png_file = data_file("telegram.png")
cmd_str = "type" if sys.platform == "win32" else "cat"
cmd = [cmd_str, str(png_file)]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=(sys.platform == "win32"))
out = telegram._utils.files.load_file(proc.stdout)
assert out[0] is None
assert out[1] == png_file.read_bytes()
try:
proc.kill()
except ProcessLookupError:
# This exception may be thrown if the process has finished before we had the chance
# to kill it.
pass

View file

@ -236,19 +236,27 @@ class TestPhoto:
unprotected = await default_bot.send_photo(chat_id, photo, protect_content=False) unprotected = await default_bot.send_photo(chat_id, photo, protect_content=False)
assert not unprotected.has_protected_content assert not unprotected.has_protected_content
async def test_send_photo_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_photo_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("photo") == expected if local_mode:
test_flag = data.get("photo") == expected
else:
test_flag = isinstance(data.get("photo"), InputFile)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_photo(chat_id, file) await bot.send_photo(chat_id, file)
assert test_flag assert test_flag
finally:
bot._local_mode = False
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -23,7 +23,7 @@ from pathlib import Path
import pytest import pytest
from flaky import flaky from flaky import flaky
from telegram import Audio, Bot, File, MaskPosition, PhotoSize, Sticker, StickerSet from telegram import Audio, Bot, File, InputFile, MaskPosition, PhotoSize, Sticker, StickerSet
from telegram.error import BadRequest, TelegramError from telegram.error import BadRequest, TelegramError
from telegram.request import RequestData from telegram.request import RequestData
from tests.conftest import ( from tests.conftest import (
@ -250,20 +250,28 @@ class TestSticker:
message = await bot.send_sticker(sticker=sticker, chat_id=chat_id) message = await bot.send_sticker(sticker=sticker, chat_id=chat_id)
assert message assert message
async def test_send_sticker_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_sticker_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("sticker") == expected if local_mode:
test_flag = data.get("sticker") == expected
else:
test_flag = isinstance(data.get("sticker"), InputFile)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_sticker(chat_id, file) await bot.send_sticker(chat_id, file)
assert test_flag assert test_flag
monkeypatch.delattr(bot, "_post") monkeypatch.delattr(bot, "_post")
finally:
bot._local_mode = False
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -645,47 +653,67 @@ class TestStickerSet:
file_id = video_sticker_set.stickers[-1].file_id file_id = video_sticker_set.stickers[-1].file_id
assert await bot.delete_sticker_from_set(file_id) assert await bot.delete_sticker_from_set(file_id)
async def test_upload_sticker_file_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_upload_sticker_file_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("png_sticker") == expected if local_mode:
test_flag = data.get("png_sticker") == expected
else:
test_flag = isinstance(data.get("png_sticker"), InputFile)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.upload_sticker_file(chat_id, file) await bot.upload_sticker_file(chat_id, file)
assert test_flag assert test_flag
monkeypatch.delattr(bot, "_post") monkeypatch.delattr(bot, "_post")
finally:
bot._local_mode = False
async def test_create_new_sticker_set_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_create_new_sticker_set_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = ( if local_mode:
data.get("png_sticker") == expected test_flag = (
and data.get("tgs_sticker") == expected data.get("png_sticker") == expected
and data.get("webm_sticker") == expected and data.get("tgs_sticker") == expected
and data.get("webm_sticker") == expected
)
else:
test_flag = (
isinstance(data.get("png_sticker"), InputFile)
and isinstance(data.get("tgs_sticker"), InputFile)
and isinstance(data.get("webm_sticker"), InputFile)
)
monkeypatch.setattr(bot, "_post", make_assertion)
await bot.create_new_sticker_set(
chat_id,
"name",
"title",
"emoji",
png_sticker=file,
tgs_sticker=file,
webm_sticker=file,
) )
assert test_flag
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.delattr(bot, "_post")
await bot.create_new_sticker_set( finally:
chat_id, bot._local_mode = False
"name",
"title",
"emoji",
png_sticker=file,
tgs_sticker=file,
webm_sticker=file,
)
assert test_flag
monkeypatch.delattr(bot, "_post")
async def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position): async def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position):
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
@ -713,35 +741,57 @@ class TestStickerSet:
) )
monkeypatch.delattr(bot, "_post") monkeypatch.delattr(bot, "_post")
async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("png_sticker") == expected and data.get("tgs_sticker") == expected if local_mode:
test_flag = (
data.get("png_sticker") == expected and data.get("tgs_sticker") == expected
)
else:
test_flag = isinstance(data.get("png_sticker"), InputFile) and isinstance(
data.get("tgs_sticker"), InputFile
)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.add_sticker_to_set(chat_id, "name", "emoji", png_sticker=file, tgs_sticker=file) await bot.add_sticker_to_set(
assert test_flag chat_id, "name", "emoji", png_sticker=file, tgs_sticker=file
monkeypatch.delattr(bot, "_post") )
assert test_flag
monkeypatch.delattr(bot, "_post")
finally:
bot._local_mode = False
async def test_set_sticker_set_thumb_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_set_sticker_set_thumb_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("thumb") == expected if local_mode:
test_flag = data.get("thumb") == expected
else:
test_flag = isinstance(data.get("thumb"), InputFile)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.set_sticker_set_thumb("name", chat_id, thumb=file) await bot.set_sticker_set_thumb("name", chat_id, thumb=file)
assert test_flag assert test_flag
monkeypatch.delattr(bot, "_post") monkeypatch.delattr(bot, "_post")
finally:
bot._local_mode = False
async def test_get_file_instance_method(self, monkeypatch, sticker): async def test_get_file_instance_method(self, monkeypatch, sticker):
async def make_assertion(*_, **kwargs): async def make_assertion(*_, **kwargs):

View file

@ -22,7 +22,7 @@ from pathlib import Path
import pytest import pytest
from flaky import flaky from flaky import flaky
from telegram import Bot, MessageEntity, PhotoSize, Video, Voice from telegram import Bot, InputFile, MessageEntity, PhotoSize, Video, Voice
from telegram.error import BadRequest, TelegramError from telegram.error import BadRequest, TelegramError
from telegram.helpers import escape_markdown from telegram.helpers import escape_markdown
from telegram.request import RequestData from telegram.request import RequestData
@ -247,19 +247,29 @@ class TestVideo:
unprotected = await default_bot.send_video(chat_id, video, protect_content=False) unprotected = await default_bot.send_video(chat_id, video, protect_content=False)
assert not unprotected.has_protected_content assert not unprotected.has_protected_content
async def test_send_video_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_video_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("video") == expected and data.get("thumb") == expected if local_mode:
test_flag = data.get("video") == expected and data.get("thumb") == expected
else:
test_flag = isinstance(data.get("video"), InputFile) and isinstance(
data.get("thumb"), InputFile
)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_video(chat_id, file, thumb=file) await bot.send_video(chat_id, file, thumb=file)
assert test_flag assert test_flag
finally:
bot._local_mode = False
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -22,7 +22,7 @@ from pathlib import Path
import pytest import pytest
from flaky import flaky from flaky import flaky
from telegram import Bot, PhotoSize, VideoNote, Voice from telegram import Bot, InputFile, PhotoSize, VideoNote, Voice
from telegram.error import BadRequest, TelegramError from telegram.error import BadRequest, TelegramError
from telegram.request import RequestData from telegram.request import RequestData
from tests.conftest import ( from tests.conftest import (
@ -177,19 +177,31 @@ class TestVideoNote:
assert video_note_dict["duration"] == video_note.duration assert video_note_dict["duration"] == video_note.duration
assert video_note_dict["file_size"] == video_note.file_size assert video_note_dict["file_size"] == video_note.file_size
async def test_send_video_note_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_video_note_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("video_note") == expected and data.get("thumb") == expected if local_mode:
test_flag = (
data.get("video_note") == expected and data.get("thumb") == expected
)
else:
test_flag = isinstance(data.get("video_note"), InputFile) and isinstance(
data.get("thumb"), InputFile
)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_video_note(chat_id, file, thumb=file) await bot.send_video_note(chat_id, file, thumb=file)
assert test_flag assert test_flag
finally:
bot._local_mode = False
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -22,7 +22,7 @@ from pathlib import Path
import pytest import pytest
from flaky import flaky from flaky import flaky
from telegram import Audio, Bot, MessageEntity, Voice from telegram import Audio, Bot, InputFile, MessageEntity, Voice
from telegram.error import BadRequest, TelegramError from telegram.error import BadRequest, TelegramError
from telegram.helpers import escape_markdown from telegram.helpers import escape_markdown
from telegram.request import RequestData from telegram.request import RequestData
@ -207,19 +207,27 @@ class TestVoice:
unprotected = await default_bot.send_voice(chat_id, voice, protect_content=False) unprotected = await default_bot.send_voice(chat_id, voice, protect_content=False)
assert not unprotected.has_protected_content assert not unprotected.has_protected_content
async def test_send_voice_local_files(self, monkeypatch, bot, chat_id): @pytest.mark.parametrize("local_mode", [True, False])
# For just test that the correct paths are passed as we have no local bot API set up async def test_send_voice_local_files(self, monkeypatch, bot, chat_id, local_mode):
test_flag = False try:
file = data_file("telegram.jpg") bot._local_mode = local_mode
expected = file.as_uri() # For just test that the correct paths are passed as we have no local bot API set up
test_flag = False
file = data_file("telegram.jpg")
expected = file.as_uri()
async def make_assertion(_, data, *args, **kwargs): async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag nonlocal test_flag
test_flag = data.get("voice") == expected if local_mode:
test_flag = data.get("voice") == expected
else:
test_flag = isinstance(data.get("voice"), InputFile)
monkeypatch.setattr(bot, "_post", make_assertion) monkeypatch.setattr(bot, "_post", make_assertion)
await bot.send_voice(chat_id, file) await bot.send_voice(chat_id, file)
assert test_flag assert test_flag
finally:
bot._local_mode = False
@flaky(3, 1) @flaky(3, 1)
@pytest.mark.parametrize( @pytest.mark.parametrize(