diff --git a/docs/source/api/handlers.rst b/docs/source/api/handlers.rst index db66d845a..9b91bf0f9 100644 --- a/docs/source/api/handlers.rst +++ b/docs/source/api/handlers.rst @@ -41,6 +41,7 @@ Index - :class:`PollHandler` - :class:`DisconnectHandler` - :class:`RawUpdateHandler` + - :class:`ErrorHandler` ----- @@ -59,3 +60,4 @@ Details .. autoclass:: PollHandler() .. autoclass:: DisconnectHandler() .. autoclass:: RawUpdateHandler() +.. autoclass:: ErrorHandler() diff --git a/hydrogram/dispatcher.py b/hydrogram/dispatcher.py index 231250d3e..7cb04989d 100644 --- a/hydrogram/dispatcher.py +++ b/hydrogram/dispatcher.py @@ -31,6 +31,7 @@ ChosenInlineResultHandler, DeletedMessagesHandler, EditedMessageHandler, + ErrorHandler, InlineQueryHandler, MessageHandler, PollHandler, @@ -79,6 +80,7 @@ def __init__(self, client: "hydrogram.Client"): self.updates_queue = asyncio.Queue() self.groups = OrderedDict() self._init_update_parsers() + self.error_handlers = [] def _init_update_parsers(self): self.update_parsers = { @@ -160,24 +162,38 @@ async def stop(self): await asyncio.gather(*self.handler_worker_tasks) self.handler_worker_tasks.clear() self.groups.clear() + self.error_handlers.clear() + log.info("Stopped %s HandlerTasks", self.client.workers) def add_handler(self, handler, group: int): async def fn(): async with asyncio.Lock(): - if group not in self.groups: - self.groups[group] = [] - self.groups = OrderedDict(sorted(self.groups.items())) - self.groups[group].append(handler) + if isinstance(handler, ErrorHandler): + if handler not in self.error_handlers: + self.error_handlers.append(handler) + else: + if group not in self.groups: + self.groups[group] = [] + self.groups = OrderedDict(sorted(self.groups.items())) + self.groups[group].append(handler) self.loop.create_task(fn()) def remove_handler(self, handler, group: int): async def fn(): async with asyncio.Lock(): - if group not in self.groups: - raise ValueError(f"Group {group} does not exist. Handler was not removed.") - self.groups[group].remove(handler) + if isinstance(handler, ErrorHandler): + if handler not in self.error_handlers: + raise ValueError( + f"Error handler {handler} does not exist. Handler was not removed." + ) + + self.error_handlers.remove(handler) + else: + if group not in self.groups: + raise ValueError(f"Group {group} does not exist. Handler was not removed.") + self.groups[group].remove(handler) self.loop.create_task(fn()) @@ -225,7 +241,23 @@ async def _handle_update(self, handler, handler_type, parsed_update, update, use except hydrogram.ContinuePropagation: pass except Exception as e: - log.exception(e) + handled_error = False + for error_handler in self.error_handlers: + try: + if await error_handler.check(self.client, e): + await error_handler.callback(self.client, e) + handled_error = True + break + except hydrogram.StopPropagation: + raise + except hydrogram.ContinuePropagation: + continue + except Exception as e: + log.exception(e) + continue + + if not handled_error: + log.exception(e) async def _execute_callback(self, handler, *args): if inspect.iscoroutinefunction(handler.callback): diff --git a/hydrogram/handlers/__init__.py b/hydrogram/handlers/__init__.py index 8c3f5f944..ed9cd02ed 100644 --- a/hydrogram/handlers/__init__.py +++ b/hydrogram/handlers/__init__.py @@ -24,6 +24,7 @@ from .deleted_messages_handler import DeletedMessagesHandler from .disconnect_handler import DisconnectHandler from .edited_message_handler import EditedMessageHandler +from .error_handler import ErrorHandler from .inline_query_handler import InlineQueryHandler from .message_handler import MessageHandler from .poll_handler import PollHandler @@ -38,6 +39,7 @@ "DeletedMessagesHandler", "DisconnectHandler", "EditedMessageHandler", + "ErrorHandler", "InlineQueryHandler", "MessageHandler", "PollHandler", diff --git a/hydrogram/handlers/error_handler.py b/hydrogram/handlers/error_handler.py new file mode 100644 index 000000000..026ecbafc --- /dev/null +++ b/hydrogram/handlers/error_handler.py @@ -0,0 +1,72 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram 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. +# +# Hydrogram 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 Hydrogram. If not, see . + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TYPE_CHECKING, Callable + +from .handler import Handler + +if TYPE_CHECKING: + import hydrogram + + +class ErrorHandler(Handler): + """The Error handler class. Used to handle errors. + It is intended to be used with :meth:`~hydrogram.Client.add_handler` + + For a nicer way to register this handler, have a look at the + :meth:`~hydrogram.Client.on_error` decorator. + + Parameters: + callback (``Callable``): + Pass a function that will be called when a new Error arrives. It takes *(client, error)* + as positional arguments (look at the section below for a detailed description). + + errors (``Exception`` | Iterable of ``Exception``, *optional*): + Pass one or more exception classes to allow only a subset of errors to be passed + in your callback function. + + Other parameters: + client (:obj:`~hydrogram.Client`): + The Client itself, useful when you want to call other API methods inside the error handler. + + error (``Exception``): + The error that was raised. + + update (:obj:`~hydrogram.Update`): + The update that caused the error. + """ + + def __init__( + self, callback: Callable, errors: type[Exception] | Iterable[type[Exception]] | None = None + ): + if errors is None: + errors = [Exception] + elif not isinstance(errors, Iterable): + errors = [errors] + + self.errors = errors + super().__init__(callback) + + async def check(self, client: hydrogram.Client, error: Exception): + return any(isinstance(error, e) for e in self.errors) + + def check_remove(self, error: Exception): + return self.errors == error or any(isinstance(error, e) for e in self.errors) diff --git a/hydrogram/methods/decorators/__init__.py b/hydrogram/methods/decorators/__init__.py index 91c050c80..031af64e6 100644 --- a/hydrogram/methods/decorators/__init__.py +++ b/hydrogram/methods/decorators/__init__.py @@ -24,6 +24,7 @@ from .on_deleted_messages import OnDeletedMessages from .on_disconnect import OnDisconnect from .on_edited_message import OnEditedMessage +from .on_error import OnError from .on_inline_query import OnInlineQuery from .on_message import OnMessage from .on_poll import OnPoll @@ -31,7 +32,7 @@ from .on_user_status import OnUserStatus -class Decorators( +class Decorators( # noqa: N818 false-positive OnMessage, OnEditedMessage, OnDeletedMessages, @@ -44,5 +45,6 @@ class Decorators( OnChosenInlineResult, OnChatMemberUpdated, OnChatJoinRequest, + OnError, ): pass diff --git a/hydrogram/methods/decorators/on_error.py b/hydrogram/methods/decorators/on_error.py new file mode 100644 index 000000000..eb3c2dfd4 --- /dev/null +++ b/hydrogram/methods/decorators/on_error.py @@ -0,0 +1,49 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram 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. +# +# Hydrogram 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 Hydrogram. If not, see . + +from typing import Callable + +import hydrogram +from hydrogram.filters import Filter + + +class OnError: + def on_error(self=None, errors=None) -> Callable: + """Decorator for handling new errors. + + This does the same thing as :meth:`~hydrogram.Client.add_handler` using the + :obj:`~hydrogram.handlers.MessageHandler`. + + Parameters: + errors (:obj:`~Exception`, *optional*): + Pass one or more errors to allow only a subset of errors to be passed + in your function. + """ + + def decorator(func: Callable) -> Callable: + if isinstance(self, hydrogram.Client): + self.add_handler(hydrogram.handlers.ErrorHandler(func, errors), 0) + elif isinstance(self, Filter) or self is None: + if not hasattr(func, "handlers"): + func.handlers = [] + + func.handlers.append((hydrogram.handlers.ErrorHandler(func, self), 0)) + + return func + + return decorator diff --git a/hydrogram/methods/utilities/__init__.py b/hydrogram/methods/utilities/__init__.py index 0e9e551eb..74da4a3db 100644 --- a/hydrogram/methods/utilities/__init__.py +++ b/hydrogram/methods/utilities/__init__.py @@ -19,6 +19,7 @@ from .add_handler import AddHandler from .export_session_string import ExportSessionString +from .remove_error_handler import RemoveErrorHandler from .remove_handler import RemoveHandler from .restart import Restart from .run import Run @@ -31,6 +32,7 @@ class Utilities( AddHandler, ExportSessionString, RemoveHandler, + RemoveErrorHandler, Restart, Run, Start, diff --git a/hydrogram/methods/utilities/remove_error_handler.py b/hydrogram/methods/utilities/remove_error_handler.py new file mode 100644 index 000000000..e70ee17d1 --- /dev/null +++ b/hydrogram/methods/utilities/remove_error_handler.py @@ -0,0 +1,41 @@ +# Hydrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2023-present Hydrogram +# +# This file is part of Hydrogram. +# +# Hydrogram 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. +# +# Hydrogram 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 Hydrogram. If not, see . + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterable + + import hydrogram + + +class RemoveErrorHandler: + def remove_error_handler( + self: hydrogram.Client, error: type[Exception] | Iterable[type[Exception]] = Exception + ): + """Remove a previously-registered error handler. (using exception classes) + + Parameters: + error (``Exception`` | Iterable of ``Exception``, *optional*): + The error(s) for handlers to be removed. + """ + for handler in self.dispatcher.error_handlers: + if handler.check_remove(error): + self.dispatcher.error_handlers.remove(handler) diff --git a/news/38.feature.rst b/news/38.feature.rst new file mode 100644 index 000000000..c01696304 --- /dev/null +++ b/news/38.feature.rst @@ -0,0 +1 @@ +Added support for error handlers.