diff --git a/README.rst b/README.rst index 00b6fa1ec..1ddad4bfd 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 29377db2a..b8425e478 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 c2d82d507..36b871c7b 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 eeb408e96..3dc01c6d7 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 000000000..0f78027c7 --- /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 cda06bc0c..7fa52a94b 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 000000000..7323a20cc --- /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 000000000..603a56f6a --- /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 e198098a9..fb3dcadd6 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -38,6 +38,7 @@ __all__ = ( # Keep this alphabetically ordered "BotCommandScopeChatMember", "BotCommandScopeDefault", "BotDescription", + "BotName", "BotShortDescription", "CallbackGame", "CallbackQuery", @@ -102,6 +103,7 @@ __all__ = ( # Keep this alphabetically ordered "InlineQueryResultLocation", "InlineQueryResultMpeg4Gif", "InlineQueryResultPhoto", + "InlineQueryResultsButton", "InlineQueryResultVenue", "InlineQueryResultVideo", "InlineQueryResultVoice", @@ -169,6 +171,7 @@ __all__ = ( # Keep this alphabetically ordered "Sticker", "StickerSet", "SuccessfulPayment", + "SwitchInlineQueryChosenChat", "TelegramObject", "Update", "User", @@ -204,6 +207,7 @@ from ._botcommandscope import ( 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.inlinequeryresultgif import InlineQueryResultGif 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 ._replykeyboardmarkup import ReplyKeyboardMarkup 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 e7b060f4a..b2aa643d6 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -56,6 +56,7 @@ except ImportError: 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._files.voice import Voice 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): .. 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): 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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): :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 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]): "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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. 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 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified. """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 000000000..05c0610db --- /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 650d7d0b0..59b306234 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 @@ class ChatMemberUpdated(TelegramObject): 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 @@ class ChatMemberUpdated(TelegramObject): 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 33759e731..9626fa1d0 100644 --- a/telegram/_inline/inlinekeyboardbutton.py +++ b/telegram/_inline/inlinekeyboardbutton.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, ClassVar, Optional, Union 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 @@ class InlineKeyboardButton(TelegramObject): 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 @@ class InlineKeyboardButton(TelegramObject): 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 @@ class InlineKeyboardButton(TelegramObject): 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 379dfb4be..ae5abe238 100644 --- a/telegram/_inline/inlinequery.py +++ b/telegram/_inline/inlinequery.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Callable, ClassVar, Optional, Sequence, Union 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 @@ class InlineQuery(TelegramObject): 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 @@ class InlineQuery(TelegramObject): 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 000000000..7d19b2ef9 --- /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 8a962f74b..bece91236 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -59,6 +59,7 @@ from telegram._utils.argumentparsing import parse_sequence_arg 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._webappdata import WebAppData 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 @@ class Message(TelegramObject): 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 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as HTML in the same way the original message was formatted. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as HTML. @@ -3374,12 +3385,12 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as HTML. @@ -3394,12 +3405,12 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as HTML in the same way the original message was formatted. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message caption with caption entities formatted as HTML. """ @@ -3413,12 +3424,12 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as HTML. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as HTML. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message caption with caption entities formatted as HTML. """ @@ -3522,6 +3533,26 @@ class Message(TelegramObject): "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 @@ class Message(TelegramObject): * |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 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as Markdown in the same way the original message was formatted. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as Markdown. """ @@ -3614,6 +3648,9 @@ class Message(TelegramObject): * |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 @@ class Message(TelegramObject): Use this if you want to retrieve the message text with the entities formatted as Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message text with entities formatted as Markdown. """ @@ -3658,6 +3695,9 @@ class Message(TelegramObject): * |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 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as Markdown in the same way the original message was formatted. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. + .. versionchanged:: NEXT.VERSION + Custom emoji entities are now supported. + Returns: :obj:`str`: Message caption with caption entities formatted as Markdown. """ @@ -3704,6 +3744,9 @@ class Message(TelegramObject): * |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 @@ class Message(TelegramObject): Use this if you want to retrieve the message caption with the caption entities formatted as Markdown. This also formats :attr:`telegram.MessageEntity.URL` as a hyperlink. - Note: - |custom_emoji_formatting_note| - .. versionchanged:: 13.10 Spoiler entities are now formatted as Markdown V2. + .. 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 000000000..459b30abf --- /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 bba6ac88b..faa5c0072 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 65569ebe9..33fe2c758 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -37,6 +37,7 @@ __all__ = [ "BotCommandLimit", "BotCommandScopeType", "BotDescriptionLimit", + "BotNameLimit", "CallbackQueryLimit", "ChatAction", "ChatID", @@ -57,6 +58,7 @@ __all__ = [ "InlineKeyboardMarkupLimit", "InlineQueryLimit", "InlineQueryResultLimit", + "InlineQueryResultsButtonLimit", "InlineQueryResultType", "InputMediaType", "InvoiceLimit", @@ -114,7 +116,7 @@ class _BotAPIVersion(NamedTuple): #: :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 a2995a467..eea4ad963 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -46,6 +46,7 @@ from telegram import ( BotCommand, BotCommandScope, BotDescription, + BotName, BotShortDescription, CallbackQuery, Chat, @@ -60,6 +61,7 @@ from telegram import ( ForumTopic, GameHighScore, InlineKeyboardMarkup, + InlineQueryResultsButton, InputMedia, InputSticker, Location, @@ -792,6 +794,7 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 @@ class ExtBot(Bot, Generic[RLARGS]): 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 632cd3647..9bf8ba730 100644 --- a/telegram/helpers.py +++ b/telegram/helpers.py @@ -44,23 +44,27 @@ if TYPE_CHECKING: 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 29675504b..15c7ef8bf 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 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase): 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 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase): 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 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase): "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 @@ class TestInlineKeyboardButtonWithoutRequest(TestInlineKeyboardButtonBase): 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 063c8b93f..7bc003c48 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -34,6 +34,7 @@ from telegram import ( BotCommand, BotCommandScopeChat, BotDescription, + BotName, BotShortDescription, CallbackQuery, Chat, @@ -44,6 +45,7 @@ from telegram import ( InlineKeyboardMarkup, InlineQueryResultArticle, InlineQueryResultDocument, + InlineQueryResultsButton, InlineQueryResultVoice, InputFile, InputMessageContent, @@ -76,7 +78,7 @@ from telegram.error import BadRequest, InvalidToken, NetworkError 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 @@ class TestBotWithoutRequest: ) # 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 @@ class TestBotWithoutRequest: }, ], "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 @@ class TestBotWithoutRequest: ), ] + 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 @@ class TestBotWithoutRequest: 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 @@ class TestBotWithoutRequest: 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 @@ class TestBotWithoutRequest: }, ], "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 @@ class TestBotWithoutRequest: 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 @@ class TestBotWithoutRequest: }, ], "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 @@ class TestBotWithoutRequest: 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 @@ class TestBotWithoutRequest: 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 000000000..89d2482ed --- /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 de3da1354..f0baf88e0 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 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase): 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 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase): "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 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase): 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 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase): == 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 8e8da7f36..9ac642a22 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 @@ class TestHelpers: 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 000000000..db87326c5 --- /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 a47c085de..86b3c0fa4 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -59,6 +59,7 @@ from telegram import ( 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): "[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 @@ class TestMessageWithoutRequest(TestMessageBase): "[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 @@ class TestMessageWithoutRequest(TestMessageBase): @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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 @@ class TestMessageWithoutRequest(TestMessageBase): "[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 @@ class TestMessageWithoutRequest(TestMessageBase): "[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 @@ class TestMessageWithoutRequest(TestMessageBase): @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 @@ class TestMessageWithoutRequest(TestMessageBase): "[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 @@ class TestMessageWithoutRequest(TestMessageBase): 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 3e829f11b..a3cba293a 100644 --- a/tests/test_official.py +++ b/tests/test_official.py @@ -154,6 +154,7 @@ BACKWARDS_COMPAT_KWARGS = { "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 000000000..dc610e449 --- /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)