Skip to content

Commit

Permalink
Release v3.1 (#40)
Browse files Browse the repository at this point in the history
* feature: register-next-step-handler (#35)

* Handle filters if sync will use run_in_executor (#34)

* async and sync for handler config (#36)

* Support async and sync config handler

* double import

* feat: implement register_next_step_handler with execution of the registered callback (#37)

* feat: receive listener data as list (#38)

* feat: accept lists as chat_id, user_id, message_id and inline_message_id on listen, ask and wait_for_click

* feat: accept lists as chat_id, user_id, message_id and inline_message_id on listen, ask and wait_for_click

* async and sync for handler config (#36)

* Support async and sync config handler

* double import

* feat: implement register_next_step_handler with execution of the registered callback (#37)

* async and sync for handler config (#36)

* Support async and sync config handler

* double import

---------

Co-authored-by: Juan Simon .D <[email protected]>

* feat!: make stop_listening async and create stop_listener

* docs: bump year on copyright

* feat: accept usernames in listen (#39)

* feat: accept usernames as chat_id and user_id on listen, ask and wait_for_click

* Delete CNAME

* Create CNAME

* hotfix: fix syntax error

* hotfix: fix login of lists in identifiers, fix call to remove_listener on future done

* feat: add startup logs

* docs: mention twice the telegram group in readme

* feat: make register_next_step_handler support lists

---------

Co-authored-by: Juan Simon .D <[email protected]>
  • Loading branch information
usernein and jusidama18 authored Oct 31, 2023
1 parent 8dbea4f commit 0753e21
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 73 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ of advanced features, simplifies conversation handling, and offers a high degree

You can find the full documentation at [pyromod.pauxis.dev](https://pyromod.pauxis.dev).

Also feel free to ask any pyromod-related questions on our [Telegram group](https://t.me/pyromodchat).

## Key Features

- **Effortless Bot Development:** pyromod streamlines the process of building conversational Telegram bots, saving you
Expand Down Expand Up @@ -148,7 +150,7 @@ or contribute in any way that aligns with our goals.

This project may include snippets of Pyrogram code

- Pyrogram - Telegram MTProto API Client Library for Python. Copyright (C) 2017-2022
- Pyrogram - Telegram MTProto API Client Library for Python. Copyright (C) 2017-2023
Dan <<https://github.com/delivrance>>

Licensed under the terms of the [GNU Lesser General Public License v3 or later (LGPLv3+)](COPYING.lesser)
Expand Down
1 change: 1 addition & 0 deletions pyromod/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
throw_exceptions=True,
unallowed_click_alert=True,
unallowed_click_alert_text=("[pyromod] You're not expected to click this button."),
disable_startup_logs=False,
)

__all__ = ["config"]
54 changes: 41 additions & 13 deletions pyromod/listen/callback_query_handler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from inspect import iscoroutinefunction
from typing import Callable, Tuple

import pyrogram
Expand All @@ -19,12 +20,13 @@ class CallbackQueryHandler(
@should_patch()
def __init__(self, callback: Callable, filters: Filter = None):
self.original_callback = callback
self.old__init__(self.resolve_future, filters)
self.old__init__(self.resolve_future_or_callback, filters)

@should_patch()
def compose_data_identifier(self, query: CallbackQuery):
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
Expand All @@ -35,12 +37,12 @@ def compose_data_identifier(self, query: CallbackQuery):
)

if query.message.chat:
chat_id = query.message.chat.id
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_id=[from_user_id, from_user_username],
inline_message_id=query.inline_message_id,
)

Expand All @@ -56,9 +58,15 @@ async def check_if_has_matching_listener(

if listener:
filters = listener.filters
listener_does_match = (
await filters(client, query) if callable(filters) else True
)
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

Expand All @@ -68,9 +76,15 @@ async def check(self, client: Client, query: CallbackQuery):
client, query
)

handler_does_match = (
await self.filters(client, query) if callable(self.filters) else True
)
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)

Expand Down Expand Up @@ -103,14 +117,28 @@ async def check(self, client: Client, query: CallbackQuery):
return listener_does_match or handler_does_match

@should_patch()
async def resolve_future(self, client: Client, query: CallbackQuery, *args):
async def resolve_future_or_callback(
self, client: 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)
if listener and listener_does_match:
client.remove_listener(listener)
raise pyrogram.StopPropagation

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)
114 changes: 80 additions & 34 deletions pyromod/listen/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
from typing import Optional, Callable, Dict, List
from inspect import iscoroutinefunction
from typing import Optional, Callable, Dict, List, Union

import pyrogram
from pyrogram.filters import Filter
Expand All @@ -9,6 +10,11 @@
from ..types import ListenerTypes, Identifier, Listener
from ..utils import should_patch, patch_into

if not config.disable_startup_logs:
print(
"Pyromod is working! If you like pyromod, please star it at https://github.com/usernein/pyromod"
)


@patch_into(pyrogram.client.Client)
class Client(pyrogram.client.Client):
Expand All @@ -27,10 +33,10 @@ async def listen(
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,
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,
):
pattern = Identifier(
from_user_id=user_id,
Expand All @@ -41,15 +47,6 @@ async def listen(

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,
Expand All @@ -59,34 +56,42 @@ async def listen(
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(config.timeout_handler):
config.timeout_handler(pattern, listener, timeout)
if iscoroutinefunction(config.timeout_handler.__call__):
await config.timeout_handler(pattern, listener, timeout)
else:
await self.loop.run_in_executor(
None, config.timeout_handler, pattern, listener, timeout
)
elif config.throw_exceptions:
raise ListenerTimeout(timeout)

@should_patch()
async def ask(
self,
chat_id: int,
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: int = None,
message_id: int = None,
inline_message_id: 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,
*args,
**kwargs,
):
sent_message = None
if text.strip() != "":
sent_message = await self.send_message(chat_id, text, *args, **kwargs)
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,
Expand All @@ -103,6 +108,35 @@ async def ask(

return response

@should_patch()
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,
):
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)

@should_patch()
def get_matching_listener(
self, pattern: Identifier, listener_type: ListenerTypes
Expand All @@ -120,7 +154,10 @@ def count_populated_attributes(listener_item: Listener):

@should_patch()
def remove_listener(self, listener: Listener):
self.listeners[listener.listener_type].remove(listener)
try:
self.listeners[listener.listener_type].remove(listener)
except ValueError:
pass

@should_patch()
def get_many_matching_listeners(
Expand All @@ -133,13 +170,13 @@ def get_many_matching_listeners(
return listeners

@should_patch()
def stop_listening(
async 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,
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,
):
pattern = Identifier(
from_user_id=user_id,
Expand All @@ -150,12 +187,21 @@ def stop_listening(
listeners = self.get_many_matching_listeners(pattern, listener_type)

for listener in listeners:
self.remove_listener(listener)
await self.stop_listener(listener)

if listener.future.done():
return

if callable(config.stopped_handler):
config.stopped_handler(pattern, listener)
elif config.throw_exceptions:
listener.future.set_exception(ListenerStopped())
@should_patch()
async def stop_listener(self, listener: Listener):
self.remove_listener(listener)

if listener.future.done():
return

if callable(config.stopped_handler):
if iscoroutinefunction(config.stopped_handler.__call__):
await config.stopped_handler(None, listener)
else:
await self.loop.run_in_executor(
None, config.stopped_handler, None, listener
)
elif config.throw_exceptions:
listener.future.set_exception(ListenerStopped())
4 changes: 2 additions & 2 deletions pyromod/listen/message.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Union
from typing import Optional, Union, List

import pyrogram

Expand All @@ -14,7 +14,7 @@ class Message(pyrogram.types.messages_and_media.message.Message):
@should_patch()
async def wait_for_click(
self,
from_user_id: Optional[int] = None,
from_user_id: Optional[Union[Union[int, str], List[Union[int, str]]]] = None,
timeout: Optional[int] = None,
filters=None,
alert: Union[str, bool] = True,
Expand Down
Loading

0 comments on commit 0753e21

Please sign in to comment.