mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-21 16:52:31 -05:00
Migration to discord.py 2.0 (#5600)
* Temporarily set d.py to use latest git revision
* Remove `bot` param to Client.start
* Switch to aware datetimes
A lot of this is removing `.replace(...)` which while not technically
needed, simplifies the code base. There's only a few changes that are
actually necessary here.
* Update to work with new Asset design
* [threads] Update core ModLog API to support threads
- Added proper support for passing `Thread` to `channel`
when creating/editing case
- Added `parent_channel_id` attribute to Modlog API's Case
- Added `parent_channel` property that tries to get parent channel
- Updated case's content to show both thread and parent information
* [threads] Disallow usage of threads in some of the commands
- announceset channel
- filter channel clear
- filter channel add
- filter channel remove
- GlobalUniqueObjectFinder converter
- permissions addglobalrule
- permissions removeglobalrule
- permissions removeserverrule
- Permissions cog does not perform any validation for IDs
when setting through YAML so that has not been touched
- streamalert twitch/youtube/picarto
- embedset channel
- set ownernotifications adddestination
* [threads] Handle threads in Red's permissions system (Requires)
- Made permissions system apply rules of (only) parent in threads
* [threads] Update embed_requested to support threads
- Threads don't have their own embed settings and inherit from parent
* [threads] Update Red.message_eligible_as_command to support threads
* [threads] Properly handle invocation of [p](un)mutechannel in threads
Usage of a (un)mutechannel will mute/unmute user in the parent channel
if it's invoked in a thread.
* [threads] Update Filter cog to properly handle threads
- `[p]filter channel list` in a threads sends list for parent channel
- Checking for filter hits for a message in a thread checks its parent
channel's word list. There's no separate word list for threads.
* [threads] Support threads in Audio cog
- Handle threads being notify channels
- Update type hint for `is_query_allowed()`
* [threads] Update type hints and documentation to reflect thread support
- Documented that `{channel}` in CCs might be a thread
- Allowed (documented) usage of threads with `Config.channel()`
- Separate thread scope is still in the picture though
if it were to be done, it's going to be in separate in PR
- GuildContext.channel might be Thread
* Use less costy channel check in customcom's on_message_without_command
This isn't needed for d.py 2.0 but whatever...
* Update for in-place edits
* Embed's bool changed behavior, I'm hoping it doesn't affect us
* Address User.permissions_in() removal
* Swap VerificationLevel.extreme with VerificationLevel.highest
* Change to keyword-only parameters
* Change of `Guild.vanity_invite()` return type
* avatar -> display_avatar
* Fix metaclass shenanigans with Converter
* Update Red.add_cog() to be inline with `dpy_commands.Bot.add_cog()`
This means adding `override` keyword-only parameter and causing
small breakage by swapping RuntimeError with discord.ClientException.
* Address all DEP-WARNs
* Remove Context.clean_prefix and use upstream implementation instead
* Remove commands.Literal and use upstream implementation instead
Honestly, this was a rather bad implementation anyway...
Breaking but actually not really - it was provisional.
* Update Command.callback's setter
Support for functools.partial is now built into d.py
* Add new perms in HUMANIZED_PERM mapping (some from d.py 1.7 it seems)
BTW, that should really be in core instead of what we have now...
* Remove the part of do_conversion that has not worked for a long while
* Stop wrapping BadArgument in ConversionFailure
This is breaking but it's best to resolve it like this.
The functionality of ConversionFailure can be replicated with
Context.current_parameter and Context.current_argument.
* Add custom errors for int and float converters
* Remove Command.__call__ as it's now implemented in d.py
* Get rid of _dpy_reimplements
These were reimplemented for the purpose of typing
so it is no longer needed now that d.py is type hinted.
* Add return to Red.remove_cog
* Ensure we don't delete messages that differ only by used sticker
* discord.InvalidArgument->ValueError
* Move from raw <t:...> syntax to discord.utils.format_dt()
* Address AsyncIter removal
* Swap to pos-only for params that are pos-only in upstream
* Update for changes to Command.params
* [threads] Support threads in ignore checks and allow ignoring them
- Updated `[p](un)ignore channel` to accept threads
- Updated `[p]ignore list` to list ignored threads
- Updated logic in `Red.ignored_channel_or_guild()`
Ignores for guild channels now work as follows (only changes for threads):
- if channel is not a thread:
- check if user has manage channels perm in channel
and allow command usage if so
- check if channel is ignored and disallow command usage if so
- allow command usage if none of the conditions above happened
- if channel is a thread:
- check if user has manage channels perm in parent channel
and allow command usage if so
- check if parent channel is ignored and disallow command usage
if so
- check if user has manage thread perm in parent channel
and allow command usage if so
- check if thread is ignored and disallow command usage if so
- allow command usage if none of the conditions above happened
* [partial] Raise TypeError when channel is of PartialMessageable type
- Red.embed_requested
- Red.ignored_channel_or_guild
* [partial] Discard command messages when channel is PartialMessageable
* [threads] Add utilities for checking appropriate perms in both channels & threads
* [threads] Update code to use can_react_in() and @bot_can_react()
* [threads] Update code to use can_send_messages_in
* [threads] Add send_messages_in_threads perm to mute role and overrides
* [threads] Update code to use (bot/user)_can_manage_channel
* [threads] Update [p]diagnoseissues to work with threads
* Type hint fix
* [threads] Patch vendored discord.ext.menus to check proper perms in threads
I guess we've reached time when we have to patch the lib we vendor...
* Make docs generation work with non-final d.py releases
* Update discord.utils.oauth_url() usage
* Swap usage of discord.Embed.Empty/discord.embeds.EmptyEmbed to None
* Update usage of Guild.member_count to work with `None`
* Switch from Guild.vanity_invite() to Guild.vanity_url
* Update startup process to work with d.py's new asynchronous startup
* Use setup_hook() for pre-connect actions
* Update core's add_cog, remove_cog, and load_extension methods
* Update all setup functions to async and add awaits to bot.add_cog calls
* Modernize cogs by using async cog_load and cog_unload
* Address StoreChannel removal
* [partial] Disallow passing PartialMessageable to Case.channel
* [partial] Update cogs and utils to work better with PartialMessageable
- Ignore messages with PartialMessageable channel in CustomCommands cog
- In Filter cog, don't pass channel to modlog.create_case()
if it's PartialMessageable
- In Trivia cog, only compare channel IDs
- Make `.utils.menus.menu()` work for messages
with PartialMessageable channel
- Make checks in `.utils.tunnel.Tunnel.communicate()` more rigid
* Add few missing DEP-WARNs
This commit is contained in:
@@ -51,7 +51,7 @@ from .settings_caches import (
|
||||
I18nManager,
|
||||
)
|
||||
from .rpc import RPCMixin
|
||||
from .utils import common_filters, AsyncIter
|
||||
from .utils import can_user_send_messages_in, common_filters, AsyncIter
|
||||
from .utils._internal_utils import send_to_owners_with_prefix_replaced
|
||||
|
||||
CUSTOM_GROUPS = "CUSTOM_GROUPS"
|
||||
@@ -375,12 +375,12 @@ class Red(
|
||||
return
|
||||
del dev.env_extensions[name]
|
||||
|
||||
def get_command(self, name: str) -> Optional[commands.Command]:
|
||||
def get_command(self, name: str, /) -> Optional[commands.Command]:
|
||||
com = super().get_command(name)
|
||||
assert com is None or isinstance(com, commands.Command)
|
||||
return com
|
||||
|
||||
def get_cog(self, name: str) -> Optional[commands.Cog]:
|
||||
def get_cog(self, name: str, /) -> Optional[commands.Cog]:
|
||||
cog = super().get_cog(name)
|
||||
assert cog is None or isinstance(cog, commands.Cog)
|
||||
return cog
|
||||
@@ -444,7 +444,7 @@ class Red(
|
||||
"""
|
||||
self._red_before_invoke_objs.discard(coro)
|
||||
|
||||
def before_invoke(self, coro: T_BIC) -> T_BIC:
|
||||
def before_invoke(self, coro: T_BIC, /) -> T_BIC:
|
||||
"""
|
||||
Overridden decorator method for Red's ``before_invoke`` behavior.
|
||||
|
||||
@@ -809,9 +809,14 @@ class Red(
|
||||
if message.author.bot:
|
||||
return False
|
||||
|
||||
# We do not consider messages with PartialMessageable channel as eligible.
|
||||
# See `process_commands()` for our handling of it.
|
||||
if isinstance(channel, discord.PartialMessageable):
|
||||
return False
|
||||
|
||||
if guild:
|
||||
assert isinstance(channel, discord.abc.GuildChannel) # nosec
|
||||
if not channel.permissions_for(guild.me).send_messages:
|
||||
assert isinstance(channel, (discord.abc.GuildChannel, discord.Thread))
|
||||
if not can_user_send_messages_in(guild.me, channel):
|
||||
return False
|
||||
if not (await self.ignored_channel_or_guild(message)):
|
||||
return False
|
||||
@@ -838,7 +843,14 @@ class Red(
|
||||
-------
|
||||
bool
|
||||
`True` if commands are allowed in the channel, `False` otherwise
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
``ctx.channel`` is of `discord.PartialMessageable` type.
|
||||
"""
|
||||
if isinstance(ctx.channel, discord.PartialMessageable):
|
||||
raise TypeError("Can't check permissions for PartialMessageable.")
|
||||
perms = ctx.channel.permissions_for(ctx.author)
|
||||
surpass_ignore = (
|
||||
isinstance(ctx.channel, discord.abc.PrivateChannel)
|
||||
@@ -846,11 +858,38 @@ class Red(
|
||||
or await self.is_owner(ctx.author)
|
||||
or await self.is_admin(ctx.author)
|
||||
)
|
||||
# guild-wide checks
|
||||
if surpass_ignore:
|
||||
return True
|
||||
guild_ignored = await self._ignored_cache.get_ignored_guild(ctx.guild)
|
||||
chann_ignored = await self._ignored_cache.get_ignored_channel(ctx.channel)
|
||||
return not (guild_ignored or chann_ignored and not perms.manage_channels)
|
||||
if guild_ignored:
|
||||
return False
|
||||
|
||||
# (parent) channel checks
|
||||
if perms.manage_channels:
|
||||
return True
|
||||
|
||||
if isinstance(ctx.channel, discord.Thread):
|
||||
channel = ctx.channel.parent
|
||||
thread = ctx.channel
|
||||
else:
|
||||
channel = ctx.channel
|
||||
thread = None
|
||||
|
||||
chann_ignored = await self._ignored_cache.get_ignored_channel(channel)
|
||||
if chann_ignored:
|
||||
return False
|
||||
if thread is None:
|
||||
return True
|
||||
|
||||
# thread checks
|
||||
if perms.manage_threads:
|
||||
return True
|
||||
thread_ignored = await self._ignored_cache.get_ignored_channel(
|
||||
thread,
|
||||
check_category=False, # already checked for parent
|
||||
)
|
||||
return not thread_ignored
|
||||
|
||||
async def get_valid_prefixes(self, guild: Optional[discord.Guild] = None) -> List[str]:
|
||||
"""
|
||||
@@ -1062,10 +1101,10 @@ class Red(
|
||||
"""
|
||||
This should only be run once, prior to connecting to Discord gateway.
|
||||
"""
|
||||
self.add_cog(Core(self))
|
||||
self.add_cog(CogManagerUI())
|
||||
await self.add_cog(Core(self))
|
||||
await self.add_cog(CogManagerUI())
|
||||
if self._cli_flags.dev:
|
||||
self.add_cog(Dev())
|
||||
await self.add_cog(Dev())
|
||||
|
||||
await modlog._init(self)
|
||||
await bank._init()
|
||||
@@ -1179,15 +1218,16 @@ class Red(
|
||||
if not self.owner_ids:
|
||||
raise _NoOwnerSet("Bot doesn't have any owner set!")
|
||||
|
||||
async def start(self, *args, **kwargs):
|
||||
"""
|
||||
Overridden start which ensures that cog load and other pre-connection tasks are handled.
|
||||
"""
|
||||
async def start(self, token: str) -> None:
|
||||
# Overriding start to call _pre_login() before login()
|
||||
await self._pre_login()
|
||||
await self.login(*args)
|
||||
await self.login(token)
|
||||
# Pre-connect actions are done by setup_hook() which is called at the end of d.py's login()
|
||||
await self.connect()
|
||||
|
||||
async def setup_hook(self) -> None:
|
||||
await self._pre_fetch_owners()
|
||||
await self._pre_connect()
|
||||
await self.connect()
|
||||
|
||||
async def send_help_for(
|
||||
self,
|
||||
@@ -1205,7 +1245,9 @@ class Red(
|
||||
|
||||
async def embed_requested(
|
||||
self,
|
||||
channel: Union[discord.TextChannel, commands.Context, discord.User, discord.Member],
|
||||
channel: Union[
|
||||
discord.TextChannel, commands.Context, discord.User, discord.Member, discord.Thread
|
||||
],
|
||||
*,
|
||||
command: Optional[commands.Command] = None,
|
||||
check_permissions: bool = True,
|
||||
@@ -1215,7 +1257,7 @@ class Red(
|
||||
|
||||
Arguments
|
||||
---------
|
||||
channel : `discord.abc.Messageable`
|
||||
channel : Union[`discord.TextChannel`, `commands.Context`, `discord.User`, `discord.Member`, `discord.Thread`]
|
||||
The target messageable object to check embed settings for.
|
||||
|
||||
Keyword Arguments
|
||||
@@ -1236,9 +1278,8 @@ class Red(
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
When the passed channel is of type `discord.GroupChannel`
|
||||
or `discord.DMChannel`
|
||||
|
||||
When the passed channel is of type `discord.GroupChannel`,
|
||||
`discord.DMChannel`, or `discord.PartialMessageable`.
|
||||
"""
|
||||
|
||||
async def get_command_setting(guild_id: int) -> Optional[bool]:
|
||||
@@ -1247,9 +1288,6 @@ class Red(
|
||||
scope = self._config.custom(COMMAND_SCOPE, command.qualified_name, guild_id)
|
||||
return await scope.embeds()
|
||||
|
||||
if isinstance(channel, (discord.GroupChannel, discord.DMChannel)):
|
||||
raise TypeError("You cannot pass a GroupChannel or DMChannel to this method")
|
||||
|
||||
# using dpy_commands.Context to keep the Messageable contract in full
|
||||
if isinstance(channel, dpy_commands.Context):
|
||||
command = command or channel.command
|
||||
@@ -1259,11 +1297,21 @@ class Red(
|
||||
else channel.channel
|
||||
)
|
||||
|
||||
if isinstance(channel, discord.TextChannel):
|
||||
if isinstance(
|
||||
channel, (discord.GroupChannel, discord.DMChannel, discord.PartialMessageable)
|
||||
):
|
||||
raise TypeError(
|
||||
"You cannot pass a GroupChannel, DMChannel, or PartialMessageable to this method."
|
||||
)
|
||||
|
||||
if isinstance(channel, (discord.TextChannel, discord.Thread)):
|
||||
channel_id = channel.parent_id if isinstance(channel, discord.Thread) else channel.id
|
||||
|
||||
if check_permissions and not channel.permissions_for(channel.guild.me).embed_links:
|
||||
return False
|
||||
|
||||
if (channel_setting := await self._config.channel(channel).embeds()) is not None:
|
||||
channel_setting = await self._config.channel_from_id(channel_id).embeds()
|
||||
if channel_setting is not None:
|
||||
return channel_setting
|
||||
|
||||
if (command_setting := await get_command_setting(channel.guild.id)) is not None:
|
||||
@@ -1282,7 +1330,7 @@ class Red(
|
||||
global_setting = await self._config.embeds()
|
||||
return global_setting
|
||||
|
||||
async def is_owner(self, user: Union[discord.User, discord.Member]) -> bool:
|
||||
async def is_owner(self, user: Union[discord.User, discord.Member], /) -> bool:
|
||||
"""
|
||||
Determines if the user should be considered a bot owner.
|
||||
|
||||
@@ -1317,10 +1365,10 @@ class Red(
|
||||
"""
|
||||
data = await self._config.all()
|
||||
commands_scope = data["invite_commands_scope"]
|
||||
scopes = ("bot", "applications.commands") if commands_scope else None
|
||||
scopes = ("bot", "applications.commands") if commands_scope else ("bot",)
|
||||
perms_int = data["invite_perm"]
|
||||
permissions = discord.Permissions(perms_int)
|
||||
return discord.utils.oauth_url(self._app_info.id, permissions, scopes=scopes)
|
||||
return discord.utils.oauth_url(self._app_info.id, permissions=permissions, scopes=scopes)
|
||||
|
||||
async def is_invite_url_public(self) -> bool:
|
||||
"""
|
||||
@@ -1336,9 +1384,8 @@ class Red(
|
||||
async def is_admin(self, member: discord.Member) -> bool:
|
||||
"""Checks if a member is an admin of their guild."""
|
||||
try:
|
||||
member_snowflakes = member._roles # DEP-WARN
|
||||
for snowflake in await self._config.guild(member.guild).admin_role():
|
||||
if member_snowflakes.has(snowflake): # Dep-WARN
|
||||
if member.get_role(snowflake):
|
||||
return True
|
||||
except AttributeError: # someone passed a webhook to this
|
||||
pass
|
||||
@@ -1347,12 +1394,11 @@ class Red(
|
||||
async def is_mod(self, member: discord.Member) -> bool:
|
||||
"""Checks if a member is a mod or admin of their guild."""
|
||||
try:
|
||||
member_snowflakes = member._roles # DEP-WARN
|
||||
for snowflake in await self._config.guild(member.guild).admin_role():
|
||||
if member_snowflakes.has(snowflake): # DEP-WARN
|
||||
if member.get_role(snowflake):
|
||||
return True
|
||||
for snowflake in await self._config.guild(member.guild).mod_role():
|
||||
if member_snowflakes.has(snowflake): # DEP-WARN
|
||||
if member.get_role(snowflake):
|
||||
return True
|
||||
except AttributeError: # someone passed a webhook to this
|
||||
pass
|
||||
@@ -1495,10 +1541,10 @@ class Red(
|
||||
for service in service_names:
|
||||
self.dispatch("red_api_tokens_update", service, MappingProxyType({}))
|
||||
|
||||
async def get_context(self, message, *, cls=commands.Context):
|
||||
async def get_context(self, message, /, *, cls=commands.Context):
|
||||
return await super().get_context(message, cls=cls)
|
||||
|
||||
async def process_commands(self, message: discord.Message):
|
||||
async def process_commands(self, message: discord.Message, /):
|
||||
"""
|
||||
Same as base method, but dispatches an additional event for cogs
|
||||
which want to handle normal messages differently to command
|
||||
@@ -1507,7 +1553,14 @@ class Red(
|
||||
"""
|
||||
if not message.author.bot:
|
||||
ctx = await self.get_context(message)
|
||||
await self.invoke(ctx)
|
||||
if ctx.invoked_with and isinstance(message.channel, discord.PartialMessageable):
|
||||
log.warning(
|
||||
"Discarded a command message (ID: %s) with PartialMessageable channel: %r",
|
||||
message.id,
|
||||
message.channel,
|
||||
)
|
||||
else:
|
||||
await self.invoke(ctx)
|
||||
else:
|
||||
ctx = None
|
||||
|
||||
@@ -1544,18 +1597,23 @@ class Red(
|
||||
raise discord.ClientException(f"extension {name} does not have a setup function")
|
||||
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(lib.setup):
|
||||
await lib.setup(self)
|
||||
else:
|
||||
lib.setup(self)
|
||||
await lib.setup(self)
|
||||
except Exception as e:
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
await self._remove_module_references(lib.__name__)
|
||||
await self._call_module_finalizers(lib, name)
|
||||
raise
|
||||
else:
|
||||
self._BotBase__extensions[name] = lib
|
||||
|
||||
def remove_cog(self, cogname: str):
|
||||
async def remove_cog(
|
||||
self,
|
||||
cogname: str,
|
||||
/,
|
||||
*,
|
||||
# DEP-WARN: MISSING is implementation detail
|
||||
guild: Optional[discord.abc.Snowflake] = discord.utils.MISSING,
|
||||
guilds: List[discord.abc.Snowflake] = discord.utils.MISSING,
|
||||
) -> Optional[commands.Cog]:
|
||||
cog = self.get_cog(cogname)
|
||||
if cog is None:
|
||||
return
|
||||
@@ -1568,13 +1626,15 @@ class Red(
|
||||
else:
|
||||
self.remove_permissions_hook(hook)
|
||||
|
||||
super().remove_cog(cogname)
|
||||
await super().remove_cog(cogname, guild=guild, guilds=guilds)
|
||||
|
||||
cog.requires.reset()
|
||||
|
||||
for meth in self.rpc_handlers.pop(cogname.upper(), ()):
|
||||
self.unregister_rpc_handler(meth)
|
||||
|
||||
return cog
|
||||
|
||||
async def is_automod_immune(
|
||||
self, to_check: Union[discord.Message, commands.Context, discord.abc.User, discord.Role]
|
||||
) -> bool:
|
||||
@@ -1656,15 +1716,28 @@ class Red(
|
||||
|
||||
return await destination.send(content=content, **kwargs)
|
||||
|
||||
def add_cog(self, cog: commands.Cog):
|
||||
async def add_cog(
|
||||
self,
|
||||
cog: commands.Cog,
|
||||
/,
|
||||
*,
|
||||
override: bool = False,
|
||||
# DEP-WARN: MISSING is implementation detail
|
||||
guild: Optional[discord.abc.Snowflake] = discord.utils.MISSING,
|
||||
guilds: List[discord.abc.Snowflake] = discord.utils.MISSING,
|
||||
) -> None:
|
||||
if not isinstance(cog, commands.Cog):
|
||||
raise RuntimeError(
|
||||
f"The {cog.__class__.__name__} cog in the {cog.__module__} package does "
|
||||
f"not inherit from the commands.Cog base class. The cog author must update "
|
||||
f"the cog to adhere to this requirement."
|
||||
)
|
||||
if cog.__cog_name__ in self.cogs:
|
||||
raise RuntimeError(f"There is already a cog named {cog.__cog_name__} loaded.")
|
||||
cog_name = cog.__cog_name__
|
||||
if cog_name in self.cogs:
|
||||
if not override:
|
||||
raise discord.ClientException(f"Cog named {cog_name!r} already loaded")
|
||||
await self.remove_cog(cog_name, guild=guild, guilds=guilds)
|
||||
|
||||
if not hasattr(cog, "requires"):
|
||||
commands.Cog.__init__(cog)
|
||||
|
||||
@@ -1680,7 +1753,7 @@ class Red(
|
||||
self.add_permissions_hook(hook)
|
||||
added_hooks.append(hook)
|
||||
|
||||
super().add_cog(cog)
|
||||
await super().add_cog(cog, guild=guild, guilds=guilds)
|
||||
self.dispatch("cog_add", cog)
|
||||
if "permissions" not in self.extensions:
|
||||
cog.requires.ready_event.set()
|
||||
@@ -1697,7 +1770,7 @@ class Red(
|
||||
del cog
|
||||
raise
|
||||
|
||||
def add_command(self, command: commands.Command) -> None:
|
||||
def add_command(self, command: commands.Command, /) -> None:
|
||||
if not isinstance(command, commands.Command):
|
||||
raise RuntimeError("Commands must be instances of `redbot.core.commands.Command`")
|
||||
|
||||
@@ -1713,7 +1786,7 @@ class Red(
|
||||
if permissions_not_loaded:
|
||||
subcommand.requires.ready_event.set()
|
||||
|
||||
def remove_command(self, name: str) -> Optional[commands.Command]:
|
||||
def remove_command(self, name: str, /) -> Optional[commands.Command]:
|
||||
command = super().remove_command(name)
|
||||
if command is None:
|
||||
return None
|
||||
@@ -1802,7 +1875,9 @@ class Red(
|
||||
ctx.permission_state = commands.PermState.DENIED_BY_HOOK
|
||||
return False
|
||||
|
||||
async def get_owner_notification_destinations(self) -> List[discord.abc.Messageable]:
|
||||
async def get_owner_notification_destinations(
|
||||
self,
|
||||
) -> List[Union[discord.TextChannel, discord.User]]:
|
||||
"""
|
||||
Gets the users and channels to send to
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user