Add support for clicking web app and user profile buttons

Co-authored-by: Shrimadhav U K <SpEcHiDe@users.noreply.github.com>
This commit is contained in:
KurimuzonAkuma 2024-05-25 15:28:04 +03:00
parent 5e543d3afc
commit 60334bebeb
6 changed files with 177 additions and 35 deletions

View File

@ -199,6 +199,10 @@ class Client(Methods):
Pass an instance of your own implementation of session storage engine. 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. 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*): init_connection_params (:obj:`~pyrogram.raw.base.JSONValue`, *optional*):
Additional initConnection parameters. Additional initConnection parameters.
For now, only the tz_offset field is supported, for specifying timezone offset in seconds. For now, only the tz_offset field is supported, for specifying timezone offset in seconds.
@ -230,36 +234,37 @@ class Client(Methods):
def __init__( def __init__(
self, self,
name: str, name: str,
api_id: Union[int, str] = None, api_id: Optional[Union[int, str]] = None,
api_hash: str = None, api_hash: Optional[str] = None,
app_version: str = APP_VERSION, app_version: str = APP_VERSION,
device_model: str = DEVICE_MODEL, device_model: str = DEVICE_MODEL,
system_version: str = SYSTEM_VERSION, system_version: str = SYSTEM_VERSION,
lang_pack: str = LANG_PACK, lang_pack: str = LANG_PACK,
lang_code: str = LANG_CODE, lang_code: str = LANG_CODE,
system_lang_code: str = SYSTEM_LANG_CODE, system_lang_code: str = SYSTEM_LANG_CODE,
ipv6: bool = False, ipv6: Optional[bool] = False,
proxy: dict = None, proxy: Optional[dict] = None,
test_mode: bool = False, test_mode: Optional[bool] = False,
bot_token: str = None, bot_token: Optional[str] = None,
session_string: str = None, session_string: Optional[str] = None,
in_memory: bool = None, in_memory: Optional[bool] = None,
phone_number: str = None, phone_number: Optional[str] = None,
phone_code: str = None, phone_code: Optional[str] = None,
password: str = None, password: Optional[str] = None,
workers: int = WORKERS, workers: int = WORKERS,
workdir: str = WORKDIR, workdir: Union[str, Path] = WORKDIR,
plugins: dict = None, plugins: Optional[dict] = None,
parse_mode: "enums.ParseMode" = enums.ParseMode.DEFAULT, parse_mode: "enums.ParseMode" = enums.ParseMode.DEFAULT,
no_updates: bool = None, no_updates: Optional[bool] = None,
skip_updates: bool = True, skip_updates: Optional[bool] = True,
takeout: bool = None, takeout: Optional[bool] = None,
sleep_threshold: int = Session.SLEEP_THRESHOLD, sleep_threshold: int = Session.SLEEP_THRESHOLD,
hide_password: bool = False, hide_password: Optional[bool] = False,
max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS, max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS,
max_message_cache_size: int = MAX_MESSAGE_CACHE_SIZE, max_message_cache_size: int = MAX_MESSAGE_CACHE_SIZE,
storage_engine: Storage = None, storage_engine: Optional[Storage] = None,
init_connection_params: "raw.base.JSONValue" = None client_platform: "enums.ClientPlatform" = enums.ClientPlatform.OTHER,
init_connection_params: Optional["raw.base.JSONValue"] = None
): ):
super().__init__() super().__init__()
@ -292,6 +297,7 @@ class Client(Methods):
self.hide_password = hide_password self.hide_password = hide_password
self.max_concurrent_transmissions = max_concurrent_transmissions self.max_concurrent_transmissions = max_concurrent_transmissions
self.max_message_cache_size = max_message_cache_size self.max_message_cache_size = max_message_cache_size
self.client_platform = client_platform
self.init_connection_params = init_connection_params self.init_connection_params = init_connection_params
self.executor = ThreadPoolExecutor(self.workers, thread_name_prefix="Handler") self.executor = ThreadPoolExecutor(self.workers, thread_name_prefix="Handler")

View File

@ -22,6 +22,7 @@ from .chat_event_action import ChatEventAction
from .chat_member_status import ChatMemberStatus from .chat_member_status import ChatMemberStatus
from .chat_members_filter import ChatMembersFilter from .chat_members_filter import ChatMembersFilter
from .chat_type import ChatType from .chat_type import ChatType
from .client_platform import ClientPlatform
from .folder_color import FolderColor from .folder_color import FolderColor
from .message_entity_type import MessageEntityType from .message_entity_type import MessageEntityType
from .message_media_type import MessageMediaType from .message_media_type import MessageMediaType
@ -43,6 +44,7 @@ __all__ = [
'ChatMemberStatus', 'ChatMemberStatus',
'ChatMembersFilter', 'ChatMembersFilter',
'ChatType', 'ChatType',
'ClientPlatform',
'FolderColor', 'FolderColor',
'MessageEntityType', 'MessageEntityType',
'MessageMediaType', 'MessageMediaType',

View File

@ -0,0 +1,49 @@
# Pyrogram - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
#
# 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 <http://www.gnu.org/licenses/>.
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"

View File

@ -16,10 +16,10 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>. # along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Union from typing import Union, Optional
import pyrogram import pyrogram
from pyrogram import raw from pyrogram import raw, utils
class RequestCallbackAnswer: class RequestCallbackAnswer:
@ -28,6 +28,7 @@ class RequestCallbackAnswer:
chat_id: Union[int, str], chat_id: Union[int, str],
message_id: int, message_id: int,
callback_data: Union[str, bytes], callback_data: Union[str, bytes],
password: Optional[str] = None,
timeout: int = 10 timeout: int = 10
): ):
"""Request a callback answer from bots. """Request a callback answer from bots.
@ -47,6 +48,10 @@ class RequestCallbackAnswer:
callback_data (``str`` | ``bytes``): callback_data (``str`` | ``bytes``):
Callback data associated with the inline button you want to get the answer from. 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 (``int``, *optional*):
Timeout in seconds. Timeout in seconds.
@ -56,6 +61,8 @@ class RequestCallbackAnswer:
Raises: Raises:
TimeoutError: In case the bot fails to answer within 10 seconds. 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: Example:
.. code-block:: python .. code-block:: python
@ -66,11 +73,18 @@ class RequestCallbackAnswer:
# Telegram only wants bytes, but we are allowed to pass strings too. # 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 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( return await self.invoke(
raw.functions.messages.GetBotCallbackAnswer( raw.functions.messages.GetBotCallbackAnswer(
peer=await self.resolve_peer(chat_id), peer=await self.resolve_peer(chat_id),
msg_id=message_id, msg_id=message_id,
data=data data=data,
password=password
), ),
retries=0, retries=0,
timeout=timeout timeout=timeout

View File

@ -16,7 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>. # along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Union from typing import Union, Optional
import pyrogram import pyrogram
from pyrogram import raw from pyrogram import raw
@ -69,19 +69,23 @@ class InlineKeyboardButton(Object):
callback_game (:obj:`~pyrogram.types.CallbackGame`, *optional*): callback_game (:obj:`~pyrogram.types.CallbackGame`, *optional*):
Description of the game that will be launched when the user presses the button. 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. **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__( def __init__(
self, self,
text: str, text: str,
callback_data: Union[str, bytes] = None, callback_data: Optional[Union[str, bytes]] = None,
url: str = None, url: Optional[str] = None,
web_app: "types.WebAppInfo" = None, web_app: Optional["types.WebAppInfo"] = None,
login_url: "types.LoginUrl" = None, login_url: Optional["types.LoginUrl"] = None,
user_id: int = None, user_id: Optional[int] = None,
switch_inline_query: str = None, switch_inline_query: Optional[str] = None,
switch_inline_query_current_chat: str = None, switch_inline_query_current_chat: Optional[str] = None,
callback_game: "types.CallbackGame" = None callback_game: Optional["types.CallbackGame"] = None,
requires_password: Optional[bool] = None
): ):
super().__init__() super().__init__()
@ -94,6 +98,7 @@ class InlineKeyboardButton(Object):
self.switch_inline_query = switch_inline_query self.switch_inline_query = switch_inline_query
self.switch_inline_query_current_chat = switch_inline_query_current_chat self.switch_inline_query_current_chat = switch_inline_query_current_chat
self.callback_game = callback_game self.callback_game = callback_game
self.requires_password = requires_password
# self.pay = pay # self.pay = pay
@staticmethod @staticmethod
@ -108,7 +113,8 @@ class InlineKeyboardButton(Object):
return InlineKeyboardButton( return InlineKeyboardButton(
text=b.text, text=b.text,
callback_data=data callback_data=data,
requires_password=getattr(b, "requires_password", None)
) )
if isinstance(b, raw.types.KeyboardButtonUrl): if isinstance(b, raw.types.KeyboardButtonUrl):
@ -162,7 +168,8 @@ class InlineKeyboardButton(Object):
return raw.types.KeyboardButtonCallback( return raw.types.KeyboardButtonCallback(
text=self.text, text=self.text,
data=data data=data,
requires_password=self.requires_password
) )
if self.url is not None: if self.url is not None:

View File

@ -4294,7 +4294,15 @@ class Message(Object, Update):
revoke=revoke 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`. """Bound method *click* of :obj:`~pyrogram.types.Message`.
Use as a shortcut for clicking a button attached to the message instead of: 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 (``int``, *optional*):
Timeout in seconds. 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: Returns:
- The result of :meth:`~pyrogram.Client.request_callback_answer` in case of inline callback button clicks. - 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. - 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 - A string in case the inline button is a URL, a *switch_inline_query* or a
*switch_inline_query_current_chat* button. *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: Raises:
RPCError: In case of a Telegram RPC error. RPCError: In case of a Telegram RPC error.
@ -4404,8 +4423,53 @@ class Message(Object, Update):
callback_data=button.callback_data, callback_data=button.callback_data,
timeout=timeout 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: elif button.url:
return 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: elif button.switch_inline_query:
return button.switch_inline_query return button.switch_inline_query
elif button.switch_inline_query_current_chat: elif button.switch_inline_query_current_chat:
@ -4413,7 +4477,7 @@ class Message(Object, Update):
else: else:
raise ValueError("This button is not supported yet") raise ValueError("This button is not supported yet")
else: 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: 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`. """Bound method *react* of :obj:`~pyrogram.types.Message`.