aboutsummaryrefslogtreecommitdiffstats
path: root/botcore
diff options
context:
space:
mode:
Diffstat (limited to 'botcore')
-rw-r--r--botcore/__init__.py2
-rw-r--r--botcore/exts/__init__.py2
-rw-r--r--botcore/utils/__init__.py5
-rw-r--r--botcore/utils/channel.py20
-rw-r--r--botcore/utils/extensions.py4
-rw-r--r--botcore/utils/members.py24
-rw-r--r--botcore/utils/monkey_patches.py83
-rw-r--r--botcore/utils/regex.py2
8 files changed, 113 insertions, 29 deletions
diff --git a/botcore/__init__.py b/botcore/__init__.py
index d910f393..a5835306 100644
--- a/botcore/__init__.py
+++ b/botcore/__init__.py
@@ -1,4 +1,4 @@
-"""Useful utilities and tools for discord bot development."""
+"""Useful utilities and tools for Discord bot development."""
from botcore import exts, utils
diff --git a/botcore/exts/__init__.py b/botcore/exts/__init__.py
index 029178a9..afd56166 100644
--- a/botcore/exts/__init__.py
+++ b/botcore/exts/__init__.py
@@ -1,4 +1,4 @@
-"""Reusable discord cogs."""
+"""Reusable Discord cogs."""
__all__ = []
__all__ = list(map(lambda module: module.__name__, __all__))
diff --git a/botcore/utils/__init__.py b/botcore/utils/__init__.py
index 71354334..890f30da 100644
--- a/botcore/utils/__init__.py
+++ b/botcore/utils/__init__.py
@@ -1,6 +1,6 @@
-"""Useful utilities and tools for discord bot development."""
+"""Useful utilities and tools for Discord bot development."""
-from botcore.utils import (caching, channel, extensions, logging, members, regex, scheduling)
+from botcore.utils import (caching, channel, extensions, logging, members, monkey_patches, regex, scheduling)
__all__ = [
caching,
@@ -8,6 +8,7 @@ __all__ = [
extensions,
logging,
members,
+ monkey_patches,
regex,
scheduling,
]
diff --git a/botcore/utils/channel.py b/botcore/utils/channel.py
index 17e70a2a..d586098f 100644
--- a/botcore/utils/channel.py
+++ b/botcore/utils/channel.py
@@ -1,14 +1,14 @@
-"""Useful helper functions for interacting with various discord.py channel objects."""
+"""Useful helper functions for interacting with various disnake channel objects."""
-import discord
-from discord.ext.commands import Bot
+import disnake
+from disnake.ext.commands import Bot
from botcore.utils import logging
log = logging.get_logger(__name__)
-def is_in_category(channel: discord.TextChannel, category_id: int) -> bool:
+def is_in_category(channel: disnake.TextChannel, category_id: int) -> bool:
"""
Return whether the given ``channel`` in the the category with the id ``category_id``.
@@ -22,22 +22,22 @@ def is_in_category(channel: discord.TextChannel, category_id: int) -> bool:
return getattr(channel, "category_id", None) == category_id
-async def get_or_fetch_channel(bot: Bot, channel_id: int) -> discord.abc.GuildChannel:
+async def get_or_fetch_channel(bot: Bot, channel_id: int) -> disnake.abc.GuildChannel:
"""
Attempt to get or fetch the given ``channel_id`` from the bots cache, and return it.
Args:
- bot: The :obj:`discord.ext.commands.Bot` instance to use for getting/fetching.
+ bot: The :obj:`disnake.ext.commands.Bot` instance to use for getting/fetching.
channel_id: The channel to get/fetch.
Raises:
- :exc:`discord.InvalidData`
+ :exc:`disnake.InvalidData`
An unknown channel type was received from Discord.
- :exc:`discord.HTTPException`
+ :exc:`disnake.HTTPException`
Retrieving the channel failed.
- :exc:`discord.NotFound`
+ :exc:`disnake.NotFound`
Invalid Channel ID.
- :exc:`discord.Forbidden`
+ :exc:`disnake.Forbidden`
You do not have permission to fetch this channel.
Returns:
diff --git a/botcore/utils/extensions.py b/botcore/utils/extensions.py
index 3f8d6e6d..053a58dc 100644
--- a/botcore/utils/extensions.py
+++ b/botcore/utils/extensions.py
@@ -1,4 +1,4 @@
-"""Utilities for loading discord extensions."""
+"""Utilities for loading Discord extensions."""
import importlib
import inspect
@@ -28,7 +28,7 @@ def walk_extensions(module: types.ModuleType) -> frozenset[str]:
module (types.ModuleType): The module to look for extensions in.
Returns:
- A set of strings that can be passed directly to :obj:`discord.ext.commands.Bot.load_extension`.
+ A set of strings that can be passed directly to :obj:`disnake.ext.commands.Bot.load_extension`.
"""
def on_error(name: str) -> NoReturn:
diff --git a/botcore/utils/members.py b/botcore/utils/members.py
index e89b4618..f4a30eca 100644
--- a/botcore/utils/members.py
+++ b/botcore/utils/members.py
@@ -1,27 +1,27 @@
-"""Useful helper functions for interactin with :obj:`discord.Member` objects."""
+"""Useful helper functions for interactin with :obj:`disnake.Member` objects."""
import typing
-import discord
+import disnake
from botcore.utils import logging
log = logging.get_logger(__name__)
-async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> typing.Optional[discord.Member]:
+async def get_or_fetch_member(guild: disnake.Guild, member_id: int) -> typing.Optional[disnake.Member]:
"""
Attempt to get a member from cache; on failure fetch from the API.
Returns:
- The :obj:`discord.Member` or :obj:`None` to indicate the member could not be found.
+ The :obj:`disnake.Member` or :obj:`None` to indicate the member could not be found.
"""
if member := guild.get_member(member_id):
log.trace(f"{member} retrieved from cache.")
else:
try:
member = await guild.fetch_member(member_id)
- except discord.errors.NotFound:
+ except disnake.errors.NotFound:
log.trace(f"Failed to fetch {member_id} from API.")
return None
log.trace(f"{member} fetched from API.")
@@ -29,28 +29,28 @@ async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> typing.Op
async def handle_role_change(
- member: discord.Member,
+ member: disnake.Member,
coro: typing.Callable[..., typing.Coroutine],
- role: discord.Role
+ role: disnake.Role
) -> None:
"""
Await the given ``coro`` with ``member`` as the sole argument.
Handle errors that we expect to be raised from
- :obj:`discord.Member.add_roles` and :obj:`discord.Member.remove_roles`.
+ :obj:`disnake.Member.add_roles` and :obj:`disnake.Member.remove_roles`.
Args:
member: The member to pass to ``coro``.
- coro: This is intended to be :obj:`discord.Member.add_roles` or :obj:`discord.Member.remove_roles`.
+ coro: This is intended to be :obj:`disnake.Member.add_roles` or :obj:`disnake.Member.remove_roles`.
"""
try:
await coro(role)
- except discord.NotFound:
+ except disnake.NotFound:
log.error(f"Failed to change role for {member} ({member.id}): member not found")
- except discord.Forbidden:
+ except disnake.Forbidden:
log.error(
f"Forbidden to change role for {member} ({member.id}); "
f"possibly due to role hierarchy"
)
- except discord.HTTPException as e:
+ except disnake.HTTPException as e:
log.error(f"Failed to change role for {member} ({member.id}): {e.status} {e.code}")
diff --git a/botcore/utils/monkey_patches.py b/botcore/utils/monkey_patches.py
new file mode 100644
index 00000000..abbb37a5
--- /dev/null
+++ b/botcore/utils/monkey_patches.py
@@ -0,0 +1,83 @@
+"""Contains all common monkey patches, used to alter disnake to fit our needs."""
+
+import logging
+from datetime import datetime, timedelta
+from functools import partial, partialmethod
+
+from disnake import Forbidden, http
+from disnake.ext import commands
+
+log = logging.getLogger(__name__)
+
+
+class _Command(commands.Command):
+ """
+ A :obj:`disnake.ext.commands.Command` subclass which supports root aliases.
+
+ A ``root_aliases`` keyword argument is added, which is a sequence of alias names that will act as
+ top-level commands rather than being aliases of the command's group. It's stored as an attribute
+ also named ``root_aliases``.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.root_aliases = kwargs.get("root_aliases", [])
+
+ if not isinstance(self.root_aliases, (list, tuple)):
+ raise TypeError("Root aliases of a command must be a list or a tuple of strings.")
+
+
+class _Group(commands.Group, _Command):
+ """
+ A :obj:`disnake.ext.commands.Group` subclass which supports root aliases.
+
+ A ``root_aliases`` keyword argument is added, which is a sequence of alias names that will act as
+ top-level groups rather than being aliases of the command's group. It's stored as an attribute
+ also named ``root_aliases``.
+ """
+
+
+def _patch_typing() -> None:
+ """
+ Sometimes Discord turns off typing events by throwing 403s.
+
+ Handle those issues by patching disnake's internal ``send_typing`` method so it ignores 403s in general.
+ """
+ log.debug("Patching send_typing, which should fix things breaking when Discord disables typing events. Stay safe!")
+
+ original = http.HTTPClient.send_typing
+ last_403 = None
+
+ async def honeybadger_type(self, channel_id: int) -> None: # noqa: ANN001
+ nonlocal last_403
+ if last_403 and (datetime.utcnow() - last_403) < timedelta(minutes=5):
+ log.warning("Not sending typing event, we got a 403 less than 5 minutes ago.")
+ return
+ try:
+ await original(self, channel_id)
+ except Forbidden:
+ last_403 = datetime.utcnow()
+ log.warning("Got a 403 from typing event!")
+
+ http.HTTPClient.send_typing = honeybadger_type
+
+
+def apply_monkey_patches() -> None:
+ """
+ Applies all common monkey patches for our bots.
+
+ Patches :obj:`disnake.ext.commands.Command` and :obj:`disnake.ext.commands.Group` to support root aliases.
+ A ``root_aliases`` keyword argument is added to these two objects, which is a sequence of alias names
+ that will act as top-level groups rather than being aliases of the command's group.
+
+ It's stored as an attribute also named ``root_aliases``
+
+ Patches disnake's internal ``send_typing`` method so that it ignores 403 errors from Discord.
+ When under heavy load Discord has added a CloudFlare worker to this route, which causes 403 errors to be thrown.
+ """
+ commands.command = partial(commands.command, cls=_Command)
+ commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=_Command)
+
+ commands.group = partial(commands.group, cls=_Group)
+ commands.GroupMixin.group = partialmethod(commands.GroupMixin.group, cls=_Group)
+ _patch_typing()
diff --git a/botcore/utils/regex.py b/botcore/utils/regex.py
index 036a5113..abcaf299 100644
--- a/botcore/utils/regex.py
+++ b/botcore/utils/regex.py
@@ -15,7 +15,7 @@ DISCORD_INVITE = re.compile(
flags=re.IGNORECASE
)
"""
-Regex for discord server invites.
+Regex for Discord server invites.
:meta hide-value:
"""