diff --git a/README.rst b/README.rst index fbce7e050e1..ee8de02d921 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.0-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -112,7 +112,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **6.0** are supported. +All types and methods of the Telegram Bot API **6.1** are supported. ========== Installing diff --git a/README_RAW.rst b/README_RAW.rst index 64955d63c34..89a3ce5581b 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.0-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -105,7 +105,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **6.0** are supported. +All types and methods of the Telegram Bot API **6.1** are supported. ========== Installing diff --git a/setup.cfg b/setup.cfg index ecfc17fad34..0873e46c17e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ filterwarnings = ; Unfortunately due to https://github.com/pytest-dev/pytest/issues/8343 we can't have this here ; and instead do a trick directly in tests/conftest.py ; ignore::telegram.utils.deprecate.TelegramDeprecationWarning +markers = dev: If you want to test a specific test, use this [coverage:run] branch = True diff --git a/telegram/__main__.py b/telegram/__main__.py index 2e7a7de1d96..fbb378c8be0 100644 --- a/telegram/__main__.py +++ b/telegram/__main__.py @@ -41,7 +41,7 @@ def print_ver_info() -> None: # skipcq: PY-D0003 git_revision = _git_revision() print(f'python-telegram-bot {telegram_ver}' + (f' ({git_revision})' if git_revision else '')) print(f'Bot API {BOT_API_VERSION}') - print(f'certifi {certifi.__version__}') # type: ignore[attr-defined] + print('certifi' + certifi.__version__) sys_version = sys.version.replace('\n', ' ') print(f'Python {sys_version}') diff --git a/telegram/bot.py b/telegram/bot.py index b37781bd412..f1348cc3ffc 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -3080,6 +3080,7 @@ def set_webhook( api_kwargs: JSONDict = None, ip_address: str = None, drop_pending_updates: bool = None, + secret_token: str = None, ) -> bool: """ Use this method to specify a url and receive incoming updates via an outgoing webhook. @@ -3087,9 +3088,9 @@ def set_webhook( specified url, containing a JSON-serialized Update. In case of an unsuccessful request, Telegram will give up after a reasonable amount of attempts. - If you'd like to make sure that the Webhook request comes from Telegram, Telegram - recommends using a secret path in the URL, e.g. https://www.example.com/. Since - nobody else knows your bot's token, you can be pretty sure it's us. + If you'd like to make sure that the Webhook was set by you, you can specify secret data in + the parameter ``secret_token``. If specified, the request will contain a header + ``X-Telegram-Bot-Api-Secret-Token`` with the secret token as content. Note: The certificate argument should be a file from disk ``open(filename, 'rb')``. @@ -3117,6 +3118,12 @@ def set_webhook( a short period of time. drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending updates. + secret_token (:obj:`str`, optional): A secret token to be sent in a header + ``X-Telegram-Bot-Api-Secret-Token`` in every webhook request, 1-256 characters. + Only characters ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + The header is useful to ensure that the request comes from a webhook set by you. + + .. versionadded:: 13.13 timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). @@ -3157,6 +3164,8 @@ def set_webhook( data['ip_address'] = ip_address if drop_pending_updates: data['drop_pending_updates'] = drop_pending_updates + if secret_token is not None: + data["secret_token"] = secret_token result = self._post('setWebhook', data, timeout=timeout, api_kwargs=api_kwargs) @@ -5914,6 +5923,135 @@ def get_chat_menu_button( ) return MenuButton.de_json(result, bot=self) # type: ignore[return-value, arg-type] + @log + def create_invoice_link( + self, + title: str, + description: str, + payload: str, + provider_token: str, + currency: str, + prices: List["LabeledPrice"], + max_tip_amount: int = None, + suggested_tip_amounts: List[int] = None, + provider_data: Union[str, object] = None, + photo_url: str = None, + photo_size: int = None, + photo_width: int = None, + photo_height: int = None, + need_name: bool = None, + need_phone_number: bool = None, + need_email: bool = None, + need_shipping_address: bool = None, + send_phone_number_to_provider: bool = None, + send_email_to_provider: bool = None, + is_flexible: bool = None, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> str: + """Use this method to create a link for an invoice. + + .. versionadded:: 13.13 + + Args: + title (:obj:`str`): Product name. 1-32 characters. + description (:obj:`str`): Product description. 1-255 characters. + payload (:obj:`str`): Bot-defined invoice payload. 1-128 bytes. This will not be + displayed to the user, use for your internal processes. + provider_token (:obj:`str`): Payments provider token, obtained via + `@BotFather `_. + currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies + `_. + prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a list + of components (e.g. product price, tax, discount, delivery cost, delivery tax, + bonus, etc.). + max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the + *smallest* units of the currency (integer, **not** float/double). For example, for + a maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the exp parameter in + `currencies.json `_, it + shows the number of digits past the decimal point for each currency (2 for the + majority of currencies). Defaults to ``0``. + suggested_tip_amounts (List[:obj:`int`], optional): An array of + suggested amounts of tips in the *smallest* units of the currency (integer, **not** + float/double). At most 4 suggested tip amounts can be specified. The suggested tip + amounts must be positive, passed in a strictly increased order and must not exceed + ``max_tip_amount``. + provider_data (:obj:`str` | :obj:`object`, optional): Data about the + invoice, which will be shared with the payment provider. A detailed description of + required fields should be provided by the payment provider. When an object is + passed, it will be encoded as JSON. + photo_url (:obj:`str`, optional): URL of the product photo for the invoice. Can be a + photo of the goods or a marketing image for a service. + photo_size (:obj:`int`, optional): Photo size in bytes. + photo_width (:obj:`int`, optional): Photo width. + photo_height (:obj:`int`, optional): Photo height. + need_name (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's full + name to complete the order. + need_phone_number (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's + phone number to complete the order. + need_email (:obj:`bool`, optional): Pass :obj:`True`, if you require the user's email + address to complete the order. + need_shipping_address (:obj:`bool`, optional): Pass :obj:`True`, if you require the + user's shipping address to complete the order. + send_phone_number_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's + phone number should be sent to provider. + send_email_to_provider (:obj:`bool`, optional): Pass :obj:`True`, if user's email + address should be sent to provider. + is_flexible (:obj:`bool`, optional): Pass :obj:`True`, if the final price depends on + the shipping method. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + + Returns: + :class:`str`: On success, the created invoice link is returned. + """ + data: JSONDict = { + "title": title, + "description": description, + "payload": payload, + "provider_token": provider_token, + "currency": currency, + "prices": [p.to_dict() for p in prices], + } + if max_tip_amount is not None: + data["max_tip_amount"] = max_tip_amount + if suggested_tip_amounts is not None: + data["suggested_tip_amounts"] = suggested_tip_amounts + if provider_data is not None: + data["provider_data"] = provider_data + if photo_url is not None: + data["photo_url"] = photo_url + if photo_size is not None: + data["photo_size"] = photo_size + if photo_width is not None: + data["photo_width"] = photo_width + if photo_height is not None: + data["photo_height"] = photo_height + if need_name is not None: + data["need_name"] = need_name + if need_phone_number is not None: + data["need_phone_number"] = need_phone_number + if need_email is not None: + data["need_email"] = need_email + if need_shipping_address is not None: + data["need_shipping_address"] = need_shipping_address + if is_flexible is not None: + data["is_flexible"] = is_flexible + if send_phone_number_to_provider is not None: + data["send_phone_number_to_provider"] = send_phone_number_to_provider + if send_email_to_provider is not None: + data["send_email_to_provider"] = send_email_to_provider + + return self._post( # type: ignore[return-value] + "createInvoiceLink", + data, + timeout=timeout, + api_kwargs=api_kwargs, + ) + def to_dict(self) -> JSONDict: """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {'id': self.id, 'username': self.username, 'first_name': self.first_name} @@ -6106,3 +6244,5 @@ def __hash__(self) -> int: """Alias for :meth:`get_my_default_administrator_rights`""" setMyDefaultAdministratorRights = set_my_default_administrator_rights """Alias for :meth:`set_my_default_administrator_rights`""" + createInvoiceLink = create_invoice_link + """Alias for :meth:`create_invoice_link`""" diff --git a/telegram/chat.py b/telegram/chat.py index 7de0e73929a..0e649bc0fa0 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -116,6 +116,16 @@ class Chat(TelegramObject): chats. Returned only in :meth:`telegram.Bot.get_chat`. location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. + join_to_send_messages (:obj:`bool`, optional): :obj:`True`, if users need to join the + supergroup before they can send messages. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.13 + join_by_request (:obj:`bool`, optional): :obj:`True`, if all users directly joining the + supergroup need to be approved by supergroup administrators. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.13 **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -160,7 +170,16 @@ class Chat(TelegramObject): chats. Returned only in :meth:`telegram.Bot.get_chat`. location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. + join_to_send_messages (:obj:`bool`): Optional. :obj:`True`, if users need to join the + supergroup before they can send messages. Returned only in + :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 13.13 + join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly joining the + supergroup need to be approved by supergroup administrators. Returned only in + :meth:`telegram.Bot.get_chat`. + .. versionadded:: 13.13 """ __slots__ = ( @@ -186,6 +205,8 @@ class Chat(TelegramObject): 'message_auto_delete_time', 'has_protected_content', 'has_private_forwards', + 'join_to_send_messages', + 'join_by_request', '_id_attrs', ) @@ -226,6 +247,8 @@ def __init__( message_auto_delete_time: int = None, has_private_forwards: bool = None, has_protected_content: bool = None, + join_to_send_messages: bool = None, + join_by_request: bool = None, **_kwargs: Any, ): # Required @@ -254,6 +277,8 @@ def __init__( self.can_set_sticker_set = can_set_sticker_set self.linked_chat_id = linked_chat_id self.location = location + self.join_to_send_messages = join_to_send_messages + self.join_by_request = join_by_request self.bot = bot self._id_attrs = (self.id,) diff --git a/telegram/constants.py b/telegram/constants.py index c8ac8afa1c0..9b132ec56d6 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -21,7 +21,7 @@ `Telegram Bots API `_. Attributes: - BOT_API_VERSION (:obj:`str`): `6.0`. Telegram Bot API version supported by this + BOT_API_VERSION (:obj:`str`): `6.1`. Telegram Bot API version supported by this version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. .. versionadded:: 13.4 @@ -247,7 +247,7 @@ """ from typing import List -BOT_API_VERSION: str = '6.0' +BOT_API_VERSION: str = '6.1' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index e7d2a24cea0..cdf0e53029c 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1004,6 +1004,38 @@ def filter(self, message: Message) -> bool: location = _Location() """Messages that contain :class:`telegram.Location`.""" + class _UserAttachment(UpdateFilter): + __slots__ = () + name = "Filters.user_attachment" + + def filter(self, update: Update) -> bool: + return bool(update.effective_user) and bool( + update.effective_user.added_to_attachment_menu + ) + + user_attachment = _UserAttachment() + """This filter filters *any* message that have a user who added the bot to their + :attr:`attachment menu ` as + :attr:`telegram.Update.effective_user`. + + .. versionadded:: 13.13 + """ + + class _UserPremium(UpdateFilter): + __slots__ = () + name = "Filters.premium_user" + + def filter(self, update: Update) -> bool: + return bool(update.effective_user) and bool(update.effective_user.is_premium) + + premium_user = _UserPremium() + """This filter filters *any* message from a + :attr:`Telegram Premium user ` as + :attr:`telegram.Update.effective_user`. + + .. versionadded:: 13.13 + """ + class _Venue(MessageFilter): __slots__ = () name = 'Filters.venue' diff --git a/telegram/ext/updater.py b/telegram/ext/updater.py index b2c05125691..b1372b6c142 100644 --- a/telegram/ext/updater.py +++ b/telegram/ext/updater.py @@ -463,6 +463,11 @@ def start_webhook( application. Else, the webhook will be started on https://listen:port/url_path. Also calls :meth:`telegram.Bot.set_webhook` as required. + Note: + ``telegram.Bot.set_webhook.secret_token`` is not checked by this webhook + implementation. If you want to use this new security parameter, either build your own + webhook server or update your code to version 20.0a2+. + .. versionchanged:: 13.4 :meth:`start_webhook` now *always* calls :meth:`telegram.Bot.set_webhook`, so pass ``webhook_url`` instead of calling ``updater.bot.set_webhook(webhook_url)`` manually. diff --git a/telegram/files/sticker.py b/telegram/files/sticker.py index e3f22a99754..bce03da0058 100644 --- a/telegram/files/sticker.py +++ b/telegram/files/sticker.py @@ -60,6 +60,10 @@ class Sticker(TelegramObject): position where the mask should be placed. file_size (:obj:`int`, optional): File size. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + premium_animation (:class:`telegram.File`, optional): Premium animation for the sticker, + if the sticker is premium. + + .. versionadded:: 13.13 **kwargs (obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -80,6 +84,10 @@ class Sticker(TelegramObject): mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position where the mask should be placed. file_size (:obj:`int`): Optional. File size. + premium_animation (:class:`telegram.File`): Optional. Premium animation for the sticker, + if the sticker is premium. + + .. versionadded:: 13.13 bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ @@ -97,6 +105,7 @@ class Sticker(TelegramObject): 'height', 'file_unique_id', 'emoji', + 'premium_animation', '_id_attrs', ) @@ -114,6 +123,7 @@ def __init__( set_name: str = None, mask_position: 'MaskPosition' = None, bot: 'Bot' = None, + premium_animation: 'File' = None, **_kwargs: Any, ): # Required @@ -130,12 +140,17 @@ def __init__( self.set_name = set_name self.mask_position = mask_position self.bot = bot + self.premium_animation = premium_animation self._id_attrs = (self.file_unique_id,) @classmethod def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Sticker']: """See :meth:`telegram.TelegramObject.de_json`.""" + # needs to be here to avoid circular imports + # pylint: disable=import-outside-toplevel + from telegram import File + data = cls._parse_data(data) if not data: @@ -143,6 +158,7 @@ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Sticker']: data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['mask_position'] = MaskPosition.de_json(data.get('mask_position'), bot) + data["premium_animation"] = File.de_json(data.get("premium_animation"), bot) return cls(bot=bot, **data) diff --git a/telegram/loginurl.py b/telegram/loginurl.py index 7d6dc561e51..51a006c825e 100644 --- a/telegram/loginurl.py +++ b/telegram/loginurl.py @@ -40,7 +40,7 @@ class LoginUrl(TelegramObject): `Checking authorization `_ Args: - url (:obj:`str`): An HTTP URL to be opened with user authorization data added to the query + url (:obj:`str`): An HTTPS URL to be opened with user authorization data added to the query string when the button is pressed. If the user refuses to provide authorization data, the original URL without information about the user will be opened. The data added is the same as described in @@ -60,7 +60,7 @@ class LoginUrl(TelegramObject): for your bot to send messages to the user. Attributes: - url (:obj:`str`): An HTTP URL to be opened with user authorization data. + url (:obj:`str`): An HTTPS URL to be opened with user authorization data. forward_text (:obj:`str`): Optional. New text of the button in forwarded messages. bot_username (:obj:`str`): Optional. Username of a bot, which will be used for user authorization. diff --git a/telegram/user.py b/telegram/user.py index f2e5d87ff7a..3825fe09a51 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -79,6 +79,13 @@ class User(TelegramObject): supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline queries. Returned only in :attr:`telegram.Bot.get_me` requests. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. + is_premium (:obj:`bool`, optional): :obj:`True`, if this user is a Telegram Premium user. + + .. versionadded:: 13.13 + added_to_attachment_menu (:obj:`bool`, optional): :obj:`True`, if this user added + the bot to the attachment menu. + + .. versionadded:: 13.13 Attributes: id (:obj:`int`): Unique identifier for this user or bot. @@ -94,6 +101,14 @@ class User(TelegramObject): supports_inline_queries (:obj:`str`): Optional. :obj:`True`, if the bot supports inline queries. Returned only in :attr:`telegram.Bot.get_me` requests. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + is_premium (:obj:`bool`): Optional. :obj:`True`, if this user is a Telegram + Premium user. + + .. versionadded:: 13.13 + added_to_attachment_menu (:obj:`bool`): Optional. :obj:`True`, if this user added + the bot to the attachment menu. + + .. versionadded:: 13.13 """ @@ -108,6 +123,8 @@ class User(TelegramObject): 'id', 'bot', 'language_code', + 'is_premium', + 'added_to_attachment_menu', '_id_attrs', ) @@ -123,6 +140,8 @@ def __init__( can_read_all_group_messages: bool = None, supports_inline_queries: bool = None, bot: 'Bot' = None, + is_premium: bool = None, + added_to_attachment_menu: bool = None, **_kwargs: Any, ): # Required @@ -136,6 +155,8 @@ def __init__( self.can_join_groups = can_join_groups self.can_read_all_group_messages = can_read_all_group_messages self.supports_inline_queries = supports_inline_queries + self.is_premium = is_premium + self.added_to_attachment_menu = added_to_attachment_menu self.bot = bot self._id_attrs = (self.id,) diff --git a/tests/test_bot.py b/tests/test_bot.py index a1a54363c9a..5e033fe4152 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1410,6 +1410,32 @@ def assertion(url, data, *args, **kwargs): assert bot.set_webhook(drop_pending_updates=drop_pending_updates) assert bot.delete_webhook(drop_pending_updates=drop_pending_updates) + def test_set_webhook_params(self, bot, monkeypatch): + # actually making calls to TG is done in + # test_set_webhook_get_webhook_info_and_delete_webhook. Sadly secret_token can't be tested + # there so we have this function \o/ + def make_assertion(*args, **_): + kwargs = args[1] + return ( + kwargs["url"] == "example.com" + and kwargs["max_connections"] == 7 + and kwargs["allowed_updates"] == ["messages"] + and kwargs["ip_address"] == "127.0.0.1" + and kwargs["drop_pending_updates"] + and kwargs["secret_token"] == "SoSecretToken" + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + + assert bot.set_webhook( + "example.com", + max_connections=7, + allowed_updates=["messages"], + ip_address="127.0.0.1", + drop_pending_updates=True, + secret_token="SoSecretToken", + ) + @flaky(3, 1) def test_leave_chat(self, bot): with pytest.raises(BadRequest, match='Chat not found'): diff --git a/tests/test_chat.py b/tests/test_chat.py index e2d56641ad8..515b1b55ed0 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -43,6 +43,8 @@ def chat(bot): location=TestChat.location, has_private_forwards=True, has_protected_content=True, + join_to_send_messages=True, + join_by_request=True, ) @@ -66,6 +68,8 @@ class TestChat: location = ChatLocation(Location(123, 456), 'Barbie World') has_protected_content = True has_private_forwards = True + join_to_send_messages = True + join_by_request = True def test_slot_behaviour(self, chat, recwarn, mro_slots): for attr in chat.__slots__: @@ -92,6 +96,8 @@ def test_de_json(self, bot): 'has_private_forwards': self.has_private_forwards, 'linked_chat_id': self.linked_chat_id, 'location': self.location.to_dict(), + 'join_to_send_messages': self.join_to_send_messages, + 'join_by_request': self.join_by_request, } chat = Chat.de_json(json_dict, bot) @@ -111,6 +117,8 @@ def test_de_json(self, bot): assert chat.linked_chat_id == self.linked_chat_id assert chat.location.location == self.location.location assert chat.location.address == self.location.address + assert chat.join_to_send_messages == self.join_to_send_messages + assert chat.join_by_request == self.join_by_request def test_to_dict(self, chat): chat_dict = chat.to_dict() @@ -129,6 +137,8 @@ def test_to_dict(self, chat): assert chat_dict['has_protected_content'] == chat.has_protected_content assert chat_dict['linked_chat_id'] == chat.linked_chat_id assert chat_dict['location'] == chat.location.to_dict() + assert chat_dict["join_to_send_messages"] == chat.join_to_send_messages + assert chat_dict["join_by_request"] == chat.join_by_request def test_link(self, chat): assert chat.link == f'https://t.me/{chat.username}' diff --git a/tests/test_filters.py b/tests/test_filters.py index 7b9904b69ed..9372eb18d38 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1189,6 +1189,19 @@ def test_filters_user_repr(self): with pytest.raises(RuntimeError, match='Cannot set name'): f.name = 'foo' + def test_filters_user_attributes(self, update): + assert not Filters.user_attachment(update) + assert not Filters.premium_user(update) + update.message.from_user.added_to_attachment_menu = True + assert Filters.user_attachment(update) + assert not Filters.premium_user(update) + update.message.from_user.is_premium = True + assert Filters.user_attachment(update) + assert Filters.premium_user(update) + update.message.from_user.added_to_attachment_menu = False + assert not Filters.user_attachment(update) + assert Filters.premium_user(update) + def test_filters_chat_init(self): with pytest.raises(RuntimeError, match='in conjunction with'): Filters.chat(chat_id=1, username='chat') diff --git a/tests/test_invoice.py b/tests/test_invoice.py index 6ed2f3c3011..711d40c4f50 100644 --- a/tests/test_invoice.py +++ b/tests/test_invoice.py @@ -100,8 +100,19 @@ def test_send_required_args_only(self, bot, chat_id, provider_token): assert message.invoice.title == self.title assert message.invoice.total_amount == self.total_amount + link = bot.create_invoice_link( + title=self.title, + description=self.description, + payload=self.payload, + provider_token=provider_token, + currency=self.currency, + prices=self.prices, + ) + assert isinstance(link, str) + assert link != "" + @flaky(3, 1) - def test_send_all_args(self, bot, chat_id, provider_token, monkeypatch): + def test_send_all_args_send_invoice(self, bot, chat_id, provider_token, monkeypatch): message = bot.send_invoice( chat_id, self.title, @@ -195,6 +206,56 @@ def make_assertion(*args, **_): protect_content=True, ) + def test_send_all_args_create_invoice_link(self, bot, chat_id, provider_token, monkeypatch): + def make_assertion(*args, **_): + kwargs = args[1] + return ( + kwargs["title"] == "title" + and kwargs["description"] == "description" + and kwargs["payload"] == "payload" + and kwargs["provider_token"] == "provider_token" + and kwargs["currency"] == "currency" + and kwargs["prices"] == [p.to_dict() for p in self.prices] + and kwargs["max_tip_amount"] == "max_tip_amount" + and kwargs["suggested_tip_amounts"] == "suggested_tip_amounts" + and kwargs["provider_data"] == "provider_data" + and kwargs["photo_url"] == "photo_url" + and kwargs["photo_size"] == "photo_size" + and kwargs["photo_width"] == "photo_width" + and kwargs["photo_height"] == "photo_height" + and kwargs["need_name"] == "need_name" + and kwargs["need_phone_number"] == "need_phone_number" + and kwargs["need_email"] == "need_email" + and kwargs["need_shipping_address"] == "need_shipping_address" + and kwargs["send_phone_number_to_provider"] == "send_phone_number_to_provider" + and kwargs["send_email_to_provider"] == "send_email_to_provider" + and kwargs["is_flexible"] == "is_flexible" + ) + + monkeypatch.setattr(bot, "_post", make_assertion) + assert bot.create_invoice_link( + title="title", + description="description", + payload="payload", + provider_token="provider_token", + currency="currency", + prices=self.prices, + max_tip_amount="max_tip_amount", + suggested_tip_amounts="suggested_tip_amounts", + provider_data="provider_data", + photo_url="photo_url", + photo_size="photo_size", + photo_width="photo_width", + photo_height="photo_height", + need_name="need_name", + need_phone_number="need_phone_number", + need_email="need_email", + need_shipping_address="need_shipping_address", + send_phone_number_to_provider="send_phone_number_to_provider", + send_email_to_provider="send_email_to_provider", + is_flexible="is_flexible", + ) + def test_send_object_as_provider_data(self, monkeypatch, bot, chat_id, provider_token): def test(url, data, **kwargs): # depends on whether we're using ujson diff --git a/tests/test_sticker.py b/tests/test_sticker.py index d45cc83f843..3a798cb4f4c 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -23,7 +23,7 @@ import pytest from flaky import flaky -from telegram import Sticker, PhotoSize, TelegramError, StickerSet, Audio, MaskPosition, Bot +from telegram import Sticker, PhotoSize, TelegramError, StickerSet, Audio, MaskPosition, Bot, File from telegram.error import BadRequest from tests.conftest import check_shortcut_call, check_shortcut_signature, check_defaults_handling @@ -87,6 +87,8 @@ class TestSticker: sticker_file_id = '5a3128a4d2a04750b5b58397f3b5e812' sticker_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + premium_animation = File("this_is_an_id", "this_is_an_unique_id") + def test_slot_behaviour(self, sticker, mro_slots, recwarn): for attr in sticker.__slots__: assert getattr(sticker, attr, 'err') != 'err', f"got extra slot '{attr}'" @@ -117,6 +119,8 @@ def test_expected_values(self, sticker): assert sticker.thumb.width == self.thumb_width assert sticker.thumb.height == self.thumb_height assert sticker.thumb.file_size == self.thumb_file_size + # we need to be a premium TG user to send a premium sticker, so the below is not tested + # assert sticker.premium_animation == self.premium_animation @flaky(3, 1) def test_send_all_args(self, bot, chat_id, sticker_file, sticker): @@ -134,6 +138,8 @@ def test_send_all_args(self, bot, chat_id, sticker_file, sticker): assert message.sticker.is_animated == sticker.is_animated assert message.sticker.is_video == sticker.is_video assert message.sticker.file_size == sticker.file_size + # we need to be a premium TG user to send a premium sticker, so the below is not tested + # assert message.sticker.premium_animation == sticker.premium_animation assert isinstance(message.sticker.thumb, PhotoSize) assert isinstance(message.sticker.thumb.file_id, str) @@ -207,6 +213,7 @@ def test_de_json(self, bot, sticker): 'thumb': sticker.thumb.to_dict(), 'emoji': self.emoji, 'file_size': self.file_size, + 'premium_animation': self.premium_animation.to_dict(), } json_sticker = Sticker.de_json(json_dict, bot) @@ -219,6 +226,7 @@ def test_de_json(self, bot, sticker): assert json_sticker.emoji == self.emoji assert json_sticker.file_size == self.file_size assert json_sticker.thumb == sticker.thumb + assert json_sticker.premium_animation == self.premium_animation def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker): def test(url, data, **kwargs): @@ -304,6 +312,24 @@ def test_error_without_required_args(self, bot, chat_id): with pytest.raises(TypeError): bot.send_sticker(chat_id) + @flaky(3, 1) + def test_premium_animation(self, bot): + # testing animation sucks a bit since we can't create a premium sticker. What we can do is + # get a sticker set which includes a premium sticker and check that specific one. + premium_sticker_set = bot.get_sticker_set("Flame") + # the first one to appear here is a sticker with unique file id of AQADOBwAAifPOElr + # this could change in the future ofc. + premium_sticker = premium_sticker_set.stickers[20] + assert premium_sticker.premium_animation.file_unique_id == "AQADOBwAAifPOElr" + assert isinstance(premium_sticker.premium_animation.file_id, str) + assert premium_sticker.premium_animation.file_id != "" + premium_sticker_dict = { + "file_unique_id": "AQADOBwAAifPOElr", + "file_id": premium_sticker.premium_animation.file_id, + "file_size": premium_sticker.premium_animation.file_size, + } + assert premium_sticker.premium_animation.to_dict() == premium_sticker_dict + def test_equality(self, sticker): a = Sticker( sticker.file_id, diff --git a/tests/test_user.py b/tests/test_user.py index 185d3fd8eb3..167871eed69 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -35,6 +35,8 @@ def json_dict(): 'can_join_groups': TestUser.can_join_groups, 'can_read_all_group_messages': TestUser.can_read_all_group_messages, 'supports_inline_queries': TestUser.supports_inline_queries, + 'is_premium': TestUser.is_premium, + 'added_to_attachment_menu': TestUser.added_to_attachment_menu, } @@ -51,6 +53,8 @@ def user(bot): can_read_all_group_messages=TestUser.can_read_all_group_messages, supports_inline_queries=TestUser.supports_inline_queries, bot=bot, + is_premium=TestUser.is_premium, + added_to_attachment_menu=TestUser.added_to_attachment_menu, ) @@ -64,6 +68,8 @@ class TestUser: can_join_groups = True can_read_all_group_messages = True supports_inline_queries = False + is_premium = True + added_to_attachment_menu = False def test_slot_behaviour(self, user, mro_slots, recwarn): for attr in user.__slots__: @@ -85,6 +91,8 @@ def test_de_json(self, json_dict, bot): assert user.can_join_groups == self.can_join_groups assert user.can_read_all_group_messages == self.can_read_all_group_messages assert user.supports_inline_queries == self.supports_inline_queries + assert user.is_premium == self.is_premium + assert user.added_to_attachment_menu == self.added_to_attachment_menu def test_de_json_without_username(self, json_dict, bot): del json_dict['username'] @@ -100,6 +108,8 @@ def test_de_json_without_username(self, json_dict, bot): assert user.can_join_groups == self.can_join_groups assert user.can_read_all_group_messages == self.can_read_all_group_messages assert user.supports_inline_queries == self.supports_inline_queries + assert user.is_premium == self.is_premium + assert user.added_to_attachment_menu == self.added_to_attachment_menu def test_de_json_without_username_and_last_name(self, json_dict, bot): del json_dict['username'] @@ -116,6 +126,8 @@ def test_de_json_without_username_and_last_name(self, json_dict, bot): assert user.can_join_groups == self.can_join_groups assert user.can_read_all_group_messages == self.can_read_all_group_messages assert user.supports_inline_queries == self.supports_inline_queries + assert user.is_premium == self.is_premium + assert user.added_to_attachment_menu == self.added_to_attachment_menu def test_name(self, user): assert user.name == '@username'