Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 2.0.0 22nd February 2022
- Breaking: Moved regex to botcore.utils namespace
- Feature: Migrate from discord.py 2.0a0 to disnake.
- Feature: Add common monkey patches.
- Feature: Port many common utilities from our bots
- caching
- channel
Expand All @@ -12,12 +14,12 @@
- Support: Added intersphinx to docs.

## 1.2.0 9th January 2022
- Feature: Code block detection regex
- Feature: Add code block detection regex.

## 1.1.0 2nd December 2021
- Support: Autogenerated docs.
- Feature: Regex utility.
- Support: Add autogenerated docs.
- Feature: Add a regex utility.


## 1.0.0 17th November 2021
- Support: Core package, poetry, and linting CI.
- Support: Add the core package, poetry, and linting CI.
2 changes: 1 addition & 1 deletion botcore/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion botcore/exts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Reusable discord cogs."""
"""Reusable Discord cogs."""
__all__ = []

__all__ = list(map(lambda module: module.__name__, __all__))
5 changes: 3 additions & 2 deletions botcore/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""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,
channel,
extensions,
logging,
members,
monkey_patches,
regex,
scheduling,
]
Expand Down
20 changes: 10 additions & 10 deletions botcore/utils/channel.py
Original file line number Diff line number Diff line change
@@ -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``.

Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions botcore/utils/extensions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Utilities for loading discord extensions."""
"""Utilities for loading Discord extensions."""

import importlib
import inspect
Expand Down Expand Up @@ -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:
Expand Down
24 changes: 12 additions & 12 deletions botcore/utils/members.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,56 @@
"""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.")
return member


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}")
83 changes: 83 additions & 0 deletions botcore/utils/monkey_patches.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion botcore/utils/regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
flags=re.IGNORECASE
)
"""
Regex for discord server invites.
Regex for Discord server invites.

:meta hide-value:
"""
Expand Down
3 changes: 3 additions & 0 deletions docs/_static/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
h1 {
font-weight: 300;
}
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
html_favicon = html_logo

html_css_files = [
"index.css",
"logo.css",
]

Expand Down Expand Up @@ -129,7 +130,7 @@ def setup(app: Sphinx) -> None:
# -- Options for intersphinx extension ---------------------------------------
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"discord": ("https://discordpy.readthedocs.io/en/master/", None),
"disnake": ("https://docs.disnake.dev/en/latest/", None),
}


Expand Down
5 changes: 5 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Reference

output/botcore

.. toctree::
:caption: Other:

Changelog <https://github.com/python-discord/bot-core/blob/main/CHANGELOG.md>


Extras
==================
Expand Down
Loading