diff --git a/README.rst b/README.rst index ee8de02d921..01371e8e8cb 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.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 @@ -112,7 +112,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **6.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 89a3ce5581b..f6f0ccf9e48 100644 --- a/README_RAW.rst +++ b/README_RAW.rst @@ -20,7 +20,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr :target: https://pypi.org/project/python-telegram-bot-raw/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-6.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 @@ -105,7 +105,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju Telegram API support ==================== -All types and methods of the Telegram Bot API **6.1** are supported. +All types and methods of the Telegram Bot API **6.2** are supported. ========== Installing diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 0c35bfae764..89df34c7108 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,6 @@ sphinx==3.5.4 +# sphinx breaks because it relies on an removed param from jinja2, so pinning to old version +Jinja2<3.1 sphinx-pypi-upload # When bumping this, make sure to rebuild the dark-mode CSS # More instructions at source/_static/dark.css diff --git a/telegram/bot.py b/telegram/bot.py index f1348cc3ffc..39976c4432c 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -2429,8 +2429,8 @@ def get_file( if result.get('file_path') and not is_local_file( # type: ignore[union-attr] result['file_path'] # type: ignore[index] ): - result['file_path'] = '{}/{}'.format( # type: ignore[index] - self.base_file_url, result['file_path'] # type: ignore[index] + result['file_path'] = ( # type: ignore[index] + f"{self.base_file_url}/" f"{result['file_path']}" # type: ignore[index] ) return File.de_json(result, self) # type: ignore[return-value, arg-type] @@ -4813,6 +4813,37 @@ def get_sticker_set( return StickerSet.de_json(result, self) # type: ignore[return-value, arg-type] + @log + def get_custom_emoji_stickers( + self, + custom_emoji_ids: List[str], + *, + timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict = None, + ) -> List[Sticker]: + """ + Use this method to get information about emoji stickers by their identifiers. + + .. versionadded:: 13.14 + + Args: + custom_emoji_ids (List[:obj:`str`]): List of custom emoji identifiers. + At most 200 custom emoji identifiers can be specified. + Keyword Args: + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during + creation of the connection pool). + api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the + Telegram API. + Returns: + List[:class:`telegram.Sticker`] + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = {"custom_emoji_ids": custom_emoji_ids} + result = self._post("getCustomEmojiStickers", data, timeout=timeout, api_kwargs=api_kwargs) + return Sticker.de_list(result, self) # type: ignore[return-value, arg-type] + @log def upload_sticker_file( self, @@ -4871,6 +4902,7 @@ def create_new_sticker_set( tgs_sticker: FileInput = None, api_kwargs: JSONDict = None, webm_sticker: FileInput = None, + sticker_type: str = None, ) -> bool: """ Use this method to create new sticker set owned by a user. @@ -4887,6 +4919,10 @@ def create_new_sticker_set( The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` + .. versionchanged:: 13.14 + The parameter ``contains_masks`` has been depreciated as of Bot API 6.2. + Use ``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 @@ -4924,6 +4960,12 @@ def create_new_sticker_set( 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:: 13.14 timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the read timeout from the server (instead of the one specified during creation of the connection pool). @@ -4951,7 +4993,8 @@ def create_new_sticker_set( # We need to_json() instead of to_dict() here, because we're sending a media # message here, which isn't json dumped by utils.request data['mask_position'] = mask_position.to_json() - + if sticker_type is not None: + data['sticker_type'] = sticker_type result = self._post('createNewStickerSet', data, timeout=timeout, api_kwargs=api_kwargs) return result # type: ignore[return-value] @@ -6206,6 +6249,8 @@ def __hash__(self) -> int: """Alias for :meth:`unpin_all_chat_messages`""" getStickerSet = get_sticker_set """Alias for :meth:`get_sticker_set`""" + getCustomEmojiStickers = get_custom_emoji_stickers + """Alias for :meth:`get_custom_emoji_stickers`""" uploadStickerFile = upload_sticker_file """Alias for :meth:`upload_sticker_file`""" createNewStickerSet = create_new_sticker_set diff --git a/telegram/chat.py b/telegram/chat.py index 0e649bc0fa0..3d6e8178870 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -126,6 +126,11 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.13 + 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:: 13.14 **kwargs (:obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -180,6 +185,11 @@ class Chat(TelegramObject): :meth:`telegram.Bot.get_chat`. .. versionadded:: 13.13 + 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:: 13.14 """ __slots__ = ( @@ -207,6 +217,7 @@ class Chat(TelegramObject): 'has_private_forwards', 'join_to_send_messages', 'join_by_request', + 'has_restricted_voice_and_video_messages', '_id_attrs', ) @@ -249,6 +260,7 @@ def __init__( 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 @@ -279,6 +291,7 @@ def __init__( 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.bot = bot self._id_attrs = (self.id,) diff --git a/telegram/constants.py b/telegram/constants.py index 9b132ec56d6..ae5175d9d2f 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -21,7 +21,7 @@ `Telegram Bots API `_. Attributes: - BOT_API_VERSION (:obj:`str`): `6.1`. Telegram Bot API version supported by this + BOT_API_VERSION (:obj:`str`): `6.2`. Telegram Bot API version supported by this version of `python-telegram-bot`. Also available as ``telegram.bot_api_version``. .. versionadded:: 13.4 @@ -144,6 +144,9 @@ MESSAGEENTITY_SPOILER (:obj:`str`): ``'spoiler'`` .. versionadded:: 13.10 + MESSAGEENTITY_CUSTOM_EMOJI (:obj:`str`): ``'custom_emoji'`` + + .. versionadded:: 13.14 MESSAGEENTITY_ALL_TYPES (List[:obj:`str`]): List of all the types of message entity. :class:`telegram.ParseMode`: @@ -160,6 +163,19 @@ POLL_QUIZ (:obj:`str`): ``'quiz'`` MAX_POLL_QUESTION_LENGTH (:obj:`int`): 300 MAX_POLL_OPTION_LENGTH (:obj:`int`): 100 +:class:`telegram.Sticker`: + +Attributes: + + STICKER_REGULAR (:obj:`str`)= ``'regular'`` + + .. versionadded:: 13.14 + STICKER_MASK (:obj:`str`) = ``'mask'`` + + .. versionadded:: 13.14 + STICKER_CUSTOM_EMOJI (:obj:`str`) = ``'custom_emoji'`` + + .. versionadded:: 13.14 :class:`telegram.MaskPosition`: @@ -247,7 +263,7 @@ """ from typing import List -BOT_API_VERSION: str = '6.1' +BOT_API_VERSION: str = '6.2' MAX_MESSAGE_LENGTH: int = 4096 MAX_CAPTION_LENGTH: int = 1024 ANONYMOUS_ADMIN_ID: int = 1087968824 @@ -325,6 +341,7 @@ MESSAGEENTITY_UNDERLINE: str = 'underline' MESSAGEENTITY_STRIKETHROUGH: str = 'strikethrough' MESSAGEENTITY_SPOILER: str = 'spoiler' +MESSAGEENTITY_CUSTOM_EMOJI: str = 'custom_emoji' MESSAGEENTITY_ALL_TYPES: List[str] = [ MESSAGEENTITY_MENTION, MESSAGEENTITY_HASHTAG, @@ -342,6 +359,7 @@ MESSAGEENTITY_UNDERLINE, MESSAGEENTITY_STRIKETHROUGH, MESSAGEENTITY_SPOILER, + MESSAGEENTITY_CUSTOM_EMOJI, ] PARSEMODE_MARKDOWN: str = 'Markdown' @@ -353,6 +371,10 @@ MAX_POLL_QUESTION_LENGTH: int = 300 MAX_POLL_OPTION_LENGTH: int = 100 +STICKER_REGULAR: str = "regular" +STICKER_MASK: str = "mask" +STICKER_CUSTOM_EMOJI: str = "custom_emoji" + STICKER_FOREHEAD: str = 'forehead' STICKER_EYES: str = 'eyes' STICKER_MOUTH: str = 'mouth' diff --git a/telegram/error.py b/telegram/error.py index 3cf41d5879f..d0b72a2e66a 100644 --- a/telegram/error.py +++ b/telegram/error.py @@ -56,7 +56,7 @@ def __init__(self, message: str): self.message = msg def __str__(self) -> str: - return '%s' % self.message + return f'{self.message}' def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self.message,) diff --git a/telegram/files/sticker.py b/telegram/files/sticker.py index bce03da0058..a461bff5d88 100644 --- a/telegram/files/sticker.py +++ b/telegram/files/sticker.py @@ -51,6 +51,11 @@ class Sticker(TelegramObject): 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:: 13.14 thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .WEBP or .JPG format. emoji (:obj:`str`, optional): Emoji associated with the sticker @@ -60,10 +65,14 @@ class Sticker(TelegramObject): position where the mask should be placed. file_size (:obj:`int`, optional): File size. bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods. - premium_animation (:class:`telegram.File`, optional): Premium animation for the sticker, - if the sticker is premium. + premium_animation (:class:`telegram.File`, optional): For premium regular stickers, + premium animation for the sticker. .. versionadded:: 13.13 + custom_emoji (:obj:`str`, optional): For custom emoji stickers, unique identifier of the + custom emoji. + + .. versionadded:: 13.14 **kwargs (obj:`dict`): Arbitrary keyword arguments. Attributes: @@ -77,6 +86,11 @@ class Sticker(TelegramObject): 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:: 13.14 thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg format. emoji (:obj:`str`): Optional. Emoji associated with the sticker. @@ -84,10 +98,14 @@ class Sticker(TelegramObject): mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position where the mask should be placed. file_size (:obj:`int`): Optional. File size. - premium_animation (:class:`telegram.File`): Optional. Premium animation for the sticker, - if the sticker is premium. + premium_animation (:class:`telegram.File`): Optional. For premium regular stickers, + premium animation for the sticker. .. versionadded:: 13.13 + custom_emoji (:obj:`str`): Optional. For custom emoji stickers, unique identifier of the + custom emoji. + + .. versionadded:: 13.14 bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. """ @@ -106,6 +124,8 @@ class Sticker(TelegramObject): 'file_unique_id', 'emoji', 'premium_animation', + 'type', + 'custom_emoji_id', '_id_attrs', ) @@ -117,6 +137,7 @@ def __init__( height: int, is_animated: bool, is_video: bool, + type: str, # pylint: disable=redefined-builtin thumb: PhotoSize = None, emoji: str = None, file_size: int = None, @@ -124,6 +145,7 @@ def __init__( mask_position: 'MaskPosition' = None, bot: 'Bot' = None, premium_animation: 'File' = None, + custom_emoji_id: str = None, **_kwargs: Any, ): # Required @@ -133,6 +155,7 @@ def __init__( self.height = int(height) self.is_animated = is_animated self.is_video = is_video + self.type = type # Optionals self.thumb = thumb self.emoji = emoji @@ -141,6 +164,7 @@ def __init__( self.mask_position = mask_position self.bot = bot self.premium_animation = premium_animation + self.custom_emoji_id = custom_emoji_id self._id_attrs = (self.file_unique_id,) @@ -178,6 +202,22 @@ def get_file( """ return self.bot.get_file(file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs) + REGULAR: ClassVar[str] = constants.STICKER_REGULAR + """:const:`telegram.constants.STICKER_REGULAR` + + .. versionadded:: 13.14 + """ + MASK: ClassVar[str] = constants.STICKER_MASK + """:const:`telegram.constants.STICKER_MASK` + + .. versionadded:: 13.14 + """ + CUSTOM_EMOJI: ClassVar[str] = constants.STICKER_CUSTOM_EMOJI + """:const:`telegram.constants.STICKER_CUSTOM_EMOJI` + + .. versionadded:: 13.14 + """ + class StickerSet(TelegramObject): """This object represents a sticker set. @@ -190,6 +230,10 @@ class StickerSet(TelegramObject): arguments had to be changed. Use keyword arguments to make sure that the arguments are passed correctly. + .. versionchanged:: 13.14: + The parameter ``contains_masks`` has been depreciated as of Bot API 6.2. + Use ``sticker_type`` instead. + Args: name (:obj:`str`): Sticker set name. title (:obj:`str`): Sticker set title. @@ -199,6 +243,11 @@ class StickerSet(TelegramObject): .. 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`, optional): Type of stickers in the set, currently one of + :attr:`telegram.Sticker.REGULAR`, :attr:`telegram.Sticker.MASK`, + :attr:`telegram.Sticker.CUSTOM_EMOJI`. + + .. versionadded:: 13.14 thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the ``.WEBP``, ``.TGS``, or ``.WEBM`` format. @@ -211,6 +260,9 @@ class StickerSet(TelegramObject): .. 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`): Optional. Type of stickers in the set. + + .. versionadded:: 13.14 thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the ``.WEBP``, ``.TGS`` or ``.WEBM`` format. @@ -224,6 +276,7 @@ class StickerSet(TelegramObject): 'title', 'stickers', 'name', + 'sticker_type', '_id_attrs', ) @@ -236,6 +289,7 @@ def __init__( stickers: List[Sticker], is_video: bool, thumb: PhotoSize = None, + sticker_type: str = None, **_kwargs: Any, ): self.name = name @@ -246,6 +300,7 @@ def __init__( self.stickers = stickers # Optionals self.thumb = thumb + self.sticker_type = sticker_type self._id_attrs = (self.name,) diff --git a/telegram/message.py b/telegram/message.py index 2d0f29d1202..951b16a9dd0 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -407,6 +407,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 @@ -2752,6 +2754,9 @@ def text_html(self) -> str: 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. @@ -2768,6 +2773,9 @@ def text_html_urled(self) -> str: 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. @@ -2785,6 +2793,9 @@ def caption_html(self) -> str: 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. @@ -2801,6 +2812,9 @@ def caption_html_urled(self) -> str: 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. @@ -2983,8 +2997,10 @@ def text_markdown(self) -> str: in the same way the original message was formatted. Note: - :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for - backward compatibility. You should use :meth:`text_markdown_v2` instead. + * :attr:`telegram.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. @@ -3004,6 +3020,9 @@ def text_markdown_v2(self) -> str: 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. @@ -3021,8 +3040,10 @@ def text_markdown_urled(self) -> str: This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. Note: - :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for - backward compatibility. You should use :meth:`text_markdown_v2_urled` instead. + * :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by 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. @@ -3042,6 +3063,9 @@ def text_markdown_v2_urled(self) -> str: 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. @@ -3059,8 +3083,10 @@ def caption_markdown(self) -> str: Markdown in the same way the original message was formatted. Note: - :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for - backward compatibility. You should use :meth:`caption_markdown_v2` instead. + * :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by 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. @@ -3080,6 +3106,9 @@ def caption_markdown_v2(self) -> str: 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. @@ -3099,8 +3128,10 @@ def caption_markdown_urled(self) -> str: Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. Note: - :attr:`telegram.ParseMode.MARKDOWN` is is a legacy mode, retained by Telegram for - backward compatibility. You should use :meth:`caption_markdown_v2_urled` instead. + * :attr:`telegram.ParseMode.MARKDOWN` is a legacy mode, retained by 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. @@ -3120,6 +3151,9 @@ def caption_markdown_v2_urled(self) -> str: 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 d4e162044d6..ac90735c4fa 100644 --- a/telegram/messageentity.py +++ b/telegram/messageentity.py @@ -40,7 +40,10 @@ class MessageEntity(TelegramObject): bot_command, url, email, phone_number, bold (bold text), italic (italic text), strikethrough, spoiler (spoiler message), code (monowidth string), pre (monowidth block), text_link (for clickable text URLs), text_mention - (for users without usernames). + (for users without usernames), custom_emoji (for inline custom emoji stickers). + + .. versionadded:: 13.14 + 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 @@ -49,6 +52,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:: 13.14 Attributes: type (:obj:`str`): Type of the entity. @@ -57,10 +65,22 @@ 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:: 13.14 """ - __slots__ = ('length', 'url', 'user', 'type', 'language', 'offset', '_id_attrs') + __slots__ = ( + 'length', + 'url', + 'user', + 'type', + 'language', + 'offset', + 'custom_emoji_id', + '_id_attrs', + ) def __init__( self, @@ -70,6 +90,7 @@ def __init__( url: str = None, user: User = None, language: str = None, + custom_emoji_id: str = None, **_kwargs: Any, ): # Required @@ -80,6 +101,7 @@ def __init__( 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) @@ -130,6 +152,11 @@ def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MessageEntit .. versionadded:: 13.10 """ + CUSTOM_EMOJI: ClassVar[str] = constants.MESSAGEENTITY_CUSTOM_EMOJI + """:const:`telegram.constants.MESSAGEENTITY_CUSTOM_EMOJI` + + .. versionadded:: 13.14 + """ ALL_TYPES: ClassVar[List[str]] = constants.MESSAGEENTITY_ALL_TYPES """:const:`telegram.constants.MESSAGEENTITY_ALL_TYPES`\n List of all the types""" diff --git a/tests/test_bot.py b/tests/test_bot.py index 5e033fe4152..eeef8a3e82d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2072,8 +2072,8 @@ def test_pin_and_unpin_message(self, bot, super_group_id): assert bot.unpin_all_chat_messages(super_group_id) # 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. def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id): diff --git a/tests/test_chat.py b/tests/test_chat.py index 515b1b55ed0..bbceb6bb799 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -45,6 +45,7 @@ def chat(bot): has_protected_content=True, join_to_send_messages=True, join_by_request=True, + has_restricted_voice_and_video_messages=True, ) @@ -70,6 +71,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, recwarn, mro_slots): for attr in chat.__slots__: @@ -98,6 +100,9 @@ def test_de_json(self, bot): '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) @@ -119,6 +124,10 @@ def test_de_json(self, bot): 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() @@ -139,6 +148,10 @@ def test_to_dict(self, chat): 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_link(self, chat): assert chat.link == f'https://t.me/{chat.username}' diff --git a/tests/test_helpers.py b/tests/test_helpers.py index bd18fbc7a06..4c435c5d875 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -272,7 +272,7 @@ def build_test_message(**kwargs): test_message.text = None test_message = build_test_message( - sticker=Sticker('sticker_id', 'unique_id', 50, 50, False, False) + sticker=Sticker('sticker_id', 'unique_id', 50, 50, False, False, Sticker.REGULAR) ) assert helpers.effective_message_type(test_message) == 'sticker' test_message.sticker = None diff --git a/tests/test_message.py b/tests/test_message.py index 4216a33bbcc..40ca7c79dd7 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -110,7 +110,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)}, @@ -522,6 +522,36 @@ def test_text_markdown_emoji(self): ) 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, ' @@ -631,6 +661,36 @@ def test_caption_markdown_emoji(self): ) 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_] + 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_official.py b/tests/test_official.py index b1798caae35..f17f6e7c88e 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -102,6 +102,8 @@ def check_method(h4): ignored |= {'current_offset'} # Added for ease of use elif name == 'promoteChatMember': ignored |= {'can_manage_voice_chats'} # for backwards compatibility + elif name == 'createNewStickerSet': + ignored |= {'contains_masks'} # for backwards compatibility assert (sig.parameters.keys() ^ checked) - ignored == set() @@ -195,7 +197,8 @@ def check_object(h4): 'voice_chat_scheduled', 'voice_chat_started', } - + elif name == 'StickerSet': + ignored |= {'contains_masks'} # for backwards compatibility assert (sig.parameters.keys() ^ checked) - ignored == set() diff --git a/tests/test_photo.py b/tests/test_photo.py index bc908ccf571..98985581d19 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -473,7 +473,15 @@ def test_equality(self, photo): 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 3a798cb4f4c..8f7ecd77100 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -83,6 +83,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' @@ -119,6 +121,7 @@ def test_expected_values(self, sticker): assert sticker.thumb.width == self.thumb_width assert sticker.thumb.height == self.thumb_height assert sticker.thumb.file_size == self.thumb_file_size + 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 @@ -150,6 +153,7 @@ def test_send_all_args(self, bot, chat_id, sticker_file, sticker): assert message.sticker.thumb.height == sticker.thumb.height assert message.sticker.thumb.file_size == sticker.thumb.file_size assert message.has_protected_content + assert message.sticker.type == sticker.type @flaky(3, 1) def test_get_and_download(self, bot, sticker): @@ -192,6 +196,7 @@ def test_send_from_url(self, bot, chat_id): 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) @@ -214,6 +219,8 @@ def test_de_json(self, bot, sticker): '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) @@ -227,6 +234,8 @@ def test_de_json(self, bot, sticker): 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 def test_send_with_sticker(self, monkeypatch, bot, chat_id, sticker): def test(url, data, **kwargs): @@ -297,6 +306,7 @@ def test_to_dict(self, sticker): 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) def test_error_send_empty_file(self, bot, chat_id): @@ -330,6 +340,16 @@ def test_premium_animation(self, bot): } assert premium_sticker.premium_animation.to_dict() == premium_sticker_dict + @flaky(3, 1) + 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 = 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, @@ -338,12 +358,35 @@ def test_equality(self, sticker): 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 ) @@ -416,8 +459,9 @@ class TestStickerSet: 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}' @@ -429,6 +473,7 @@ def test_de_json(self, bot, sticker): '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) @@ -439,6 +484,7 @@ def test_de_json(self, bot, sticker): 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 def test_create_sticker_set( self, bot, chat_id, sticker_file, animated_sticker_file, video_sticker_file @@ -526,6 +572,8 @@ def test_sticker_set_to_dict(self, sticker_set): 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) def test_bot_methods_2_png(self, bot, sticker_set): @@ -626,6 +674,32 @@ def make_assertion(_, data, *args, **kwargs): assert test_flag monkeypatch.delattr(bot, '_post') + def test_create_new_sticker_all_params(self, monkeypatch, bot, chat_id, mask_position): + 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.to_json() + 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) + 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") + 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 @@ -675,6 +749,8 @@ def test_equality(self): self.contains_masks, self.stickers, self.is_video, + None, + self.sticker_type, ) b = StickerSet( self.name, @@ -684,9 +760,16 @@ def test_equality(self): self.stickers, self.is_video, ) - c = StickerSet(self.name, None, None, None, None, None) + c = StickerSet(self.name, None, None, 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.contains_masks, + self.stickers, + self.is_video, + None, + self.sticker_type, ) e = Audio(self.name, '', 0, None, None) @@ -762,3 +845,23 @@ def test_equality(self): assert a != e assert hash(a) != hash(e) + + +class TestGetCustomEmojiSticker: + def test_custom_emoji_sticker(self, bot): + # we use the same ID as in test_custom_emoji + emoji_sticker_list = 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"