diff --git a/README.rst b/README.rst index 00b6fa1ec72..1ddad4bfd1a 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.6-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.7-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.6** are supported. +All types and methods of the Telegram Bot API **6.7** are supported. Installing ========== diff --git a/README_RAW.rst b/README_RAW.rst index 29377db2a3b..b8425e4787a 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.6-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-6.7-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.6** are supported. +All types and methods of the Telegram Bot API **6.7** are supported. Installing ========== diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index c2d82d50702..36b871c7b73 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -196,6 +196,10 @@ - Used for setting the short description of the bot * - :meth:`~telegram.Bot.get_my_short_description` - Used for obtaining the short description of the bot + * - :meth:`~telegram.Bot.set_my_name` + - Used for setting the name of the bot + * - :meth:`~telegram.Bot.get_my_name` + - Used for obtaining the name of the bot .. raw:: html diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index eeb408e967f..3dc01c6d7e2 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -16,6 +16,7 @@ Available Types telegram.botcommandscopechatmember telegram.botcommandscopedefault telegram.botdescription + telegram.botname telegram.botshortdescription telegram.callbackquery telegram.chat @@ -78,6 +79,7 @@ Available Types telegram.replykeyboardmarkup telegram.replykeyboardremove telegram.sentwebappmessage + telegram.switchinlinequerychosenchat telegram.telegramobject telegram.update telegram.user diff --git a/docs/source/telegram.botname.rst b/docs/source/telegram.botname.rst new file mode 100644 index 00000000000..0f78027c7ba --- /dev/null +++ b/docs/source/telegram.botname.rst @@ -0,0 +1,6 @@ +BotName +======= + +.. autoclass:: telegram.BotName + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.inline-tree.rst b/docs/source/telegram.inline-tree.rst index cda06bc0c93..7fa52a94b58 100644 --- a/docs/source/telegram.inline-tree.rst +++ b/docs/source/telegram.inline-tree.rst @@ -24,6 +24,7 @@ Inline Mode telegram.inlinequeryresultlocation telegram.inlinequeryresultmpeg4gif telegram.inlinequeryresultphoto + telegram.inlinequeryresultsbutton telegram.inlinequeryresultvenue telegram.inlinequeryresultvideo telegram.inlinequeryresultvoice diff --git a/docs/source/telegram.inlinequeryresultsbutton.rst b/docs/source/telegram.inlinequeryresultsbutton.rst new file mode 100644 index 00000000000..7323a20cc60 --- /dev/null +++ b/docs/source/telegram.inlinequeryresultsbutton.rst @@ -0,0 +1,6 @@ +InlineQueryResultsButton +======================== + +.. autoclass:: telegram.InlineQueryResultsButton + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.switchinlinequerychosenchat.rst b/docs/source/telegram.switchinlinequerychosenchat.rst new file mode 100644 index 00000000000..603a56f6ad2 --- /dev/null +++ b/docs/source/telegram.switchinlinequerychosenchat.rst @@ -0,0 +1,6 @@ +SwitchInlineQueryChosenChat +=========================== + +.. autoclass:: telegram.SwitchInlineQueryChosenChat + :members: + :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index e198098a99a..fb3dcadd6df 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -38,6 +38,7 @@ "BotCommandScopeChatMember", "BotCommandScopeDefault", "BotDescription", + "BotName", "BotShortDescription", "CallbackGame", "CallbackQuery", @@ -102,6 +103,7 @@ "InlineQueryResultLocation", "InlineQueryResultMpeg4Gif", "InlineQueryResultPhoto", + "InlineQueryResultsButton", "InlineQueryResultVenue", "InlineQueryResultVideo", "InlineQueryResultVoice", @@ -169,6 +171,7 @@ "Sticker", "StickerSet", "SuccessfulPayment", + "SwitchInlineQueryChosenChat", "TelegramObject", "Update", "User", @@ -204,6 +207,7 @@ BotCommandScopeDefault, ) from ._botdescription import BotDescription, BotShortDescription +from ._botname import BotName from ._callbackquery import CallbackQuery from ._chat import Chat from ._chatadministratorrights import ChatAdministratorRights @@ -280,6 +284,7 @@ from ._inline.inlinequeryresultlocation import InlineQueryResultLocation from ._inline.inlinequeryresultmpeg4gif import InlineQueryResultMpeg4Gif from ._inline.inlinequeryresultphoto import InlineQueryResultPhoto +from ._inline.inlinequeryresultsbutton import InlineQueryResultsButton from ._inline.inlinequeryresultvenue import InlineQueryResultVenue from ._inline.inlinequeryresultvideo import InlineQueryResultVideo from ._inline.inlinequeryresultvoice import InlineQueryResultVoice @@ -336,6 +341,7 @@ from ._replykeyboardremove import ReplyKeyboardRemove from ._sentwebappmessage import SentWebAppMessage from ._shared import ChatShared, UserShared +from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from ._telegramobject import TelegramObject from ._update import Update from ._user import User diff --git a/telegram/_bot.py b/telegram/_bot.py index e7b060f4a06..b2aa643d62d 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -56,6 +56,7 @@ from telegram._botcommand import BotCommand from telegram._botcommandscope import BotCommandScope from telegram._botdescription import BotDescription, BotShortDescription +from telegram._botname import BotName from telegram._chat import Chat from telegram._chatadministratorrights import ChatAdministratorRights from telegram._chatinvitelink import ChatInviteLink @@ -79,6 +80,7 @@ from telegram._forumtopic import ForumTopic from telegram._games.gamehighscore import GameHighScore from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup +from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._menubutton import MenuButton from telegram._message import Message from telegram._messageid import MessageId @@ -2811,8 +2813,15 @@ async def answer_inline_query( cache_time: int = None, is_personal: bool = None, next_offset: str = None, + # Deprecated params since bot api 6.7 + # <---- switch_pm_text: str = None, switch_pm_parameter: str = None, + # ---> + # New params since bot api 6.7 + # <---- + button: InlineQueryResultsButton = None, + # ---> *, current_offset: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2825,15 +2834,6 @@ async def answer_inline_query( Use this method to send answers to an inline query. No more than :tg-const:`telegram.InlineQuery.MAX_RESULTS` results per query are allowed. - Example: - An inline bot that sends YouTube videos can ask the user to connect the bot to their - YouTube account to adapt search results accordingly. To do this, it displays a - 'Connect your YouTube account' button above the results, or even before showing any. - The user presses the button, switches to a private chat with the bot and, in doing so, - passes a start parameter that instructs the bot to return an OAuth link. Once done, the - bot can offer a switch_inline button so that the user can easily return to the chat - where they wanted to use the bot's inline capabilities. - Warning: In most use cases :paramref:`current_offset` should not be passed manually. Instead of calling this method directly, use the shortcut :meth:`telegram.InlineQuery.answer` with @@ -2842,6 +2842,9 @@ async def answer_inline_query( .. seealso:: :wiki:`Working with Files and Media ` + .. |api6_7_depr| replace:: Since Bot API 6.7, this argument is deprecated in favour of + :paramref:`button`. + Args: inline_query_id (:obj:`str`): Unique identifier for the answered query. results (List[:class:`telegram.InlineQueryResult`] | Callable): A list of results for @@ -2862,12 +2865,22 @@ async def answer_inline_query( switch_pm_text (:obj:`str`, optional): If passed, clients will display a button with specified text that switches the user to a private chat with the bot and sends the bot a start message with the parameter :paramref:`switch_pm_parameter`. + + .. deprecated:: NEXT.VERSION + |api6_7_depr| switch_pm_parameter (:obj:`str`, optional): Deep-linking parameter for the :guilabel:`/start` message sent to the bot when user presses the switch button. :tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`- :tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters, only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + .. deprecated:: NEXT.VERSION + |api6_7_depr| + button (:class:`telegram.InlineQueryResultsButton`, optional): A button to be shown + above the inline query results. + + .. versionadded:: NEXT.VERSION + Keyword Args: current_offset (:obj:`str`, optional): The :attr:`telegram.InlineQuery.offset` of the inline query to answer. If passed, PTB will automatically take care of @@ -2881,6 +2894,26 @@ async def answer_inline_query( :class:`telegram.error.TelegramError` """ + if (switch_pm_text or switch_pm_parameter) and button: + raise TypeError( + "Since Bot API 6.7, the parameter `button is mutually exclusive to the deprecated " + "parameters `switch_pm_text` and `switch_pm_parameter`. Please use the new " + "parameter `button`." + ) + + if switch_pm_text and switch_pm_parameter: + self._warn( + "Since Bot API 6.7, the parameters `switch_pm_text` and `switch_pm_parameter` are " + "deprecated in favour of the new parameter `button`. Please use the new parameter " + "`button` instead.", + category=PTBDeprecationWarning, + stacklevel=3, + ) + button = InlineQueryResultsButton( + text=switch_pm_text, + start_parameter=switch_pm_parameter, + ) + effective_results, next_offset = self._effective_inline_results( results=results, next_offset=next_offset, current_offset=current_offset ) @@ -2896,8 +2929,7 @@ async def answer_inline_query( "next_offset": next_offset, "cache_time": cache_time, "is_personal": is_personal, - "switch_pm_text": switch_pm_text, - "switch_pm_parameter": switch_pm_parameter, + "button": button, } return await self._post( @@ -8138,6 +8170,94 @@ async def get_my_short_description( bot=self, ) + @_log + async def set_my_name( + self, + name: str = None, + language_code: str = 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, + ) -> bool: + """ + Use this method to change the bot's name. + + .. versionadded:: NEXT.VERSION + + Args: + name (:obj:`str`, optional): New bot name; + 0-:tg-const:`telegram.constants.BotNameLimit.MAX_NAME_LENGTH` + characters. Pass an empty string to remove the dedicated name for the given + language. + + Caution: + If :paramref:`language_code` is not specified, a :paramref:`name` *must* + be specified. + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code. If empty, + the name will be applied to all users for whose language there is no + dedicated name. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = {"name": name, "language_code": language_code} + + return await self._post( + "setMyName", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + @_log + async def get_my_name( + self, + language_code: str = 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, + ) -> BotName: + """ + Use this method to get the current bot name for the given user language. + + Args: + language_code (:obj:`str`, optional): A two-letter ISO 639-1 language code or an empty + string. + + Returns: + :class:`telegram.BotName`: On success, the bot name is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data = {"language_code": language_code} + return BotName.de_json( # type: ignore[return-value] + await self._post( + "getMyName", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + def to_dict(self, recursive: bool = True) -> JSONDict: # skipcq: PYL-W0613 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -8382,3 +8502,7 @@ def __hash__(self) -> int: """Alias for :meth:`set_sticker_keywords`""" setStickerMaskPosition = set_sticker_mask_position """Alias for :meth:`set_sticker_mask_position`""" + setMyName = set_my_name + """Alias for :meth:`set_my_name`""" + getMyName = get_my_name + """Alias for :meth:`get_my_name`""" diff --git a/telegram/_botname.py b/telegram/_botname.py new file mode 100644 index 00000000000..05c0610db68 --- /dev/null +++ b/telegram/_botname.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2023 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represent a Telegram bots name.""" +from typing import ClassVar + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class BotName(TelegramObject): + """This object represents the bot's name. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`name` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + name (:obj:`str`): The bot's name. + + Attributes: + name (:obj:`str`): The bot's name. + + """ + + __slots__ = ("name",) + + def __init__(self, name: str, *, api_kwargs: JSONDict = None): + super().__init__(api_kwargs=api_kwargs) + self.name = name + + self._id_attrs = (self.name,) + + self._freeze() + + MAX_LENGTH: ClassVar[int] = constants.BotNameLimit.MAX_NAME_LENGTH + """:const:`telegram.constants.BotNameLimit.MAX_NAME_LENGTH`""" diff --git a/telegram/_chatmemberupdated.py b/telegram/_chatmemberupdated.py index 650d7d0b00a..59b30623488 100644 --- a/telegram/_chatmemberupdated.py +++ b/telegram/_chatmemberupdated.py @@ -59,6 +59,10 @@ class ChatMemberUpdated(TelegramObject): new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. invite_link (:class:`telegram.ChatInviteLink`, optional): Chat invite link, which was used by the user to join the chat. For joining by invite link events only. + via_chat_folder_invite_link (:obj:`bool`, optional): :obj:`True`, if the user joined the + chat via a chat folder invite link + + .. versionadded:: NEXT.VERSION Attributes: chat (:class:`telegram.Chat`): Chat the user belongs to. @@ -72,6 +76,10 @@ class ChatMemberUpdated(TelegramObject): new_chat_member (:class:`telegram.ChatMember`): New information about the chat member. invite_link (:class:`telegram.ChatInviteLink`): Optional. Chat invite link, which was used by the user to join the chat. For joining by invite link events only. + via_chat_folder_invite_link (:obj:`bool`): Optional. :obj:`True`, if the user joined the + chat via a chat folder invite link + + .. versionadded:: NEXT.VERSION """ @@ -82,6 +90,7 @@ class ChatMemberUpdated(TelegramObject): "old_chat_member", "new_chat_member", "invite_link", + "via_chat_folder_invite_link", ) def __init__( @@ -92,6 +101,7 @@ def __init__( old_chat_member: ChatMember, new_chat_member: ChatMember, invite_link: ChatInviteLink = None, + via_chat_folder_invite_link: bool = None, *, api_kwargs: JSONDict = None, ): @@ -102,6 +112,7 @@ def __init__( self.date: datetime.datetime = date self.old_chat_member: ChatMember = old_chat_member self.new_chat_member: ChatMember = new_chat_member + self.via_chat_folder_invite_link: Optional[bool] = via_chat_folder_invite_link # Optionals self.invite_link: Optional[ChatInviteLink] = invite_link diff --git a/telegram/_inline/inlinekeyboardbutton.py b/telegram/_inline/inlinekeyboardbutton.py index 33759e7318e..9626fa1d0c3 100644 --- a/telegram/_inline/inlinekeyboardbutton.py +++ b/telegram/_inline/inlinekeyboardbutton.py @@ -23,6 +23,7 @@ from telegram import constants from telegram._games.callbackgame import CallbackGame from telegram._loginurl import LoginUrl +from telegram._switchinlinequerychosenchat import SwitchInlineQueryChosenChat from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict from telegram._webappinfo import WebAppInfo @@ -111,6 +112,10 @@ class InlineKeyboardButton(TelegramObject): in inline mode when they are currently in a private chat with it. Especially useful when combined with ``switch_pm*`` actions - in this case the user will be automatically returned to the chat they switched from, skipping the chat selection screen. + + Tip: + This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`, + but gives no control over which chats can be selected. switch_inline_query_current_chat (:obj:`str`, optional): If set, pressing the button will insert the bot's username and the specified inline query in the current chat's input field. Can be empty, in which case only the bot's username will be inserted. This @@ -122,6 +127,20 @@ class InlineKeyboardButton(TelegramObject): pay (:obj:`bool`, optional): Specify :obj:`True`, to send a Pay button. This type of button **must** always be the **first** button in the first row and can only be used in invoice messages. + switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`, optional): + If set, pressing the button will prompt the user to select one of their chats of the + specified type, open that chat and insert the bot's username and the specified inline + query in the input field. + + .. versionadded:: NEXT.VERSION + + Tip: + This is similar to :paramref:`switch_inline_query`, but gives more control on + which chats can be selected. + + Caution: + The PTB team has discovered that this field works correctly only if your Telegram + client is released after April 20th 2023. Attributes: text (:obj:`str`): Label text on the button. @@ -154,6 +173,10 @@ class InlineKeyboardButton(TelegramObject): in inline mode when they are currently in a private chat with it. Especially useful when combined with ``switch_pm*`` actions - in this case the user will be automatically returned to the chat they switched from, skipping the chat selection screen. + + Tip: + This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`, + but gives no control over which chats can be selected. switch_inline_query_current_chat (:obj:`str`): Optional. If set, pressing the button will insert the bot's username and the specified inline query in the current chat's input field. Can be empty, in which case only the bot's username will be inserted. This @@ -165,7 +188,20 @@ class InlineKeyboardButton(TelegramObject): pay (:obj:`bool`): Optional. Specify :obj:`True`, to send a Pay button. This type of button **must** always be the **first** button in the first row and can only be used in invoice messages. + switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`): Optional. + If set, pressing the button will prompt the user to select one of their chats of the + specified type, open that chat and insert the bot's username and the specified inline + query in the input field. + .. versionadded:: NEXT.VERSION + + Tip: + This is similar to :attr:`switch_inline_query`, but gives more control on + which chats can be selected. + + Caution: + The PTB team has discovered that this field works correctly only if your Telegram + client is released after April 20th 2023. """ __slots__ = ( @@ -178,6 +214,7 @@ class InlineKeyboardButton(TelegramObject): "text", "login_url", "web_app", + "switch_inline_query_chosen_chat", ) def __init__( @@ -191,6 +228,7 @@ def __init__( pay: bool = None, login_url: LoginUrl = None, web_app: WebAppInfo = None, + switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat = None, *, api_kwargs: JSONDict = None, ): @@ -207,6 +245,9 @@ def __init__( self.callback_game: Optional[CallbackGame] = callback_game self.pay: Optional[bool] = pay self.web_app: Optional[WebAppInfo] = web_app + self.switch_inline_query_chosen_chat: Optional[ + SwitchInlineQueryChosenChat + ] = switch_inline_query_chosen_chat self._id_attrs = () self._set_id_attrs() @@ -236,6 +277,9 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineKeyboa data["login_url"] = LoginUrl.de_json(data.get("login_url"), bot) data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot) data["callback_game"] = CallbackGame.de_json(data.get("callback_game"), bot) + data["switch_inline_query_chosen_chat"] = SwitchInlineQueryChosenChat.de_json( + data.get("switch_inline_query_chosen_chat"), bot + ) return super().de_json(data=data, bot=bot) diff --git a/telegram/_inline/inlinequery.py b/telegram/_inline/inlinequery.py index 379dfb4be6c..ae5abe23847 100644 --- a/telegram/_inline/inlinequery.py +++ b/telegram/_inline/inlinequery.py @@ -23,6 +23,7 @@ from telegram import constants from telegram._files.location import Location +from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils.defaultvalue import DEFAULT_NONE @@ -146,6 +147,7 @@ async def answer( next_offset: str = None, switch_pm_text: str = None, switch_pm_parameter: str = None, + button: InlineQueryResultsButton = None, *, current_offset: str = None, auto_pagination: bool = False, @@ -192,6 +194,7 @@ async def answer( next_offset=next_offset, switch_pm_text=switch_pm_text, switch_pm_parameter=switch_pm_parameter, + button=button, read_timeout=read_timeout, write_timeout=write_timeout, connect_timeout=connect_timeout, diff --git a/telegram/_inline/inlinequeryresultsbutton.py b/telegram/_inline/inlinequeryresultsbutton.py new file mode 100644 index 00000000000..7d19b2ef95c --- /dev/null +++ b/telegram/_inline/inlinequeryresultsbutton.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2023 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +# pylint: disable=redefined-builtin +"""This module contains the class that represent a Telegram InlineQueryResultsButton.""" + +from typing import TYPE_CHECKING, ClassVar, Optional + +from telegram import constants +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict +from telegram._webappinfo import WebAppInfo + +if TYPE_CHECKING: + from telegram import Bot + + +class InlineQueryResultsButton(TelegramObject): + """This object represents a button to be shown above inline query results. You **must** use + exactly one of the optional fields. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`text`, :attr:`web_app` and :attr:`start_parameter` are equal. + + Args: + text (:obj:`str`): Label text on the button. + web_app (:class:`telegram.WebAppInfo`, optional): Description of the + `Web App `_ that will be launched when the + user presses the button. The Web App will be able to switch back to the inline mode + using the method + `switchInlineQuery `_ + inside the Web App. + start_parameter (:obj:`str`, optional): Deep-linking parameter for the + :guilabel:`/start` message sent to the bot when user presses the switch button. + :tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`- + :tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters, + only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + + Example: + An inline bot that sends YouTube videos can ask the user to connect the bot to + their YouTube account to adapt search results accordingly. To do this, it displays + a 'Connect your YouTube account' button above the results, or even before showing + any. The user presses the button, switches to a private chat with the bot and, in + doing so, passes a start parameter that instructs the bot to return an OAuth link. + Once done, the bot can offer a switch_inline button so that the user can easily + return to the chat where they wanted to use the bot's inline capabilities. + + Attributes: + text (:obj:`str`): Label text on the button. + web_app (:class:`telegram.WebAppInfo`): Optional. Description of the + `Web App `_ that will be launched when the + user presses the button. The Web App will be able to switch back to the inline mode + using the method ``web_app_switch_inline_query`` inside the Web App. + start_parameter (:obj:`str`): Optional. Deep-linking parameter for the + :guilabel:`/start` message sent to the bot when user presses the switch button. + :tg-const:`telegram.InlineQuery.MIN_SWITCH_PM_TEXT_LENGTH`- + :tg-const:`telegram.InlineQuery.MAX_SWITCH_PM_TEXT_LENGTH` characters, + only ``A-Z``, ``a-z``, ``0-9``, ``_`` and ``-`` are allowed. + + """ + + __slots__ = ("text", "web_app", "start_parameter") + + def __init__( + self, + text: str, + web_app: WebAppInfo = None, + start_parameter: str = None, + *, + api_kwargs: JSONDict = None, + ): + super().__init__(api_kwargs=api_kwargs) + + # Required + self.text: str = text + + # Optional + self.web_app: Optional[WebAppInfo] = web_app + self.start_parameter: Optional[str] = start_parameter + + self._id_attrs = (self.text, self.web_app, self.start_parameter) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InlineQueryResultsButton"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + if not data: + return None + + data["web_app"] = WebAppInfo.de_json(data.get("web_app"), bot) + + return super().de_json(data=data, bot=bot) + + MIN_START_PARAMETER_LENGTH: ClassVar[ + int + ] = constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH + """:const:`telegram.constants.InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`""" + MAX_START_PARAMETER_LENGTH: ClassVar[ + int + ] = constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH + """:const:`telegram.constants.InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`""" diff --git a/telegram/_message.py b/telegram/_message.py index 8a962f74bc5..bece9123664 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -59,6 +59,7 @@ from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue from telegram._utils.types import DVInput, FileInput, JSONDict, ODVInput, ReplyMarkup +from telegram._utils.warnings import warn from telegram._videochat import ( VideoChatEnded, VideoChatParticipantsInvited, @@ -69,6 +70,7 @@ from telegram._writeaccessallowed import WriteAccessAllowed from telegram.constants import MessageAttachmentType, ParseMode from telegram.helpers import escape_markdown +from telegram.warnings import PTBDeprecationWarning if TYPE_CHECKING: from telegram import ( @@ -578,8 +580,13 @@ class Message(TelegramObject): .. versionadded:: 20.1 - .. |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. + .. |custom_emoji_formatting_note| replace:: Custom emoji entities will be ignored by this + function. Instead, the supplied replacement for the emoji will be used. + + .. |custom_emoji_md1_deprecation| replace:: Since custom emoji entities are not supported by + :attr:`~telegram.constants.ParseMode.MARKDOWN`, this method will raise a + :exc:`ValueError` in future versions instead of falling back to the supplied replacement + for the emoji. """ # fmt: on @@ -3317,6 +3324,10 @@ def _parse_html( insert = f"{escaped_text}" elif entity.type == MessageEntity.SPOILER: insert = f'{escaped_text}' + elif entity.type == MessageEntity.CUSTOM_EMOJI: + insert = ( + f'{escaped_text}' + ) else: insert = escaped_text @@ -3355,12 +3366,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as HTML. @@ -3374,12 +3385,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as HTML. @@ -3394,12 +3405,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message caption with caption entities formatted as HTML. """ @@ -3413,12 +3424,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message caption with caption entities formatted as HTML. """ @@ -3522,6 +3533,26 @@ def _parse_markdown( "Spoiler entities are not supported for Markdown version 1" ) insert = f"||{escaped_text}||" + elif entity.type == MessageEntity.CUSTOM_EMOJI: + if version == 1: + # this ensures compatibility to previous PTB versions + insert = escaped_text + warn( + "Custom emoji entities are not supported for Markdown version 1. " + "Future version of PTB will raise a ValueError instead of falling " + "back to the alternative standard emoji.", + stacklevel=3, + category=PTBDeprecationWarning, + ) + else: + # This should never be needed because ids are numeric but the documentation + # specifically mentions it so here we are + custom_emoji_id = escape_markdown( + entity.custom_emoji_id, + version=version, + entity_type=MessageEntity.CUSTOM_EMOJI, + ) + insert = f"![{escaped_text}](tg://emoji?id={custom_emoji_id})" else: insert = escaped_text @@ -3570,6 +3601,9 @@ def text_markdown(self) -> str: * |custom_emoji_formatting_note| + .. deprecated:: NEXT.VERSION + |custom_emoji_md1_deprecation| + Returns: :obj:`str`: Message text with entities formatted as Markdown. @@ -3588,12 +3622,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as Markdown. """ @@ -3614,6 +3648,9 @@ def text_markdown_urled(self) -> str: * |custom_emoji_formatting_note| + .. deprecated:: NEXT.VERSION + |custom_emoji_md1_deprecation| + Returns: :obj:`str`: Message text with entities formatted as Markdown. @@ -3632,12 +3669,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as Markdown. """ @@ -3658,6 +3695,9 @@ def caption_markdown(self) -> str: * |custom_emoji_formatting_note| + .. deprecated:: NEXT.VERSION + |custom_emoji_md1_deprecation| + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. @@ -3676,12 +3716,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. """ @@ -3704,6 +3744,9 @@ def caption_markdown_urled(self) -> str: * |custom_emoji_formatting_note| + .. deprecated:: NEXT.VERSION + |custom_emoji_md1_deprecation| + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. @@ -3722,12 +3765,12 @@ 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. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. """ diff --git a/telegram/_switchinlinequerychosenchat.py b/telegram/_switchinlinequerychosenchat.py new file mode 100644 index 00000000000..459b30abf75 --- /dev/null +++ b/telegram/_switchinlinequerychosenchat.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2023 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +"""This module contains a class that represents a Telegram SwitchInlineQueryChosenChat.""" + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class SwitchInlineQueryChosenChat(TelegramObject): + """ + This object represents an inline button that switches the current user to inline mode in a + chosen chat, with an optional default inline query. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`query`, :attr:`allow_user_chats`, :attr:`allow_bot_chats`, + :attr:`allow_group_chats`, and :attr:`allow_channel_chats` are equal. + + .. versionadded:: NEXT.VERSION + + Caution: + The PTB team has discovered that you must pass at least one of + :paramref:`allow_user_chats`, :paramref:`allow_bot_chats`, :paramref:`allow_group_chats`, + or :paramref:`allow_channel_chats` to Telegram. Otherwise, an error will be raised. + + Args: + query (:obj:`str`, optional): The default inline query to be inserted in the input field. + If left empty, only the bot's username will be inserted. + allow_user_chats (:obj:`bool`, optional): Pass :obj:`True`, if private chats with users + can be chosen. + allow_bot_chats (:obj:`bool`, optional): Pass :obj:`True`, if private chats with bots can + be chosen. + allow_group_chats (:obj:`bool`, optional): Pass :obj:`True`, if group and supergroup chats + can be chosen. + allow_channel_chats (:obj:`bool`, optional): Pass :obj:`True`, if channel chats can be + chosen. + + Attributes: + query (:obj:`str`): Optional. The default inline query to be inserted in the input field. + If left empty, only the bot's username will be inserted. + allow_user_chats (:obj:`bool`): Optional. :obj:`True`, if private chats with users can be + chosen. + allow_bot_chats (:obj:`bool`): Optional. :obj:`True`, if private chats with bots can be + chosen. + allow_group_chats (:obj:`bool`): Optional. :obj:`True`, if group and supergroup chats can + be chosen. + allow_channel_chats (:obj:`bool`): Optional. :obj:`True`, if channel chats can be chosen. + + """ + + __slots__ = ( + "query", + "allow_user_chats", + "allow_bot_chats", + "allow_group_chats", + "allow_channel_chats", + ) + + def __init__( + self, + query: str = None, + allow_user_chats: bool = None, + allow_bot_chats: bool = None, + allow_group_chats: bool = None, + allow_channel_chats: bool = None, + *, + api_kwargs: JSONDict = None, + ): + super().__init__(api_kwargs=api_kwargs) + # Optional + self.query = query + self.allow_user_chats = allow_user_chats + self.allow_bot_chats = allow_bot_chats + self.allow_group_chats = allow_group_chats + self.allow_channel_chats = allow_channel_chats + + self._id_attrs = ( + self.query, + self.allow_user_chats, + self.allow_bot_chats, + self.allow_group_chats, + self.allow_channel_chats, + ) + + self._freeze() diff --git a/telegram/_writeaccessallowed.py b/telegram/_writeaccessallowed.py index bba6ac88ba1..faa5c0072e3 100644 --- a/telegram/_writeaccessallowed.py +++ b/telegram/_writeaccessallowed.py @@ -17,21 +17,35 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects related to the write access allowed service message.""" +from typing import Optional + from telegram._telegramobject import TelegramObject from telegram._utils.types import JSONDict class WriteAccessAllowed(TelegramObject): """ - This object represents a service message about a user allowing a bot added to the attachment - menu to write messages. Currently holds no information. + This object represents a service message about a user allowing a bot to write messages after + adding the bot to the attachment menu or launching a Web App from a link. .. versionadded:: 20.0 + + Args: + web_app_name (:obj:`str`, optional): Name of the Web App which was launched from a link. + + .. versionadded:: NEXT.VERSION + + Attributes: + web_app_name (:obj:`str`): Optional. Name of the Web App which was launched from a link. + + .. versionadded:: NEXT.VERSION + """ - __slots__ = () + __slots__ = ("web_app_name",) - def __init__(self, *, api_kwargs: JSONDict = None): + def __init__(self, web_app_name: str = None, *, api_kwargs: JSONDict = None): super().__init__(api_kwargs=api_kwargs) + self.web_app_name: Optional[str] = web_app_name self._freeze() diff --git a/telegram/constants.py b/telegram/constants.py index 65569ebe939..33fe2c758a6 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -37,6 +37,7 @@ "BotCommandLimit", "BotCommandScopeType", "BotDescriptionLimit", + "BotNameLimit", "CallbackQueryLimit", "ChatAction", "ChatID", @@ -57,6 +58,7 @@ "InlineKeyboardMarkupLimit", "InlineQueryLimit", "InlineQueryResultLimit", + "InlineQueryResultsButtonLimit", "InlineQueryResultType", "InputMediaType", "InvoiceLimit", @@ -114,7 +116,7 @@ def __str__(self) -> str: #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=6) +BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=7) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -209,6 +211,21 @@ class BotDescriptionLimit(IntEnum): """ +class BotNameLimit(IntEnum): + """This enum contains limitations for the methods :meth:`telegram.Bot.set_my_name`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MAX_NAME_LENGTH = 64 + """:obj:`int`: Maximum length for the parameter :paramref:`~telegram.Bot.set_my_name.name` of + :meth:`telegram.Bot.set_my_name` + """ + + class CallbackQueryLimit(IntEnum): """This enum contains limitations for :class:`telegram.CallbackQuery`/ :meth:`telegram.Bot.answer_callback_query`. The enum members of this enumeration are instances @@ -735,11 +752,19 @@ class InlineQueryLimit(IntEnum): MIN_SWITCH_PM_TEXT_LENGTH = 1 """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the :paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of - :meth:`telegram.Bot.answer_inline_query`.""" + :meth:`telegram.Bot.answer_inline_query`. + + .. deprecated:: NEXT.VERSION + Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MIN_START_PARAMETER_LENGTH`. + """ MAX_SWITCH_PM_TEXT_LENGTH = 64 """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the :paramref:`~telegram.Bot.answer_inline_query.switch_pm_parameter` parameter of - :meth:`telegram.Bot.answer_inline_query`.""" + :meth:`telegram.Bot.answer_inline_query`. + + .. deprecated:: NEXT.VERSION + Deprecated in favor of :attr:`InlineQueryResultsButtonLimit.MAX_START_PARAMETER_LENGTH`. + """ class InlineQueryResultLimit(IntEnum): @@ -763,6 +788,26 @@ class InlineQueryResultLimit(IntEnum): """ +class InlineQueryResultsButtonLimit(IntEnum): + """This enum contains limitations for :class:`telegram.InlineQueryResultsButton`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MIN_START_PARAMETER_LENGTH = InlineQueryLimit.MIN_SWITCH_PM_TEXT_LENGTH + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of + :meth:`telegram.InlineQueryResultsButton`.""" + + MAX_START_PARAMETER_LENGTH = InlineQueryLimit.MAX_SWITCH_PM_TEXT_LENGTH + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.InlineQueryResultsButton.start_parameter` parameter of + :meth:`telegram.InlineQueryResultsButton`.""" + + class InlineQueryResultType(StringEnum): """This enum contains the available types of :class:`telegram.InlineQueryResult`. The enum members of this enumeration are instances of :class:`str` and can be treated as such. diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index a2995a46783..eea4ad963ae 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -46,6 +46,7 @@ BotCommand, BotCommandScope, BotDescription, + BotName, BotShortDescription, CallbackQuery, Chat, @@ -60,6 +61,7 @@ ForumTopic, GameHighScore, InlineKeyboardMarkup, + InlineQueryResultsButton, InputMedia, InputSticker, Location, @@ -792,6 +794,7 @@ async def answer_inline_query( next_offset: str = None, switch_pm_text: str = None, switch_pm_parameter: str = None, + button: InlineQueryResultsButton = None, *, current_offset: str = None, read_timeout: ODVInput[float] = DEFAULT_NONE, @@ -814,6 +817,7 @@ async def answer_inline_query( write_timeout=write_timeout, connect_timeout=connect_timeout, pool_timeout=pool_timeout, + button=button, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) @@ -3581,6 +3585,48 @@ async def get_my_short_description( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def set_my_name( + self, + name: str = None, + language_code: str = 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, + rate_limit_args: RLARGS = None, + ) -> bool: + return await super().set_my_name( + name=name, + language_code=language_code, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def get_my_name( + self, + language_code: str = 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, + rate_limit_args: RLARGS = None, + ) -> BotName: + return await super().get_my_name( + language_code=language_code, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def set_custom_emoji_sticker_set_thumbnail( self, name: str, @@ -3823,3 +3869,5 @@ async def set_sticker_mask_position( setStickerEmojiList = set_sticker_emoji_list setStickerKeywords = set_sticker_keywords setStickerMaskPosition = set_sticker_mask_position + setMyName = set_my_name + getMyName = get_my_name diff --git a/telegram/helpers.py b/telegram/helpers.py index 632cd364742..9bf8ba7309b 100644 --- a/telegram/helpers.py +++ b/telegram/helpers.py @@ -44,23 +44,27 @@ def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str: """Helper function to escape telegram markup symbols. + .. versionchanged:: NEXT.VERSION + Custom emoji entity escaping is now supported. + Args: text (:obj:`str`): The text. version (:obj:`int` | :obj:`str`): Use to specify the version of telegrams Markdown. Either ``1`` or ``2``. Defaults to ``1``. entity_type (:obj:`str`, optional): For the entity types :tg-const:`telegram.MessageEntity.PRE`, :tg-const:`telegram.MessageEntity.CODE` and - the link part of :tg-const:`telegram.MessageEntity.TEXT_LINK`, only certain characters - need to be escaped in :tg-const:`telegram.constants.ParseMode.MARKDOWN_V2`. - See the official API documentation for details. Only valid in combination with - ``version=2``, will be ignored else. + the link part of :tg-const:`telegram.MessageEntity.TEXT_LINK` and + :tg-const:`telegram.MessageEntity.CUSTOM_EMOJI`, only certain characters need to be + escaped in :tg-const:`telegram.constants.ParseMode.MARKDOWN_V2`. See the `official API + documentation `_ for details. + Only valid in combination with ``version=2``, will be ignored else. """ if int(version) == 1: escape_chars = r"_*`[" elif int(version) == 2: if entity_type in ["pre", "code"]: escape_chars = r"\`" - elif entity_type == "text_link": + elif entity_type in ["text_link", "custom_emoji"]: escape_chars = r"\)" else: escape_chars = r"\_*[]()~`>#+-=|{}.!" diff --git a/tests/_inline/test_inlinekeyboardbutton.py b/tests/_inline/test_inlinekeyboardbutton.py index 29675504b81..15c7ef8bf5d 100644 --- a/tests/_inline/test_inlinekeyboardbutton.py +++ b/tests/_inline/test_inlinekeyboardbutton.py @@ -19,7 +19,13 @@ import pytest -from telegram import CallbackGame, InlineKeyboardButton, LoginUrl, WebAppInfo +from telegram import ( + CallbackGame, + InlineKeyboardButton, + LoginUrl, + SwitchInlineQueryChosenChat, + WebAppInfo, +) from tests.auxil.slots import mro_slots @@ -35,6 +41,7 @@ def inline_keyboard_button(): pay=TestInlineKeyboardButtonBase.pay, login_url=TestInlineKeyboardButtonBase.login_url, web_app=TestInlineKeyboardButtonBase.web_app, + switch_inline_query_chosen_chat=TestInlineKeyboardButtonBase.switch_inline_query_chosen_chat, # noqa: E501 ) @@ -48,6 +55,7 @@ class TestInlineKeyboardButtonBase: pay = True login_url = LoginUrl("http://google.com") web_app = WebAppInfo(url="https://example.com") + switch_inline_query_chosen_chat = SwitchInlineQueryChosenChat("a_bot", True, False, True, True) class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase): @@ -70,6 +78,10 @@ def test_expected_values(self, inline_keyboard_button): assert inline_keyboard_button.pay == self.pay assert inline_keyboard_button.login_url == self.login_url assert inline_keyboard_button.web_app == self.web_app + assert ( + inline_keyboard_button.switch_inline_query_chosen_chat + == self.switch_inline_query_chosen_chat + ) def test_to_dict(self, inline_keyboard_button): inline_keyboard_button_dict = inline_keyboard_button.to_dict() @@ -95,6 +107,10 @@ def test_to_dict(self, inline_keyboard_button): inline_keyboard_button_dict["login_url"] == inline_keyboard_button.login_url.to_dict() ) assert inline_keyboard_button_dict["web_app"] == inline_keyboard_button.web_app.to_dict() + assert ( + inline_keyboard_button_dict["switch_inline_query_chosen_chat"] + == inline_keyboard_button.switch_inline_query_chosen_chat.to_dict() + ) def test_de_json(self, bot): json_dict = { @@ -107,6 +123,7 @@ def test_de_json(self, bot): "web_app": self.web_app.to_dict(), "login_url": self.login_url.to_dict(), "pay": self.pay, + "switch_inline_query_chosen_chat": self.switch_inline_query_chosen_chat.to_dict(), } inline_keyboard_button = InlineKeyboardButton.de_json(json_dict, None) @@ -124,6 +141,10 @@ def test_de_json(self, bot): assert inline_keyboard_button.pay == self.pay assert inline_keyboard_button.login_url == self.login_url assert inline_keyboard_button.web_app == self.web_app + assert ( + inline_keyboard_button.switch_inline_query_chosen_chat + == self.switch_inline_query_chosen_chat + ) none = InlineKeyboardButton.de_json({}, bot) assert none is None diff --git a/tests/test_bot.py b/tests/test_bot.py index 063c8b93f2d..7bc003c4889 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -34,6 +34,7 @@ BotCommand, BotCommandScopeChat, BotDescription, + BotName, BotShortDescription, CallbackQuery, Chat, @@ -44,6 +45,7 @@ InlineKeyboardMarkup, InlineQueryResultArticle, InlineQueryResultDocument, + InlineQueryResultsButton, InlineQueryResultVoice, InputFile, InputMessageContent, @@ -76,7 +78,7 @@ from telegram.ext import ExtBot, InvalidCallbackData from telegram.helpers import escape_markdown from telegram.request import BaseRequest, HTTPXRequest, RequestData -from telegram.warnings import PTBUserWarning +from telegram.warnings import PTBDeprecationWarning, PTBUserWarning from tests.auxil.bot_method_checks import check_defaults_handling from tests.auxil.ci_bots import FALLBACKS from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS @@ -655,10 +657,11 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): ) # TODO: Needs improvement. We need incoming inline query to test answer. - async def test_answer_inline_query(self, monkeypatch, bot, raw_bot): + @pytest.mark.parametrize("button_type", ["start", "web_app", "backward_compat"]) + async def test_answer_inline_query(self, monkeypatch, bot, raw_bot, button_type): # For now just test that our internals pass the correct data async def make_assertion(url, request_data: RequestData, *args, **kwargs): - return request_data.parameters == { + expected = { "cache_time": 300, "results": [ { @@ -685,12 +688,22 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): }, ], "next_offset": "42", - "switch_pm_parameter": "start_pm", "inline_query_id": 1234, "is_personal": True, - "switch_pm_text": "switch pm", } + if button_type in ["start", "backward_compat"]: + button_dict = {"text": "button_text", "start_parameter": "start_parameter"} + else: + button_dict = { + "text": "button_text", + "web_app": {"url": "https://python-telegram-bot.org"}, + } + + expected["button"] = button_dict + + return request_data.parameters == expected + results = [ InlineQueryResultArticle("11", "first", InputTextMessageContent("first")), InlineQueryResultArticle("12", "second", InputMessageContentDWPP("second")), @@ -705,6 +718,17 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): ), ] + if button_type == "start": + button = InlineQueryResultsButton( + text="button_text", start_parameter="start_parameter" + ) + elif button_type == "web_app": + button = InlineQueryResultsButton( + text="button_text", web_app=WebAppInfo("https://python-telegram-bot.org") + ) + else: + button = None + copied_results = copy.copy(results) ext_bot = bot for bot in (ext_bot, raw_bot): @@ -717,8 +741,11 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): cache_time=300, is_personal=True, next_offset="42", - switch_pm_text="switch pm", - switch_pm_parameter="start_pm", + switch_pm_text="button_text" if button_type == "backward_compat" else None, + switch_pm_parameter="start_parameter" + if button_type == "backward_compat" + else None, + button=button, ) # 1) @@ -739,6 +766,43 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): monkeypatch.delattr(bot.request, "post") + @pytest.mark.parametrize("bot_class", ["Bot", "ExtBot"]) + async def test_answer_inline_query_deprecated_args( + self, monkeypatch, recwarn, bot_class, bot, raw_bot + ): + async def mock_post(*args, **kwargs): + return True + + bot = raw_bot if bot_class == "Bot" else bot + + monkeypatch.setattr(bot.request, "post", mock_post) + + with pytest.raises( + TypeError, match="6.7, the parameter `button is mutually exclusive to the deprecated" + ): + await bot.answer_inline_query( + inline_query_id="123", + results=[], + button=True, + switch_pm_text="text", + switch_pm_parameter="param", + ) + + recwarn.clear() + assert await bot.answer_inline_query( + inline_query_id="123", + results=[], + switch_pm_text="text", + switch_pm_parameter="parameter", + ) + assert len(recwarn) == 1 + assert recwarn[-1].category is PTBDeprecationWarning + assert str(recwarn[-1].message).startswith( + "Since Bot API 6.7, the parameters `switch_pm_text` and `switch_pm_parameter` are " + "deprecated" + ) + assert recwarn[-1].filename == __file__, "stacklevel is incorrect!" + async def test_answer_inline_query_no_default_parse_mode(self, monkeypatch, bot): async def make_assertion(url, request_data: RequestData, *args, **kwargs): return request_data.parameters == { @@ -769,10 +833,8 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): }, ], "next_offset": "42", - "switch_pm_parameter": "start_pm", "inline_query_id": 1234, "is_personal": True, - "switch_pm_text": "switch pm", } monkeypatch.setattr(bot.request, "post", make_assertion) @@ -797,8 +859,6 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): cache_time=300, is_personal=True, next_offset="42", - switch_pm_text="switch pm", - switch_pm_parameter="start_pm", ) # make sure that the results were not edited in-place assert results == copied_results @@ -862,10 +922,8 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): }, ], "next_offset": "42", - "switch_pm_parameter": "start_pm", "inline_query_id": 1234, "is_personal": True, - "switch_pm_text": "switch pm", } monkeypatch.setattr(default_bot.request, "post", make_assertion) @@ -890,8 +948,6 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): cache_time=300, is_personal=True, next_offset="42", - switch_pm_text="switch pm", - switch_pm_parameter="start_pm", ) # make sure that the results were not edited in-place assert results == copied_results @@ -1680,6 +1736,80 @@ async def test_http2_runtime_error(self, recwarn, bot_class): assert warning.filename == __file__, "wrong stacklevel!" assert warning.category is PTBUserWarning + async def test_set_get_my_name(self, bot, monkeypatch): + """We only test that we pass the correct values to TG since this endpoint is heavily + rate limited which makes automated tests rather infeasible.""" + default_name = "default_bot_name" + en_name = "en_bot_name" + de_name = "de_bot_name" + + # We predefine the responses that we would TG expect to send us + set_stack = asyncio.Queue() + get_stack = asyncio.Queue() + await set_stack.put({"name": default_name}) + await set_stack.put({"name": en_name, "language_code": "en"}) + await set_stack.put({"name": de_name, "language_code": "de"}) + await get_stack.put({"name": default_name, "language_code": None}) + await get_stack.put({"name": en_name, "language_code": "en"}) + await get_stack.put({"name": de_name, "language_code": "de"}) + + await set_stack.put({"name": default_name}) + await set_stack.put({"language_code": "en"}) + await set_stack.put({"language_code": "de"}) + await get_stack.put({"name": default_name, "language_code": None}) + await get_stack.put({"name": default_name, "language_code": "en"}) + await get_stack.put({"name": default_name, "language_code": "de"}) + + async def post(url, request_data: RequestData, *args, **kwargs): + # The mock-post now just fetches the predefined responses from the queues + if "setMyName" in url: + expected = await set_stack.get() + assert request_data.json_parameters == expected + set_stack.task_done() + return True + + bot_name = await get_stack.get() + if "language_code" in request_data.json_parameters: + assert request_data.json_parameters == {"language_code": bot_name["language_code"]} + else: + assert request_data.json_parameters == {} + get_stack.task_done() + return bot_name + + monkeypatch.setattr(bot.request, "post", post) + + # Set the names + assert all( + await asyncio.gather( + bot.set_my_name(default_name), + bot.set_my_name(en_name, language_code="en"), + bot.set_my_name(de_name, language_code="de"), + ) + ) + + # Check that they were set correctly + assert await asyncio.gather( + bot.get_my_name(), bot.get_my_name("en"), bot.get_my_name("de") + ) == [ + BotName(default_name), + BotName(en_name), + BotName(de_name), + ] + + # Delete the names + assert all( + await asyncio.gather( + bot.set_my_name(default_name), + bot.set_my_name(None, language_code="en"), + bot.set_my_name(None, language_code="de"), + ) + ) + + # Check that they were deleted correctly + assert await asyncio.gather( + bot.get_my_name(), bot.get_my_name("en"), bot.get_my_name("de") + ) == 3 * [BotName(default_name)] + class TestBotWithRequest: """ diff --git a/tests/test_botname.py b/tests/test_botname.py new file mode 100644 index 00000000000..89d2482ed31 --- /dev/null +++ b/tests/test_botname.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2023 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import pytest + +from telegram import BotName +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def bot_name(bot): + return BotName(TestBotNameBase.name) + + +class TestBotNameBase: + name = "This is a test name" + + +class TestBotNameWithoutRequest(TestBotNameBase): + def test_slot_behaviour(self, bot_name): + for attr in bot_name.__slots__: + assert getattr(bot_name, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(bot_name)) == len(set(mro_slots(bot_name))), "duplicate slot" + + def test_to_dict(self, bot_name): + bot_name_dict = bot_name.to_dict() + + assert isinstance(bot_name_dict, dict) + assert bot_name_dict["name"] == self.name + + def test_equality(self): + a = BotName(self.name) + b = BotName(self.name) + c = BotName("text.com") + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) diff --git a/tests/test_chatmemberupdated.py b/tests/test_chatmemberupdated.py index de3da135435..f0baf88e038 100644 --- a/tests/test_chatmemberupdated.py +++ b/tests/test_chatmemberupdated.py @@ -79,7 +79,7 @@ def invite_link(user): @pytest.fixture(scope="module") def chat_member_updated(user, chat, old_chat_member, new_chat_member, invite_link, time): - return ChatMemberUpdated(chat, user, time, old_chat_member, new_chat_member, invite_link) + return ChatMemberUpdated(chat, user, time, old_chat_member, new_chat_member, invite_link, True) class TestChatMemberUpdatedBase: @@ -113,6 +113,7 @@ def test_de_json_required_args(self, bot, user, chat, old_chat_member, new_chat_ assert chat_member_updated.old_chat_member == old_chat_member assert chat_member_updated.new_chat_member == new_chat_member assert chat_member_updated.invite_link is None + assert chat_member_updated.via_chat_folder_invite_link is None def test_de_json_all_args( self, bot, user, time, invite_link, chat, old_chat_member, new_chat_member @@ -124,6 +125,7 @@ def test_de_json_all_args( "old_chat_member": old_chat_member.to_dict(), "new_chat_member": new_chat_member.to_dict(), "invite_link": invite_link.to_dict(), + "via_chat_folder_invite_link": True, } chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot) @@ -136,6 +138,7 @@ def test_de_json_all_args( assert chat_member_updated.old_chat_member == old_chat_member assert chat_member_updated.new_chat_member == new_chat_member assert chat_member_updated.invite_link == invite_link + assert chat_member_updated.via_chat_folder_invite_link is True def test_de_json_localization( self, bot, raw_bot, tz_bot, user, chat, old_chat_member, new_chat_member, time, invite_link @@ -178,6 +181,10 @@ def test_to_dict(self, chat_member_updated): == chat_member_updated.new_chat_member.to_dict() ) assert chat_member_updated_dict["invite_link"] == chat_member_updated.invite_link.to_dict() + assert ( + chat_member_updated_dict["via_chat_folder_invite_link"] + == chat_member_updated.via_chat_folder_invite_link + ) def test_equality(self, time, old_chat_member, new_chat_member, invite_link): a = ChatMemberUpdated( diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8e8da7f3636..9ac642a2276 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -32,8 +32,9 @@ class TestHelpers: ("_italic_", r"\_italic\_"), ("`code`", r"\`code\`"), ("[text_link](https://github.com/)", r"\[text\_link](https://github.com/)"), + ("![👍](tg://emoji?id=1)", r"!\[👍](tg://emoji?id=1)"), ], - ids=["bold", "italic", "code", "text_link"], + ids=["bold", "italic", "code", "text_link", "custom_emoji_id"], ) def test_escape_markdown(self, test_str, expected): assert expected == helpers.escape_markdown(test_str) @@ -68,13 +69,16 @@ def test_escape_markdown_v2_monospaced(self, test_str, expected): test_str, version=2, entity_type=MessageEntity.CODE ) - def test_escape_markdown_v2_text_link(self): + def test_escape_markdown_v2_links(self): test_str = "https://url.containing/funny)cha)\\ra\\)cter\\s" expected_str = "https://url.containing/funny\\)cha\\)\\\\ra\\\\\\)cter\\\\s" assert expected_str == helpers.escape_markdown( test_str, version=2, entity_type=MessageEntity.TEXT_LINK ) + assert expected_str == helpers.escape_markdown( + test_str, version=2, entity_type=MessageEntity.CUSTOM_EMOJI + ) def test_markdown_invalid_version(self): with pytest.raises(ValueError, match="Markdown version must be either"): diff --git a/tests/test_inlinequeryresultsbutton.py b/tests/test_inlinequeryresultsbutton.py new file mode 100644 index 00000000000..db87326c556 --- /dev/null +++ b/tests/test_inlinequeryresultsbutton.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2023 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import pytest + +from telegram import InlineQueryResultsButton, WebAppInfo +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def inline_query_results_button(): + return InlineQueryResultsButton( + text=TestInlineQueryResultsButtonBase.text, + start_parameter=TestInlineQueryResultsButtonBase.start_parameter, + web_app=TestInlineQueryResultsButtonBase.web_app, + ) + + +class TestInlineQueryResultsButtonBase: + text = "text" + start_parameter = "start_parameter" + web_app = WebAppInfo(url="https://python-telegram-bot.org") + + +class TestInlineQueryResultsButtonWithoutRequest(TestInlineQueryResultsButtonBase): + def test_slot_behaviour(self, inline_query_results_button): + inst = inline_query_results_button + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_to_dict(self, inline_query_results_button): + inline_query_results_button_dict = inline_query_results_button.to_dict() + assert isinstance(inline_query_results_button_dict, dict) + assert inline_query_results_button_dict["text"] == self.text + assert inline_query_results_button_dict["start_parameter"] == self.start_parameter + assert inline_query_results_button_dict["web_app"] == self.web_app.to_dict() + + def test_de_json(self, bot): + assert InlineQueryResultsButton.de_json(None, bot) is None + assert InlineQueryResultsButton.de_json({}, bot) is None + + json_dict = { + "text": self.text, + "start_parameter": self.start_parameter, + "web_app": self.web_app.to_dict(), + } + inline_query_results_button = InlineQueryResultsButton.de_json(json_dict, bot) + + assert inline_query_results_button.text == self.text + assert inline_query_results_button.start_parameter == self.start_parameter + assert inline_query_results_button.web_app == self.web_app + + def test_equality(self): + a = InlineQueryResultsButton(self.text, self.start_parameter, self.web_app) + b = InlineQueryResultsButton(self.text, self.start_parameter, self.web_app) + c = InlineQueryResultsButton(self.text, "", self.web_app) + d = InlineQueryResultsButton(self.text, self.start_parameter, None) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_message.py b/tests/test_message.py index a47c085de18..86b3c0fa470 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -59,6 +59,7 @@ from telegram._utils.datetime import UTC from telegram.constants import ChatAction, ParseMode from telegram.ext import Defaults +from telegram.warnings import PTBDeprecationWarning from tests._passport.test_passport import RAW_PASSPORT_DATA from tests.auxil.bot_method_checks import ( check_defaults_handling, @@ -322,10 +323,11 @@ class TestMessageBase: {"length": 9, "offset": 101, "type": "strikethrough"}, {"length": 10, "offset": 129, "type": "pre", "language": "python"}, {"length": 7, "offset": 141, "type": "spoiler"}, + {"length": 2, "offset": 150, "type": "custom_emoji", "custom_emoji_id": "1"}, ] test_text_v2 = ( r"Test for trgh nested in italic. Python pre. Spoiled." + "http://google.com and bold nested in strk>trgh nested in italic. Python pre. Spoiled. 👍." ) test_message = Message( message_id=1, @@ -513,7 +515,8 @@ def test_text_html_simple(self): r"
`\pre
. http://google.com " "and bold nested in strk>trgh nested in italic. " '
Python pre
. ' - 'Spoiled.' + 'Spoiled. ' + '👍.' ) text_html = self.test_message_v2.text_html assert text_html == test_html_string @@ -532,7 +535,8 @@ def test_text_html_urled(self): r'
`\pre
. http://google.com ' "and bold nested in strk>trgh nested in italic. " '
Python pre
. ' - 'Spoiled.' + 'Spoiled. ' + '👍.' ) text_html = self.test_message_v2.text_html_urled assert text_html == test_html_string @@ -553,7 +557,7 @@ def test_text_markdown_v2_simple(self): "[links](http://github.com/abc\\\\\\)def), " "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " r"http://google\.com and _bold *nested in ~strk\>trgh~ nested in* italic_\. " - "```python\nPython pre```\\. ||Spoiled||\\." + "```python\nPython pre```\\. ||Spoiled||\\. ![👍](tg://emoji?id=1)\\." ) text_markdown = self.test_message_v2.text_markdown_v2 assert text_markdown == test_md_string @@ -603,7 +607,8 @@ def test_text_markdown_v2_urled(self): "[links](http://github.com/abc\\\\\\)def), " "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " r"[http://google\.com](http://google.com) and _bold *nested in ~strk\>trgh~ " - "nested in* italic_\\. ```python\nPython pre```\\. ||Spoiled||\\." + "nested in* italic_\\. ```python\nPython pre```\\. ||Spoiled||\\. " + "![👍](tg://emoji?id=1)\\." ) text_markdown = self.test_message_v2.text_markdown_v2_urled assert text_markdown == test_md_string @@ -634,17 +639,72 @@ def test_text_markdown_emoji(self): @pytest.mark.parametrize( "type_", argvalues=[ - "text_html", - "text_html_urled", "text_markdown", "text_markdown_urled", + ], + ) + def test_text_custom_emoji_md_v1(self, type_, recwarn): + 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 == getattr(message, type_) + + assert len(recwarn) == 1 + assert recwarn[0].category is PTBDeprecationWarning + assert str(recwarn[0].message).startswith( + "Custom emoji entities are not supported for Markdown version 1" + ) + assert recwarn[0].filename == __file__ + + @pytest.mark.parametrize( + "type_", + argvalues=[ "text_markdown_v2", "text_markdown_v2_urled", ], ) - def test_text_custom_emoji(self, type_): + def test_text_custom_emoji_md_v2(self, type_): text = "Look a custom emoji: 😎" - expected = "Look a custom emoji: 😎" + expected = "Look a custom emoji: ![😎](tg://emoji?id=5472409228461217725)" + 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_] + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "text_html", + "text_html_urled", + ], + ) + def test_text_custom_emoji_html(self, type_): + text = "Look a custom emoji: 😎" + expected = 'Look a custom emoji: 😎' emoji_entity = MessageEntity( type=MessageEntity.CUSTOM_EMOJI, offset=21, @@ -670,7 +730,8 @@ def test_caption_html_simple(self): r"
`\pre
. http://google.com " "and bold nested in strk>trgh nested in italic. " '
Python pre
. ' - 'Spoiled.' + 'Spoiled. ' + '👍.' ) caption_html = self.test_message_v2.caption_html assert caption_html == test_html_string @@ -689,7 +750,8 @@ def test_caption_html_urled(self): r'
`\pre
. http://google.com ' "and bold nested in strk>trgh nested in italic. " '
Python pre
. ' - 'Spoiled.' + 'Spoiled. ' + '👍.' ) caption_html = self.test_message_v2.caption_html_urled assert caption_html == test_html_string @@ -710,7 +772,7 @@ def test_caption_markdown_v2_simple(self): "[links](http://github.com/abc\\\\\\)def), " "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " r"http://google\.com and _bold *nested in ~strk\>trgh~ nested in* italic_\. " - "```python\nPython pre```\\. ||Spoiled||\\." + "```python\nPython pre```\\. ||Spoiled||\\. ![👍](tg://emoji?id=1)\\." ) caption_markdown = self.test_message_v2.caption_markdown_v2 assert caption_markdown == test_md_string @@ -737,7 +799,8 @@ def test_caption_markdown_v2_urled(self): "[links](http://github.com/abc\\\\\\)def), " "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " r"[http://google\.com](http://google.com) and _bold *nested in ~strk\>trgh~ " - "nested in* italic_\\. ```python\nPython pre```\\. ||Spoiled||\\." + "nested in* italic_\\. ```python\nPython pre```\\. ||Spoiled||\\. " + "![👍](tg://emoji?id=1)\\." ) caption_markdown = self.test_message_v2.caption_markdown_v2_urled assert caption_markdown == test_md_string @@ -773,17 +836,72 @@ def test_caption_markdown_emoji(self): @pytest.mark.parametrize( "type_", argvalues=[ - "caption_html", - "caption_html_urled", "caption_markdown", "caption_markdown_urled", + ], + ) + def test_caption_custom_emoji_md_v1(self, type_, recwarn): + 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 == getattr(message, type_) + + assert len(recwarn) == 1 + assert recwarn[0].category is PTBDeprecationWarning + assert str(recwarn[0].message).startswith( + "Custom emoji entities are not supported for Markdown version 1" + ) + assert recwarn[0].filename == __file__ + + @pytest.mark.parametrize( + "type_", + argvalues=[ "caption_markdown_v2", "caption_markdown_v2_urled", ], ) - def test_caption_custom_emoji(self, type_): + def test_caption_custom_emoji_md_v2(self, type_): caption = "Look a custom emoji: 😎" - expected = "Look a custom emoji: 😎" + expected = "Look a custom emoji: ![😎](tg://emoji?id=5472409228461217725)" + 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_] + + @pytest.mark.parametrize( + "type_", + argvalues=[ + "caption_html", + "caption_html_urled", + ], + ) + def test_caption_custom_emoji_html(self, type_): + caption = "Look a custom emoji: 😎" + expected = 'Look a custom emoji: 😎' emoji_entity = MessageEntity( type=MessageEntity.CUSTOM_EMOJI, offset=21, @@ -955,7 +1073,7 @@ async def test_reply_markdown_v2(self, monkeypatch, message): "[links](http://github.com/abc\\\\\\)def), " "[text\\-mention](tg://user?id=123456789) and ```\\`\\\\pre```\\. " r"http://google\.com and _bold *nested in ~strk\>trgh~ nested in* italic_\. " - "```python\nPython pre```\\. ||Spoiled||\\." + "```python\nPython pre```\\. ||Spoiled||\\. ![👍](tg://emoji?id=1)\\." ) async def make_assertion(*_, **kwargs): @@ -995,7 +1113,8 @@ async def test_reply_html(self, monkeypatch, message): r"
`\pre
. http://google.com " "and bold nested in strk>trgh nested in italic. " '
Python pre
. ' - 'Spoiled.' + 'Spoiled. ' + '👍.' ) async def make_assertion(*_, **kwargs): diff --git a/tests/test_official.py b/tests/test_official.py index 3e829f11b7c..a3cba293aa9 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -154,6 +154,7 @@ def ignored_param_requirements(object_name) -> Set[str]: "thumb_url", }, "InlineQueryResult(Game|Gif|Mpeg4Gif)": {"thumb_mime_type"}, + "answer_inline_query": {"switch_pm_text", "switch_pm_parameter"}, } diff --git a/tests/test_switchinlinequerychosenchat.py b/tests/test_switchinlinequerychosenchat.py new file mode 100644 index 00000000000..dc610e449dc --- /dev/null +++ b/tests/test_switchinlinequerychosenchat.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2023 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import pytest + +from telegram import SwitchInlineQueryChosenChat +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def switch_inline_query_chosen_chat(): + return SwitchInlineQueryChosenChat( + query=TestSwitchInlineQueryChosenChatBase.query, + allow_user_chats=TestSwitchInlineQueryChosenChatBase.allow_user_chats, + allow_bot_chats=TestSwitchInlineQueryChosenChatBase.allow_bot_chats, + allow_channel_chats=TestSwitchInlineQueryChosenChatBase.allow_channel_chats, + allow_group_chats=TestSwitchInlineQueryChosenChatBase.allow_group_chats, + ) + + +class TestSwitchInlineQueryChosenChatBase: + query = "query" + allow_user_chats = True + allow_bot_chats = True + allow_channel_chats = False + allow_group_chats = True + + +class TestSwitchInlineQueryChosenChat(TestSwitchInlineQueryChosenChatBase): + def test_slot_behaviour(self, switch_inline_query_chosen_chat): + inst = switch_inline_query_chosen_chat + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_expected_values(self, switch_inline_query_chosen_chat): + assert switch_inline_query_chosen_chat.query == self.query + assert switch_inline_query_chosen_chat.allow_user_chats == self.allow_user_chats + assert switch_inline_query_chosen_chat.allow_bot_chats == self.allow_bot_chats + assert switch_inline_query_chosen_chat.allow_channel_chats == self.allow_channel_chats + assert switch_inline_query_chosen_chat.allow_group_chats == self.allow_group_chats + + def test_to_dict(self, switch_inline_query_chosen_chat): + siqcc = switch_inline_query_chosen_chat.to_dict() + + assert isinstance(siqcc, dict) + assert siqcc["query"] == switch_inline_query_chosen_chat.query + assert siqcc["allow_user_chats"] == switch_inline_query_chosen_chat.allow_user_chats + assert siqcc["allow_bot_chats"] == switch_inline_query_chosen_chat.allow_bot_chats + assert siqcc["allow_channel_chats"] == switch_inline_query_chosen_chat.allow_channel_chats + assert siqcc["allow_group_chats"] == switch_inline_query_chosen_chat.allow_group_chats + + def test_equality(self): + siqcc = SwitchInlineQueryChosenChat + a = siqcc(self.query, self.allow_user_chats, self.allow_bot_chats) + b = siqcc(self.query, self.allow_user_chats, self.allow_bot_chats) + c = siqcc(self.query, self.allow_user_chats) + d = siqcc("", self.allow_user_chats, self.allow_bot_chats) + e = siqcc(self.query, self.allow_user_chats, self.allow_bot_chats, self.allow_group_chats) + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e)