Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
This commit is contained in:
Poolitzer 2022-06-27 18:54:11 +02:00 committed by GitHub
parent 01d643913e
commit 08e223ba90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 652 additions and 40 deletions

View file

@ -14,7 +14,7 @@
:target: https://pypi.org/project/python-telegram-bot/ :target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions :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 :target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions :alt: Supported Bot API versions
@ -93,7 +93,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support 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 Installing
========== ==========

View file

@ -14,7 +14,7 @@
:target: https://pypi.org/project/python-telegram-bot-raw/ :target: https://pypi.org/project/python-telegram-bot-raw/
:alt: Supported Python versions :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 :target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions :alt: Supported Bot API versions
@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
Telegram API support 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 Installing
========== ==========

View file

@ -263,6 +263,8 @@
:align: left :align: left
:widths: 1 4 :widths: 1 4
* - :meth:`~telegram.Bot.create_invoice_link`
- Used to generate an HTTP link for an invoice
* - :meth:`~telegram.Bot.close` * - :meth:`~telegram.Bot.close`
- Used for closing server instance when switching to another local server - Used for closing server instance when switching to another local server
* - :meth:`~telegram.Bot.log_out` * - :meth:`~telegram.Bot.log_out`

View file

@ -3795,6 +3795,7 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
allowed_updates: List[str] = None, allowed_updates: List[str] = None,
ip_address: str = None, ip_address: str = None,
drop_pending_updates: bool = None, drop_pending_updates: bool = None,
secret_token: str = None,
*, *,
read_timeout: ODVInput[float] = DEFAULT_NONE, read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -3808,9 +3809,9 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
specified url, containing An Update. In case of an unsuccessful request, specified url, containing An Update. In case of an unsuccessful request,
Telegram will give up after a reasonable amount of attempts. 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 If you'd like to make sure that the Webhook was set by you, you can specify secret data in
recommends using a secret path in the URL, e.g. https://www.example.com/<token>. Since the parameter :paramref:`secret_token`. If specified, the request will contain a header
nobody else knows your bot's token, you can be pretty sure it's them. ``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')``. The certificate argument should be a file from disk ``open(filename, 'rb')``.
@ -3839,6 +3840,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
a short period of time. a short period of time.
drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending drop_pending_updates (:obj:`bool`, optional): Pass :obj:`True` to drop all pending
updates. 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,
:tg-const:`telegram.constants.WebhookLimit.MIN_SECRET_TOKEN_LENGTH`-
:tg-const:`telegram.constants.WebhookLimit.MAX_SECRET_TOKEN_LENGTH` 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:: 20.0
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
@ -3889,6 +3898,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
data["ip_address"] = ip_address data["ip_address"] = ip_address
if drop_pending_updates: if drop_pending_updates:
data["drop_pending_updates"] = drop_pending_updates data["drop_pending_updates"] = drop_pending_updates
if secret_token is not None:
data["secret_token"] = secret_token
result = await self._post( result = await self._post(
"setWebhook", "setWebhook",
@ -4593,26 +4604,32 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
Args: Args:
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``).
title (:obj:`str`): Product name, 1-32 characters. title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
description (:obj:`str`): Product description, 1-255 characters. :tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
payload (:obj:`str`): Bot-defined invoice payload, 1-128 bytes. This will not be description (:obj:`str`): Product description.
:tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`-
:tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters.
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
displayed to the user, use for your internal processes. displayed to the user, use for your internal processes.
provider_token (:obj:`str`): Payments provider token, obtained via provider_token (:obj:`str`): Payments provider token, obtained via
`@BotFather <https://t.me/BotFather>`_. `@BotFather <https://t.me/BotFather>`_.
currency (:obj:`str`): Three-letter ISO 4217 currency code. currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
<https://core.telegram.org/bots/payments#supported-currencies>`_.
prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a list prices (List[:class:`telegram.LabeledPrice`)]: Price breakdown, a list
of components (e.g. product price, tax, discount, delivery cost, delivery tax, of components (e.g. product price, tax, discount, delivery cost, delivery tax,
bonus, etc.). bonus, etc.).
max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the 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 *smallest* units of the currency (integer, **not** float/double). For example, for
maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the exp parameter in a maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the exp parameter in
`currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it `currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it
shows the number of digits past the decimal point for each currency (2 for the shows the number of digits past the decimal point for each currency (2 for the
majority of currencies). Defaults to ``0``. majority of currencies). Defaults to ``0``.
.. versionadded:: 13.5 .. versionadded:: 13.5
suggested_tip_amounts (List[:obj:`int`], optional): An array of suggested_tip_amounts (List[:obj:`int`], optional): An array of
suggested amounts of tips in the smallest units of the currency (integer, not 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 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 amounts must be positive, passed in a strictly increased order and must not exceed
``max_tip_amount``. ``max_tip_amount``.
@ -7693,6 +7710,159 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
) )
return MenuButton.de_json(result, bot=self) # type: ignore[return-value, arg-type] return MenuButton.de_json(result, bot=self) # type: ignore[return-value, arg-type]
@_log
async 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,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: JSONDict = None,
) -> str:
"""Use this method to create a link for an invoice.
.. versionadded:: 20.0
Args:
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
:tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
description (:obj:`str`): Product description.
:tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`-
:tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters.
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be
displayed to the user, use for your internal processes.
provider_token (:obj:`str`): Payments provider token, obtained via
`@BotFather <https://t.me/BotFather>`_.
currency (:obj:`str`): Three-letter ISO 4217 currency code, see `more on currencies
<https://core.telegram.org/bots/payments#supported-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 <https://core.telegram.org/bots/payments/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
:paramref:`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.
Keyword Args:
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
:paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
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": 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 await self._post( # type: ignore[return-value]
"createInvoiceLink",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
def to_dict(self) -> JSONDict: def to_dict(self) -> JSONDict:
"""See :meth:`telegram.TelegramObject.to_dict`.""" """See :meth:`telegram.TelegramObject.to_dict`."""
data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name}
@ -7881,3 +8051,5 @@ class Bot(TelegramObject, AbstractAsyncContextManager):
"""Alias for :meth:`get_my_default_administrator_rights`""" """Alias for :meth:`get_my_default_administrator_rights`"""
setMyDefaultAdministratorRights = set_my_default_administrator_rights setMyDefaultAdministratorRights = set_my_default_administrator_rights
"""Alias for :meth:`set_my_default_administrator_rights`""" """Alias for :meth:`set_my_default_administrator_rights`"""
createInvoiceLink = create_invoice_link
"""Alias for :meth:`create_invoice_link`"""

View file

@ -124,6 +124,16 @@ class Chat(TelegramObject):
chats. Returned only in :meth:`telegram.Bot.get_chat`. chats. Returned only in :meth:`telegram.Bot.get_chat`.
location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. 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:: 20.0
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:: 20.0
**kwargs (:obj:`dict`): Arbitrary keyword arguments. **kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes: Attributes:
@ -168,6 +178,16 @@ class Chat(TelegramObject):
chats. Returned only in :meth:`telegram.Bot.get_chat`. chats. Returned only in :meth:`telegram.Bot.get_chat`.
location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`. 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:: 20.0
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:: 20.0
""" """
@ -193,6 +213,8 @@ class Chat(TelegramObject):
"message_auto_delete_time", "message_auto_delete_time",
"has_protected_content", "has_protected_content",
"has_private_forwards", "has_private_forwards",
"join_to_send_messages",
"join_by_request",
) )
SENDER: ClassVar[str] = constants.ChatType.SENDER SENDER: ClassVar[str] = constants.ChatType.SENDER
@ -232,6 +254,8 @@ class Chat(TelegramObject):
message_auto_delete_time: int = None, message_auto_delete_time: int = None,
has_private_forwards: bool = None, has_private_forwards: bool = None,
has_protected_content: bool = None, has_protected_content: bool = None,
join_to_send_messages: bool = None,
join_by_request: bool = None,
**_kwargs: Any, **_kwargs: Any,
): ):
# Required # Required
@ -260,6 +284,8 @@ class Chat(TelegramObject):
self.can_set_sticker_set = can_set_sticker_set self.can_set_sticker_set = can_set_sticker_set
self.linked_chat_id = linked_chat_id self.linked_chat_id = linked_chat_id
self.location = location self.location = location
self.join_to_send_messages = join_to_send_messages
self.join_by_request = join_by_request
self.set_bot(bot) self.set_bot(bot)
self._id_attrs = (self.id,) self._id_attrs = (self.id,)

View file

@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, List, Optional
from telegram import constants from telegram import constants
from telegram._files._basethumbedmedium import _BaseThumbedMedium from telegram._files._basethumbedmedium import _BaseThumbedMedium
from telegram._files.file import File
from telegram._files.photosize import PhotoSize from telegram._files.photosize import PhotoSize
from telegram._telegramobject import TelegramObject from telegram._telegramobject import TelegramObject
from telegram._utils.types import JSONDict from telegram._utils.types import JSONDict
@ -62,6 +63,10 @@ class Sticker(_BaseThumbedMedium):
position where the mask should be placed. position where the mask should be placed.
file_size (:obj:`int`, optional): File size in bytes. file_size (:obj:`int`, optional): File size in bytes.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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:: 20.0
_kwargs (:obj:`dict`): Arbitrary keyword arguments. _kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes: Attributes:
@ -83,6 +88,10 @@ class Sticker(_BaseThumbedMedium):
where the mask should be placed. where the mask should be placed.
file_size (:obj:`int`): Optional. File size in bytes. file_size (:obj:`int`): Optional. File size in bytes.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. 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:: 20.0
""" """
@ -94,6 +103,7 @@ class Sticker(_BaseThumbedMedium):
"mask_position", "mask_position",
"set_name", "set_name",
"width", "width",
"premium_animation",
) )
def __init__( def __init__(
@ -110,6 +120,7 @@ class Sticker(_BaseThumbedMedium):
set_name: str = None, set_name: str = None,
mask_position: "MaskPosition" = None, mask_position: "MaskPosition" = None,
bot: "Bot" = None, bot: "Bot" = None,
premium_animation: "File" = None,
**_kwargs: Any, **_kwargs: Any,
): ):
super().__init__( super().__init__(
@ -128,6 +139,7 @@ class Sticker(_BaseThumbedMedium):
self.emoji = emoji self.emoji = emoji
self.set_name = set_name self.set_name = set_name
self.mask_position = mask_position self.mask_position = mask_position
self.premium_animation = premium_animation
@classmethod @classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Sticker"]: def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Sticker"]:
@ -139,6 +151,7 @@ class Sticker(_BaseThumbedMedium):
data["thumb"] = PhotoSize.de_json(data.get("thumb"), bot) data["thumb"] = PhotoSize.de_json(data.get("thumb"), bot)
data["mask_position"] = MaskPosition.de_json(data.get("mask_position"), 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) return cls(bot=bot, **data)

View file

@ -39,9 +39,14 @@ class InputInvoiceMessageContent(InputMessageContent):
.. versionadded:: 13.5 .. versionadded:: 13.5
Args: Args:
title (:obj:`str`): Product name, 1-32 characters title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
description (:obj:`str`): Product description, 1-255 characters :tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed description (:obj:`str`): Product description.
:tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`-
:tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters.
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed
to the user, use for your internal processes. to the user, use for your internal processes.
provider_token (:obj:`str`): Payment provider token, obtained via provider_token (:obj:`str`): Payment provider token, obtained via
`@Botfather <https://t.me/Botfather>`_. `@Botfather <https://t.me/Botfather>`_.
@ -50,15 +55,15 @@ class InputInvoiceMessageContent(InputMessageContent):
prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a list of prices (List[:class:`telegram.LabeledPrice`]): Price breakdown, a list of
components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus,
etc.) etc.)
max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the smallest max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the
units of the currency (integer, not float/double). For example, for a maximum tip of *smallest* units of the currency (integer, **not** float/double). For example, for a
US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in maximum tip of US$ 1.45 pass ``max_tip_amount = 145``. See the ``exp`` parameter in
`currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it `currencies.json <https://core.telegram.org/bots/payments/currencies.json>`_, it
shows the number of digits past the decimal point for each currency (2 for the majority shows the number of digits past the decimal point for each currency (2 for the majority
of currencies). Defaults to ``0``. of currencies). Defaults to ``0``.
suggested_tip_amounts (List[:obj:`int`], optional): An array of suggested suggested_tip_amounts (List[:obj:`int`], optional): An array of suggested
amounts of tip in the smallest units of the currency (integer, not float/double). At amounts of tip in the *smallest* units of the currency (integer, **not** float/double).
most 4 suggested tip amounts can be specified. The suggested tip amounts must be 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 positive, passed in a strictly increased order and must not exceed
:attr:`max_tip_amount`. :attr:`max_tip_amount`.
provider_data (:obj:`str`, optional): An object for data about the invoice, provider_data (:obj:`str`, optional): An object for data about the invoice,
@ -87,9 +92,14 @@ class InputInvoiceMessageContent(InputMessageContent):
**kwargs (:obj:`dict`): Arbitrary keyword arguments. **kwargs (:obj:`dict`): Arbitrary keyword arguments.
Attributes: Attributes:
title (:obj:`str`): Product name, 1-32 characters title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
description (:obj:`str`): Product description, 1-255 characters :tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
payload (:obj:`str`):Bot-defined invoice payload, 1-128 bytes. This will not be displayed description (:obj:`str`): Product description.
:tg-const:`telegram.Invoice.MIN_DESCRIPTION_LENGTH`-
:tg-const:`telegram.Invoice.MAX_DESCRIPTION_LENGTH` characters.
payload (:obj:`str`): Bot-defined invoice payload.
:tg-const:`telegram.Invoice.MIN_PAYLOAD_LENGTH`-
:tg-const:`telegram.Invoice.MAX_PAYLOAD_LENGTH` bytes. This will not be displayed
to the user, use for your internal processes. to the user, use for your internal processes.
provider_token (:obj:`str`): Payment provider token, obtained via provider_token (:obj:`str`): Payment provider token, obtained via
`@Botfather <https://t.me/Botfather>`_. `@Botfather <https://t.me/Botfather>`_.

View file

@ -39,7 +39,7 @@ class LoginUrl(TelegramObject):
`Checking authorization <https://core.telegram.org/widgets/login#checking-authorization>`_ `Checking authorization <https://core.telegram.org/widgets/login#checking-authorization>`_
Args: 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, 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 original URL without information about the user will be opened. The data added is
the same as described in the same as described in
@ -59,7 +59,7 @@ class LoginUrl(TelegramObject):
for your bot to send messages to the user. for your bot to send messages to the user.
Attributes: 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. 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 bot_username (:obj:`str`): Optional. Username of a bot, which will be used for user
authorization. authorization.

View file

@ -18,8 +18,9 @@
# along with this program. If not, see [http://www.gnu.org/licenses/]. # along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Invoice.""" """This module contains an object that represents a Telegram Invoice."""
from typing import Any from typing import Any, ClassVar
from telegram import constants
from telegram._telegramobject import TelegramObject from telegram._telegramobject import TelegramObject
@ -83,3 +84,34 @@ class Invoice(TelegramObject):
self.currency, self.currency,
self.total_amount, self.total_amount,
) )
MIN_TITLE_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_TITLE_LENGTH
""":const:`telegram.constants.InvoiceLimit.MIN_TITLE_LENGTH`
.. versionadded:: 20.0
"""
MAX_TITLE_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_TITLE_LENGTH
""":const:`telegram.constants.InvoiceLimit.MAX_TITLE_LENGTH`
.. versionadded:: 20.0
"""
MIN_DESCRIPTION_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH
""":const:`telegram.constants.InvoiceLimit.MIN_DESCRIPTION_LENGTH`
.. versionadded:: 20.0
"""
MAX_DESCRIPTION_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH
""":const:`telegram.constants.InvoiceLimit.MAX_DESCRIPTION_LENGTH`
.. versionadded:: 20.0
"""
MIN_PAYLOAD_LENGTH: ClassVar[int] = constants.InvoiceLimit.MIN_PAYLOAD_LENGTH
""":const:`telegram.constants.InvoiceLimit.MIN_PAYLOAD_LENGTH`
.. versionadded:: 20.0
"""
MAX_PAYLOAD_LENGTH: ClassVar[int] = constants.InvoiceLimit.MAX_PAYLOAD_LENGTH
""":const:`telegram.constants.InvoiceLimit.MAX_PAYLOAD_LENGTH`
.. versionadded:: 20.0
"""

View file

@ -83,6 +83,13 @@ class User(TelegramObject):
supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline
queries. Returned only in :attr:`telegram.Bot.get_me` requests. queries. Returned only in :attr:`telegram.Bot.get_me` requests.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. 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:: 20.0
added_to_attachment_menu (:obj:`bool`, optional): :obj:`True`, if this user added
the bot to the attachment menu.
.. versionadded:: 20.0
Attributes: Attributes:
id (:obj:`int`): Unique identifier for this user or bot. id (:obj:`int`): Unique identifier for this user or bot.
@ -98,7 +105,14 @@ class User(TelegramObject):
supports_inline_queries (:obj:`str`): Optional. :obj:`True`, if the bot supports inline supports_inline_queries (:obj:`str`): Optional. :obj:`True`, if the bot supports inline
queries. Returned only in :attr:`telegram.Bot.get_me` requests. queries. Returned only in :attr:`telegram.Bot.get_me` requests.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. 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:: 20.0
added_to_attachment_menu (:obj:`bool`): Optional. :obj:`True`, if this user added
the bot to the attachment menu.
.. versionadded:: 20.0
""" """
__slots__ = ( __slots__ = (
@ -111,6 +125,8 @@ class User(TelegramObject):
"supports_inline_queries", "supports_inline_queries",
"id", "id",
"language_code", "language_code",
"is_premium",
"added_to_attachment_menu",
) )
def __init__( def __init__(
@ -125,6 +141,8 @@ class User(TelegramObject):
can_read_all_group_messages: bool = None, can_read_all_group_messages: bool = None,
supports_inline_queries: bool = None, supports_inline_queries: bool = None,
bot: "Bot" = None, bot: "Bot" = None,
is_premium: bool = None,
added_to_attachment_menu: bool = None,
**_kwargs: Any, **_kwargs: Any,
): ):
# Required # Required
@ -138,6 +156,8 @@ class User(TelegramObject):
self.can_join_groups = can_join_groups self.can_join_groups = can_join_groups
self.can_read_all_group_messages = can_read_all_group_messages self.can_read_all_group_messages = can_read_all_group_messages
self.supports_inline_queries = supports_inline_queries self.supports_inline_queries = supports_inline_queries
self.is_premium = is_premium
self.added_to_attachment_menu = added_to_attachment_menu
self.set_bot(bot) self.set_bot(bot)
self._id_attrs = (self.id,) self._id_attrs = (self.id,)

View file

@ -45,6 +45,7 @@ __all__ = [
"InlineQueryLimit", "InlineQueryLimit",
"InlineQueryResultType", "InlineQueryResultType",
"InputMediaType", "InputMediaType",
"InvoiceLimit",
"LocationLimit", "LocationLimit",
"MaskPosition", "MaskPosition",
"MenuButtonType", "MenuButtonType",
@ -56,6 +57,7 @@ __all__ = [
"PollLimit", "PollLimit",
"PollType", "PollType",
"SUPPORTED_WEBHOOK_PORTS", "SUPPORTED_WEBHOOK_PORTS",
"WebhookLimit",
"UpdateType", "UpdateType",
] ]
@ -90,7 +92,7 @@ class _BotAPIVersion(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`. #: :data:`telegram.__bot_api_version_info__`.
#: #:
#: .. versionadded:: 20.0 #: .. versionadded:: 20.0
BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=0) BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=1)
#: :obj:`str`: Telegram Bot API #: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as #: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`. #: :data:`telegram.__bot_api_version__`.
@ -809,3 +811,41 @@ class UpdateType(StringEnum):
""":obj:`str`: Updates with :attr:`telegram.Update.chat_member`.""" """:obj:`str`: Updates with :attr:`telegram.Update.chat_member`."""
CHAT_JOIN_REQUEST = "chat_join_request" CHAT_JOIN_REQUEST = "chat_join_request"
""":obj:`str`: Updates with :attr:`telegram.Update.chat_join_request`.""" """:obj:`str`: Updates with :attr:`telegram.Update.chat_join_request`."""
class InvoiceLimit(IntEnum):
"""This enum contains limitations for :meth:`telegram.Bot.create_invoice_link`. The enum
members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 20.0
"""
__slots__ = ()
MIN_TITLE_LENGTH = 1
""":obj:`int`: Minimum number of characters of the invoice title."""
MAX_TITLE_LENGTH = 32
""":obj:`int`: Maximum number of characters of the invoice title."""
MIN_DESCRIPTION_LENGTH = 1
""":obj:`int`: Minimum number of characters of the invoice description."""
MAX_DESCRIPTION_LENGTH = 255
""":obj:`int`: Maximum number of characters of the invoice description."""
MIN_PAYLOAD_LENGTH = 1
""":obj:`int`: Minimum amount of bytes for the internal payload."""
MAX_PAYLOAD_LENGTH = 128
""":obj:`int`: Maximum amount of bytes for the internal payload."""
class WebhookLimit(IntEnum):
"""This enum contains limitations for :paramref:`telegram.Bot.set_webhook.secret_token`. The
enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: 20.0
"""
__slots__ = ()
MIN_SECRET_TOKEN_LENGTH = 1
""":obj:`int`: Minimum length of the secret token."""
MAX_SECRET_TOKEN_LENGTH = 256
""":obj:`int`: Maximum length of the secret token."""

View file

@ -662,6 +662,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
max_connections: int = 40, max_connections: int = 40,
close_loop: bool = True, close_loop: bool = True,
stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE, stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE,
secret_token: str = None,
) -> None: ) -> None:
"""Convenience method that takes care of initializing and starting the app, """Convenience method that takes care of initializing and starting the app,
polling updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and polling updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and
@ -724,6 +725,16 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
:meth:`asyncio.loop.add_signal_handler`. Most notably, the standard event loop :meth:`asyncio.loop.add_signal_handler`. Most notably, the standard event loop
on Windows, :class:`asyncio.ProactorEventLoop`, does not implement this method. on Windows, :class:`asyncio.ProactorEventLoop`, does not implement this method.
If this method is not available, stop signals can not be set. If this method is not available, stop signals can not be set.
secret_token (:obj:`str`, optional): Secret token to ensure webhook requests originate
from Telegram. See :paramref:`telegram.Bot.set_webhook.secret_token` for more
details.
When added, the web server started by this call will expect the token to be set in
the ``X-Telegram-Bot-Api-Secret-Token`` header of an incoming request and will
raise a :class:`http.HTTPStatus.FORBIDDEN <http.HTTPStatus>` error if either the
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
""" """
if not self.updater: if not self.updater:
raise RuntimeError( raise RuntimeError(
@ -743,6 +754,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AbstractAsyncContextManager)
allowed_updates=allowed_updates, allowed_updates=allowed_updates,
ip_address=ip_address, ip_address=ip_address,
max_connections=max_connections, max_connections=max_connections,
secret_token=secret_token,
), ),
close_loop=close_loop, close_loop=close_loop,
stop_signals=stop_signals, stop_signals=stop_signals,

View file

@ -369,6 +369,7 @@ class Updater(AbstractAsyncContextManager):
drop_pending_updates: bool = None, drop_pending_updates: bool = None,
ip_address: str = None, ip_address: str = None,
max_connections: int = 40, max_connections: int = 40,
secret_token: str = None,
) -> asyncio.Queue: ) -> asyncio.Queue:
""" """
Starts a small http server to listen for updates via webhook. If :paramref:`cert` Starts a small http server to listen for updates via webhook. If :paramref:`cert`
@ -395,6 +396,7 @@ class Updater(AbstractAsyncContextManager):
key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file. key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file.
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
Telegram servers before actually starting to poll. Default is :obj:`False`. Telegram servers before actually starting to poll. Default is :obj:`False`.
.. versionadded :: 13.4 .. versionadded :: 13.4
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
:class:`telegram.ext.Updater` will retry on failures on the Telegram server. :class:`telegram.ext.Updater` will retry on failures on the Telegram server.
@ -407,12 +409,23 @@ class Updater(AbstractAsyncContextManager):
:paramref:`port`, :paramref:`url_path`, :paramref:`cert`, and :paramref:`key`. :paramref:`port`, :paramref:`url_path`, :paramref:`cert`, and :paramref:`key`.
ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.
Defaults to :obj:`None`. Defaults to :obj:`None`.
.. versionadded :: 13.4 .. versionadded :: 13.4
allowed_updates (List[:obj:`str`], optional): Passed to allowed_updates (List[:obj:`str`], optional): Passed to
:meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`. :meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`.
max_connections (:obj:`int`, optional): Passed to max_connections (:obj:`int`, optional): Passed to
:meth:`telegram.Bot.set_webhook`. Defaults to ``40``. :meth:`telegram.Bot.set_webhook`. Defaults to ``40``.
.. versionadded:: 13.6 .. versionadded:: 13.6
secret_token (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.
Defaults to :obj:`None`.
When added, the web server started by this call will expect the token to be set in
the ``X-Telegram-Bot-Api-Secret-Token`` header of an incoming request and will
raise a :class:`http.HTTPStatus.FORBIDDEN <http.HTTPStatus>` error if either the
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
Returns: Returns:
:class:`queue.Queue`: The update queue that can be filled from the main thread. :class:`queue.Queue`: The update queue that can be filled from the main thread.
@ -444,6 +457,7 @@ class Updater(AbstractAsyncContextManager):
ready=webhook_ready, ready=webhook_ready,
ip_address=ip_address, ip_address=ip_address,
max_connections=max_connections, max_connections=max_connections,
secret_token=secret_token,
) )
self._logger.debug("Waiting for webhook server to start") self._logger.debug("Waiting for webhook server to start")
@ -470,6 +484,7 @@ class Updater(AbstractAsyncContextManager):
ready: asyncio.Event = None, ready: asyncio.Event = None,
ip_address: str = None, ip_address: str = None,
max_connections: int = 40, max_connections: int = 40,
secret_token: str = None,
) -> None: ) -> None:
self._logger.debug("Updater thread started (webhook)") self._logger.debug("Updater thread started (webhook)")
@ -477,7 +492,7 @@ class Updater(AbstractAsyncContextManager):
url_path = f"/{url_path}" url_path = f"/{url_path}"
# Create Tornado app instance # Create Tornado app instance
app = WebhookAppClass(url_path, self.bot, self.update_queue) app = WebhookAppClass(url_path, self.bot, self.update_queue, secret_token)
# Form SSL Context # Form SSL Context
# An SSLError is raised if the private key does not match with the certificate # An SSLError is raised if the private key does not match with the certificate
@ -517,6 +532,7 @@ class Updater(AbstractAsyncContextManager):
allowed_updates=allowed_updates, allowed_updates=allowed_updates,
ip_address=ip_address, ip_address=ip_address,
max_connections=max_connections, max_connections=max_connections,
secret_token=secret_token,
) )
await self._httpd.serve_forever(ready=ready) await self._httpd.serve_forever(ready=ready)
@ -591,6 +607,7 @@ class Updater(AbstractAsyncContextManager):
bootstrap_interval: float = 1, bootstrap_interval: float = 1,
ip_address: str = None, ip_address: str = None,
max_connections: int = 40, max_connections: int = 40,
secret_token: str = None,
) -> None: ) -> None:
"""Prepares the setup for fetching updates: delete or set the webhook and drop pending """Prepares the setup for fetching updates: delete or set the webhook and drop pending
updates if appropriate. If there are unsuccessful attempts, this will retry as specified by updates if appropriate. If there are unsuccessful attempts, this will retry as specified by
@ -616,6 +633,7 @@ class Updater(AbstractAsyncContextManager):
ip_address=ip_address, ip_address=ip_address,
drop_pending_updates=drop_pending_updates, drop_pending_updates=drop_pending_updates,
max_connections=max_connections, max_connections=max_connections,
secret_token=secret_token,
) )
return False return False

View file

@ -83,8 +83,14 @@ class WebhookServer:
class WebhookAppClass(tornado.web.Application): class WebhookAppClass(tornado.web.Application):
"""Application used in the Webserver""" """Application used in the Webserver"""
def __init__(self, webhook_path: str, bot: "Bot", update_queue: asyncio.Queue): def __init__(
self.shared_objects = {"bot": bot, "update_queue": update_queue} self, webhook_path: str, bot: "Bot", update_queue: asyncio.Queue, secret_token: str = None
):
self.shared_objects = {
"bot": bot,
"update_queue": update_queue,
"secret_token": secret_token,
}
handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)] # noqa handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)] # noqa
tornado.web.Application.__init__(self, handlers) # type: ignore tornado.web.Application.__init__(self, handlers) # type: ignore
@ -96,16 +102,21 @@ class WebhookAppClass(tornado.web.Application):
class TelegramHandler(tornado.web.RequestHandler): class TelegramHandler(tornado.web.RequestHandler):
"""BaseHandler that processes incoming requests from Telegram""" """BaseHandler that processes incoming requests from Telegram"""
__slots__ = ("bot", "update_queue", "_logger") __slots__ = ("bot", "update_queue", "_logger", "secret_token")
SUPPORTED_METHODS = ("POST",) # type: ignore[assignment] SUPPORTED_METHODS = ("POST",) # type: ignore[assignment]
def initialize(self, bot: "Bot", update_queue: asyncio.Queue) -> None: def initialize(self, bot: "Bot", update_queue: asyncio.Queue, secret_token: str) -> None:
"""Initialize for each request - that's the interface provided by tornado""" """Initialize for each request - that's the interface provided by tornado"""
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
self.bot = bot self.bot = bot
self.update_queue = update_queue self.update_queue = update_queue
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
self.secret_token = secret_token
if secret_token:
self._logger.debug(
"The webhook server has a secret token, " "expecting it in incoming requests now"
)
def set_default_headers(self) -> None: def set_default_headers(self) -> None:
"""Sets default headers""" """Sets default headers"""
@ -144,6 +155,19 @@ class TelegramHandler(tornado.web.RequestHandler):
ct_header = self.request.headers.get("Content-Type", None) ct_header = self.request.headers.get("Content-Type", None)
if ct_header != "application/json": if ct_header != "application/json":
raise tornado.web.HTTPError(HTTPStatus.FORBIDDEN) raise tornado.web.HTTPError(HTTPStatus.FORBIDDEN)
# verifying that the secret token is the one the user set when the user set one
if self.secret_token is not None:
token = self.request.headers.get("X-Telegram-Bot-Api-Secret-Token")
if not token:
self._logger.debug("Request did not include the secret token")
raise tornado.web.HTTPError(
HTTPStatus.FORBIDDEN, reason="Request did not include the secret token"
)
if token != self.secret_token:
self._logger.debug("Request had the wrong secret token: %s", token)
raise tornado.web.HTTPError(
HTTPStatus.FORBIDDEN, reason="Request had the wrong secret token"
)
def log_exception( def log_exception(
self, self,

View file

@ -76,6 +76,8 @@ __all__ = (
"TEXT", "TEXT",
"Text", "Text",
"USER", "USER",
"USER_ATTACHMENT",
"PREMIUM_USER",
"UpdateFilter", "UpdateFilter",
"UpdateType", "UpdateType",
"User", "User",
@ -1949,6 +1951,21 @@ class Sticker:
.. versionadded:: 20.0 .. versionadded:: 20.0
""" """
class _Premium(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.sticker) and bool(
message.sticker.premium_animation # type: ignore
)
PREMIUM = _Premium(name="filters.Sticker.PREMIUM")
"""Messages that contain :attr:`telegram.Message.sticker` and have a
:attr:`premium animation <telegram.Sticker.premium_animation>`.
.. versionadded:: 20.0
"""
class _SuccessfulPayment(MessageFilter): class _SuccessfulPayment(MessageFilter):
__slots__ = () __slots__ = ()
@ -2185,6 +2202,41 @@ USER = _User(name="filters.USER")
"""This filter filters *any* message that has a :attr:`telegram.Message.from_user`.""" """This filter filters *any* message that has a :attr:`telegram.Message.from_user`."""
class _UserAttachment(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return bool(update.effective_user) and bool(
update.effective_user.added_to_attachment_menu # type: ignore
)
USER_ATTACHMENT = _UserAttachment(name="filters.USER_ATTACHMENT")
"""This filter filters *any* message that have a user who added the bot to their
:attr:`attachment menu <telegram.User.added_to_attachment_menu>` as
:attr:`telegram.Update.effective_user`.
.. versionadded:: 20.0
"""
class _UserPremium(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return bool(update.effective_user) and bool(
update.effective_user.is_premium # type: ignore
)
PREMIUM_USER = _UserPremium(name="filters.PREMIUM_USER")
"""This filter filters *any* message from a
:attr:`Telegram Premium user <telegram.User.is_premium>` as :attr:`telegram.Update.effective_user`.
.. versionadded:: 20.0
"""
class _Venue(MessageFilter): class _Venue(MessageFilter):
__slots__ = () __slots__ = ()

View file

@ -830,10 +830,13 @@ async def send_webhook_message(
content_len: int = -1, content_len: int = -1,
content_type: str = "application/json", content_type: str = "application/json",
get_method: str = None, get_method: str = None,
secret_token: str = None,
) -> Response: ) -> Response:
headers = { headers = {
"content-type": content_type, "content-type": content_type,
} }
if secret_token:
headers["X-Telegram-Bot-Api-Secret-Token"] = secret_token
if not payload_str: if not payload_str:
content_len = None content_len = None

View file

@ -1634,6 +1634,35 @@ 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):
# 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/
async def make_assertion(*args, **_):
kwargs = args[1]
return (
kwargs["url"] == "example.com"
and kwargs["certificate"].input_file_content
== data_file("sslcert.pem").read_bytes()
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 await bot.set_webhook(
"example.com",
data_file("sslcert.pem").read_bytes(),
7,
["messages"],
"127.0.0.1",
True,
"SoSecretToken",
)
@flaky(3, 1) @flaky(3, 1)
async def test_leave_chat(self, bot): async def test_leave_chat(self, bot):
with pytest.raises(BadRequest, match="Chat not found"): with pytest.raises(BadRequest, match="Chat not found"):
@ -1833,7 +1862,7 @@ class TestBot:
# We assume that the other game score tests ran within 20 sec # We assume that the other game score tests ran within 20 sec
assert high_scores[0].score == BASE_GAME_SCORE - 10 assert high_scores[0].score == BASE_GAME_SCORE - 10
# send_invoice is tested in test_invoice # send_invoice and create_invoice_link is tested in test_invoice
# TODO: Needs improvement. Need incoming shipping queries to test # TODO: Needs improvement. Need incoming shipping queries to test
async def test_answer_shipping_query_ok(self, monkeypatch, bot): async def test_answer_shipping_query_ok(self, monkeypatch, bot):

View file

@ -42,6 +42,8 @@ def chat(bot):
location=TestChat.location, location=TestChat.location,
has_private_forwards=True, has_private_forwards=True,
has_protected_content=True, has_protected_content=True,
join_to_send_messages=True,
join_by_request=True,
) )
@ -64,6 +66,8 @@ class TestChat:
location = ChatLocation(Location(123, 456), "Barbie World") location = ChatLocation(Location(123, 456), "Barbie World")
has_protected_content = True has_protected_content = True
has_private_forwards = True has_private_forwards = True
join_to_send_messages = True
join_by_request = True
def test_slot_behaviour(self, chat, mro_slots): def test_slot_behaviour(self, chat, mro_slots):
for attr in chat.__slots__: for attr in chat.__slots__:
@ -86,6 +90,8 @@ class TestChat:
"has_private_forwards": self.has_private_forwards, "has_private_forwards": self.has_private_forwards,
"linked_chat_id": self.linked_chat_id, "linked_chat_id": self.linked_chat_id,
"location": self.location.to_dict(), "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) chat = Chat.de_json(json_dict, bot)
@ -104,6 +110,8 @@ class TestChat:
assert chat.linked_chat_id == self.linked_chat_id assert chat.linked_chat_id == self.linked_chat_id
assert chat.location.location == self.location.location assert chat.location.location == self.location.location
assert chat.location.address == self.location.address 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): def test_to_dict(self, chat):
chat_dict = chat.to_dict() chat_dict = chat.to_dict()
@ -121,6 +129,8 @@ class TestChat:
assert chat_dict["has_protected_content"] == chat.has_protected_content assert chat_dict["has_protected_content"] == chat.has_protected_content
assert chat_dict["linked_chat_id"] == chat.linked_chat_id assert chat_dict["linked_chat_id"] == chat.linked_chat_id
assert chat_dict["location"] == chat.location.to_dict() 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_enum_init(self): def test_enum_init(self):
chat = Chat(id=1, type="foo") chat = Chat(id=1, type="foo")

View file

@ -27,6 +27,7 @@ from telegram import (
Chat, Chat,
Dice, Dice,
Document, Document,
File,
Message, Message,
MessageEntity, MessageEntity,
Sticker, Sticker,
@ -830,15 +831,26 @@ class TestFilters:
update.message.sticker = Sticker("1", "uniq", 1, 2, False, False) update.message.sticker = Sticker("1", "uniq", 1, 2, False, False)
assert filters.Sticker.ALL.check_update(update) assert filters.Sticker.ALL.check_update(update)
assert filters.Sticker.STATIC.check_update(update) assert filters.Sticker.STATIC.check_update(update)
assert not filters.Sticker.VIDEO.check_update(update)
assert not filters.Sticker.PREMIUM.check_update(update)
update.message.sticker.is_animated = True update.message.sticker.is_animated = True
assert filters.Sticker.ANIMATED.check_update(update) assert filters.Sticker.ANIMATED.check_update(update)
assert not filters.Sticker.VIDEO.check_update(update) assert not filters.Sticker.VIDEO.check_update(update)
assert not filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.STATIC.check_update(update)
assert not filters.Sticker.PREMIUM.check_update(update)
update.message.sticker.is_animated = False update.message.sticker.is_animated = False
update.message.sticker.is_video = True update.message.sticker.is_video = True
assert not filters.Sticker.ANIMATED.check_update(update) assert not filters.Sticker.ANIMATED.check_update(update)
assert not filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.STATIC.check_update(update)
assert filters.Sticker.VIDEO.check_update(update) assert filters.Sticker.VIDEO.check_update(update)
assert not filters.Sticker.PREMIUM.check_update(update)
update.message.sticker.premium_animation = File("string", "uniqueString")
assert not filters.Sticker.ANIMATED.check_update(update)
# premium stickers can be animated, video, or probably also static,
# it doesn't really matter for the test
assert not filters.Sticker.STATIC.check_update(update)
assert filters.Sticker.VIDEO.check_update(update)
assert filters.Sticker.PREMIUM.check_update(update)
def test_filters_video(self, update): def test_filters_video(self, update):
assert not filters.VIDEO.check_update(update) assert not filters.VIDEO.check_update(update)
@ -1168,6 +1180,19 @@ class TestFilters:
with pytest.raises(RuntimeError, match="Cannot set name"): with pytest.raises(RuntimeError, match="Cannot set name"):
f.name = "foo" f.name = "foo"
def test_filters_user_attributes(self, update):
assert not filters.USER_ATTACHMENT.check_update(update)
assert not filters.PREMIUM_USER.check_update(update)
update.message.from_user.added_to_attachment_menu = True
assert filters.USER_ATTACHMENT.check_update(update)
assert not filters.PREMIUM_USER.check_update(update)
update.message.from_user.is_premium = True
assert filters.USER_ATTACHMENT.check_update(update)
assert filters.PREMIUM_USER.check_update(update)
update.message.from_user.added_to_attachment_menu = False
assert not filters.USER_ATTACHMENT.check_update(update)
assert filters.PREMIUM_USER.check_update(update)
def test_filters_chat_init(self): def test_filters_chat_init(self):
with pytest.raises(RuntimeError, match="in conjunction with"): with pytest.raises(RuntimeError, match="in conjunction with"):
filters.Chat(chat_id=1, username="chat") filters.Chat(chat_id=1, username="chat")

View file

@ -98,8 +98,18 @@ class TestInvoice:
assert message.invoice.title == self.title assert message.invoice.title == self.title
assert message.invoice.total_amount == self.total_amount assert message.invoice.total_amount == self.total_amount
@flaky(3, 1) link = await bot.create_invoice_link(
async def test_send_all_args(self, bot, chat_id, provider_token, monkeypatch): 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 != ""
async def test_send_all_args_send_invoice(self, bot, chat_id, provider_token, monkeypatch):
message = await bot.send_invoice( message = await bot.send_invoice(
chat_id, chat_id,
self.title, self.title,
@ -193,6 +203,58 @@ class TestInvoice:
protect_content=True, protect_content=True,
) )
async def test_send_all_args_create_invoice_link(
self, bot, chat_id, provider_token, monkeypatch
):
async 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"] == 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 await 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",
)
async def test_send_object_as_provider_data(self, monkeypatch, bot, chat_id, provider_token): async def test_send_object_as_provider_data(self, monkeypatch, bot, chat_id, provider_token):
async def make_assertion(url, request_data: RequestData, *args, **kwargs): async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.json_parameters["provider_data"] == '{"test_data": 123456789}' return request_data.json_parameters["provider_data"] == '{"test_data": 123456789}'

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, MaskPosition, PhotoSize, Sticker, StickerSet from telegram import Audio, Bot, File, 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 (
@ -91,6 +91,8 @@ class TestSticker:
sticker_file_id = "5a3128a4d2a04750b5b58397f3b5e812" sticker_file_id = "5a3128a4d2a04750b5b58397f3b5e812"
sticker_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" 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): def test_slot_behaviour(self, sticker, mro_slots, recwarn):
for attr in sticker.__slots__: for attr in sticker.__slots__:
assert getattr(sticker, attr, "err") != "err", f"got extra slot '{attr}'" assert getattr(sticker, attr, "err") != "err", f"got extra slot '{attr}'"
@ -118,6 +120,8 @@ class TestSticker:
assert sticker.thumb.width == self.thumb_width assert sticker.thumb.width == self.thumb_width
assert sticker.thumb.height == self.thumb_height assert sticker.thumb.height == self.thumb_height
assert sticker.thumb.file_size == self.thumb_file_size 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) @flaky(3, 1)
async def test_send_all_args(self, bot, chat_id, sticker_file, sticker): async def test_send_all_args(self, bot, chat_id, sticker_file, sticker):
@ -135,6 +139,8 @@ class TestSticker:
assert message.sticker.is_animated == sticker.is_animated assert message.sticker.is_animated == sticker.is_animated
assert message.sticker.is_video == sticker.is_video assert message.sticker.is_video == sticker.is_video
assert message.sticker.file_size == sticker.file_size 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, PhotoSize)
assert isinstance(message.sticker.thumb.file_id, str) assert isinstance(message.sticker.thumb.file_id, str)
@ -212,6 +218,7 @@ class TestSticker:
"thumb": sticker.thumb.to_dict(), "thumb": sticker.thumb.to_dict(),
"emoji": self.emoji, "emoji": self.emoji,
"file_size": self.file_size, "file_size": self.file_size,
"premium_animation": self.premium_animation.to_dict(),
} }
json_sticker = Sticker.de_json(json_dict, bot) json_sticker = Sticker.de_json(json_dict, bot)
@ -224,6 +231,7 @@ class TestSticker:
assert json_sticker.emoji == self.emoji assert json_sticker.emoji == self.emoji
assert json_sticker.file_size == self.file_size assert json_sticker.file_size == self.file_size
assert json_sticker.thumb == sticker.thumb assert json_sticker.thumb == sticker.thumb
assert json_sticker.premium_animation == self.premium_animation
async def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker): async def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker):
async def make_assertion(url, request_data: RequestData, *args, **kwargs): async def make_assertion(url, request_data: RequestData, *args, **kwargs):
@ -317,6 +325,24 @@ class TestSticker:
with pytest.raises(TypeError): with pytest.raises(TypeError):
await bot.send_sticker(chat_id) await bot.send_sticker(chat_id)
@flaky(3, 1)
async 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 = await 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): def test_equality(self, sticker):
a = Sticker( a = Sticker(
sticker.file_id, sticker.file_id,

View file

@ -504,7 +504,10 @@ class TestUpdater:
@pytest.mark.parametrize("ext_bot", [True, False]) @pytest.mark.parametrize("ext_bot", [True, False])
@pytest.mark.parametrize("drop_pending_updates", (True, False)) @pytest.mark.parametrize("drop_pending_updates", (True, False))
async def test_webhook_basic(self, monkeypatch, updater, drop_pending_updates, ext_bot): @pytest.mark.parametrize("secret_token", ["SecretToken", None])
async def test_webhook_basic(
self, monkeypatch, updater, drop_pending_updates, ext_bot, secret_token
):
# Testing with both ExtBot and Bot to make sure any logic in WebhookHandler # Testing with both ExtBot and Bot to make sure any logic in WebhookHandler
# that depends on this distinction works # that depends on this distinction works
if ext_bot and not isinstance(updater.bot, ExtBot): if ext_bot and not isinstance(updater.bot, ExtBot):
@ -533,13 +536,16 @@ class TestUpdater:
ip_address=ip, ip_address=ip,
port=port, port=port,
url_path="TOKEN", url_path="TOKEN",
secret_token=secret_token,
) )
assert return_value is updater.update_queue assert return_value is updater.update_queue
assert updater.running assert updater.running
# Now, we send an update to the server # Now, we send an update to the server
update = make_message_update("Webhook") update = make_message_update("Webhook")
await send_webhook_message(ip, port, update.to_json(), "TOKEN") await send_webhook_message(
ip, port, update.to_json(), "TOKEN", secret_token=secret_token
)
assert (await updater.update_queue.get()).to_dict() == update.to_dict() assert (await updater.update_queue.get()).to_dict() == update.to_dict()
# Returns Not Found if path is incorrect # Returns Not Found if path is incorrect
@ -550,6 +556,22 @@ class TestUpdater:
response = await send_webhook_message(ip, port, None, "TOKEN", get_method="HEAD") response = await send_webhook_message(ip, port, None, "TOKEN", get_method="HEAD")
assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED assert response.status_code == HTTPStatus.METHOD_NOT_ALLOWED
if secret_token:
# Returns Forbidden if no secret token is set
response_text = "<html><title>403: {0}</title><body>403: {0}</body></html>"
response = await send_webhook_message(ip, port, update.to_json(), "TOKEN")
assert response.status_code == HTTPStatus.FORBIDDEN
assert response.text == response_text.format(
"Request did not include the secret token"
)
# Returns Forbidden if the secret token is wrong
response = await send_webhook_message(
ip, port, update.to_json(), "TOKEN", secret_token="NotTheSecretToken"
)
assert response.status_code == HTTPStatus.FORBIDDEN
assert response.text == response_text.format("Request had the wrong secret token")
await updater.stop() await updater.stop()
assert not updater.running assert not updater.running
@ -600,6 +622,7 @@ class TestUpdater:
max_connections=40, max_connections=40,
allowed_updates=None, allowed_updates=None,
ip_address=None, ip_address=None,
secret_token=None,
**expected_delete_webhook, **expected_delete_webhook,
) )
@ -641,6 +664,7 @@ class TestUpdater:
max_connections=47, max_connections=47,
allowed_updates=["message"], allowed_updates=["message"],
ip_address="123.456.789", ip_address="123.456.789",
secret_token=None,
**expected_delete_webhook, **expected_delete_webhook,
) )

View file

@ -35,6 +35,8 @@ def json_dict():
"can_join_groups": TestUser.can_join_groups, "can_join_groups": TestUser.can_join_groups,
"can_read_all_group_messages": TestUser.can_read_all_group_messages, "can_read_all_group_messages": TestUser.can_read_all_group_messages,
"supports_inline_queries": TestUser.supports_inline_queries, "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, can_read_all_group_messages=TestUser.can_read_all_group_messages,
supports_inline_queries=TestUser.supports_inline_queries, supports_inline_queries=TestUser.supports_inline_queries,
bot=bot, 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_join_groups = True
can_read_all_group_messages = True can_read_all_group_messages = True
supports_inline_queries = False supports_inline_queries = False
is_premium = True
added_to_attachment_menu = False
def test_slot_behaviour(self, user, mro_slots): def test_slot_behaviour(self, user, mro_slots):
for attr in user.__slots__: for attr in user.__slots__:
@ -82,6 +88,8 @@ class TestUser:
assert user.can_join_groups == self.can_join_groups assert user.can_join_groups == self.can_join_groups
assert user.can_read_all_group_messages == self.can_read_all_group_messages assert user.can_read_all_group_messages == self.can_read_all_group_messages
assert user.supports_inline_queries == self.supports_inline_queries 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): def test_de_json_without_username(self, json_dict, bot):
del json_dict["username"] del json_dict["username"]
@ -97,6 +105,8 @@ class TestUser:
assert user.can_join_groups == self.can_join_groups assert user.can_join_groups == self.can_join_groups
assert user.can_read_all_group_messages == self.can_read_all_group_messages assert user.can_read_all_group_messages == self.can_read_all_group_messages
assert user.supports_inline_queries == self.supports_inline_queries 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): def test_de_json_without_username_and_last_name(self, json_dict, bot):
del json_dict["username"] del json_dict["username"]
@ -113,6 +123,8 @@ class TestUser:
assert user.can_join_groups == self.can_join_groups assert user.can_join_groups == self.can_join_groups
assert user.can_read_all_group_messages == self.can_read_all_group_messages assert user.can_read_all_group_messages == self.can_read_all_group_messages
assert user.supports_inline_queries == self.supports_inline_queries 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): def test_name(self, user):
assert user.name == "@username" assert user.name == "@username"