Skip to content

Commit c57fae8

Browse files
Handle discord.Forbidden 90001 errors by default in create_task() (#177)
Co-authored-by: Chris Lovering <chris.lovering.95@gmail.com>
1 parent 5ee9489 commit c57fae8

File tree

4 files changed

+55
-7
lines changed

4 files changed

+55
-7
lines changed

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
=========
66
- :release:`9.7.0 <10th June 2023>`
77
- :feature:`179` Add paste service utility to upload text to our paste service.
8+
- :feature:`177` Automatically handle discord.Forbidden 90001 errors in all schedules
89
- :feature:`176` Migrate repo to use ruff for linting
910

1011

pydis_core/utils/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Useful utilities and tools for Discord bot development."""
22

33
from pydis_core.utils import (
4-
_monkey_patches, caching, channel, commands, cooldown, function, interactions, logging, members, paste_service,
5-
regex, scheduling
4+
_monkey_patches, caching, channel, commands, cooldown, error_handling, function, interactions, logging, members,
5+
paste_service, regex, scheduling
66
)
77
from pydis_core.utils._extensions import unqualify
88

@@ -29,6 +29,7 @@ def apply_monkey_patches() -> None:
2929
channel,
3030
commands,
3131
cooldown,
32+
error_handling,
3233
function,
3334
interactions,
3435
logging,

pydis_core/utils/error_handling.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from discord import Forbidden, Message
2+
3+
from pydis_core.utils import logging
4+
5+
log = logging.get_logger(__name__)
6+
7+
8+
async def handle_forbidden_from_block(error: Forbidden, message: Message | None = None) -> None:
9+
"""
10+
Handles ``discord.Forbidden`` 90001 errors, or re-raises if ``error`` isn't a 90001 error.
11+
12+
Args:
13+
error: The raised ``discord.Forbidden`` to check.
14+
message: The message to reply to and include in logs, if error is 90001 and message is provided.
15+
"""
16+
if error.code != 90001:
17+
# The error ISN'T caused by the bot attempting to add a reaction
18+
# to a message whose author has blocked the bot, so re-raise it
19+
raise error
20+
21+
if not message:
22+
log.info("Failed to add reaction(s) to a message since the message author has blocked the bot")
23+
return
24+
25+
if message:
26+
log.info(
27+
"Failed to add reaction(s) to message %d-%d since the message author (%d) has blocked the bot",
28+
message.channel.id,
29+
message.id,
30+
message.author.id,
31+
)
32+
await message.channel.send(
33+
f":x: {message.author.mention} failed to add reaction(s) to your message as you've blocked me.",
34+
delete_after=30
35+
)

pydis_core/utils/scheduling.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
from datetime import datetime, timezone
99
from functools import partial
1010

11+
from discord.errors import Forbidden
12+
1113
from pydis_core.utils import logging
14+
from pydis_core.utils.error_handling import handle_forbidden_from_block
1215

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

@@ -77,7 +80,7 @@ def schedule(self, task_id: abc.Hashable, coroutine: abc.Coroutine) -> None:
7780
coroutine.close()
7881
return
7982

80-
task = asyncio.create_task(coroutine, name=f"{self.name}_{task_id}")
83+
task = asyncio.create_task(_coro_wrapper(coroutine), name=f"{self.name}_{task_id}")
8184
task.add_done_callback(partial(self._task_done_callback, task_id))
8285

8386
self._scheduled_tasks[task_id] = task
@@ -238,21 +241,29 @@ def create_task(
238241
asyncio.Task: The wrapped task.
239242
"""
240243
if event_loop is not None:
241-
task = event_loop.create_task(coro, **kwargs)
244+
task = event_loop.create_task(_coro_wrapper(coro), **kwargs)
242245
else:
243-
task = asyncio.create_task(coro, **kwargs)
246+
task = asyncio.create_task(_coro_wrapper(coro), **kwargs)
244247

245248
_background_tasks.add(task)
246249
task.add_done_callback(_background_tasks.discard)
247250
task.add_done_callback(partial(_log_task_exception, suppressed_exceptions=suppressed_exceptions))
248251
return task
249252

250253

254+
async def _coro_wrapper(coro: abc.Coroutine[typing.Any, typing.Any, TASK_RETURN]) -> None:
255+
"""Wraps `coro` in a try/except block that will handle 90001 Forbidden errors."""
256+
try:
257+
await coro
258+
except Forbidden as e:
259+
await handle_forbidden_from_block(e)
260+
261+
251262
def _log_task_exception(task: asyncio.Task, *, suppressed_exceptions: tuple[type[Exception], ...]) -> None:
252-
"""Retrieve and log the exception raised in ``task`` if one exists."""
263+
"""Retrieve and log the exception raised in ``task``, if one exists and it's not suppressed."""
253264
with contextlib.suppress(asyncio.CancelledError):
254265
exception = task.exception()
255-
# Log the exception if one exists.
266+
# Log the exception if one exists and it's not suppressed/handled.
256267
if exception and not isinstance(exception, suppressed_exceptions):
257268
log = logging.get_logger(__name__)
258269
log.error(f"Error in task {task.get_name()} {id(task)}!", exc_info=exception)

0 commit comments

Comments
 (0)