diff options
Diffstat (limited to 'botcore')
| -rw-r--r-- | botcore/__init__.py | 2 | ||||
| -rw-r--r-- | botcore/exts/__init__.py | 2 | ||||
| -rw-r--r-- | botcore/utils/__init__.py | 5 | ||||
| -rw-r--r-- | botcore/utils/channel.py | 20 | ||||
| -rw-r--r-- | botcore/utils/extensions.py | 4 | ||||
| -rw-r--r-- | botcore/utils/members.py | 24 | ||||
| -rw-r--r-- | botcore/utils/monkey_patches.py | 83 | ||||
| -rw-r--r-- | botcore/utils/regex.py | 2 | 
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:  """  |