Skip to content

Commit

Permalink
Handle discord.Forbidden 90001 errors by default in create_task() (#…
Browse files Browse the repository at this point in the history
…177)

Co-authored-by: Chris Lovering <[email protected]>
  • Loading branch information
TizzySaurus and ChrisLovering authored Jun 10, 2023
1 parent 5ee9489 commit c57fae8
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 7 deletions.
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Changelog
=========
- :release:`9.7.0 <10th June 2023>`
- :feature:`179` Add paste service utility to upload text to our paste service.
- :feature:`177` Automatically handle discord.Forbidden 90001 errors in all schedules
- :feature:`176` Migrate repo to use ruff for linting


Expand Down
5 changes: 3 additions & 2 deletions pydis_core/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Useful utilities and tools for Discord bot development."""

from pydis_core.utils import (
_monkey_patches, caching, channel, commands, cooldown, function, interactions, logging, members, paste_service,
regex, scheduling
_monkey_patches, caching, channel, commands, cooldown, error_handling, function, interactions, logging, members,
paste_service, regex, scheduling
)
from pydis_core.utils._extensions import unqualify

Expand All @@ -29,6 +29,7 @@ def apply_monkey_patches() -> None:
channel,
commands,
cooldown,
error_handling,
function,
interactions,
logging,
Expand Down
35 changes: 35 additions & 0 deletions pydis_core/utils/error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from discord import Forbidden, Message

from pydis_core.utils import logging

log = logging.get_logger(__name__)


async def handle_forbidden_from_block(error: Forbidden, message: Message | None = None) -> None:
"""
Handles ``discord.Forbidden`` 90001 errors, or re-raises if ``error`` isn't a 90001 error.
Args:
error: The raised ``discord.Forbidden`` to check.
message: The message to reply to and include in logs, if error is 90001 and message is provided.
"""
if error.code != 90001:
# The error ISN'T caused by the bot attempting to add a reaction
# to a message whose author has blocked the bot, so re-raise it
raise error

if not message:
log.info("Failed to add reaction(s) to a message since the message author has blocked the bot")
return

if message:
log.info(
"Failed to add reaction(s) to message %d-%d since the message author (%d) has blocked the bot",
message.channel.id,
message.id,
message.author.id,
)
await message.channel.send(
f":x: {message.author.mention} failed to add reaction(s) to your message as you've blocked me.",
delete_after=30
)
21 changes: 16 additions & 5 deletions pydis_core/utils/scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from datetime import datetime, timezone
from functools import partial

from discord.errors import Forbidden

from pydis_core.utils import logging
from pydis_core.utils.error_handling import handle_forbidden_from_block

_background_tasks: set[asyncio.Task] = set()

Expand Down Expand Up @@ -77,7 +80,7 @@ def schedule(self, task_id: abc.Hashable, coroutine: abc.Coroutine) -> None:
coroutine.close()
return

task = asyncio.create_task(coroutine, name=f"{self.name}_{task_id}")
task = asyncio.create_task(_coro_wrapper(coroutine), name=f"{self.name}_{task_id}")
task.add_done_callback(partial(self._task_done_callback, task_id))

self._scheduled_tasks[task_id] = task
Expand Down Expand Up @@ -238,21 +241,29 @@ def create_task(
asyncio.Task: The wrapped task.
"""
if event_loop is not None:
task = event_loop.create_task(coro, **kwargs)
task = event_loop.create_task(_coro_wrapper(coro), **kwargs)
else:
task = asyncio.create_task(coro, **kwargs)
task = asyncio.create_task(_coro_wrapper(coro), **kwargs)

_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
task.add_done_callback(partial(_log_task_exception, suppressed_exceptions=suppressed_exceptions))
return task


async def _coro_wrapper(coro: abc.Coroutine[typing.Any, typing.Any, TASK_RETURN]) -> None:
"""Wraps `coro` in a try/except block that will handle 90001 Forbidden errors."""
try:
await coro
except Forbidden as e:
await handle_forbidden_from_block(e)


def _log_task_exception(task: asyncio.Task, *, suppressed_exceptions: tuple[type[Exception], ...]) -> None:
"""Retrieve and log the exception raised in ``task`` if one exists."""
"""Retrieve and log the exception raised in ``task``, if one exists and it's not suppressed."""
with contextlib.suppress(asyncio.CancelledError):
exception = task.exception()
# Log the exception if one exists.
# Log the exception if one exists and it's not suppressed/handled.
if exception and not isinstance(exception, suppressed_exceptions):
log = logging.get_logger(__name__)
log.error(f"Error in task {task.get_name()} {id(task)}!", exc_info=exception)

0 comments on commit c57fae8

Please sign in to comment.