pyrofork: Add support for clicking web app and user profile buttons

Co-authored-by: Shrimadhav U K <SpEcHiDe@users.noreply.github.com>
Signed-off-by: wulan17 <wulan17@nusantararom.org>
This commit is contained in:
KurimuzonAkuma 2024-05-25 15:28:04 +03:00 committed by wulan17
parent ed6403db3c
commit ef002e0416
No known key found for this signature in database
GPG key ID: 318CD6CD3A6AC0A5
6 changed files with 178 additions and 36 deletions

View file

@ -196,6 +196,10 @@ class Client(Methods):
Set the maximum amount of concurrent transmissions (uploads & downloads). Set the maximum amount of concurrent transmissions (uploads & downloads).
A value that is too high may result in network related issues. A value that is too high may result in network related issues.
Defaults to 1. Defaults to 1.
client_platform (:obj:`~pyrogram.enums.ClientPlatform`, *optional*):
The platform where this client is running.
Defaults to 'other'
""" """
APP_VERSION = f"Pyrogram {__version__}" APP_VERSION = f"Pyrogram {__version__}"
@ -221,33 +225,34 @@ 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_code: str = LANG_CODE, lang_code: str = LANG_CODE,
ipv6: bool = False, ipv6: Optional[bool] = False,
alt_port: bool = False, alt_port: 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,
mongodb: dict = None, mongodb: Optional[dict] = None,
storage: Storage = None, storage: Optional[Storage] = 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,
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,
client_platform: "enums.ClientPlatform" = enums.ClientPlatform.OTHER
): ):
super().__init__() super().__init__()
@ -278,6 +283,7 @@ class Client(Methods):
self.sleep_threshold = sleep_threshold self.sleep_threshold = sleep_threshold
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.client_platform = client_platform
self.executor = ThreadPoolExecutor(self.workers, thread_name_prefix="Handler") self.executor = ThreadPoolExecutor(self.workers, thread_name_prefix="Handler")

View file

@ -23,6 +23,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 .listerner_types import ListenerTypes from .listerner_types import ListenerTypes
from .message_entity_type import MessageEntityType from .message_entity_type import MessageEntityType
from .message_media_type import MessageMediaType from .message_media_type import MessageMediaType
@ -46,6 +47,7 @@ __all__ = [
'ChatMemberStatus', 'ChatMemberStatus',
'ChatMembersFilter', 'ChatMembersFilter',
'ChatType', 'ChatType',
'ClientPlatform',
'ListenerTypes', 'ListenerTypes',
'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

@ -17,10 +17,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 Pyrofork. If not, see <http://www.gnu.org/licenses/>. # along with Pyrofork. 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:
@ -29,6 +29,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.
@ -49,6 +50,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.
@ -58,6 +63,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
@ -68,11 +75,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

@ -17,7 +17,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 Pyrofork. If not, see <http://www.gnu.org/licenses/>. # along with Pyrofork. 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
@ -70,19 +70,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__()
@ -95,6 +99,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
@ -109,7 +114,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):
@ -163,7 +169,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

@ -4501,7 +4501,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:
@ -4553,11 +4561,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.
@ -4611,8 +4630,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:
@ -4620,7 +4684,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: str = "", big: bool = False, add_to_recent: bool = True) -> "types.MessageReactions": async def react(self, emoji: str = "", big: bool = False, add_to_recent: bool = True) -> "types.MessageReactions":
"""Bound method *react* of :obj:`~pyrogram.types.Message`. """Bound method *react* of :obj:`~pyrogram.types.Message`.