From 0802f7e6b21a9e0a2f99d37647e7a6d00233738a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 14:12:32 +0100 Subject: [PATCH] Rework methods to manage event handlers --- readthedocs/misc/v2-migration-guide.rst | 1 + telethon/_client/telegrambaseclient.py | 1 + telethon/_client/telegramclient.py | 100 +++++++++++++++++----- telethon/_client/updates.py | 108 +++++++++++++++++------- telethon/_events/base.py | 21 +++++ telethon/_events/newmessage.py | 2 +- telethon/_events/userupdate.py | 2 +- telethon/events.py | 3 +- 8 files changed, 184 insertions(+), 54 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index ce077d13..c473ac76 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -788,3 +788,4 @@ it's now this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming. events.register, unregister, is_handler and list are gone. now you can typehint instead. def handler(event: events.NewMessage) +client.on, add, and remove have changed parameters/retval \ No newline at end of file diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 92dc1b65..277fd6cf 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -148,6 +148,7 @@ def init( self._no_updates = not receive_updates self._updates_queue = asyncio.Queue(maxsize=max_queued_updates) self._updates_handle = None + self._update_handlers = [] # sorted list self._message_box = MessageBox() self._entity_cache = EntityCache() # required for proper update handling (to know when to getDifference) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 136a6d31..851f39d1 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2791,15 +2791,24 @@ class TelegramClient: """ @forward_call(updates.on) - def on(self: 'TelegramClient', event: EventBuilder): + def on(self: 'TelegramClient', *events, priority=0, **filters): """ Decorator used to `add_event_handler` more conveniently. + This decorator should be above other decorators which modify the function. Arguments - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. + event (`type` | `tuple`): + The event type(s) you wish to receive, for instance ``events.NewMessage``. + This may also be raw update types. + The same handler is registered multiple times, one per type. + + priority (`int`): + The event priority. Events with higher priority are dispatched first. + The order between events with the same priority is arbitrary. + + filters (any): + Filters passed to `make_filter`. Example .. code-block:: python @@ -2808,7 +2817,12 @@ class TelegramClient: client = TelegramClient(...) # Here we use client.on - @client.on(events.NewMessage) + @client.on(events.NewMessage, priority=100) + async def handler(event): + ... + + # Both new incoming messages and incoming edits + @client.on(events.NewMessage, events.MessageEdited, incoming=True) async def handler(event): ... """ @@ -2816,8 +2830,11 @@ class TelegramClient: @forward_call(updates.add_event_handler) def add_event_handler( self: 'TelegramClient', - callback: updates.Callback, - event: EventBuilder = None): + callback: updates.Callback = None, + event: EventBuilder = None, + priority=0, + **filters + ): """ Registers a new event handler callback. @@ -2827,17 +2844,29 @@ class TelegramClient: callback (`callable`): The callable function accepting one parameter to be used. - Note that if you have used `telethon.events.register` in - the callback, ``event`` will be ignored, and instead the - events you previously registered will be used. + If `None`, the method can be used as a decorator. Note that the handler function + will be replaced with the `EventHandler` instance in this case, but it will still + be callable. event (`_EventBuilder` | `type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. - If left unspecified, `telethon.events.raw.Raw` (the - :tl:`Update` objects with no further processing) will - be passed instead. + If left unspecified, it will be inferred from the type hint + used in the handler, or be `telethon.events.raw.Raw` (the + :tl:`Update` objects with no further processing) if there is + none. Note that the type hint must be the desired type. It + cannot be a string, an union, or anything more complex. + + priority (`int`): + The event priority. Events with higher priority are dispatched first. + The order between events with the same priority is arbitrary. + + filters (any): + Filters passed to `make_filter`. + + Returns + An `EventHandler` instance, which can be used Example .. code-block:: python @@ -2845,22 +2874,47 @@ class TelegramClient: from telethon import TelegramClient, events client = TelegramClient(...) + # Adding a handler, the "boring" way async def handler(event): ... - client.add_event_handler(handler, events.NewMessage) + client.add_event_handler(handler, events.NewMessage, priority=50) + + # Automatic type + async def handler(event: events.MessageEdited) + ... + + client.add_event_handler(handler, outgoing=False) + + # Streamlined adding + @client.add_event_handler + async def handler(event: events.MessageDeleted): + ... """ @forward_call(updates.remove_event_handler) def remove_event_handler( self: 'TelegramClient', - callback: updates.Callback, - event: EventBuilder = None) -> int: + callback: updates.Callback = None, + event: EventBuilder = None, + priority=None, + ) -> int: """ Inverse operation of `add_event_handler()`. If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. + Returns a list in arbitrary order with all removed `EventHandler` instances. + + Arguments + callback (`callable`): + The callable function accepting one parameter to be used. + If passed an `EventHandler` instance, both `event` and `priority` are ignored. + + event (`_EventBuilder` | `type`, optional): + The event builder class or instance to be used when searching. + + priority (`int`): + The event priority to be used when searching. Example .. code-block:: python @@ -2876,6 +2930,12 @@ class TelegramClient: # "handler" will stop receiving anything client.remove_event_handler(handler) + + # Remove all handlers with priority 50 + client.remove_event_handler(priority=50) + + # Remove all deleted-message handlers + client.remove_event_handler(event=events.MessageDeleted) """ @forward_call(updates.list_event_handlers) @@ -2885,7 +2945,7 @@ class TelegramClient: Lists all registered event handlers. Returns - A list of pairs consisting of ``(callback, event)``. + A list of all registered `EventHandler` in arbitrary order. Example .. code-block:: python @@ -2895,8 +2955,8 @@ class TelegramClient: '''Greets someone''' await event.reply('Hi') - for callback, event in client.list_event_handlers(): - print(id(callback), type(event)) + for handler in client.list_event_handlers(): + print(id(handler.callback), handler.event) """ @forward_call(updates.catch_up) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index a9875bba..70c0c094 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -7,12 +7,15 @@ import time import traceback import typing import logging +import inspect +import bisect +import warnings from collections import deque from ..errors._rpcbase import RpcError -from .._events.base import EventBuilder from .._events.raw import Raw -from .._events.base import StopPropagation, _get_handlers +from .._events.base import StopPropagation, EventBuilder, EventHandler +from .._events.filters import make_filter from .._misc import utils from .. import _tl @@ -33,51 +36,96 @@ async def run_until_disconnected(self: 'TelegramClient'): await self(_tl.fn.updates.GetState()) await self._sender.wait_disconnected() -def on(self: 'TelegramClient', event: EventBuilder): +def on(self: 'TelegramClient', *events, priority=0, **filters): def decorator(f): - self.add_event_handler(f, event) + for event in events: + self.add_event_handler(f, event, priority=priority, **filters) return f return decorator def add_event_handler( self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None): - builders = _get_handlers(callback) - if builders is not None: - for event in builders: - self._event_builders.append((event, callback)) - return + callback=None, + event=None, + priority=0, + **filters +): + if callback is None: + return functools.partial(add_event_handler, self, event=event, priority=priority, **filters) - if isinstance(event, type): - event = event() - elif not event: - event = Raw() + if event is None: + for param in inspect.signature(callback).parameters.values(): + if not issubclass(param.annotation, EventBuilder): + raise TypeError(f'unrecognized event handler type: {param.annotation!r}') + event = param.annotation + break # only check the first parameter - self._event_builders.append((event, callback)) + if event is None: + event = Raw + + handler = EventHandler(event, callback, priority, make_filter(**filters)) + bisect.insort(self._update_handlers, handler) + return handler def remove_event_handler( self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None) -> int: - found = 0 - if event and not isinstance(event, type): - event = type(event) + callback, + event, + priority, +): + if callback is None and event is None and priority is None: + raise ValueError('must specify at least one of callback, event or priority') - i = len(self._event_builders) - while i: - i -= 1 - ev, cb = self._event_builders[i] - if cb == callback and (not event or isinstance(ev, event)): - del self._event_builders[i] - found += 1 + if not self._update_handlers: + return [] # won't be removing anything (some code paths rely on non-empty lists) - return found + if isinstance(callback, EventHandler): + if event is not None or priority is not None: + warnings.warn('event and priority are ignored when removing EventHandler instances') + + index = bisect.bisect_left(self._update_handlers, callback) + try: + if self._update_handlers[index] == callback: + return [self._update_handlers.pop(index)] + except IndexError: + pass + return [] + + if priority is not None: + # can binary-search (using a dummy EventHandler) + index = bisect.bisect_right(self._update_handlers, EventHandler(None, None, priority, None)) + try: + while self._update_handlers[index].priority == priority: + index += 1 + except IndexError: + pass + + removed = [] + while index > 0 and self._update_handlers[index - 1].priority == priority: + index -= 1 + if callback is not None and self._update_handlers[index].callback != callback: + continue + if event is not None and self._update_handlers[index].event != event: + continue + removed.append(self._update_handlers.pop(index)) + + return removed + + # slow-path, remove all matching + removed = [] + for index, handler in reversed(enumerate(self._update_handlers)): + if callback is not None and handler.callback != callback: + continue + if event is not None and handler.event != event: + continue + removed.append(self._update_handlers.pop(index)) + + return removed def list_event_handlers(self: 'TelegramClient')\ -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': - return [(callback, event) for event, callback in self._event_builders] + return self._update_handlers[:] async def catch_up(self: 'TelegramClient'): # The update loop is probably blocked on either timeout or an update to arrive. diff --git a/telethon/_events/base.py b/telethon/_events/base.py index 303c5976..1964b22f 100644 --- a/telethon/_events/base.py +++ b/telethon/_events/base.py @@ -1,4 +1,5 @@ import abc +import functools class StopPropagation(Exception): @@ -41,3 +42,23 @@ class EventBuilder(abc.ABC): `self_id` should be the current user's ID, since it is required for some events which lack this information but still need it. """ + + +@functools.total_ordering +class EventHandler: + __slots__ = ('_event', '_callback', '_priority', '_filter') + + def __init__(self, event, callback, priority, filter): + self._event = event + self._callback = callback + self._priority = priority + self._filter = filter + + def __eq__(self, other): + return self is other + + def __lt__(self, other): + return self._priority < other._priority + + def __call__(self, *args, **kwargs): + return self._callback(*args, **kwargs) diff --git a/telethon/_events/newmessage.py b/telethon/_events/newmessage.py index e42aba7b..ee62c655 100644 --- a/telethon/_events/newmessage.py +++ b/telethon/_events/newmessage.py @@ -6,7 +6,7 @@ from .. import _tl from ..types import _custom -class NewMessageEvent(EventBuilder, Message): +class NewMessage(EventBuilder, _custom.Message): """ Represents the event of a new message. This event can be treated to all effects as a `Message `, diff --git a/telethon/_events/userupdate.py b/telethon/_events/userupdate.py index e5c938e5..b9af86e7 100644 --- a/telethon/_events/userupdate.py +++ b/telethon/_events/userupdate.py @@ -32,7 +32,7 @@ def _requires_status(function): return wrapped -class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): +class UserUpdate(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever a user goes online, starts typing, etc. diff --git a/telethon/events.py b/telethon/events.py index 5dca03ae..125e6b81 100644 --- a/telethon/events.py +++ b/telethon/events.py @@ -1,6 +1,5 @@ -from ._events.base import StopPropagation, register, unregister, is_handler, list +from ._events.base import StopPropagation from ._events.raw import Raw - from ._events.album import Album from ._events.chataction import ChatAction from ._events.messagedeleted import MessageDeleted