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:
jack1142
2022-04-03 03:21:20 +02:00
committed by GitHub
parent c9a0971945
commit febca8ccbb
104 changed files with 1427 additions and 999 deletions

View File

@@ -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
"""