diff --git a/pyrogram/client.py b/pyrogram/client.py
index d516c0fe8..0ab159e6d 100644
--- a/pyrogram/client.py
+++ b/pyrogram/client.py
@@ -57,10 +57,11 @@
pass
else:
from pyrogram.storage import MongoStorage
-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
@@ -331,7 +332,7 @@ def __init__(
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):
@@ -364,6 +365,307 @@ async def updates_watchdog(self):
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: Union[Union[int, str], List[Union[int, str]]] = None,
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
+ message_id: Union[int, List[int]] = None,
+ inline_message_id: Union[str, List[str]] = None,
+ ):
+ """
+ Creates a listener and waits for it to be fulfilled.
+
+ :param filters: A filter to check if the listener should be fulfilled.
+ :param listener_type: The type of listener to create. Defaults to :attr:`pyromod.types.ListenerTypes.MESSAGE`.
+ :param timeout: The maximum amount of time to wait for the listener to be fulfilled. Defaults to ``None``.
+ :param unallowed_click_alert: Whether to alert the user if they click on a button that is not intended for them. Defaults to ``True``.
+ :param chat_id: The chat ID(s) to listen for. Defaults to ``None``.
+ :param user_id: The user ID(s) to listen for. Defaults to ``None``.
+ :param message_id: The message ID(s) to listen for. Defaults to ``None``.
+ :param inline_message_id: The inline message ID(s) to listen for. Defaults to ``None``.
+ :return: The Message or CallbackQuery that fulfilled the listener.
+ """
+ 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()
+
+ listener = Listener(
+ future=future,
+ filters=filters,
+ unallowed_click_alert=unallowed_click_alert,
+ identifier=pattern,
+ listener_type=listener_type,
+ )
+
+ future.add_done_callback(lambda _future: self.remove_listener(listener))
+
+ self.listeners[listener_type].append(listener)
+
+ try:
+ return await asyncio.wait_for(future, timeout)
+ except asyncio.exceptions.TimeoutError:
+ if callable(PyromodConfig.timeout_handler):
+ if inspect.iscoroutinefunction(PyromodConfig.timeout_handler.__call__):
+ await PyromodConfig.timeout_handler(pattern, listener, timeout)
+ else:
+ await self.loop.run_in_executor(
+ None, PyromodConfig.timeout_handler, pattern, listener, timeout
+ )
+ elif PyromodConfig.throw_exceptions:
+ raise ListenerTimeout(timeout)
+
+ async def ask(
+ self,
+ chat_id: Union[Union[int, str], List[Union[int, str]]],
+ text: str,
+ filters: Optional[Filter] = None,
+ listener_type: ListenerTypes = ListenerTypes.MESSAGE,
+ timeout: Optional[int] = None,
+ unallowed_click_alert: bool = True,
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
+ message_id: Union[int, List[int]] = None,
+ inline_message_id: Union[str, List[str]] = None,
+ *args,
+ **kwargs,
+ ):
+ """
+ Sends a message and waits for a response.
+
+ :param chat_id: The chat ID(s) to wait for a message from. The first chat ID will be used to send the message.
+ :param text: The text to send.
+ :param filters: Same as :meth:`pyromod.types.Client.listen`.
+ :param listener_type: Same as :meth:`pyromod.types.Client.listen`.
+ :param timeout: Same as :meth:`pyromod.types.Client.listen`.
+ :param unallowed_click_alert: Same as :meth:`pyromod.types.Client.listen`.
+ :param user_id: Same as :meth:`pyromod.types.Client.listen`.
+ :param message_id: Same as :meth:`pyromod.types.Client.listen`.
+ :param inline_message_id: Same as :meth:`pyromod.types.Client.listen`.
+ :param args: Additional arguments to pass to :meth:`pyrogram.Client.send_message`.
+ :param kwargs: Additional keyword arguments to pass to :meth:`pyrogram.Client.send_message`.
+ :return:
+ Same as :meth:`pyromod.types.Client.listen`. The sent message is returned as the attribute ``sent_message``.
+ """
+ sent_message = None
+ if text.strip() != "":
+ chat_to_ask = chat_id[0] if isinstance(chat_id, list) else chat_id
+ sent_message = await self.send_message(chat_to_ask, 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 remove_listener(self, listener: Listener):
+ """
+ Removes a listener from the :meth:`pyromod.types.Client.listeners` dictionary.
+
+ :param listener: The listener to remove.
+ :return: ``void``
+ """
+ try:
+ self.listeners[listener.listener_type].remove(listener)
+ except ValueError:
+ pass
+
+ def get_listener_matching_with_data(
+ self, data: Identifier, listener_type: ListenerTypes
+ ) -> Optional[Listener]:
+ """
+ Gets a listener that matches the given data.
+
+ :param data: A :class:`pyromod.types.Identifier` to match against.
+ :param listener_type: The type of listener to get. Must be a value from :class:`pyromod.types.ListenerTypes`.
+ :return: The listener that matches the given data or ``None`` if no listener matches.
+ """
+ matching = []
+ for listener in self.listeners[listener_type]:
+ if listener.identifier.matches(data):
+ 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 get_listener_matching_with_identifier_pattern(
+ self, pattern: Identifier, listener_type: ListenerTypes
+ ) -> Optional[Listener]:
+ """
+ Gets a listener that matches the given identifier pattern.
+
+ The difference from :meth:`pyromod.types.Client.get_listener_matching_with_data` is that this method
+ intends to get a listener by passing partial info of the listener identifier, while the other method
+ intends to get a listener by passing the full info of the update data, which the listener should match with.
+
+ :param pattern: A :class:`pyromod.types.Identifier` to match against.
+ :param listener_type: The type of listener to get. Must be a value from :class:`pyromod.types.ListenerTypes`.
+ :return: The listener that matches the given identifier pattern or ``None`` if no listener matches.
+ """
+ matching = []
+ for listener in self.listeners[listener_type]:
+ if pattern.matches(listener.identifier):
+ 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 get_many_listeners_matching_with_data(
+ self,
+ data: Identifier,
+ listener_type: ListenerTypes,
+ ) -> List[Listener]:
+ """
+ Same of :meth:`pyromod.types.Client.get_listener_matching_with_data` but returns a list of listeners instead of one.
+
+ :param data: Same as :meth:`pyromod.types.Client.get_listener_matching_with_data`.
+ :param listener_type: Same as :meth:`pyromod.types.Client.get_listener_matching_with_data`.
+ :return: A list of listeners that match the given data.
+ """
+ listeners = []
+ for listener in self.listeners[listener_type]:
+ if listener.identifier.matches(data):
+ listeners.append(listener)
+ return listeners
+
+ def get_many_listeners_matching_with_identifier_pattern(
+ self,
+ pattern: Identifier,
+ listener_type: ListenerTypes,
+ ) -> List[Listener]:
+ """
+ Same of :meth:`pyromod.types.Client.get_listener_matching_with_identifier_pattern` but returns a list of listeners instead of one.
+
+ :param pattern: Same as :meth:`pyromod.types.Client.get_listener_matching_with_identifier_pattern`.
+ :param listener_type: Same as :meth:`pyromod.types.Client.get_listener_matching_with_identifier_pattern`.
+ :return: A list of listeners that match the given identifier pattern.
+ """
+ listeners = []
+ for listener in self.listeners[listener_type]:
+ if pattern.matches(listener.identifier):
+ listeners.append(listener)
+ return listeners
+
+ async def stop_listening(
+ self,
+ listener_type: ListenerTypes = ListenerTypes.MESSAGE,
+ chat_id: Union[Union[int, str], List[Union[int, str]]] = None,
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
+ message_id: Union[int, List[int]] = None,
+ inline_message_id: Union[str, List[str]] = None,
+ ):
+ """
+ Stops all listeners that match the given identifier pattern.
+ Uses :meth:`pyromod.types.Client.get_many_listeners_matching_with_identifier_pattern`.
+
+ :param listener_type: The type of listener to stop. Must be a value from :class:`pyromod.types.ListenerTypes`.
+ :param chat_id: The chat_id to match against.
+ :param user_id: The user_id to match against.
+ :param message_id: The message_id to match against.
+ :param inline_message_id: The inline_message_id to match against.
+ :return: ``void``
+ """
+ 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_listeners_matching_with_identifier_pattern(
+ pattern, listener_type
+ )
+
+ for listener in listeners:
+ await self.stop_listener(listener)
+
+ async def stop_listener(self, listener: Listener):
+ """
+ Stops a listener, calling stopped_handler if applicable or raising ListenerStopped if throw_exceptions is True.
+
+ :param listener: The :class:`pyromod.types.Listener` to stop.
+ :return: ``void``
+ :raises ListenerStopped: If throw_exceptions is True.
+ """
+ self.remove_listener(listener)
+
+ if listener.future.done():
+ return
+
+ if callable(PyromodConfig.stopped_handler):
+ if inspect.iscoroutinefunction(PyromodConfig.stopped_handler.__call__):
+ await PyromodConfig.stopped_handler(None, listener)
+ else:
+ await self.loop.run_in_executor(
+ None, PyromodConfig.stopped_handler, None, listener
+ )
+ elif PyromodConfig.throw_exceptions:
+ listener.future.set_exception(ListenerStopped())
+
+ def register_next_step_handler(
+ self,
+ callback: Callable,
+ filters: Optional[Filter] = None,
+ listener_type: ListenerTypes = ListenerTypes.MESSAGE,
+ unallowed_click_alert: bool = True,
+ chat_id: Union[Union[int, str], List[Union[int, str]]] = None,
+ user_id: Union[Union[int, str], List[Union[int, str]]] = None,
+ message_id: Union[int, List[int]] = None,
+ inline_message_id: Union[str, List[str]] = None,
+ ):
+ """
+ Registers a listener with a callback to be called when the listener is fulfilled.
+
+ :param callback: The callback to call when the listener is fulfilled.
+ :param filters: Same as :meth:`pyromod.types.Client.listen`.
+ :param listener_type: Same as :meth:`pyromod.types.Client.listen`.
+ :param unallowed_click_alert: Same as :meth:`pyromod.types.Client.listen`.
+ :param chat_id: Same as :meth:`pyromod.types.Client.listen`.
+ :param user_id: Same as :meth:`pyromod.types.Client.listen`.
+ :param message_id: Same as :meth:`pyromod.types.Client.listen`.
+ :param inline_message_id: Same as :meth:`pyromod.types.Client.listen`.
+ :return: ``void``
+ """
+ pattern = Identifier(
+ from_user_id=user_id,
+ chat_id=chat_id,
+ message_id=message_id,
+ inline_message_id=inline_message_id,
+ )
+
+ listener = Listener(
+ callback=callback,
+ filters=filters,
+ unallowed_click_alert=unallowed_click_alert,
+ identifier=pattern,
+ listener_type=listener_type,
+ )
+
+ self.listeners[listener_type].append(listener)
+
async def authorize(self) -> User:
if self.bot_token:
return await self.sign_in_bot(self.bot_token)
diff --git a/pyrogram/dispatcher.py b/pyrogram/dispatcher.py
index a145e5312..ea94b2a88 100644
--- a/pyrogram/dispatcher.py
+++ b/pyrogram/dispatcher.py
@@ -26,7 +26,7 @@
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 @@ def __init__(self, client: "pyrogram.Client"):
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,
diff --git a/pyrogram/errors/__init__.py b/pyrogram/errors/__init__.py
index aa3a042c5..c084f877d 100644
--- a/pyrogram/errors/__init__.py
+++ b/pyrogram/errors/__init__.py
@@ -17,6 +17,7 @@
# along with Pyrogram. If not, see .
from .exceptions import *
+from .pyromod import *
from .rpc_error import UnknownError
diff --git a/pyrogram/errors/pyromod/__init__.py b/pyrogram/errors/pyromod/__init__.py
new file mode 100644
index 000000000..4f3fdfae2
--- /dev/null
+++ b/pyrogram/errors/pyromod/__init__.py
@@ -0,0 +1,4 @@
+from .listener_stopped import ListenerStopped
+from .listener_timeout import ListenerTimeout
+
+__all__ = ["ListenerStopped", "ListenerTimeout"]
\ No newline at end of file
diff --git a/pyrogram/errors/pyromod/listener_stopped.py b/pyrogram/errors/pyromod/listener_stopped.py
new file mode 100644
index 000000000..fa939e04d
--- /dev/null
+++ b/pyrogram/errors/pyromod/listener_stopped.py
@@ -0,0 +1,2 @@
+class ListenerStopped(Exception):
+ pass
\ No newline at end of file
diff --git a/pyrogram/errors/pyromod/listener_timeout.py b/pyrogram/errors/pyromod/listener_timeout.py
new file mode 100644
index 000000000..1a97e68ce
--- /dev/null
+++ b/pyrogram/errors/pyromod/listener_timeout.py
@@ -0,0 +1,2 @@
+class ListenerTimeout(Exception):
+ pass
\ No newline at end of file
diff --git a/pyrogram/handlers/__init__.py b/pyrogram/handlers/__init__.py
index 805ca051e..5a28eeb5a 100644
--- a/pyrogram/handlers/__init__.py
+++ b/pyrogram/handlers/__init__.py
@@ -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
diff --git a/pyrogram/handlers/callback_query_handler.py b/pyrogram/handlers/callback_query_handler.py
index b924fffa2..65ade3a70 100644
--- a/pyrogram/handlers/callback_query_handler.py
+++ b/pyrogram/handlers/callback_query_handler.py
@@ -16,7 +16,13 @@
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see .
-from typing import Callable
+from asyncio import iscoroutinefunction
+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 +52,155 @@ class CallbackQueryHandler(Handler):
"""
def __init__(self, callback: Callable, filters=None):
- super().__init__(callback, filters)
+ self.original_callback = callback
+ super().__init__(self.resolve_future_or_callback, filters)
+
+ def compose_data_identifier(self, query: CallbackQuery):
+ """
+ Composes an Identifier object from a CallbackQuery object.
+
+ :param query: The CallbackQuery object to compose of.
+ :return: An Identifier object.
+ """
+ from_user = query.from_user
+ from_user_id = from_user.id if from_user else None
+ from_user_username = from_user.username 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, query.message.chat.username]
+
+ return Identifier(
+ message_id=message_id,
+ chat_id=chat_id,
+ from_user_id=[from_user_id, from_user_username],
+ inline_message_id=query.inline_message_id,
+ )
+
+ async def check_if_has_matching_listener(
+ self, client: "pyrogram.Client", query: CallbackQuery
+ ) -> Tuple[bool, Listener]:
+ """
+ Checks if the CallbackQuery object has a matching listener.
+
+ :param client: The Client object to check with.
+ :param query: The CallbackQuery object to check with.
+ :return: A tuple of a boolean and a Listener object. The boolean indicates whether
+ the found listener has filters and its filters matches with the CallbackQuery object.
+ The Listener object is the matching listener.
+ """
+ data = self.compose_data_identifier(query)
+
+ listener = client.get_listener_matching_with_data(
+ data, ListenerTypes.CALLBACK_QUERY
+ )
+
+ listener_does_match = False
+
+ if listener:
+ filters = listener.filters
+ if callable(filters):
+ if iscoroutinefunction(filters.__call__):
+ listener_does_match = await filters(client, query)
+ else:
+ listener_does_match = await client.loop.run_in_executor(
+ None, filters, client, query
+ )
+ else:
+ listener_does_match = True
+
+ return listener_does_match, listener
+
+ async def check(self, client: "pyrogram.Client", query: CallbackQuery):
+ """
+ Checks if the CallbackQuery object has a matching listener or handler.
+
+ :param client: The Client object to check with.
+ :param query: The CallbackQuery object to check with.
+ :return: A boolean indicating whether the CallbackQuery object has a matching listener or the handler
+ filter matches.
+ """
+ listener_does_match, listener = await self.check_if_has_matching_listener(
+ client, query
+ )
+
+ if callable(self.filters):
+ if iscoroutinefunction(self.filters.__call__):
+ handler_does_match = await self.filters(client, query)
+ else:
+ handler_does_match = await client.loop.run_in_executor(
+ None, self.filters, client, query
+ )
+ else:
+ handler_does_match = 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_or_callback(
+ self, client: "pyrogram.Client", query: CallbackQuery, *args
+ ):
+ """
+ Resolves the future or calls the callback of the listener. Will call the original handler if no listener.
+
+ :param client: The Client object to resolve or call with.
+ :param query: The CallbackQuery object to resolve or call with.
+ :param args: The arguments to call the callback with.
+ :return: None
+ """
+ listener_does_match, listener = await self.check_if_has_matching_listener(
+ client, query
+ )
+
+ if listener and listener_does_match:
+ client.remove_listener(listener)
+
+ if listener.future and not listener.future.done():
+ listener.future.set_result(query)
+
+ raise pyrogram.StopPropagation
+ elif listener.callback:
+ if iscoroutinefunction(listener.callback):
+ await listener.callback(client, query, *args)
+ else:
+ listener.callback(client, query, *args)
+
+ raise pyrogram.StopPropagation
+ else:
+ raise ValueError("Listener must have either a future or a callback")
+ else:
+ await self.original_callback(client, query, *args)
diff --git a/pyrogram/handlers/conversation_handler.py b/pyrogram/handlers/conversation_handler.py
new file mode 100644
index 000000000..03de1e84d
--- /dev/null
+++ b/pyrogram/handlers/conversation_handler.py
@@ -0,0 +1,68 @@
+# 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 .
+
+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]
\ No newline at end of file
diff --git a/pyrogram/handlers/message_handler.py b/pyrogram/handlers/message_handler.py
index f5a35b553..cf4a922b0 100644
--- a/pyrogram/handlers/message_handler.py
+++ b/pyrogram/handlers/message_handler.py
@@ -15,8 +15,11 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see .
-
+from inspect import iscoroutinefunction
from typing import Callable
+import pyrogram
+
+from pyrogram.types import ListenerTypes, Message, Identifier
from .handler import Handler
@@ -46,4 +49,102 @@ class MessageHandler(Handler):
"""
def __init__(self, callback: Callable, filters=None):
- super().__init__(callback, filters)
+ self.original_callback = callback
+ super().__init__(self.resolve_future_or_callback, filters)
+
+ async def check_if_has_matching_listener(self, client: "pyrogram.Client", message: Message):
+ """
+ Checks if the message has a matching listener.
+
+ :param client: The Client object to check with.
+ :param message: The Message object to check with.
+ :return: A tuple of whether the message has a matching listener and its filters does match with the Message
+ and the matching listener;
+ """
+ from_user = message.from_user
+ from_user_id = from_user.id if from_user else None
+ from_user_username = from_user.username 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, message.chat.username],
+ from_user_id=[from_user_id, from_user_username],
+ )
+
+ listener = client.get_listener_matching_with_data(data, ListenerTypes.MESSAGE)
+
+ listener_does_match = False
+
+ if listener:
+ filters = listener.filters
+ if callable(filters):
+ if iscoroutinefunction(filters.__call__):
+ listener_does_match = await filters(client, message)
+ else:
+ listener_does_match = await client.loop.run_in_executor(
+ None, filters, client, message
+ )
+ else:
+ listener_does_match = True
+
+ return listener_does_match, listener
+
+ async def check(self, client: "pyrogram.Client", message: Message):
+ """
+ Checks if the message has a matching listener or handler and its filters does match with the Message.
+
+ :param client: Client object to check with.
+ :param message: Message object to check with.
+ :return: Whether the message has a matching listener or handler and its filters does match with the Message.
+ """
+ listener_does_match = (
+ await self.check_if_has_matching_listener(client, message)
+ )[0]
+
+ if callable(self.filters):
+ if iscoroutinefunction(self.filters.__call__):
+ handler_does_match = await self.filters(client, message)
+ else:
+ handler_does_match = await client.loop.run_in_executor(
+ None, self.filters, client, message
+ )
+ else:
+ handler_does_match = 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_or_callback(self, client: "pyrogram.Client", message: Message, *args):
+ """
+ Resolves the future or calls the callback of the listener if the message has a matching listener.
+
+ :param client: Client object to resolve or call with.
+ :param message: Message object to resolve or call with.
+ :param args: Arguments to call the callback with.
+ :return: None
+ """
+ listener_does_match, listener = await self.check_if_has_matching_listener(
+ client, message
+ )
+
+ if listener and listener_does_match:
+ client.remove_listener(listener)
+
+ if listener.future and not listener.future.done():
+ listener.future.set_result(message)
+
+ raise pyrogram.StopPropagation
+ elif listener.callback:
+ if iscoroutinefunction(listener.callback):
+ await listener.callback(client, message, *args)
+ else:
+ listener.callback(client, message, *args)
+
+ raise pyrogram.StopPropagation
+ else:
+ raise ValueError("Listener must have either a future or a callback")
+ else:
+ await self.original_callback(client, message, *args)
\ No newline at end of file
diff --git a/pyrogram/helpers/__init__.py b/pyrogram/helpers/__init__.py
new file mode 100644
index 000000000..786d7e01e
--- /dev/null
+++ b/pyrogram/helpers/__init__.py
@@ -0,0 +1,16 @@
+"""
+pyromod - A monkeypatcher add-on for Pyrogram
+Copyright (C) 2020 Cezar H.
+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 .
+"""
+from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply
\ No newline at end of file
diff --git a/pyrogram/helpers/helpers.py b/pyrogram/helpers/helpers.py
new file mode 100644
index 000000000..8aecc1700
--- /dev/null
+++ b/pyrogram/helpers/helpers.py
@@ -0,0 +1,138 @@
+from pyrogram.types import (
+ InlineKeyboardButton,
+ InlineKeyboardMarkup,
+ KeyboardButton,
+ ReplyKeyboardMarkup,
+ ForceReply,
+)
+
+
+def ikb(rows=None):
+ """
+ Create an InlineKeyboardMarkup from a list of lists of buttons.
+ :param rows: List of lists of buttons. Defaults to empty list.
+ :return: InlineKeyboardMarkup
+ """
+ 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"):
+ """
+ Create an InlineKeyboardButton.
+
+ :param text: Text of the button.
+ :param value: Value of the button.
+ :param type: Type of the button. Defaults to "callback_data".
+ :return: InlineKeyboardButton
+ """
+ return InlineKeyboardButton(text, **{type: value})
+ # return {'text': text, type: value}
+
+
+# The inverse of above
+def bki(keyboard):
+ """
+ Create a list of lists of buttons from an InlineKeyboardMarkup.
+
+ :param keyboard: InlineKeyboardMarkup
+ :return: List of lists of buttons
+ """
+ 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):
+ """
+ Create a button list from an InlineKeyboardButton.
+
+ :param button: InlineKeyboardButton
+ :return: Button as a list to be used in btn()
+ """
+ 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):
+ """
+ Create a ReplyKeyboardMarkup from a list of lists of buttons.
+
+ :param rows: List of lists of buttons. Defaults to empty list.
+ :param kwargs: Other arguments to pass to ReplyKeyboardMarkup.
+ :return: ReplyKeyboardMarkup
+ """
+ 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
+"""
+Create a KeyboardButton.
+"""
+
+
+def force_reply(selective=True):
+ """
+ Create a ForceReply.
+
+ :param selective: Whether the reply should be selective. Defaults to True.
+ :return: ForceReply
+ """
+ return ForceReply(selective=selective)
+
+
+def array_chunk(input_array, size):
+ """
+ Split an array into chunks.
+
+ :param input_array: The array to split.
+ :param size: The size of each chunk.
+ :return: List of chunks.
+ """
+ return [input_array[i: i + size] for i in range(0, len(input_array), size)]
diff --git a/pyrogram/methods/messages/__init__.py b/pyrogram/methods/messages/__init__.py
index 3007290cf..94a6b80e0 100644
--- a/pyrogram/methods/messages/__init__.py
+++ b/pyrogram/methods/messages/__init__.py
@@ -65,6 +65,8 @@
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
diff --git a/pyrogram/methods/messages/wait_for_callback_query.py b/pyrogram/methods/messages/wait_for_callback_query.py
new file mode 100644
index 000000000..f85eb6d45
--- /dev/null
+++ b/pyrogram/methods/messages/wait_for_callback_query.py
@@ -0,0 +1,71 @@
+# 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 .
+
+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)
\ No newline at end of file
diff --git a/pyrogram/methods/messages/wait_for_message.py b/pyrogram/methods/messages/wait_for_message.py
new file mode 100644
index 000000000..53e698ca5
--- /dev/null
+++ b/pyrogram/methods/messages/wait_for_message.py
@@ -0,0 +1,71 @@
+# 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 .
+
+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)
\ No newline at end of file
diff --git a/pyrogram/nav/__init__.py b/pyrogram/nav/__init__.py
new file mode 100644
index 000000000..9bdf52980
--- /dev/null
+++ b/pyrogram/nav/__init__.py
@@ -0,0 +1,16 @@
+"""
+pyromod - A monkeypatcher add-on for Pyrogram
+Copyright (C) 2020 Cezar H.
+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 .
+"""
+from .pagination import Pagination
\ No newline at end of file
diff --git a/pyrogram/nav/pagination.py b/pyrogram/nav/pagination.py
new file mode 100644
index 000000000..bd107140a
--- /dev/null
+++ b/pyrogram/nav/pagination.py
@@ -0,0 +1,94 @@
+"""
+pyromod - A monkeypatcher add-on for Pyrogram
+Copyright (C) 2020 Cezar H.
+
+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 .
+"""
+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
\ No newline at end of file
diff --git a/pyrogram/types/__init__.py b/pyrogram/types/__init__.py
index e0859bbab..bb2090f1d 100644
--- a/pyrogram/types/__init__.py
+++ b/pyrogram/types/__init__.py
@@ -26,3 +26,4 @@
from .object import Object
from .update import *
from .user_and_chats import *
+from .pyromod import *
diff --git a/pyrogram/types/messages_and_media/message.py b/pyrogram/types/messages_and_media/message.py
index 8df53e5c3..994514106 100644
--- a/pyrogram/types/messages_and_media/message.py
+++ b/pyrogram/types/messages_and_media/message.py
@@ -546,6 +546,35 @@ def __init__(
self.web_app_data = web_app_data
self.reactions = reactions
+ async def wait_for_click(
+ self,
+ from_user_id: Optional[Union[Union[int, str], List[Union[int, str]]]] = None,
+ timeout: Optional[int] = None,
+ filters=None,
+ alert: Union[str, bool] = True,
+ ):
+ """
+ Waits for a callback query to be clicked on the message.
+
+ :param from_user_id: The user ID(s) to wait for. If None, waits for any user.
+ :param timeout: The timeout in seconds. If None, waits forever.
+ :param filters: The filters to pass to Client.listen().
+ :param alert: The alert to show when the button is clicked by users that are not allowed in from_user_id.
+ :return: The callback query that was clicked.
+ """
+ message_id = getattr(self, "id", getattr(self, "message_id", None))
+
+ 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",
@@ -4358,3 +4387,88 @@ async def unpin(self) -> bool:
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
\ No newline at end of file
diff --git a/pyrogram/types/pyromod/__init__.py b/pyrogram/types/pyromod/__init__.py
new file mode 100644
index 000000000..da751887c
--- /dev/null
+++ b/pyrogram/types/pyromod/__init__.py
@@ -0,0 +1,5 @@
+from .identifier import Identifier
+from .listener import Listener
+from .listener_types import ListenerTypes
+
+__all__ = ["Identifier", "Listener", "ListenerTypes"]
\ No newline at end of file
diff --git a/pyrogram/types/pyromod/identifier.py b/pyrogram/types/pyromod/identifier.py
new file mode 100644
index 000000000..9dff7cd97
--- /dev/null
+++ b/pyrogram/types/pyromod/identifier.py
@@ -0,0 +1,41 @@
+from dataclasses import dataclass
+from typing import Optional, Union, List
+
+
+@dataclass
+class Identifier:
+ inline_message_id: Optional[Union[str, List[str]]] = None
+ chat_id: Optional[Union[Union[int, str], List[Union[int, str]]]] = None
+ message_id: Optional[Union[int, List[int]]] = None
+ from_user_id: Optional[Union[Union[int, str], List[Union[int, str]]]] = None
+
+ def matches(self, update: "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__:
+ pattern_value = getattr(self, field)
+ update_value = getattr(update, field)
+
+ if pattern_value is not None:
+ if isinstance(update_value, list):
+ if isinstance(pattern_value, list):
+ if not set(update_value).intersection(set(pattern_value)):
+ return False
+ elif pattern_value not in update_value:
+ return False
+ elif isinstance(pattern_value, list):
+ if update_value not in pattern_value:
+ return False
+ elif update_value != pattern_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
\ No newline at end of file
diff --git a/pyrogram/types/pyromod/listener.py b/pyrogram/types/pyromod/listener.py
new file mode 100644
index 000000000..1f1fe6feb
--- /dev/null
+++ b/pyrogram/types/pyromod/listener.py
@@ -0,0 +1,19 @@
+from asyncio import Future
+from dataclasses import dataclass
+from typing import Callable
+
+import pyrogram
+from pyrogram import filters
+
+from .identifier import Identifier
+from .listener_types import ListenerTypes
+
+
+@dataclass
+class Listener:
+ listener_type: ListenerTypes
+ filters: "pyrogram.filters.Filter"
+ unallowed_click_alert: bool
+ identifier: Identifier
+ future: Future = None
+ callback: Callable = None
diff --git a/pyrogram/types/pyromod/listener_types.py b/pyrogram/types/pyromod/listener_types.py
new file mode 100644
index 000000000..25ad06dde
--- /dev/null
+++ b/pyrogram/types/pyromod/listener_types.py
@@ -0,0 +1,6 @@
+from enum import Enum
+
+
+class ListenerTypes(Enum):
+ MESSAGE = "message"
+ CALLBACK_QUERY = "callback_query"
\ No newline at end of file
diff --git a/pyrogram/types/user_and_chats/chat.py b/pyrogram/types/user_and_chats/chat.py
index e57e32a58..94d3fcc1e 100644
--- a/pyrogram/types/user_and_chats/chat.py
+++ b/pyrogram/types/user_and_chats/chat.py
@@ -402,6 +402,36 @@ def _parse_chat(client, chat: Union[raw.types.Chat, raw.types.User, raw.types.Ch
else:
return Chat._parse_channel_chat(client, chat)
+ def listen(self, *args, **kwargs):
+ """
+ Listens for messages in the chat. Calls Client.listen() with the chat_id set to the chat's id.
+
+ :param args: Arguments to pass to Client.listen().
+ :param kwargs: Keyword arguments to pass to Client.listen().
+ :return: The return value of Client.listen().
+ """
+ return self._client.listen(*args, chat_id=self.id, **kwargs)
+
+ def ask(self, text, *args, **kwargs):
+ """
+ Asks a question in the chat. Calls Client.ask() with the chat_id set to the chat's id.
+ :param text: The text to send.
+ :param args: Arguments to pass to Client.ask().
+ :param kwargs: Keyword arguments to pass to Client.ask().
+ :return: The return value of Client.ask().
+ """
+ return self._client.ask(self.id, text, *args, **kwargs)
+
+ def stop_listening(self, *args, **kwargs):
+ """
+ Stops listening for messages in the chat. Calls Client.stop_listening() with the chat_id set to the chat's id.
+
+ :param args: Arguments to pass to Client.stop_listening().
+ :param kwargs: Keyword arguments to pass to Client.stop_listening().
+ :return: The return value of Client.stop_listening().
+ """
+ return self._client.stop_listening(*args, chat_id=self.id, **kwargs)
+
async def archive(self):
"""Bound method *archive* of :obj:`~pyrogram.types.Chat`.
diff --git a/pyrogram/types/user_and_chats/user.py b/pyrogram/types/user_and_chats/user.py
index b120a2706..35358e2fa 100644
--- a/pyrogram/types/user_and_chats/user.py
+++ b/pyrogram/types/user_and_chats/user.py
@@ -308,6 +308,37 @@ def _parse_user_status(client, user_status: "raw.types.UpdateUserStatus"):
client=client
)
+ def listen(self, *args, **kwargs):
+ """
+ Listens for messages from the user. Calls Client.listen() with the user_id set to the user's id.
+
+ :param args: Arguments to pass to Client.listen().
+ :param kwargs: Keyword arguments to pass to Client.listen().
+ :return: The return value of Client.listen().
+ """
+ return self._client.listen(*args, user_id=self.id, **kwargs)
+
+ def ask(self, text, *args, **kwargs):
+ """
+ Asks a question to the user. Calls Client.ask() with both chat_id and user_id set to the user's id.
+
+ :param text: The text to send.
+ :param args: Arguments to pass to Client.ask().
+ :param kwargs: Keyword arguments to pass to Client.ask().
+ :return: The return value of Client.ask().
+ """
+ return self._client.ask(self.id, text, *args, user_id=self.id, **kwargs)
+
+ def stop_listening(self, *args, **kwargs):
+ """
+ Stops listening for messages from the user. Calls Client.stop_listening() with the user_id set to the user's id.
+
+ :param args: Arguments to pass to Client.stop_listening().
+ :param kwargs: Keyword arguments to pass to Client.stop_listening().
+ :return: The return value of Client.stop_listening().
+ """
+ return self._client.stop_listening(*args, user_id=self.id, **kwargs)
+
async def archive(self):
"""Bound method *archive* of :obj:`~pyrogram.types.User`.
diff --git a/pyrogram/utils.py b/pyrogram/utils.py
index 6ae670218..e9765e995 100644
--- a/pyrogram/utils.py
+++ b/pyrogram/utils.py
@@ -27,6 +27,7 @@
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.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: