From cf6c298b8227bbcb16b93502e09eec9c29c31acf Mon Sep 17 00:00:00 2001 From: Poolitzer Date: Thu, 25 Aug 2022 19:36:55 +0200 Subject: [PATCH] API 6.2 (#3195) Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- README.rst | 4 +- README_RAW.rst | 4 +- docs/source/inclusions/bot_methods.rst | 2 + telegram/_bot.py | 79 +++++++++++++-- telegram/_chat.py | 14 +++ telegram/_files/sticker.py | 57 +++++++++-- telegram/_message.py | 34 +++++++ telegram/_messageentity.py | 23 ++++- telegram/constants.py | 42 +++++++- telegram/ext/filters.py | 1 + tests/test_bot.py | 4 +- tests/test_chat.py | 13 +++ tests/test_filters.py | 2 +- tests/test_message.py | 62 +++++++++++- tests/test_photo.py | 10 +- tests/test_sticker.py | 127 ++++++++++++++++++++++--- 16 files changed, 436 insertions(+), 42 deletions(-) diff --git a/README.rst b/README.rst index dbf6ee3f6..1d0a13b8f 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -93,7 +93,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.1** are supported. +All types and methods of the Telegram Bot API **6.2** are supported. Installing ========== diff --git a/README_RAW.rst b/README_RAW.rst index 04e1ce75f..1ea1f9af4 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -14,7 +14,7 @@ :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.1-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API versions @@ -89,7 +89,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.1** are supported. +All types and methods of the Telegram Bot API **6.2** are supported. Installing ========== diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 282c5669b..3be887306 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -206,6 +206,8 @@ - Used for getting a sticker set * - :meth:`~telegram.Bot.upload_sticker_file` - Used for uploading a sticker file + * - :meth:`~telegram.Bot.get_custom_emoji_stickers` + - Used for getting custom emoji files based on their IDs .. raw:: html diff --git a/telegram/_bot.py b/telegram/_bot.py index fda5cf2f3..fcfeeaaa0 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -6217,6 +6217,61 @@ class Bot(TelegramObject, AbstractAsyncContextManager): ) return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type] + @_log + async def get_custom_emoji_stickers( + self, + custom_emoji_ids: List[str], + *, + 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, + ) -> List[Sticker]: + # skipcq: FLK-D207 + """ + Use this method to get information about emoji stickers by their identifiers. + + Args: + custom_emoji_ids (List[:obj:`str`]): List of custom emoji identifiers. + At most :tg-const:`telegram.constants.CustomEmojiStickerLimit.\ +CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. + + 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: + List[:class:`telegram.Sticker`] + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"custom_emoji_ids": custom_emoji_ids} + result = await self._post( + "getCustomEmojiStickers", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return Sticker.de_list(result, self) # type: ignore[return-value, arg-type] + @_log async def upload_sticker_file( self, @@ -6290,10 +6345,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager): title: str, emojis: str, png_sticker: FileInput = None, - contains_masks: bool = None, mask_position: MaskPosition = None, tgs_sticker: FileInput = None, webm_sticker: FileInput = None, + sticker_type: str = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = 20, @@ -6304,8 +6359,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """ Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. - You must use exactly one of the fields :paramref:`png_sticker`, :paramref:`tgs_sticker`, or - :paramref:`webm_sticker`. + You must use exactly one of the fields :paramref:`png_sticker`, :paramref:`tgs_sticker`, + or :paramref:`webm_sticker`. Warning: As of API 4.7 :paramref:`png_sticker` is an optional argument and therefore the order @@ -6316,6 +6371,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager): The :paramref:`png_sticker` and :paramref:`tgs_sticker` argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` + .. versionchanged:: 20.0 + The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` + instead. + Args: user_id (:obj:`int`): User identifier of created sticker set owner. name (:obj:`str`): Short name of sticker set, to be used in t.me/addstickers/ URLs @@ -6349,10 +6408,14 @@ class Bot(TelegramObject, AbstractAsyncContextManager): .. versionadded:: 13.11 emojis (:obj:`str`): One or more emoji corresponding to the sticker. - contains_masks (:obj:`bool`, optional): Pass :obj:`True`, if a set of mask stickers - should be created. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should be placed on faces. + sticker_type (:obj:`str`, optional): Type of stickers in the set, pass + :attr:`telegram.Sticker.REGULAR` or :attr:`telegram.Sticker.MASK`. Custom emoji + sticker sets can't be created via the Bot API at the moment. By default, a + regular sticker set is created. + + .. versionadded:: 20.0 Keyword Args: read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to @@ -6385,10 +6448,10 @@ class Bot(TelegramObject, AbstractAsyncContextManager): data["tgs_sticker"] = parse_file_input(tgs_sticker) if webm_sticker is not None: data["webm_sticker"] = parse_file_input(webm_sticker) - if contains_masks is not None: - data["contains_masks"] = contains_masks if mask_position is not None: data["mask_position"] = mask_position + if sticker_type is not None: + data["sticker_type"] = sticker_type result = await self._post( "createNewStickerSet", @@ -8034,6 +8097,8 @@ class Bot(TelegramObject, AbstractAsyncContextManager): """Alias for :meth:`unpin_chat_message`""" unpinAllChatMessages = unpin_all_chat_messages """Alias for :meth:`unpin_all_chat_messages`""" + getCustomEmojiStickers = get_custom_emoji_stickers + """Alias for :meth:`get_custom_emoji_stickers`""" getStickerSet = get_sticker_set """Alias for :meth:`get_sticker_set`""" uploadStickerFile = upload_sticker_file diff --git a/telegram/_chat.py b/telegram/_chat.py index 0b7074deb..da82d65a1 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -134,6 +134,12 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. .. versionadded:: 20.0 + has_restricted_voice_and_video_messages (:obj:`bool`, optional): :obj:`True`, if the + privacy settings of the other party restrict sending voice and video note messages + in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. + + .. versionadded:: 20.0 + **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -187,6 +193,11 @@ class Chat(TelegramObject): joining the supergroup need to be approved by supergroup administrators. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 + has_restricted_voice_and_video_messages (:obj:`bool`): Optional. :obj:`True`, if the + privacy settings of the other party restrict sending voice and video note messages + in the private chat. Returned only in :meth:`telegram.Bot.get_chat`. + .. versionadded:: 20.0 """ @@ -215,6 +226,7 @@ class Chat(TelegramObject): "has_private_forwards", "join_to_send_messages", "join_by_request", + "has_restricted_voice_and_video_messages", ) SENDER: ClassVar[str] = constants.ChatType.SENDER @@ -256,6 +268,7 @@ class Chat(TelegramObject): has_protected_content: bool = None, join_to_send_messages: bool = None, join_by_request: bool = None, + has_restricted_voice_and_video_messages: bool = None, **_kwargs: Any, ): # Required @@ -286,6 +299,7 @@ class Chat(TelegramObject): self.location = location self.join_to_send_messages = join_to_send_messages self.join_by_request = join_by_request + self.has_restricted_voice_and_video_messages = has_restricted_voice_and_video_messages self.set_bot(bot) self._id_attrs = (self.id,) diff --git a/telegram/_files/sticker.py b/telegram/_files/sticker.py index 564e9e5c4..45ddea8bb 100644 --- a/telegram/_files/sticker.py +++ b/telegram/_files/sticker.py @@ -54,6 +54,11 @@ class Sticker(_BaseThumbedMedium): is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker. .. versionadded:: 13.11 + type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`, + :attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its + format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`. + + .. versionadded:: 20.0 thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the ``.WEBP`` or ``.JPG`` format. emoji (:obj:`str`, optional): Emoji associated with the sticker @@ -63,8 +68,12 @@ class Sticker(_BaseThumbedMedium): position where the mask should be placed. file_size (:obj:`int`, optional): File size in bytes. 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. + premium_animation (:class:`telegram.File`, optional): For premium regular stickers, + premium animation for the sticker. + + .. versionadded:: 20.0 + custom_emoji (:obj:`str`, optional): For custom emoji stickers, unique identifier of the + custom emoji. .. versionadded:: 20.0 _kwargs (:obj:`dict`): Arbitrary keyword arguments. @@ -80,6 +89,11 @@ class Sticker(_BaseThumbedMedium): is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker. .. versionadded:: 13.11 + type (:obj:`str`): Type of the sticker. Currently one of :attr:`REGULAR`, + :attr:`MASK`, :attr:`CUSTOM_EMOJI`. The type of the sticker is independent from its + format, which is determined by the fields :attr:`is_animated` and :attr:`is_video`. + + .. versionadded:: 20.0 thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the ``.WEBP`` or ``.JPG`` format. emoji (:obj:`str`): Optional. Emoji associated with the sticker. @@ -88,11 +102,14 @@ class Sticker(_BaseThumbedMedium): where the mask should be placed. file_size (:obj:`int`): Optional. File size in bytes. 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. + premium_animation (:class:`telegram.File`): Optional. For premium regular stickers, + premium animation for the sticker. .. versionadded:: 20.0 + custom_emoji (:obj:`str`): Optional. For custom emoji stickers, unique identifier of the + custom emoji. + .. versionadded:: 20.0 """ __slots__ = ( @@ -104,6 +121,8 @@ class Sticker(_BaseThumbedMedium): "set_name", "width", "premium_animation", + "type", + "custom_emoji_id", ) def __init__( @@ -114,6 +133,7 @@ class Sticker(_BaseThumbedMedium): height: int, is_animated: bool, is_video: bool, + type: str, # pylint: disable=redefined-builtin thumb: PhotoSize = None, emoji: str = None, file_size: int = None, @@ -121,6 +141,7 @@ class Sticker(_BaseThumbedMedium): mask_position: "MaskPosition" = None, bot: "Bot" = None, premium_animation: "File" = None, + custom_emoji_id: str = None, **_kwargs: Any, ): super().__init__( @@ -135,11 +156,20 @@ class Sticker(_BaseThumbedMedium): self.height = height self.is_animated = is_animated self.is_video = is_video + self.type = type # Optional self.emoji = emoji self.set_name = set_name self.mask_position = mask_position self.premium_animation = premium_animation + self.custom_emoji_id = custom_emoji_id + + REGULAR: ClassVar[str] = constants.StickerType.REGULAR + """:const:`telegram.constants.StickerType.REGULAR`""" + MASK: ClassVar[str] = constants.StickerType.MASK + """:const:`telegram.constants.StickerType.MASK`""" + CUSTOM_EMOJI: ClassVar[str] = constants.StickerType.CUSTOM_EMOJI + """:const:`telegram.constants.StickerType.CUSTOM_EMOJI`""" @classmethod def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Sticker"]: @@ -167,6 +197,9 @@ class StickerSet(TelegramObject): arguments had to be changed. Use keyword arguments to make sure that the arguments are passed correctly. + .. versionchanged:: 20.0: + The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead. + Args: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. @@ -174,8 +207,12 @@ class StickerSet(TelegramObject): is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 - contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + sticker_type (:obj:`str`): Type of stickers in the set, currently one of + :attr:`telegram.Sticker.REGULAR`, :attr:`telegram.Sticker.MASK`, + :attr:`telegram.Sticker.CUSTOM_EMOJI`. + + .. versionadded:: 20.0 thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``, ``.TGS``, or ``.WEBM`` format. @@ -186,21 +223,23 @@ class StickerSet(TelegramObject): is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers. .. versionadded:: 13.11 - contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + sticker_type (:obj:`str`): Type of stickers in the set. + + .. versionadded:: 20.0 thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``, ``.TGS`` or ``.WEBM`` format. """ __slots__ = ( - "contains_masks", "is_animated", "is_video", "name", "stickers", "thumb", "title", + "sticker_type", ) def __init__( @@ -208,9 +247,9 @@ class StickerSet(TelegramObject): name: str, title: str, is_animated: bool, - contains_masks: bool, stickers: List[Sticker], is_video: bool, + sticker_type: str, thumb: PhotoSize = None, **_kwargs: Any, ): @@ -218,8 +257,8 @@ class StickerSet(TelegramObject): self.title = title self.is_animated = is_animated self.is_video = is_video - self.contains_masks = contains_masks self.stickers = stickers + self.sticker_type = sticker_type # Optional self.thumb = thumb diff --git a/telegram/_message.py b/telegram/_message.py index d3fa198f6..2c80aaa62 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -370,6 +370,8 @@ class Message(TelegramObject): to the message. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. + .. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored + by this function. Instead, the supplied replacement for the emoji will be used. """ # fmt: on @@ -2864,6 +2866,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as HTML in the same way the original message was formatted. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. @@ -2880,6 +2885,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. @@ -2897,6 +2905,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as HTML in the same way the original message was formatted. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. @@ -2913,6 +2924,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. @@ -3093,6 +3107,8 @@ class Message(TelegramObject): :tg-const:`telegram.constants.ParseMode.MARKDOWN` is a legacy mode, retained by Telegram for backward compatibility. You should use :meth:`text_markdown_v2` instead. + |custom_emoji_formatting_note| + Returns: :obj:`str`: Message text with entities formatted as Markdown. @@ -3111,6 +3127,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as Markdown in the same way the original message was formatted. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. @@ -3132,6 +3151,8 @@ class Message(TelegramObject): Telegram for backward compatibility. You should use :meth:`text_markdown_v2_urled` instead. + |custom_emoji_formatting_note| + Returns: :obj:`str`: Message text with entities formatted as Markdown. @@ -3150,6 +3171,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. @@ -3171,6 +3195,8 @@ class Message(TelegramObject): Telegram for backward compatibility. You should use :meth:`caption_markdown_v2` instead. + |custom_emoji_formatting_note| + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. @@ -3189,6 +3215,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as Markdown in the same way the original message was formatted. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. @@ -3212,6 +3241,8 @@ class Message(TelegramObject): Telegram for backward compatibility. You should use :meth:`caption_markdown_v2_urled` instead. + |custom_emoji_formatting_note| + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. @@ -3230,6 +3261,9 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. + Note: + |custom_emoji_formatting_note| + .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. diff --git a/telegram/_messageentity.py b/telegram/_messageentity.py index 1235ed86e..0debe9adf 100644 --- a/telegram/_messageentity.py +++ b/telegram/_messageentity.py @@ -44,7 +44,11 @@ class MessageEntity(TelegramObject): :attr:`URL`, :attr:`EMAIL`, :attr:`PHONE_NUMBER`, :attr:`BOLD` (bold text), :attr:`ITALIC` (italic text), :attr:`STRIKETHROUGH`, :attr:`SPOILER` (spoiler message), :attr:`CODE` (monowidth string), :attr:`PRE` (monowidth block), :attr:`TEXT_LINK` (for - clickable text URLs), :attr:`TEXT_MENTION` (for users without usernames). + clickable text URLs), :attr:`TEXT_MENTION` (for users without usernames), + :attr:`CUSTOM_EMOJI` (for inline custom emoji stickers). + + .. versionadded:: 20.0 + added inline custom emoji offset (:obj:`int`): Offset in UTF-16 code units to the start of the entity. length (:obj:`int`): Length of the entity in UTF-16 code units. url (:obj:`str`, optional): For :attr:`TEXT_LINK` only, url that will be opened after @@ -53,6 +57,11 @@ class MessageEntity(TelegramObject): user. language (:obj:`str`, optional): For :attr:`PRE` only, the programming language of the entity text. + custom_emoji_id (:obj:`str`, optional): For :attr:`CUSTOM_EMOJI` only, unique identifier + of the custom emoji. Use :meth:`telegram.Bot.get_custom_emoji_stickers` to get full + information about the sticker. + + .. versionadded:: 20.0 Attributes: type (:obj:`str`): Type of the entity. @@ -61,10 +70,13 @@ class MessageEntity(TelegramObject): url (:obj:`str`): Optional. Url that will be opened after user taps on the text. user (:class:`telegram.User`): Optional. The mentioned user. language (:obj:`str`): Optional. Programming language of the entity text. + custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji. + + .. versionadded:: 20.0 """ - __slots__ = ("length", "url", "user", "type", "language", "offset") + __slots__ = ("length", "url", "user", "type", "language", "offset", "custom_emoji_id") def __init__( self, @@ -74,6 +86,7 @@ class MessageEntity(TelegramObject): url: str = None, user: User = None, language: str = None, + custom_emoji_id: str = None, **_kwargs: Any, ): # Required @@ -84,6 +97,7 @@ class MessageEntity(TelegramObject): self.url = url self.user = user self.language = language + self.custom_emoji_id = custom_emoji_id self._id_attrs = (self.type, self.offset, self.length) @@ -134,5 +148,10 @@ class MessageEntity(TelegramObject): .. versionadded:: 13.10 """ + CUSTOM_EMOJI: ClassVar[str] = constants.MessageEntityType.CUSTOM_EMOJI + """:const:`telegram.constants.MessageEntityType.CUSTOM_EMOJI` + + .. versionadded:: 20.0 + """ ALL_TYPES: ClassVar[List[str]] = list(constants.MessageEntityType) """List[:obj:`str`]: A list of all available message entity types.""" diff --git a/telegram/constants.py b/telegram/constants.py index 57b378132..db3c072fa 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -39,6 +39,7 @@ __all__ = [ "ChatInviteLinkLimit", "ChatMemberStatus", "ChatType", + "CustomEmojiStickerLimit", "DiceEmoji", "FileSizeLimit", "FloodLimit", @@ -58,6 +59,7 @@ __all__ = [ "PollLimit", "PollType", "SUPPORTED_WEBHOOK_PORTS", + "StickerType", "WebhookLimit", "UpdateType", ] @@ -92,7 +94,7 @@ class _BotAPIVersion(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=1) +BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=2) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -277,6 +279,22 @@ class ChatType(StringEnum): """:obj:`str`: A :class:`telegram.Chat` that is a channel.""" +class CustomEmojiStickerLimit(IntEnum): + """This enum contains limitations for :meth:`telegram.Bot.get_custom_emoji_stickers`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + CUSTOM_EMOJI_IDENTIFIER_LIMIT = 200 + """:obj:`int`: Maximum amount of custom emoji identifiers which can be specified for the + :paramref:`~telegram.Bot.get_custom_emoji_stickers.custom_emoji_ids` parameter of + :meth:`telegram.Bot.get_custom_emoji_stickers`. + """ + + class DiceEmoji(StringEnum): """This enum contains the available emoji for :class:`telegram.Dice`/ :meth:`telegram.Bot.send_dice`. The enum @@ -605,6 +623,11 @@ class MessageEntityType(StringEnum): """:obj:`str`: Message entities representing strikethrough text.""" SPOILER = "spoiler" """:obj:`str`: Message entities representing spoiler text.""" + CUSTOM_EMOJI = "custom_emoji" + """:obj:`str`: Message entities representing inline custom emoji stickers. + + .. versionadded:: 20.0 + """ class MessageLimit(IntEnum): @@ -718,6 +741,23 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.video_chat_participants_invited`.""" +class StickerType(StringEnum): + """This enum contains the available types of :class:`telegram.Sticker`. The enum + members of this enumeration are instances of :class:`str` and can be treated as such. + + .. versionadded:: 20.0 + """ + + __slots__ = () + + REGULAR = "regular" + """:obj:`str`: Regular sticker.""" + MASK = "mask" + """:obj:`str`: Mask sticker.""" + CUSTOM_EMOJI = "custom_emoji" + """:obj:`str`: Custom emoji sticker.""" + + class ParseMode(StringEnum): """This enum contains the available parse modes. The enum members of this enumeration are instances of :class:`str` and can be treated as such. diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 3c730fc90..5a705ee3f 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1965,6 +1965,7 @@ class Sticker: .. versionadded:: 20.0 """ + # neither mask nor emoji can be a message.sticker, so no filters for them class _SuccessfulPayment(MessageFilter): diff --git a/tests/test_bot.py b/tests/test_bot.py index 402dbd383..a0b585373 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2239,8 +2239,8 @@ class TestBot: ) # get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set, - # set_sticker_position_in_set and delete_sticker_from_set are tested in the - # test_sticker module. + # set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers + # are tested in the test_sticker module. async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): # Use BaseException that's not a subclass of Exception such that diff --git a/tests/test_chat.py b/tests/test_chat.py index 979ce01b2..b3c89eff8 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -44,6 +44,7 @@ def chat(bot): has_protected_content=True, join_to_send_messages=True, join_by_request=True, + has_restricted_voice_and_video_messages=True, ) @@ -68,6 +69,7 @@ class TestChat: has_private_forwards = True join_to_send_messages = True join_by_request = True + has_restricted_voice_and_video_messages = True def test_slot_behaviour(self, chat, mro_slots): for attr in chat.__slots__: @@ -92,6 +94,9 @@ class TestChat: "location": self.location.to_dict(), "join_to_send_messages": self.join_to_send_messages, "join_by_request": self.join_by_request, + "has_restricted_voice_and_video_messages": ( + self.has_restricted_voice_and_video_messages + ), } chat = Chat.de_json(json_dict, bot) @@ -112,6 +117,10 @@ class TestChat: 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 + assert ( + chat.has_restricted_voice_and_video_messages + == self.has_restricted_voice_and_video_messages + ) def test_to_dict(self, chat): chat_dict = chat.to_dict() @@ -131,6 +140,10 @@ class TestChat: 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 + assert ( + chat_dict["has_restricted_voice_and_video_messages"] + == chat.has_restricted_voice_and_video_messages + ) def test_enum_init(self): chat = Chat(id=1, type="foo") diff --git a/tests/test_filters.py b/tests/test_filters.py index 23e285e28..a3fcb5c32 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -828,7 +828,7 @@ class TestFilters: def test_filters_sticker(self, update): assert not filters.Sticker.ALL.check_update(update) - update.message.sticker = Sticker("1", "uniq", 1, 2, False, False) + update.message.sticker = Sticker("1", "uniq", 1, 2, False, False, Sticker.REGULAR) assert filters.Sticker.ALL.check_update(update) assert filters.Sticker.STATIC.check_update(update) assert not filters.Sticker.VIDEO.check_update(update) diff --git a/tests/test_message.py b/tests/test_message.py index 1e904f457..8f3fb6fcf 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -105,7 +105,7 @@ def message(bot): ) }, {"photo": [PhotoSize("photo_id", "unique_id", 50, 50)], "caption": "photo_file"}, - {"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False)}, + {"sticker": Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR)}, {"video": Video("video_id", "unique_id", 12, 12, 12), "caption": "video_file"}, {"voice": Voice("voice_id", "unique_id", 5)}, {"video_note": VideoNote("video_note_id", "unique_id", 20, 12)}, @@ -542,6 +542,36 @@ class TestMessage: ) assert expected == message.text_markdown + @pytest.mark.parametrize( + "type_", + argvalues=[ + "text_html", + "text_html_urled", + "text_markdown", + "text_markdown_urled", + "text_markdown_v2", + "text_markdown_v2_urled", + ], + ) + def test_text_custom_emoji(self, type_): + text = "Look a custom emoji: 😎" + expected = "Look a custom emoji: 😎" + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + text=text, + entities=[emoji_entity], + ) + assert expected == message[type_] + def test_caption_html_simple(self): test_html_string = ( "Test for <bold, ita_lic, " @@ -651,6 +681,36 @@ class TestMessage: ) assert expected == message.caption_markdown + @pytest.mark.parametrize( + "type_", + argvalues=[ + "caption_html", + "caption_html_urled", + "caption_markdown", + "caption_markdown_urled", + "caption_markdown_v2", + "caption_markdown_v2_urled", + ], + ) + def test_caption_custom_emoji(self, type_): + caption = "Look a custom emoji: 😎" + expected = "Look a custom emoji: 😎" + emoji_entity = MessageEntity( + type=MessageEntity.CUSTOM_EMOJI, + offset=21, + length=2, + custom_emoji_id="5472409228461217725", + ) + message = Message( + 1, + from_user=self.from_user, + date=self.date, + chat=self.chat, + caption=caption, + caption_entities=[emoji_entity], + ) + assert expected == message[type_] + async def test_parse_entities_url_emoji(self): url = b"http://github.com/?unicode=\\u2713\\U0001f469".decode("unicode-escape") text = "some url" diff --git a/tests/test_photo.py b/tests/test_photo.py index 3c42ba9f2..7098bd09a 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -468,7 +468,15 @@ class TestPhoto: b = PhotoSize("", photo.file_unique_id, self.width, self.height) c = PhotoSize(photo.file_id, photo.file_unique_id, 0, 0) d = PhotoSize("", "", self.width, self.height) - e = Sticker(photo.file_id, photo.file_unique_id, self.width, self.height, False, False) + e = Sticker( + photo.file_id, + photo.file_unique_id, + self.width, + self.height, + False, + False, + Sticker.REGULAR, + ) assert a == b assert hash(a) == hash(b) diff --git a/tests/test_sticker.py b/tests/test_sticker.py index f44f1556e..80fc47f26 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -87,6 +87,8 @@ class TestSticker: thumb_width = 319 thumb_height = 320 thumb_file_size = 21472 + type = Sticker.REGULAR + custom_emoji_id = "ThisIsSuchACustomEmojiID" sticker_file_id = "5a3128a4d2a04750b5b58397f3b5e812" sticker_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e" @@ -120,6 +122,7 @@ class TestSticker: assert sticker.thumb.width == self.thumb_width assert sticker.thumb.height == self.thumb_height assert sticker.thumb.file_size == self.thumb_file_size + assert sticker.type == self.type # 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 @@ -139,6 +142,8 @@ class TestSticker: assert message.sticker.is_animated == sticker.is_animated assert message.sticker.is_video == sticker.is_video assert message.sticker.file_size == sticker.file_size + assert message.sticker.type == sticker.type + assert message.has_protected_content # 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 @@ -150,7 +155,6 @@ class TestSticker: assert message.sticker.thumb.width == sticker.thumb.width assert message.sticker.thumb.height == sticker.thumb.height assert message.sticker.thumb.file_size == sticker.thumb.file_size - assert message.has_protected_content @flaky(3, 1) async def test_get_and_download(self, bot, sticker): @@ -197,6 +201,7 @@ class TestSticker: assert message.sticker.is_animated == sticker.is_animated assert message.sticker.is_video == sticker.is_video assert message.sticker.file_size == sticker.file_size + assert message.sticker.type == sticker.type assert isinstance(message.sticker.thumb, PhotoSize) assert isinstance(message.sticker.thumb.file_id, str) @@ -219,6 +224,8 @@ class TestSticker: "emoji": self.emoji, "file_size": self.file_size, "premium_animation": self.premium_animation.to_dict(), + "type": self.type, + "custom_emoji_id": self.custom_emoji_id, } json_sticker = Sticker.de_json(json_dict, bot) @@ -232,6 +239,8 @@ class TestSticker: assert json_sticker.file_size == self.file_size assert json_sticker.thumb == sticker.thumb assert json_sticker.premium_animation == self.premium_animation + assert json_sticker.type == self.type + assert json_sticker.custom_emoji_id == self.custom_emoji_id async def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker): async def make_assertion(url, request_data: RequestData, *args, **kwargs): @@ -310,6 +319,7 @@ class TestSticker: assert sticker_dict["is_video"] == sticker.is_video assert sticker_dict["file_size"] == sticker.file_size assert sticker_dict["thumb"] == sticker.thumb.to_dict() + assert sticker_dict["type"] == sticker.type @flaky(3, 1) async def test_error_send_empty_file(self, bot, chat_id): @@ -343,6 +353,16 @@ class TestSticker: } assert premium_sticker.premium_animation.to_dict() == premium_sticker_dict + @flaky(3, 1) + async def test_custom_emoji(self, bot): + # testing custom emoji stickers is as much of an annoyance as the premium animation, see + # in test_premium_animation + custom_emoji_set = await bot.get_sticker_set("PTBStaticEmojiTestPack") + # the first one to appear here is a sticker with unique file id of AQADjBsAAkKD0Uty + # this could change in the future ofc. + custom_emoji_sticker = custom_emoji_set.stickers[0] + assert custom_emoji_sticker.custom_emoji_id == "6046140249875156202" + def test_equality(self, sticker): a = Sticker( sticker.file_id, @@ -351,14 +371,41 @@ class TestSticker: self.height, self.is_animated, self.is_video, + self.type, ) b = Sticker( - "", sticker.file_unique_id, self.width, self.height, self.is_animated, self.is_video + "", + sticker.file_unique_id, + self.width, + self.height, + self.is_animated, + self.is_video, + self.type, + ) + c = Sticker( + sticker.file_id, + sticker.file_unique_id, + 0, + 0, + False, + True, + self.type, + ) + d = Sticker( + "", + "", + self.width, + self.height, + self.is_animated, + self.is_video, + self.type, ) - c = Sticker(sticker.file_id, sticker.file_unique_id, 0, 0, False, True) - d = Sticker("", "", self.width, self.height, self.is_animated, self.is_video) e = PhotoSize( - sticker.file_id, sticker.file_unique_id, self.width, self.height, self.is_animated + sticker.file_id, + sticker.file_unique_id, + self.width, + self.height, + self.is_animated, ) assert a == b @@ -427,9 +474,9 @@ class TestStickerSet: title = "Test stickers" is_animated = True is_video = True - contains_masks = False - stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True)] + stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True, Sticker.REGULAR)] name = "NOTAREALNAME" + sticker_type = Sticker.REGULAR def test_de_json(self, bot, sticker): name = f"test_by_{bot.username}" @@ -438,9 +485,9 @@ class TestStickerSet: "title": self.title, "is_animated": self.is_animated, "is_video": self.is_video, - "contains_masks": self.contains_masks, "stickers": [x.to_dict() for x in self.stickers], "thumb": sticker.thumb.to_dict(), + "sticker_type": self.sticker_type, } sticker_set = StickerSet.de_json(json_dict, bot) @@ -448,9 +495,9 @@ class TestStickerSet: assert sticker_set.title == self.title assert sticker_set.is_animated == self.is_animated assert sticker_set.is_video == self.is_video - assert sticker_set.contains_masks == self.contains_masks assert sticker_set.stickers == self.stickers assert sticker_set.thumb == sticker.thumb + assert sticker_set.sticker_type == self.sticker_type async def test_create_sticker_set( self, bot, chat_id, sticker_file, animated_sticker_file, video_sticker_file @@ -536,8 +583,9 @@ class TestStickerSet: assert sticker_set_dict["title"] == sticker_set.title assert sticker_set_dict["is_animated"] == sticker_set.is_animated assert sticker_set_dict["is_video"] == sticker_set.is_video - assert sticker_set_dict["contains_masks"] == sticker_set.contains_masks assert sticker_set_dict["stickers"][0] == sticker_set.stickers[0].to_dict() + assert sticker_set_dict["thumb"] == sticker_set.thumb.to_dict() + assert sticker_set_dict["sticker_type"] == sticker_set.sticker_type @flaky(3, 1) async def test_bot_methods_2_png(self, bot, sticker_set): @@ -639,6 +687,32 @@ class TestStickerSet: assert test_flag monkeypatch.delattr(bot, "_post") + async def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position): + async def make_assertion(_, data, *args, **kwargs): + assert data["user_id"] == chat_id + assert data["name"] == "name" + assert data["title"] == "title" + assert data["emojis"] == "emoji" + assert data["mask_position"] == mask_position + assert data["png_sticker"] == "wow.png" + assert data["tgs_sticker"] == "wow.tgs" + assert data["webm_sticker"] == "wow.webm" + assert data["sticker_type"] == Sticker.MASK + + monkeypatch.setattr(bot, "_post", make_assertion) + await bot.create_new_sticker_set( + chat_id, + "name", + "title", + "emoji", + mask_position=mask_position, + png_sticker="wow.png", + tgs_sticker="wow.tgs", + webm_sticker="wow.webm", + sticker_type=Sticker.MASK, + ) + monkeypatch.delattr(bot, "_post") + async def test_add_sticker_to_set_local_files(self, monkeypatch, bot, chat_id): # For just test that the correct paths are passed as we have no local bot API set up test_flag = False @@ -685,21 +759,26 @@ class TestStickerSet: self.name, self.title, self.is_animated, - self.contains_masks, self.stickers, self.is_video, + self.sticker_type, ) b = StickerSet( self.name, self.title, self.is_animated, - self.contains_masks, self.stickers, self.is_video, + self.sticker_type, ) - c = StickerSet(self.name, None, None, None, None, None) + c = StickerSet(self.name, None, None, None, None, Sticker.CUSTOM_EMOJI) d = StickerSet( - "blah", self.title, self.is_animated, self.contains_masks, self.stickers, self.is_video + "blah", + self.title, + self.is_animated, + self.stickers, + self.is_video, + self.sticker_type, ) e = Audio(self.name, "", 0, None, None) @@ -775,3 +854,23 @@ class TestMaskPosition: assert a != e assert hash(a) != hash(e) + + +class TestGetCustomEmojiSticker: + async def test_custom_emoji_sticker(self, bot): + # we use the same ID as in test_custom_emoji + emoji_sticker_list = await bot.get_custom_emoji_stickers(["6046140249875156202"]) + assert emoji_sticker_list[0].emoji == "😎" + assert emoji_sticker_list[0].height == 100 + assert emoji_sticker_list[0].width == 100 + assert not emoji_sticker_list[0].is_animated + assert not emoji_sticker_list[0].is_video + assert emoji_sticker_list[0].set_name == "PTBStaticEmojiTestPack" + assert emoji_sticker_list[0].type == Sticker.CUSTOM_EMOJI + assert emoji_sticker_list[0].custom_emoji_id == "6046140249875156202" + assert emoji_sticker_list[0].thumb.width == 100 + assert emoji_sticker_list[0].thumb.height == 100 + assert emoji_sticker_list[0].thumb.file_size == 3614 + assert emoji_sticker_list[0].thumb.file_unique_id == "AQAD6gwAAoY06FNy" + assert emoji_sticker_list[0].file_size == 3678 + assert emoji_sticker_list[0].file_unique_id == "AgAD6gwAAoY06FM"