Added Conversation Support Based on Pyromodv2

Signed-off-by: Yasir Aris M <git@yasirdev.my.id>
This commit is contained in:
Yasir Aris M 2023-11-03 11:01:15 +07:00
parent 542bd41608
commit 7fceeef32b
26 changed files with 926 additions and 8 deletions

View file

@ -51,10 +51,11 @@ from pyrogram.handlers.handler import Handler
from pyrogram.methods import Methods
from pyrogram.session import Auth, Session
from pyrogram.storage import FileStorage, MemoryStorage, MongoStorage, Storage
from pyrogram.types import User, TermsOfService
from pyrogram.utils import ainput
from pyrogram.types import User, TermsOfService, ListenerTypes, Identifier, Listener
from pyrogram.utils import ainput, PyromodConfig
from .dispatcher import Dispatcher
from .file_id import FileId, FileType, ThumbnailSource
from .filters import Filter
from .mime_types import mime_types
from .parser import Parser
from .session.internals import MsgId
@ -316,7 +317,7 @@ class Client(Methods):
self.updates_watchdog_task = None
self.updates_watchdog_event = asyncio.Event()
self.last_update_time = datetime.now()
self.listeners = {listener_type: [] for listener_type in ListenerTypes}
self.loop = asyncio.get_event_loop()
def __enter__(self):
@ -349,6 +350,140 @@ class Client(Methods):
if datetime.now() - self.last_update_time > timedelta(seconds=self.UPDATES_WATCHDOG_INTERVAL):
await self.invoke(raw.functions.updates.GetState())
async def listen(
self,
filters: Optional[Filter] = None,
listener_type: ListenerTypes = ListenerTypes.MESSAGE,
timeout: Optional[int] = None,
unallowed_click_alert: bool = True,
chat_id: int = None,
user_id: int = None,
message_id: int = None,
inline_message_id: str = None,
):
pattern = Identifier(
from_user_id=user_id,
chat_id=chat_id,
message_id=message_id,
inline_message_id=inline_message_id,
)
loop = asyncio.get_event_loop()
future = loop.create_future()
future.add_done_callback(
lambda f: self.stop_listening(
listener_type,
user_id=user_id,
chat_id=chat_id,
message_id=message_id,
inline_message_id=inline_message_id,
)
)
listener = Listener(
future=future,
filters=filters,
unallowed_click_alert=unallowed_click_alert,
identifier=pattern,
listener_type=listener_type,
)
self.listeners[listener_type].append(listener)
try:
return await asyncio.wait_for(future, timeout)
except asyncio.exceptions.TimeoutError:
if callable(PyromodConfig.timeout_handler):
PyromodConfig.timeout_handler(pattern, listener, timeout)
elif PyromodConfig.throw_exceptions:
raise ListenerTimeout(timeout)
async def ask(
self,
chat_id: int,
text: str,
filters: Optional[Filter] = None,
listener_type: ListenerTypes = ListenerTypes.MESSAGE,
timeout: Optional[int] = None,
unallowed_click_alert: bool = True,
user_id: int = None,
message_id: int = None,
inline_message_id: str = None,
*args,
**kwargs,
):
sent_message = None
if text.strip() != "":
sent_message = await self.send_message(chat_id, text, *args, **kwargs)
response = await self.listen(
filters=filters,
listener_type=listener_type,
timeout=timeout,
unallowed_click_alert=unallowed_click_alert,
chat_id=chat_id,
user_id=user_id,
message_id=message_id,
inline_message_id=inline_message_id,
)
if response:
response.sent_message = sent_message
return response
def get_matching_listener(
self, pattern: Identifier, listener_type: ListenerTypes
) -> Optional[Listener]:
matching = []
for listener in self.listeners[listener_type]:
if listener.identifier.matches(pattern):
matching.append(listener)
# in case of multiple matching listeners, the most specific should be returned
def count_populated_attributes(listener_item: Listener):
return listener_item.identifier.count_populated()
return max(matching, key=count_populated_attributes, default=None)
def remove_listener(self, listener: Listener):
self.listeners[listener.listener_type].remove(listener)
def get_many_matching_listeners(
self, pattern: Identifier, listener_type: ListenerTypes
) -> List[Listener]:
listeners = []
for listener in self.listeners[listener_type]:
if listener.identifier.matches(pattern):
listeners.append(listener)
return listeners
def stop_listening(
self,
listener_type: ListenerTypes = ListenerTypes.MESSAGE,
chat_id: int = None,
user_id: int = None,
message_id: int = None,
inline_message_id: str = None,
):
pattern = Identifier(
from_user_id=user_id,
chat_id=chat_id,
message_id=message_id,
inline_message_id=inline_message_id,
)
listeners = self.get_many_matching_listeners(pattern, listener_type)
for listener in listeners:
self.remove_listener(listener)
if listener.future.done():
return
if callable(PyromodConfig.stopped_handler):
PyromodConfig.stopped_handler(pattern, listener)
elif PyromodConfig.throw_exceptions:
listener.future.set_exception(ListenerStopped())
async def authorize(self) -> User:
if self.bot_token:
return await self.sign_in_bot(self.bot_token)

View file

@ -26,7 +26,7 @@ import pyrogram
from pyrogram import utils
from pyrogram.handlers import (
CallbackQueryHandler, MessageHandler, EditedMessageHandler, DeletedMessagesHandler,
UserStatusHandler, RawUpdateHandler, InlineQueryHandler, PollHandler,
UserStatusHandler, RawUpdateHandler, InlineQueryHandler, PollHandler, ConversationHandler,
ChosenInlineResultHandler, ChatMemberUpdatedHandler, ChatJoinRequestHandler, StoryHandler
)
from pyrogram.raw.types import (
@ -65,6 +65,9 @@ class Dispatcher:
self.updates_queue = asyncio.Queue()
self.groups = OrderedDict()
self.conversation_handler = ConversationHandler()
self.groups[0] = [self.conversation_handler]
async def message_parser(update, users, chats):
return (
await pyrogram.types.Message._parse(self.client, update.message, users, chats,

View file

@ -17,6 +17,7 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from .exceptions import *
from .pyromod import *
from .rpc_error import UnknownError

View file

@ -0,0 +1,4 @@
from .listener_stopped import ListenerStopped
from .listener_timeout import ListenerTimeout
__all__ = ["ListenerStopped", "ListenerTimeout"]

View file

@ -0,0 +1,2 @@
class ListenerStopped(Exception):
pass

View file

@ -0,0 +1,2 @@
class ListenerTimeout(Exception):
pass

View file

@ -20,6 +20,7 @@
from .callback_query_handler import CallbackQueryHandler
from .chat_join_request_handler import ChatJoinRequestHandler
from .chat_member_updated_handler import ChatMemberUpdatedHandler
from .conversation_handler import ConversationHandler
from .chosen_inline_result_handler import ChosenInlineResultHandler
from .deleted_messages_handler import DeletedMessagesHandler
from .disconnect_handler import DisconnectHandler

View file

@ -16,7 +16,12 @@
# 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 typing import Callable
from typing import Callable, Tuple
import pyrogram
from pyrogram.utils import PyromodConfig
from pyrogram.types import ListenerTypes, CallbackQuery, Identifier, Listener
from .handler import Handler
@ -46,4 +51,95 @@ class CallbackQueryHandler(Handler):
"""
def __init__(self, callback: Callable, filters=None):
super().__init__(callback, filters)
self.original_callback = callback
super().__init__(self.resolve_future, filters)
def compose_data_identifier(self, query: CallbackQuery):
from_user = query.from_user
from_user_id = from_user.id if from_user else None
chat_id = None
message_id = None
if query.message:
message_id = getattr(
query.message, "id", getattr(query.message, "message_id", None)
)
if query.message.chat:
chat_id = query.message.chat.id
return Identifier(
message_id=message_id,
chat_id=chat_id,
from_user_id=from_user_id,
inline_message_id=query.inline_message_id,
)
async def check_if_has_matching_listener(
self, client: "pyrogram.Client", query: CallbackQuery
) -> Tuple[bool, Listener]:
data = self.compose_data_identifier(query)
listener = client.get_matching_listener(data, ListenerTypes.CALLBACK_QUERY)
listener_does_match = False
if listener:
filters = listener.filters
listener_does_match = (
await filters(client, query) if callable(filters) else True
)
return listener_does_match, listener
async def check(self, client: "pyrogram.Client", query: CallbackQuery):
listener_does_match, listener = await self.check_if_has_matching_listener(
client, query
)
handler_does_match = (
await self.filters(client, query) if callable(self.filters) else True
)
data = self.compose_data_identifier(query)
if PyromodConfig.unallowed_click_alert:
# matches with the current query but from any user
permissive_identifier = Identifier(
chat_id=data.chat_id,
message_id=data.message_id,
inline_message_id=data.inline_message_id,
from_user_id=None,
)
matches = permissive_identifier.matches(data)
if (
listener
and (matches and not listener_does_match)
and listener.unallowed_click_alert
):
alert = (
listener.unallowed_click_alert
if isinstance(listener.unallowed_click_alert, str)
else PyromodConfig.unallowed_click_alert_text
)
await query.answer(alert)
return False
# let handler get the chance to handle if listener
# exists but its filters doesn't match
return listener_does_match or handler_does_match
async def resolve_future(self, client: "pyrogram.Client", query: CallbackQuery, *args):
listener_does_match, listener = await self.check_if_has_matching_listener(
client, query
)
if listener and not listener.future.done():
listener.future.set_result(query)
client.remove_listener(listener)
raise pyrogram.StopPropagation
else:
await self.original_callback(client, query, *args)

View file

@ -0,0 +1,68 @@
# 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/>.
import inspect
from typing import Union
import pyrogram
from pyrogram.types import Message, CallbackQuery
from .message_handler import MessageHandler
from .callback_query_handler import CallbackQueryHandler
class ConversationHandler(MessageHandler, CallbackQueryHandler):
"""The Conversation handler class."""
def __init__(self):
self.waiters = {}
async def check(self, client: "pyrogram.Client", update: Union[Message, CallbackQuery]):
if isinstance(update, Message) and update.outgoing:
return False
try:
chat_id = update.chat.id if isinstance(update, Message) else update.message.chat.id
except AttributeError:
return False
waiter = self.waiters.get(chat_id)
if not waiter or not isinstance(update, waiter['update_type']) or waiter['future'].done():
return False
filters = waiter.get('filters')
if callable(filters):
if inspect.iscoroutinefunction(filters.__call__):
filtered = await filters(client, update)
else:
filtered = await client.loop.run_in_executor(
client.executor,
filters,
client, update
)
if not filtered or waiter['future'].done():
return False
waiter['future'].set_result(update)
return True
@staticmethod
async def callback(_, __):
pass
def delete_waiter(self, chat_id, future):
if future == self.waiters[chat_id]['future']:
del self.waiters[chat_id]

View file

@ -17,6 +17,9 @@
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
from typing import Callable
import pyrogram
from pyrogram.types import ListenerTypes, Message, Identifier
from .handler import Handler
@ -46,4 +49,53 @@ class MessageHandler(Handler):
"""
def __init__(self, callback: Callable, filters=None):
super().__init__(callback, filters)
self.original_callback = callback
super().__init__(self.resolve_future, filters)
async def check_if_has_matching_listener(self, client: "pyrogram.Client", message: Message):
from_user = message.from_user
from_user_id = from_user.id if from_user else None
message_id = getattr(message, "id", getattr(message, "message_id", None))
data = Identifier(
message_id=message_id, chat_id=message.chat.id, from_user_id=from_user_id
)
listener = client.get_matching_listener(data, ListenerTypes.MESSAGE)
listener_does_match = False
if listener:
filters = listener.filters
listener_does_match = (
await filters(client, message) if callable(filters) else True
)
return listener_does_match, listener
async def check(self, client: "pyrogram.Client", message: Message):
listener_does_match = (
await self.check_if_has_matching_listener(client, message)
)[0]
handler_does_match = (
await self.filters(client, message) if callable(self.filters) else True
)
# let handler get the chance to handle if listener
# exists but its filters doesn't match
return listener_does_match or handler_does_match
async def resolve_future(self, client: "pyrogram.Client", message: Message, *args):
listener_does_match, listener = await self.check_if_has_matching_listener(
client, message
)
if listener_does_match:
if not listener.future.done():
listener.future.set_result(message)
client.remove_listener(listener)
raise pyrogram.StopPropagation
else:
await self.original_callback(client, message, *args)

View file

@ -0,0 +1,16 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply

View file

@ -0,0 +1,90 @@
from pyrogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
KeyboardButton,
ReplyKeyboardMarkup,
ForceReply,
)
def ikb(rows=None):
if rows is None:
rows = []
lines = []
for row in rows:
line = []
for button in row:
button = (
btn(button, button) if isinstance(button, str) else btn(*button)
) # InlineKeyboardButton
line.append(button)
lines.append(line)
return InlineKeyboardMarkup(inline_keyboard=lines)
# return {'inline_keyboard': lines}
def btn(text, value, type="callback_data"):
return InlineKeyboardButton(text, **{type: value})
# return {'text': text, type: value}
# The inverse of above
def bki(keyboard):
lines = []
for row in keyboard.inline_keyboard:
line = []
for button in row:
button = ntb(button) # btn() format
line.append(button)
lines.append(line)
return lines
# return ikb() format
def ntb(button):
for btn_type in [
"callback_data",
"url",
"switch_inline_query",
"switch_inline_query_current_chat",
"callback_game",
]:
value = getattr(button, btn_type)
if value:
break
button = [button.text, value]
if btn_type != "callback_data":
button.append(btn_type)
return button
# return {'text': text, type: value}
def kb(rows=None, **kwargs):
if rows is None:
rows = []
lines = []
for row in rows:
line = []
for button in row:
button_type = type(button)
if button_type == str:
button = KeyboardButton(button)
elif button_type == dict:
button = KeyboardButton(**button)
line.append(button)
lines.append(line)
return ReplyKeyboardMarkup(keyboard=lines, **kwargs)
kbtn = KeyboardButton
def force_reply(selective=True):
return ForceReply(selective=selective)
def array_chunk(input_array, size):
return [input_array[i : i + size] for i in range(0, len(input_array), size)]

View file

@ -65,6 +65,8 @@ from .send_web_page import SendWebPage
from .stop_poll import StopPoll
from .stream_media import StreamMedia
from .vote_poll import VotePoll
from .wait_for_message import WaitForMessage
from .wait_for_callback_query import WaitForCallbackQuery
class Messages(
@ -116,6 +118,8 @@ class Messages(
GetDiscussionReplies,
GetDiscussionRepliesCount,
StreamMedia,
GetCustomEmojiStickers
GetCustomEmojiStickers,
WaitForMessage,
WaitForCallbackQuery
):
pass

View file

@ -0,0 +1,71 @@
# 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/>.
import asyncio
from typing import Union
from functools import partial
from pyrogram import types
from pyrogram.filters import Filter
class WaitForCallbackQuery:
async def wait_for_callback_query(
self,
chat_id: Union[int, str],
filters: Filter = None,
timeout: int = None
) -> "types.CallbackQuery":
"""Wait for callback query.
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of callback queries to be passed
in your callback function.
timeout (``int``, *optional*):
Timeout in seconds.
Returns:
:obj:`~pyrogram.types.CallbackQuery`: On success, the callback query is returned.
Raises:
asyncio.TimeoutError: In case callback query not received within the timeout.
Example:
.. code-block:: python
# Simple example
callback_query = app.wait_for_callback_query(chat_id)
# Example with filter
callback_query = app.wait_for_callback_query(chat_id, filters=filters.user(user_id))
# Example with timeout
callback_query = app.wait_for_callback_query(chat_id, timeout=60)
"""
if not isinstance(chat_id, int):
chat = await self.get_chat(chat_id)
chat_id = chat.id
conversation_handler = self.dispatcher.conversation_handler
future = self.loop.create_future()
future.add_done_callback(
partial(
conversation_handler.delete_waiter,
chat_id
)
)
waiter = dict(future=future, filters=filters, update_type=types.CallbackQuery)
conversation_handler.waiters[chat_id] = waiter
return await asyncio.wait_for(future, timeout=timeout)

View file

@ -0,0 +1,71 @@
# 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/>.
import asyncio
from typing import Union
from functools import partial
from pyrogram import types
from pyrogram.filters import Filter
class WaitForMessage:
async def wait_for_message(
self,
chat_id: Union[int, str],
filters: Filter = None,
timeout: int = None
) -> "types.Message":
"""Wait for message.
Parameters:
chat_id (``int`` | ``str``):
Unique identifier (int) or username (str) of the target chat.
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of callback queries to be passed
in your callback function.
timeout (``int``, *optional*):
Timeout in seconds.
Returns:
:obj:`~pyrogram.types.Message`: On success, the reply message is returned.
Raises:
asyncio.TimeoutError: In case message not received within the timeout.
Example:
.. code-block:: python
# Simple example
reply_message = app.wait_for_message(chat_id)
# Example with filter
reply_message = app.wait_for_message(chat_id, filters=filters.text)
# Example with timeout
reply_message = app.wait_for_message(chat_id, timeout=60)
"""
if not isinstance(chat_id, int):
chat = await self.get_chat(chat_id)
chat_id = chat.id
conversation_handler = self.dispatcher.conversation_handler
future = self.loop.create_future()
future.add_done_callback(
partial(
conversation_handler.delete_waiter,
chat_id
)
)
waiter = dict(future=future, filters=filters, update_type=types.Message)
conversation_handler.waiters[chat_id] = waiter
return await asyncio.wait_for(future, timeout=timeout)

16
pyrogram/nav/__init__.py Normal file
View file

@ -0,0 +1,16 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
from .pagination import Pagination

View file

@ -0,0 +1,94 @@
"""
pyromod - A monkeypatcher add-on for Pyrogram
Copyright (C) 2020 Cezar H. <https://github.com/usernein>
This file is part of pyromod.
pyromod is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
pyromod 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with pyromod. If not, see <https://www.gnu.org/licenses/>.
"""
import math
from pyrogram.helpers import array_chunk
class Pagination:
def __init__(self, objects, page_data=None, item_data=None, item_title=None):
def default_page_callback(x):
return str(x)
def default_item_callback(i, pg):
return f"[{pg}] {i}"
self.objects = objects
self.page_data = page_data or default_page_callback
self.item_data = item_data or default_item_callback
self.item_title = item_title or default_item_callback
def create(self, page, lines=5, columns=1):
quant_per_page = lines * columns
page = 1 if page <= 0 else page
offset = (page - 1) * quant_per_page
stop = offset + quant_per_page
cutted = self.objects[offset:stop]
total = len(self.objects)
pages_range = [
*range(1, math.ceil(total / quant_per_page) + 1)
] # each item is a page
last_page = len(pages_range)
nav = []
if page <= 3:
for n in [1, 2, 3]:
if n not in pages_range:
continue
text = f"· {n} ·" if n == page else n
nav.append((text, self.page_data(n)))
if last_page >= 4:
nav.append(("4 " if last_page > 5 else 4, self.page_data(4)))
if last_page > 4:
nav.append(
(
f"{last_page} »" if last_page > 5 else last_page,
self.page_data(last_page),
)
)
elif page >= last_page - 2:
nav.extend(
[
("« 1" if last_page - 4 > 1 else 1, self.page_data(1)),
(
f" {last_page - 3}" if last_page - 4 > 1 else last_page - 3,
self.page_data(last_page - 3),
),
]
)
for n in range(last_page - 2, last_page + 1):
text = f"· {n} ·" if n == page else n
nav.append((text, self.page_data(n)))
else:
nav = [
("« 1", self.page_data(1)),
(f" {page - 1}", self.page_data(page - 1)),
(f"· {page} ·", "noop"),
(f"{page + 1} ", self.page_data(page + 1)),
(f"{last_page} »", self.page_data(last_page)),
]
buttons = []
for item in cutted:
buttons.append((self.item_title(item, page), self.item_data(item, page)))
kb_lines = array_chunk(buttons, columns)
if last_page > 1:
kb_lines.append(nav)
return kb_lines

View file

@ -26,3 +26,4 @@ from .messages_and_media import *
from .object import Object
from .update import *
from .user_and_chats import *
from .pyromod import *

View file

@ -541,6 +541,26 @@ class Message(Object, Update):
self.web_app_data = web_app_data
self.reactions = reactions
async def wait_for_click(
self,
from_user_id: Optional[int] = None,
timeout: Optional[int] = None,
filters=None,
alert: Union[str, bool] = True,
):
message_id = self.id
return await self._client.listen(
(self.chat.id, from_user_id, self.id),
listener_type=types.ListenerTypes.CALLBACK_QUERY,
timeout=timeout,
filters=filters,
unallowed_click_alert=alert,
chat_id=self.chat.id,
user_id=from_user_id,
message_id=message_id,
)
@staticmethod
async def _parse(
client: "pyrogram.Client",
@ -4156,3 +4176,88 @@ class Message(Object, Update):
chat_id=self.chat.id,
message_id=self.id
)
async def ask(
self,
text: str,
quote: bool = None,
parse_mode: Optional["enums.ParseMode"] = None,
entities: List["types.MessageEntity"] = None,
disable_web_page_preview: bool = None,
disable_notification: bool = None,
reply_to_message_id: int = None,
reply_markup=None,
filters=None,
timeout: int = None
) -> "Message":
"""Bound method *ask* of :obj:`~pyrogram.types.Message`.
Use as a shortcut for:
.. code-block:: python
client.send_message(chat_id, "What is your name?")
client.wait_for_message(chat_id)
Example:
.. code-block:: python
message.ask("What is your name?")
Parameters:
text (``str``):
Text of the message to be sent.
quote (``bool``, *optional*):
If ``True``, the message will be sent as a reply to this message.
If *reply_to_message_id* is passed, this parameter will be ignored.
Defaults to ``True`` in group chats and ``False`` in private chats.
parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*):
By default, texts are parsed using both Markdown and HTML styles.
You can combine both syntaxes together.
Pass "markdown" or "md" to enable Markdown-style parsing only.
Pass "html" to enable HTML-style parsing only.
Pass None to completely disable style parsing.
entities (List of :obj:`~pyrogram.types.MessageEntity`):
List of special entities that appear in message text, which can be specified instead of *parse_mode*.
disable_web_page_preview (``bool``, *optional*):
Disables link previews for links in this message.
disable_notification (``bool``, *optional*):
Sends the message silently.
Users will receive a notification with no sound.
reply_to_message_id (``int``, *optional*):
If the message is a reply, ID of the original message.
reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*):
Additional interface options. An object for an inline keyboard, custom reply keyboard,
instructions to remove reply keyboard or to force a reply from the user.
filters (:obj:`Filters`):
Pass one or more filters to allow only a subset of callback queries to be passed
in your callback function.
timeout (``int``, *optional*):
Timeout in seconds.
Returns:
:obj:`~pyrogram.types.Message`: On success, the reply message is returned.
Raises:
RPCError: In case of a Telegram RPC error.
asyncio.TimeoutError: In case reply not received within the timeout.
"""
if quote is None:
quote = self.chat.type != "private"
if reply_to_message_id is None and quote:
reply_to_message_id = self.id
request = await self._client.send_message(
chat_id=self.chat.id,
text=text,
parse_mode=parse_mode,
entities=entities,
disable_web_page_preview=disable_web_page_preview,
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
reply_markup=reply_markup
)
reply_message = await self._client.wait_for_message(
self.chat.id,
filters=filters,
timeout=timeout
)
reply_message.request = request
return reply_message

View file

@ -0,0 +1,5 @@
from .identifier import Identifier
from .listener import Listener
from .listener_types import ListenerTypes
__all__ = ["Identifier", "Listener", "ListenerTypes"]

View file

@ -0,0 +1,31 @@
from dataclasses import dataclass
from typing import Optional
@dataclass
class Identifier:
inline_message_id: Optional[str] = None
chat_id: Optional[int] = None
message_id: Optional[int] = None
from_user_id: Optional[int] = None
def matches(self, other: "Identifier") -> bool:
# Compare each property of other with the corresponding property in self
# If the property in self is None, the property in other can be anything
# If the property in self is not None, the property in other must be the same
for field in self.__annotations__:
self_value = getattr(self, field)
other_value = getattr(other, field)
if self_value is not None and other_value != self_value:
return False
return True
def count_populated(self):
non_null_count = 0
for attr in self.__annotations__:
if getattr(self, attr) is not None:
non_null_count += 1
return non_null_count

View file

@ -0,0 +1,16 @@
from asyncio import Future
from dataclasses import dataclass
from pyrogram import filters
from .identifier import Identifier
from .listener_types import ListenerTypes
@dataclass
class Listener:
listener_type: ListenerTypes
future: Future
filters: "filters.Filter"
unallowed_click_alert: bool
identifier: Identifier

View file

@ -0,0 +1,6 @@
from enum import Enum
class ListenerTypes(Enum):
MESSAGE = "message"
CALLBACK_QUERY = "callback_query"

View file

@ -402,6 +402,15 @@ class Chat(Object):
else:
return Chat._parse_channel_chat(client, chat)
def listen(self, *args, **kwargs):
return self._client.listen(*args, chat_id=self.id, **kwargs)
def ask(self, text, *args, **kwargs):
return self._client.ask(self.id, text, *args, **kwargs)
def stop_listening(self, *args, **kwargs):
return self._client.stop_listening(*args, chat_id=self.id, **kwargs)
async def archive(self):
"""Bound method *archive* of :obj:`~pyrogram.types.Chat`.

View file

@ -308,6 +308,15 @@ class User(Object, Update):
client=client
)
def listen(self, *args, **kwargs):
return self._client.listen(*args, user_id=self.id, **kwargs)
def ask(self, text, *args, **kwargs):
return self._client.ask(self.id, text, *args, user_id=self.id, **kwargs)
def stop_listening(self, *args, **kwargs):
return self._client.stop_listening(*args, user_id=self.id, **kwargs)
async def archive(self):
"""Bound method *archive* of :obj:`~pyrogram.types.User`.

View file

@ -27,6 +27,7 @@ from concurrent.futures.thread import ThreadPoolExecutor
from datetime import datetime, timezone
from getpass import getpass
from typing import Union, List, Dict, Optional, Any, Callable, TypeVar
from types import SimpleNamespace
import pyrogram
from pyrogram import raw, enums
@ -34,6 +35,15 @@ from pyrogram import types
from pyrogram.file_id import FileId, FileType, PHOTO_TYPES, DOCUMENT_TYPES
PyromodConfig = SimpleNamespace(
timeout_handler=None,
stopped_handler=None,
throw_exceptions=True,
unallowed_click_alert=True,
unallowed_click_alert_text=("[pyromod] You're not expected to click this button."),
)
async def ainput(prompt: str = "", *, hide: bool = False):
"""Just like the built-in input, but async"""
with ThreadPoolExecutor(1) as executor: