pyrofork: Add Error handler

Signed-off-by: Yasir Aris M <git@yasirdev.my.id>
This commit is contained in:
Yasir Aris M 2024-12-20 19:59:22 +07:00 committed by Yasir
parent bd46ff3977
commit 481817649b
8 changed files with 264 additions and 64 deletions

View file

@ -82,3 +82,4 @@ Details
.. autodecorator:: pyrogram.Client.on_poll() .. autodecorator:: pyrogram.Client.on_poll()
.. autodecorator:: pyrogram.Client.on_disconnect() .. autodecorator:: pyrogram.Client.on_disconnect()
.. autodecorator:: pyrogram.Client.on_raw_update() .. autodecorator:: pyrogram.Client.on_raw_update()
.. autodecorator:: pyrogram.Client.on_error()

View file

@ -23,9 +23,8 @@ import logging
from collections import OrderedDict from collections import OrderedDict
import pyrogram import pyrogram
from pyrogram import errors from pyrogram import errors, raw, types, utils
from pyrogram import utils from pyrogram.handlers.handler import Handler
from pyrogram import raw
from pyrogram.handlers import ( from pyrogram.handlers import (
BotBusinessConnectHandler, BotBusinessConnectHandler,
BotBusinessMessageHandler, BotBusinessMessageHandler,
@ -33,6 +32,7 @@ from pyrogram.handlers import (
MessageHandler, MessageHandler,
EditedMessageHandler, EditedMessageHandler,
EditedBotBusinessMessageHandler, EditedBotBusinessMessageHandler,
ErrorHandler,
DeletedMessagesHandler, DeletedMessagesHandler,
DeletedBotBusinessMessagesHandler, DeletedBotBusinessMessagesHandler,
MessageReactionUpdatedHandler, MessageReactionUpdatedHandler,
@ -97,6 +97,7 @@ class Dispatcher:
self.handler_worker_tasks = [] self.handler_worker_tasks = []
self.locks_list = [] self.locks_list = []
self.error_handlers = []
self.updates_queue = asyncio.Queue() self.updates_queue = asyncio.Queue()
self.groups = OrderedDict() self.groups = OrderedDict()
@ -286,6 +287,7 @@ class Dispatcher:
self.handler_worker_tasks.clear() self.handler_worker_tasks.clear()
self.groups.clear() self.groups.clear()
self.error_handlers.clear()
log.info("Stopped %s HandlerTasks", self.client.workers) log.info("Stopped %s HandlerTasks", self.client.workers)
@ -295,11 +297,14 @@ class Dispatcher:
await lock.acquire() await lock.acquire()
try: try:
if group not in self.groups: if isinstance(handler, ErrorHandler):
self.groups[group] = [] if handler not in self.error_handlers:
self.groups = OrderedDict(sorted(self.groups.items())) self.error_handlers.append(handler)
else:
self.groups[group].append(handler) if group not in self.groups:
self.groups[group] = []
self.groups = OrderedDict(sorted(self.groups.items()))
self.groups[group].append(handler)
finally: finally:
for lock in self.locks_list: for lock in self.locks_list:
lock.release() lock.release()
@ -312,76 +317,94 @@ class Dispatcher:
await lock.acquire() await lock.acquire()
try: try:
if group not in self.groups: if isinstance(handler, ErrorHandler):
raise ValueError(f"Group {group} does not exist. Handler was not removed.") if handler not in self.error_handlers:
raise ValueError(
self.groups[group].remove(handler) 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)
finally: finally:
for lock in self.locks_list: for lock in self.locks_list:
lock.release() lock.release()
self.loop.create_task(fn()) self.loop.create_task(fn())
async def handler_worker(self, lock): async def handler_worker(self, lock: asyncio.Lock):
while True: while True:
packet = await self.updates_queue.get() packet = await self.updates_queue.get()
if packet is None: if packet is None:
break break
await self._process_packet(packet, lock)
try: async def _process_packet(
update, users, chats = packet self,
parser = self.update_parsers.get(type(update), None) packet: tuple[raw.core.TLObject, dict[int, types.Update], dict[int, types.Update]],
lock: asyncio.Lock,
):
try:
update, users, chats = packet
parser = self.update_parsers.get(type(update))
parsed_update, handler_type = ( if parser is not None:
await parser(update, users, chats) parsed_result = parser(update, users, chats)
if parser is not None if inspect.isawaitable(parsed_result):
else (None, type(None)) parsed_update, handler_type = await parsed_result
) else:
parsed_update, handler_type = parsed_result
async with lock: else:
for group in self.groups.values(): parsed_update, handler_type = (None, type(None))
for handler in group:
args = None
if isinstance(handler, handler_type):
try:
if await handler.check(self.client, parsed_update):
args = (parsed_update,)
except Exception as e:
log.exception(e)
continue
async with lock:
for group in self.groups.values():
for handler in group:
try:
if parsed_update is not None:
if isinstance(handler, handler_type) and await handler.check(
self.client, parsed_update
):
await self._execute_callback(handler, parsed_update)
break
elif isinstance(handler, RawUpdateHandler): elif isinstance(handler, RawUpdateHandler):
try: await self._execute_callback(handler, update, users, chats)
if await handler.check(self.client, update): break
args = (update, users, chats) except (pyrogram.StopPropagation, pyrogram.ContinuePropagation) as e:
except Exception as e: if isinstance(e, pyrogram.StopPropagation):
log.exception(e)
continue
if args is None:
continue
try:
if inspect.iscoroutinefunction(handler.callback):
await handler.callback(self.client, *args)
else:
await self.loop.run_in_executor(
self.client.executor,
handler.callback,
self.client,
*args
)
except pyrogram.StopPropagation:
raise raise
except pyrogram.ContinuePropagation: except Exception as exception:
continue if parsed_update is not None:
except Exception as e: await self._handle_exception(parsed_update, exception)
log.exception(e) except pyrogram.StopPropagation:
pass
except Exception as e:
log.exception(e)
finally:
self.updates_queue.task_done()
break async def _handle_exception(self, parsed_update: types.Update, exception: Exception):
handled_error = False
for error_handler in self.error_handlers:
try:
if await error_handler.check(self.client, parsed_update, exception):
handled_error = True
break
except pyrogram.StopPropagation: except pyrogram.StopPropagation:
pass raise
except Exception as e: except pyrogram.ContinuePropagation:
log.exception(e) continue
except Exception as inner_exception:
log.exception("Error in error handler: %s", inner_exception)
if not handled_error:
log.exception("Unhandled exception: %s", exception)
async def _execute_callback(self, handler: Handler, *args):
if inspect.iscoroutinefunction(handler.callback):
await handler.callback(self.client, *args)
else:
await self.client.loop.run_in_executor(
self.client.executor, handler.callback, self.client, *args
)

View file

@ -29,6 +29,7 @@ from .deleted_bot_business_messages_handler import DeletedBotBusinessMessagesHan
from .disconnect_handler import DisconnectHandler from .disconnect_handler import DisconnectHandler
from .edited_message_handler import EditedMessageHandler from .edited_message_handler import EditedMessageHandler
from .edited_bot_business_message_handler import EditedBotBusinessMessageHandler from .edited_bot_business_message_handler import EditedBotBusinessMessageHandler
from .error_handler import ErrorHandler
from .inline_query_handler import InlineQueryHandler from .inline_query_handler import InlineQueryHandler
from .message_handler import MessageHandler from .message_handler import MessageHandler
from .poll_handler import PollHandler from .poll_handler import PollHandler

View file

@ -0,0 +1,79 @@
# Pyrofork - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
#
# This file is part of Pyrofork.
#
# Pyrofork 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.
#
# Pyrofork 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 Pyrofork. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from collections.abc import Iterable
from typing import Callable
from .handler import Handler
import pyrogram
from pyrogram.types import Update
class ErrorHandler(Handler):
"""The Error handler class. Used to handle errors.
It is intended to be used with :meth:`~pyrogram.Client.add_handler`
For a nicer way to register this handler, have a look at the
:meth:`~pyrogram.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).
exceptions (``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:`~pyrogram.Client`):
The Client itself, useful when you want to call other API methods inside the error handler.
update (:obj:`~pyrogram.Update`):
The update that caused the error.
error (``Exception``):
The error that was raised.
"""
def __init__(
self,
callback: Callable,
exceptions: type[Exception] | Iterable[type[Exception]] | None = None,
):
self.exceptions = (
tuple(exceptions)
if isinstance(exceptions, Iterable)
else (exceptions,)
if exceptions
else (Exception,)
)
super().__init__(callback)
async def check(self, client: pyrogram.Client, update: Update, exception: Exception) -> bool:
if isinstance(exception, self.exceptions):
await self.callback(client, update, exception)
return True
return False
def check_remove(self, error: type[Exception] | Iterable[type[Exception]]) -> bool:
return isinstance(error, self.exceptions)

View file

@ -28,6 +28,7 @@ from .on_deleted_bot_business_messages import OnDeletedBotBusinessMessages
from .on_disconnect import OnDisconnect from .on_disconnect import OnDisconnect
from .on_edited_message import OnEditedMessage from .on_edited_message import OnEditedMessage
from .on_edited_bot_business_message import OnEditedBotBusinessMessage from .on_edited_bot_business_message import OnEditedBotBusinessMessage
from .on_error import OnError
from .on_inline_query import OnInlineQuery from .on_inline_query import OnInlineQuery
from .on_message import OnMessage from .on_message import OnMessage
from .on_poll import OnPoll from .on_poll import OnPoll
@ -47,6 +48,7 @@ class Decorators(
OnBotBusinessMessage, OnBotBusinessMessage,
OnEditedMessage, OnEditedMessage,
OnEditedBotBusinessMessage, OnEditedBotBusinessMessage,
OnError,
OnDeletedMessages, OnDeletedMessages,
OnDeletedBotBusinessMessages, OnDeletedBotBusinessMessages,
OnCallbackQuery, OnCallbackQuery,

View file

@ -0,0 +1,50 @@
# Pyrofork - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
#
# This file is part of Pyrofork.
#
# Pyrofork 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.
#
# Pyrofork 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 Pyrofork. If not, see <http://www.gnu.org/licenses/>.
from typing import Callable
import pyrogram
from pyrogram.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:`~pyrogram.Client.add_handler` using the
:obj:`~pyrogram.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, pyrogram.Client):
self.add_handler(pyrogram.handlers.ErrorHandler(func, errors), 0)
elif isinstance(self, Filter) or self is None:
if not hasattr(func, "handlers"):
func.handlers = []
func.handlers.append((pyrogram.handlers.ErrorHandler(func, self), 0))
return func
return decorator

View file

@ -20,6 +20,7 @@
from .add_handler import AddHandler from .add_handler import AddHandler
from .export_session_string import ExportSessionString from .export_session_string import ExportSessionString
from .remove_handler import RemoveHandler from .remove_handler import RemoveHandler
from .remove_error_handler import RemoveErrorHandler
from .restart import Restart from .restart import Restart
from .run import Run from .run import Run
from .run_sync import RunSync from .run_sync import RunSync
@ -32,6 +33,7 @@ class Utilities(
AddHandler, AddHandler,
ExportSessionString, ExportSessionString,
RemoveHandler, RemoveHandler,
RemoveErrorHandler,
Restart, Restart,
Run, Run,
RunSync, RunSync,

View file

@ -0,0 +1,42 @@
# Pyrofork - Telegram MTProto API Client Library for Python
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
# Copyright (C) 2022-present Mayuri-Chan <https://github.com/Mayuri-Chan>
#
# This file is part of Pyrofork.
#
# Pyrofork 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.
#
# Pyrofork 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 Pyrofork. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from collections.abc import Iterable
import pyrogram
class RemoveErrorHandler:
def remove_error_handler(
self: pyrogram.Client,
exception: type[Exception] | Iterable[type[Exception]] = Exception,
):
"""Remove a previously registered error handler using exception classes.
Parameters:
exception (``Exception`` | Iterable of ``Exception``, *optional*):
The error(s) for handlers to be removed. Defaults to Exception.
"""
to_remove = [
handler
for handler in self.dispatcher.error_handlers
if handler.check_remove(exception)
]
for handler in to_remove:
self.dispatcher.error_handlers.remove(handler)