From 60334bebebe60623af9baa8620ec91aa8f3084ac Mon Sep 17 00:00:00 2001 From: KurimuzonAkuma Date: Sat, 25 May 2024 15:28:04 +0300 Subject: [PATCH] Add support for clicking web app and user profile buttons Co-authored-by: Shrimadhav U K --- pyrogram/client.py | 44 ++++++------ pyrogram/enums/__init__.py | 2 + pyrogram/enums/client_platform.py | 49 +++++++++++++ .../methods/bots/request_callback_answer.py | 20 +++++- .../inline_keyboard_button.py | 29 +++++--- pyrogram/types/messages_and_media/message.py | 68 ++++++++++++++++++- 6 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 pyrogram/enums/client_platform.py diff --git a/pyrogram/client.py b/pyrogram/client.py index a62bbd70..330f02ca 100644 --- a/pyrogram/client.py +++ b/pyrogram/client.py @@ -199,6 +199,10 @@ class Client(Methods): Pass an instance of your own implementation of session storage engine. Useful when you want to store your session in databases like Mongo, Redis, etc. + client_platform (:obj:`~pyrogram.enums.ClientPlatform`, *optional*): + The platform where this client is running. + Defaults to 'other' + init_connection_params (:obj:`~pyrogram.raw.base.JSONValue`, *optional*): Additional initConnection parameters. For now, only the tz_offset field is supported, for specifying timezone offset in seconds. @@ -230,36 +234,37 @@ class Client(Methods): def __init__( self, name: str, - api_id: Union[int, str] = None, - api_hash: str = None, + api_id: Optional[Union[int, str]] = None, + api_hash: Optional[str] = None, app_version: str = APP_VERSION, device_model: str = DEVICE_MODEL, system_version: str = SYSTEM_VERSION, lang_pack: str = LANG_PACK, lang_code: str = LANG_CODE, system_lang_code: str = SYSTEM_LANG_CODE, - ipv6: bool = False, - proxy: dict = None, - test_mode: bool = False, - bot_token: str = None, - session_string: str = None, - in_memory: bool = None, - phone_number: str = None, - phone_code: str = None, - password: str = None, + ipv6: Optional[bool] = False, + proxy: Optional[dict] = None, + test_mode: Optional[bool] = False, + bot_token: Optional[str] = None, + session_string: Optional[str] = None, + in_memory: Optional[bool] = None, + phone_number: Optional[str] = None, + phone_code: Optional[str] = None, + password: Optional[str] = None, workers: int = WORKERS, - workdir: str = WORKDIR, - plugins: dict = None, + workdir: Union[str, Path] = WORKDIR, + plugins: Optional[dict] = None, parse_mode: "enums.ParseMode" = enums.ParseMode.DEFAULT, - no_updates: bool = None, - skip_updates: bool = True, - takeout: bool = None, + no_updates: Optional[bool] = None, + skip_updates: Optional[bool] = True, + takeout: Optional[bool] = None, sleep_threshold: int = Session.SLEEP_THRESHOLD, - hide_password: bool = False, + hide_password: Optional[bool] = False, max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS, max_message_cache_size: int = MAX_MESSAGE_CACHE_SIZE, - storage_engine: Storage = None, - init_connection_params: "raw.base.JSONValue" = None + storage_engine: Optional[Storage] = None, + client_platform: "enums.ClientPlatform" = enums.ClientPlatform.OTHER, + init_connection_params: Optional["raw.base.JSONValue"] = None ): super().__init__() @@ -292,6 +297,7 @@ class Client(Methods): self.hide_password = hide_password self.max_concurrent_transmissions = max_concurrent_transmissions self.max_message_cache_size = max_message_cache_size + self.client_platform = client_platform self.init_connection_params = init_connection_params self.executor = ThreadPoolExecutor(self.workers, thread_name_prefix="Handler") diff --git a/pyrogram/enums/__init__.py b/pyrogram/enums/__init__.py index 41d065fb..2e9a5dce 100644 --- a/pyrogram/enums/__init__.py +++ b/pyrogram/enums/__init__.py @@ -22,6 +22,7 @@ from .chat_event_action import ChatEventAction from .chat_member_status import ChatMemberStatus from .chat_members_filter import ChatMembersFilter from .chat_type import ChatType +from .client_platform import ClientPlatform from .folder_color import FolderColor from .message_entity_type import MessageEntityType from .message_media_type import MessageMediaType @@ -43,6 +44,7 @@ __all__ = [ 'ChatMemberStatus', 'ChatMembersFilter', 'ChatType', + 'ClientPlatform', 'FolderColor', 'MessageEntityType', 'MessageMediaType', diff --git a/pyrogram/enums/client_platform.py b/pyrogram/enums/client_platform.py new file mode 100644 index 00000000..3d392e0e --- /dev/null +++ b/pyrogram/enums/client_platform.py @@ -0,0 +1,49 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from enum import auto + +from .auto_name import AutoName + + +class ClientPlatform(AutoName): + """Valid platforms for a :obj:`~pyrogram.Client`.""" + + ANDROID = auto() + "Android" + + IOS = auto() + "iOS" + + WP = auto() + "Windows Phone" + + BB = auto() + "Blackberry" + + DESKTOP = auto() + "Desktop" + + WEB = auto() + "Web" + + UBP = auto() + "Ubuntu Phone" + + OTHER = auto() + "Other" diff --git a/pyrogram/methods/bots/request_callback_answer.py b/pyrogram/methods/bots/request_callback_answer.py index 22debcbe..97ffe927 100644 --- a/pyrogram/methods/bots/request_callback_answer.py +++ b/pyrogram/methods/bots/request_callback_answer.py @@ -16,10 +16,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union +from typing import Union, Optional import pyrogram -from pyrogram import raw +from pyrogram import raw, utils class RequestCallbackAnswer: @@ -28,6 +28,7 @@ class RequestCallbackAnswer: chat_id: Union[int, str], message_id: int, callback_data: Union[str, bytes], + password: Optional[str] = None, timeout: int = 10 ): """Request a callback answer from bots. @@ -47,6 +48,10 @@ class RequestCallbackAnswer: callback_data (``str`` | ``bytes``): Callback data associated with the inline button you want to get the answer from. + password (``str``, *optional*): + When clicking certain buttons (such as BotFather's confirmation button to transfer ownership), if your account has 2FA enabled, you need to provide your account's password. + The 2-step verification password for the current user. Only applicable, if the :obj:`~pyrogram.types.InlineKeyboardButton` contains ``callback_data_with_password``. + timeout (``int``, *optional*): Timeout in seconds. @@ -56,6 +61,8 @@ class RequestCallbackAnswer: Raises: TimeoutError: In case the bot fails to answer within 10 seconds. + ValueError: In case of invalid arguments. + RPCError: In case of Telegram RPC error. Example: .. code-block:: python @@ -66,11 +73,18 @@ class RequestCallbackAnswer: # Telegram only wants bytes, but we are allowed to pass strings too. data = bytes(callback_data, "utf-8") if isinstance(callback_data, str) else callback_data + if password: + r = await self.invoke( + raw.functions.account.GetPassword() + ) + password = utils.compute_password_check(r, password) + return await self.invoke( raw.functions.messages.GetBotCallbackAnswer( peer=await self.resolve_peer(chat_id), msg_id=message_id, - data=data + data=data, + password=password ), retries=0, timeout=timeout diff --git a/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py b/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py index a1d8a7ad..d016ea2b 100644 --- a/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py +++ b/pyrogram/types/bots_and_keyboards/inline_keyboard_button.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -from typing import Union +from typing import Union, Optional import pyrogram from pyrogram import raw @@ -69,19 +69,23 @@ class InlineKeyboardButton(Object): callback_game (:obj:`~pyrogram.types.CallbackGame`, *optional*): Description of the game that will be launched when the user presses the button. **NOTE**: This type of button **must** always be the first button in the first row. + + callback_data_with_password (``bytes``, *optional*): + A button that asks for the 2-step verification password of the current user and then sends a callback query to a bot Data to be sent to the bot via a callback query. """ def __init__( self, text: str, - callback_data: Union[str, bytes] = None, - url: str = None, - web_app: "types.WebAppInfo" = None, - login_url: "types.LoginUrl" = None, - user_id: int = None, - switch_inline_query: str = None, - switch_inline_query_current_chat: str = None, - callback_game: "types.CallbackGame" = None + callback_data: Optional[Union[str, bytes]] = None, + url: Optional[str] = None, + web_app: Optional["types.WebAppInfo"] = None, + login_url: Optional["types.LoginUrl"] = None, + user_id: Optional[int] = None, + switch_inline_query: Optional[str] = None, + switch_inline_query_current_chat: Optional[str] = None, + callback_game: Optional["types.CallbackGame"] = None, + requires_password: Optional[bool] = None ): super().__init__() @@ -94,6 +98,7 @@ class InlineKeyboardButton(Object): self.switch_inline_query = switch_inline_query self.switch_inline_query_current_chat = switch_inline_query_current_chat self.callback_game = callback_game + self.requires_password = requires_password # self.pay = pay @staticmethod @@ -108,7 +113,8 @@ class InlineKeyboardButton(Object): return InlineKeyboardButton( text=b.text, - callback_data=data + callback_data=data, + requires_password=getattr(b, "requires_password", None) ) if isinstance(b, raw.types.KeyboardButtonUrl): @@ -162,7 +168,8 @@ class InlineKeyboardButton(Object): return raw.types.KeyboardButtonCallback( text=self.text, - data=data + data=data, + requires_password=self.requires_password ) if self.url is not None: diff --git a/pyrogram/types/messages_and_media/message.py b/pyrogram/types/messages_and_media/message.py index e423563a..3c0d8d5e 100644 --- a/pyrogram/types/messages_and_media/message.py +++ b/pyrogram/types/messages_and_media/message.py @@ -4294,7 +4294,15 @@ class Message(Object, Update): revoke=revoke ) - async def click(self, x: Union[int, str] = 0, y: int = None, quote: bool = None, timeout: int = 10): + async def click( + self, + x: Union[int, str] = 0, + y: int = None, + quote: bool = None, + timeout: int = 10, + request_write_access: bool = True, + password: str = None + ): """Bound method *click* of :obj:`~pyrogram.types.Message`. Use as a shortcut for clicking a button attached to the message instead of: @@ -4346,11 +4354,22 @@ class Message(Object, Update): timeout (``int``, *optional*): Timeout in seconds. + request_write_access (``bool``, *optional*): + Only used in case of :obj:`~pyrogram.types.LoginUrl` button. + True, if the bot can send messages to the user. + Defaults to ``True``. + + password (``str``, *optional*): + When clicking certain buttons (such as BotFather's confirmation button to transfer ownership), if your account has 2FA enabled, you need to provide your account's password. + The 2-step verification password for the current user. Only applicable, if the :obj:`~pyrogram.types.InlineKeyboardButton` contains ``requires_password``. + Returns: - The result of :meth:`~pyrogram.Client.request_callback_answer` in case of inline callback button clicks. - The result of :meth:`~Message.reply()` in case of normal button clicks. - A string in case the inline button is a URL, a *switch_inline_query* or a *switch_inline_query_current_chat* button. + - A string URL with the user details, in case of a WebApp button. + - A :obj:`~pyrogram.types.Chat` object in case of a ``KeyboardButtonUserProfile`` button. Raises: RPCError: In case of a Telegram RPC error. @@ -4404,8 +4423,53 @@ class Message(Object, Update): callback_data=button.callback_data, timeout=timeout ) + elif button.requires_password: + if password is None: + raise ValueError( + "This button requires a password" + ) + + return await self._client.request_callback_answer( + chat_id=self.chat.id, + message_id=self.id, + callback_data=button.callback_data, + password=password, + timeout=timeout + ) elif button.url: return button.url + elif button.web_app: + web_app = button.web_app + + bot_peer_id = ( + self.via_bot and + self.via_bot.id + ) or ( + self.from_user and + self.from_user.is_bot and + self.from_user.id + ) or None + + if not bot_peer_id: + raise ValueError( + "This button requires a bot as the sender" + ) + + r = await self._client.invoke( + raw.functions.messages.RequestWebView( + peer=await self._client.resolve_peer(self.chat.id), + bot=await self._client.resolve_peer(bot_peer_id), + url=web_app.url, + platform=self._client.client_platform.value, + # TODO + ) + ) + return r.url + elif button.user_id: + return await self._client.get_chat( + button.user_id, + force_full=False + ) elif button.switch_inline_query: return button.switch_inline_query elif button.switch_inline_query_current_chat: @@ -4413,7 +4477,7 @@ class Message(Object, Update): else: raise ValueError("This button is not supported yet") else: - await self.reply(button, quote=quote) + await self.reply(text=button, quote=quote) async def react(self, emoji: Union[int, str, List[Union[int, str]]] = None, big: bool = False) -> bool: """Bound method *react* of :obj:`~pyrogram.types.Message`.