mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-07 01:42:30 -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:
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Awaitable, Callable, Iterable, List, Optional,
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator
|
||||
from redbot.core.utils import can_user_send_messages_in
|
||||
from redbot.core.utils.chat_formatting import (
|
||||
bold,
|
||||
escape,
|
||||
@@ -37,7 +38,7 @@ class IssueDiagnoserBase:
|
||||
self,
|
||||
bot: Red,
|
||||
original_ctx: commands.Context,
|
||||
channel: discord.TextChannel,
|
||||
channel: Union[discord.TextChannel, discord.Thread],
|
||||
author: discord.Member,
|
||||
command: commands.Command,
|
||||
) -> None:
|
||||
@@ -59,6 +60,7 @@ class IssueDiagnoserBase:
|
||||
self.message.channel = self.channel
|
||||
self.message.content = self._original_ctx.prefix + self.command.qualified_name
|
||||
# clear the cached properties
|
||||
# DEP-WARN
|
||||
for attr in self.message._CACHED_SLOTS: # type: ignore[attr-defined]
|
||||
try:
|
||||
delattr(self.message, attr)
|
||||
@@ -117,18 +119,27 @@ class DetailedGlobalCallOnceChecksMixin(IssueDiagnoserBase):
|
||||
|
||||
async def _check_can_bot_send_messages(self) -> CheckResult:
|
||||
label = _("Check if the bot can send messages in the given channel")
|
||||
if self.channel.permissions_for(self.guild.me).send_messages:
|
||||
return CheckResult(True, label)
|
||||
return CheckResult(
|
||||
False,
|
||||
label,
|
||||
_("Bot doesn't have permission to send messages in the given channel."),
|
||||
_(
|
||||
"To fix this issue, ensure that the permissions setup allows the bot"
|
||||
" to send messages per Discord's role hierarchy:\n"
|
||||
"https://support.discord.com/hc/en-us/articles/206141927"
|
||||
),
|
||||
)
|
||||
# This is checked by send messages check but this allows us to
|
||||
# give more detailed information.
|
||||
if not self.guild.me.guild_permissions.administrator and self.guild.me.is_timed_out():
|
||||
return CheckResult(
|
||||
False,
|
||||
label,
|
||||
_("Bot is timed out in the given channel."),
|
||||
_("To fix this issue, remove timeout from the bot."),
|
||||
)
|
||||
if not can_user_send_messages_in(self.guild.me, self.channel):
|
||||
return CheckResult(
|
||||
False,
|
||||
label,
|
||||
_("Bot doesn't have permission to send messages in the given channel."),
|
||||
_(
|
||||
"To fix this issue, ensure that the permissions setup allows the bot"
|
||||
" to send messages per Discord's role hierarchy:\n"
|
||||
"https://support.discord.com/hc/en-us/articles/206141927"
|
||||
),
|
||||
)
|
||||
return CheckResult(True, label)
|
||||
|
||||
# While the following 2 checks could show even more precise error message,
|
||||
# it would require a usage of private attribute rather than the public API
|
||||
@@ -139,24 +150,47 @@ class DetailedGlobalCallOnceChecksMixin(IssueDiagnoserBase):
|
||||
return CheckResult(True, label)
|
||||
|
||||
if self.channel.category is None:
|
||||
resolution = _(
|
||||
"To fix this issue, check the list returned by the {command} command"
|
||||
" and ensure that the {channel} channel and the server aren't a part of that list."
|
||||
).format(
|
||||
command=self._format_command_name("ignore list"),
|
||||
channel=self.channel.mention,
|
||||
)
|
||||
if isinstance(self.channel, discord.Thread):
|
||||
resolution = _(
|
||||
"To fix this issue, check the list returned by the {command} command"
|
||||
" and ensure that the {thread} thread, its parent channel,"
|
||||
" and the server aren't a part of that list."
|
||||
).format(
|
||||
command=self._format_command_name("ignore list"),
|
||||
thread=self.channel.mention,
|
||||
)
|
||||
else:
|
||||
resolution = _(
|
||||
"To fix this issue, check the list returned by the {command} command"
|
||||
" and ensure that the {channel} channel"
|
||||
" and the server aren't a part of that list."
|
||||
).format(
|
||||
command=self._format_command_name("ignore list"),
|
||||
channel=self.channel.mention,
|
||||
)
|
||||
else:
|
||||
resolution = _(
|
||||
"To fix this issue, check the list returned by the {command} command"
|
||||
" and ensure that the {channel} channel,"
|
||||
" the channel category it belongs to ({channel_category}),"
|
||||
" and the server aren't a part of that list."
|
||||
).format(
|
||||
command=self._format_command_name("ignore list"),
|
||||
channel=self.channel.mention,
|
||||
channel_category=self.channel.category.mention,
|
||||
)
|
||||
if isinstance(self.channel, discord.Thread):
|
||||
resolution = _(
|
||||
"To fix this issue, check the list returned by the {command} command"
|
||||
" and ensure that the {thread} thread, its parent channel,"
|
||||
" the channel category it belongs to ({channel_category}),"
|
||||
" and the server aren't a part of that list."
|
||||
).format(
|
||||
command=self._format_command_name("ignore list"),
|
||||
thread=self.channel.mention,
|
||||
channel_category=self.channel.category.mention,
|
||||
)
|
||||
else:
|
||||
resolution = _(
|
||||
"To fix this issue, check the list returned by the {command} command"
|
||||
" and ensure that the {channel} channel,"
|
||||
" the channel category it belongs to ({channel_category}),"
|
||||
" and the server aren't a part of that list."
|
||||
).format(
|
||||
command=self._format_command_name("ignore list"),
|
||||
channel=self.channel.mention,
|
||||
channel_category=self.channel.category.mention,
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
False,
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -29,13 +29,11 @@ from .converter import (
|
||||
parse_timedelta as parse_timedelta,
|
||||
NoParseOptional as NoParseOptional,
|
||||
UserInputOptional as UserInputOptional,
|
||||
Literal as Literal,
|
||||
RawUserIdConverter as RawUserIdConverter,
|
||||
CogConverter as CogConverter,
|
||||
CommandConverter as CommandConverter,
|
||||
)
|
||||
from .errors import (
|
||||
ConversionFailure as ConversionFailure,
|
||||
BotMissingPermissions as BotMissingPermissions,
|
||||
UserFeedbackCheckFailure as UserFeedbackCheckFailure,
|
||||
ArgParserFailure as ArgParserFailure,
|
||||
@@ -57,33 +55,23 @@ from .requires import (
|
||||
permissions_check as permissions_check,
|
||||
bot_has_permissions as bot_has_permissions,
|
||||
bot_in_a_guild as bot_in_a_guild,
|
||||
bot_can_manage_channel as bot_can_manage_channel,
|
||||
bot_can_react as bot_can_react,
|
||||
has_permissions as has_permissions,
|
||||
can_manage_channel as can_manage_channel,
|
||||
has_guild_permissions as has_guild_permissions,
|
||||
is_owner as is_owner,
|
||||
guildowner as guildowner,
|
||||
guildowner_or_can_manage_channel as guildowner_or_can_manage_channel,
|
||||
guildowner_or_permissions as guildowner_or_permissions,
|
||||
admin as admin,
|
||||
admin_or_can_manage_channel as admin_or_can_manage_channel,
|
||||
admin_or_permissions as admin_or_permissions,
|
||||
mod as mod,
|
||||
mod_or_can_manage_channel as mod_or_can_manage_channel,
|
||||
mod_or_permissions as mod_or_permissions,
|
||||
)
|
||||
|
||||
from ._dpy_reimplements import (
|
||||
check as check,
|
||||
guild_only as guild_only,
|
||||
cooldown as cooldown,
|
||||
dm_only as dm_only,
|
||||
is_nsfw as is_nsfw,
|
||||
has_role as has_role,
|
||||
has_any_role as has_any_role,
|
||||
bot_has_role as bot_has_role,
|
||||
when_mentioned_or as when_mentioned_or,
|
||||
when_mentioned as when_mentioned,
|
||||
bot_has_any_role as bot_has_any_role,
|
||||
before_invoke as before_invoke,
|
||||
after_invoke as after_invoke,
|
||||
)
|
||||
|
||||
### DEP-WARN: Check this *every* discord.py update
|
||||
from discord.ext.commands import (
|
||||
BadArgument as BadArgument,
|
||||
@@ -137,7 +125,6 @@ from discord.ext.commands import (
|
||||
ColorConverter as ColorConverter,
|
||||
VoiceChannelConverter as VoiceChannelConverter,
|
||||
StageChannelConverter as StageChannelConverter,
|
||||
StoreChannelConverter as StoreChannelConverter,
|
||||
NSFWChannelRequired as NSFWChannelRequired,
|
||||
IDConverter as IDConverter,
|
||||
MissingRequiredArgument as MissingRequiredArgument,
|
||||
@@ -167,4 +154,39 @@ from discord.ext.commands import (
|
||||
EmojiNotFound as EmojiNotFound,
|
||||
PartialEmojiConversionFailure as PartialEmojiConversionFailure,
|
||||
BadBoolArgument as BadBoolArgument,
|
||||
TooManyFlags as TooManyFlags,
|
||||
MissingRequiredFlag as MissingRequiredFlag,
|
||||
flag as flag,
|
||||
FlagError as FlagError,
|
||||
ObjectNotFound as ObjectNotFound,
|
||||
GuildStickerNotFound as GuildStickerNotFound,
|
||||
ThreadNotFound as ThreadNotFound,
|
||||
GuildChannelConverter as GuildChannelConverter,
|
||||
run_converters as run_converters,
|
||||
Flag as Flag,
|
||||
BadFlagArgument as BadFlagArgument,
|
||||
BadColorArgument as BadColorArgument,
|
||||
dynamic_cooldown as dynamic_cooldown,
|
||||
BadLiteralArgument as BadLiteralArgument,
|
||||
DynamicCooldownMapping as DynamicCooldownMapping,
|
||||
ThreadConverter as ThreadConverter,
|
||||
GuildStickerConverter as GuildStickerConverter,
|
||||
ObjectConverter as ObjectConverter,
|
||||
FlagConverter as FlagConverter,
|
||||
MissingFlagArgument as MissingFlagArgument,
|
||||
ScheduledEventConverter as ScheduledEventConverter,
|
||||
ScheduledEventNotFound as ScheduledEventNotFound,
|
||||
check as check,
|
||||
guild_only as guild_only,
|
||||
cooldown as cooldown,
|
||||
dm_only as dm_only,
|
||||
is_nsfw as is_nsfw,
|
||||
has_role as has_role,
|
||||
has_any_role as has_any_role,
|
||||
bot_has_role as bot_has_role,
|
||||
when_mentioned_or as when_mentioned_or,
|
||||
when_mentioned as when_mentioned,
|
||||
bot_has_any_role as bot_has_any_role,
|
||||
before_invoke as before_invoke,
|
||||
after_invoke as after_invoke,
|
||||
)
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import inspect
|
||||
import functools
|
||||
from typing import (
|
||||
TypeVar,
|
||||
Callable,
|
||||
Awaitable,
|
||||
Coroutine,
|
||||
Union,
|
||||
Type,
|
||||
TYPE_CHECKING,
|
||||
List,
|
||||
Any,
|
||||
Generator,
|
||||
Protocol,
|
||||
overload,
|
||||
)
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as dpy_commands
|
||||
|
||||
# So much of this can be stripped right back out with proper stubs.
|
||||
if not TYPE_CHECKING:
|
||||
from discord.ext.commands import (
|
||||
check as check,
|
||||
guild_only as guild_only,
|
||||
dm_only as dm_only,
|
||||
is_nsfw as is_nsfw,
|
||||
has_role as has_role,
|
||||
has_any_role as has_any_role,
|
||||
bot_has_role as bot_has_role,
|
||||
bot_has_any_role as bot_has_any_role,
|
||||
cooldown as cooldown,
|
||||
before_invoke as before_invoke,
|
||||
after_invoke as after_invoke,
|
||||
)
|
||||
|
||||
from ..i18n import Translator
|
||||
from .context import Context
|
||||
from .commands import Command
|
||||
|
||||
|
||||
_ = Translator("nah", __file__)
|
||||
|
||||
|
||||
"""
|
||||
Anything here is either a reimplementation or re-export
|
||||
of a discord.py function or class with more lies for mypy
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"check",
|
||||
# "check_any", # discord.py 1.3
|
||||
"guild_only",
|
||||
"dm_only",
|
||||
"is_nsfw",
|
||||
"has_role",
|
||||
"has_any_role",
|
||||
"bot_has_role",
|
||||
"bot_has_any_role",
|
||||
"when_mentioned_or",
|
||||
"cooldown",
|
||||
"when_mentioned",
|
||||
"before_invoke",
|
||||
"after_invoke",
|
||||
]
|
||||
|
||||
_CT = TypeVar("_CT", bound=Context)
|
||||
_T = TypeVar("_T")
|
||||
_F = TypeVar("_F")
|
||||
CheckType = Union[Callable[[_CT], bool], Callable[[_CT], Coroutine[Any, Any, bool]]]
|
||||
CoroLike = Callable[..., Union[Awaitable[_T], Generator[Any, None, _T]]]
|
||||
InvokeHook = Callable[[_CT], Coroutine[Any, Any, bool]]
|
||||
|
||||
|
||||
class CheckDecorator(Protocol):
|
||||
predicate: Coroutine[Any, Any, bool]
|
||||
|
||||
@overload
|
||||
def __call__(self, func: _CT) -> _CT:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __call__(self, func: CoroLike) -> CoroLike:
|
||||
...
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def check(predicate: CheckType) -> CheckDecorator:
|
||||
...
|
||||
|
||||
def guild_only() -> CheckDecorator:
|
||||
...
|
||||
|
||||
def dm_only() -> CheckDecorator:
|
||||
...
|
||||
|
||||
def is_nsfw() -> CheckDecorator:
|
||||
...
|
||||
|
||||
def has_role() -> CheckDecorator:
|
||||
...
|
||||
|
||||
def has_any_role() -> CheckDecorator:
|
||||
...
|
||||
|
||||
def bot_has_role() -> CheckDecorator:
|
||||
...
|
||||
|
||||
def bot_has_any_role() -> CheckDecorator:
|
||||
...
|
||||
|
||||
def cooldown(rate: int, per: float, type: dpy_commands.BucketType = ...) -> Callable[[_F], _F]:
|
||||
...
|
||||
|
||||
def before_invoke(coro: InvokeHook) -> Callable[[_F], _F]:
|
||||
...
|
||||
|
||||
def after_invoke(coro: InvokeHook) -> Callable[[_F], _F]:
|
||||
...
|
||||
|
||||
|
||||
PrefixCallable = Callable[[dpy_commands.bot.BotBase, discord.Message], List[str]]
|
||||
|
||||
|
||||
def when_mentioned(bot: dpy_commands.bot.BotBase, msg: discord.Message) -> List[str]:
|
||||
return [f"<@{bot.user.id}> ", f"<@!{bot.user.id}> "]
|
||||
|
||||
|
||||
def when_mentioned_or(*prefixes) -> PrefixCallable:
|
||||
def inner(bot: dpy_commands.bot.BotBase, msg: discord.Message) -> List[str]:
|
||||
r = list(prefixes)
|
||||
r = when_mentioned(bot, msg) + r
|
||||
return r
|
||||
|
||||
return inner
|
||||
@@ -285,13 +285,6 @@ class Command(CogCommandMixin, DPYCommand):
|
||||
(type used will be of the inner type instead)
|
||||
"""
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if self.cog:
|
||||
# We need to inject cog as self here
|
||||
return self.callback(self.cog, *args, **kwargs)
|
||||
else:
|
||||
return self.callback(*args, **kwargs)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.ignore_optional_for_conversion = kwargs.pop("ignore_optional_for_conversion", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -323,60 +316,27 @@ class Command(CogCommandMixin, DPYCommand):
|
||||
|
||||
@callback.setter
|
||||
def callback(self, function):
|
||||
"""
|
||||
Below should be mostly the same as discord.py
|
||||
# Below should be mostly the same as discord.py
|
||||
#
|
||||
# Here's the list of cases where the behavior differs:
|
||||
# - `typing.Optional` behavior is changed
|
||||
# when `ignore_optional_for_conversion` option is used
|
||||
super(Command, Command).callback.__set__(self, function)
|
||||
|
||||
Currently, we modify behavior for
|
||||
if not self.ignore_optional_for_conversion:
|
||||
return
|
||||
|
||||
- functools.partial support
|
||||
- typing.Optional behavior change as an option
|
||||
"""
|
||||
self._callback = function
|
||||
if isinstance(function, functools.partial):
|
||||
self.module = function.func.__module__
|
||||
globals_ = function.func.__globals__
|
||||
else:
|
||||
self.module = function.__module__
|
||||
globals_ = function.__globals__
|
||||
|
||||
signature = inspect.signature(function)
|
||||
self.params = signature.parameters.copy()
|
||||
|
||||
# PEP-563 allows postponing evaluation of annotations with a __future__
|
||||
# import. When postponed, Parameter.annotation will be a string and must
|
||||
# be replaced with the real value for the converters to work later on
|
||||
_NoneType = type(None)
|
||||
for key, value in self.params.items():
|
||||
if isinstance(value.annotation, str):
|
||||
self.params[key] = value = value.replace(
|
||||
annotation=eval(value.annotation, globals_)
|
||||
)
|
||||
|
||||
# fail early for when someone passes an unparameterized Greedy type
|
||||
if value.annotation is Greedy:
|
||||
raise TypeError("Unparameterized Greedy[...] is disallowed in signature.")
|
||||
|
||||
if not self.ignore_optional_for_conversion:
|
||||
continue # reduces indentation compared to alternative
|
||||
|
||||
try:
|
||||
vtype = value.annotation.__origin__
|
||||
if vtype is Union:
|
||||
_NoneType = type if TYPE_CHECKING else type(None)
|
||||
args = value.annotation.__args__
|
||||
if _NoneType in args:
|
||||
args = tuple(a for a in args if a is not _NoneType)
|
||||
if len(args) == 1:
|
||||
# can't have a union of 1 or 0 items
|
||||
# 1 prevents this from becoming 0
|
||||
# we need to prevent 2 become 1
|
||||
# (Don't change that to becoming, it's intentional :musical_note:)
|
||||
self.params[key] = value = value.replace(annotation=args[0])
|
||||
else:
|
||||
# and mypy wretches at the correct Union[args]
|
||||
temp_type = type if TYPE_CHECKING else Union[args]
|
||||
self.params[key] = value = value.replace(annotation=temp_type)
|
||||
except AttributeError:
|
||||
origin = getattr(value.annotation, "__origin__", None)
|
||||
if origin is not Union:
|
||||
continue
|
||||
args = value.annotation.__args__
|
||||
if _NoneType in args:
|
||||
args = tuple(a for a in args if a is not _NoneType)
|
||||
# typing.Union is automatically deduplicated and flattened
|
||||
# so we don't need to anything else here
|
||||
self.params[key] = value = value.replace(annotation=Union[args])
|
||||
|
||||
@property
|
||||
def help(self):
|
||||
@@ -420,6 +380,7 @@ class Command(CogCommandMixin, DPYCommand):
|
||||
async def can_run(
|
||||
self,
|
||||
ctx: "Context",
|
||||
/,
|
||||
*,
|
||||
check_all_parents: bool = False,
|
||||
change_permission_state: bool = False,
|
||||
@@ -476,7 +437,7 @@ class Command(CogCommandMixin, DPYCommand):
|
||||
if not change_permission_state:
|
||||
ctx.permission_state = original_state
|
||||
|
||||
async def prepare(self, ctx):
|
||||
async def prepare(self, ctx, /):
|
||||
ctx.command = self
|
||||
|
||||
if not self.enabled:
|
||||
@@ -502,39 +463,6 @@ class Command(CogCommandMixin, DPYCommand):
|
||||
await self._max_concurrency.release(ctx)
|
||||
raise
|
||||
|
||||
async def do_conversion(
|
||||
self, ctx: "Context", converter, argument: str, param: inspect.Parameter
|
||||
):
|
||||
"""Convert an argument according to its type annotation.
|
||||
|
||||
Raises
|
||||
------
|
||||
ConversionFailure
|
||||
If doing the conversion failed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Any
|
||||
The converted argument.
|
||||
|
||||
"""
|
||||
# Let's not worry about all of this junk if it's just a str converter
|
||||
if converter is str:
|
||||
return argument
|
||||
|
||||
try:
|
||||
return await super().do_conversion(ctx, converter, argument, param)
|
||||
except BadArgument as exc:
|
||||
raise ConversionFailure(converter, argument, param, *exc.args) from exc
|
||||
except ValueError as exc:
|
||||
# Some common converters need special treatment...
|
||||
if converter in (int, float):
|
||||
message = _('"{argument}" is not a number.').format(argument=argument)
|
||||
raise ConversionFailure(converter, argument, param, message) from exc
|
||||
|
||||
# We should expose anything which might be a bug in the converter
|
||||
raise exc
|
||||
|
||||
async def can_see(self, ctx: "Context"):
|
||||
"""Check if this command is visible in the given context.
|
||||
|
||||
@@ -636,7 +564,7 @@ class Command(CogCommandMixin, DPYCommand):
|
||||
break
|
||||
return old_rule, new_rule
|
||||
|
||||
def error(self, coro):
|
||||
def error(self, coro, /):
|
||||
"""
|
||||
A decorator that registers a coroutine as a local error handler.
|
||||
|
||||
@@ -796,7 +724,7 @@ class Group(GroupMixin, Command, CogGroupMixin, DPYGroup):
|
||||
self.autohelp = kwargs.pop("autohelp", True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def invoke(self, ctx: "Context"):
|
||||
async def invoke(self, ctx: "Context", /):
|
||||
# we skip prepare in some cases to avoid some things
|
||||
# We still always want this part of the behavior though
|
||||
ctx.command = self
|
||||
@@ -971,7 +899,7 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
|
||||
"""
|
||||
raise RedUnhandledAPI()
|
||||
|
||||
async def can_run(self, ctx: "Context", **kwargs) -> bool:
|
||||
async def can_run(self, ctx: "Context", /, **kwargs) -> bool:
|
||||
"""
|
||||
This really just exists to allow easy use with other methods using can_run
|
||||
on commands and groups such as help formatters.
|
||||
@@ -999,7 +927,7 @@ class CogMixin(CogGroupMixin, CogCommandMixin):
|
||||
|
||||
return can_run
|
||||
|
||||
async def can_see(self, ctx: "Context") -> bool:
|
||||
async def can_see(self, ctx: "Context", /) -> bool:
|
||||
"""Check if this cog is visible in the given context.
|
||||
|
||||
In short, this will verify whether
|
||||
@@ -1112,7 +1040,7 @@ class _AlwaysAvailableMixin:
|
||||
This particular class is not supported for 3rd party use
|
||||
"""
|
||||
|
||||
async def can_run(self, ctx, *args, **kwargs) -> bool:
|
||||
async def can_run(self, ctx, /, *args, **kwargs) -> bool:
|
||||
return not ctx.author.bot
|
||||
|
||||
can_see = can_run
|
||||
@@ -1161,7 +1089,7 @@ class _ForgetMeSpecialCommand(_RuleDropper, Command):
|
||||
We need special can_run behavior here
|
||||
"""
|
||||
|
||||
async def can_run(self, ctx, *args, **kwargs) -> bool:
|
||||
async def can_run(self, ctx, /, *args, **kwargs) -> bool:
|
||||
return await ctx.bot._config.datarequests.allow_user_requests()
|
||||
|
||||
can_see = can_run
|
||||
|
||||
@@ -11,7 +11,7 @@ from discord.ext.commands import Context as DPYContext
|
||||
from .requires import PermState
|
||||
from ..utils.chat_formatting import box
|
||||
from ..utils.predicates import MessagePredicate
|
||||
from ..utils import common_filters
|
||||
from ..utils import can_user_react_in, common_filters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .commands import Command
|
||||
@@ -139,7 +139,7 @@ class Context(DPYContext):
|
||||
:code:`True` if adding the reaction succeeded.
|
||||
"""
|
||||
try:
|
||||
if not self.channel.permissions_for(self.me).add_reactions:
|
||||
if not can_user_react_in(self.me, self.channel):
|
||||
raise RuntimeError
|
||||
await self.message.add_reaction(reaction)
|
||||
except (RuntimeError, discord.HTTPException):
|
||||
@@ -283,16 +283,6 @@ class Context(DPYContext):
|
||||
allowed_mentions=discord.AllowedMentions(everyone=False, roles=False, users=False),
|
||||
)
|
||||
|
||||
@property
|
||||
def clean_prefix(self) -> str:
|
||||
"""
|
||||
str: The command prefix, but with a sanitized version of the bot's mention if it was used as prefix.
|
||||
This can be used in a context where discord user mentions might not render properly.
|
||||
"""
|
||||
me = self.me
|
||||
pattern = re.compile(rf"<@!?{me.id}>")
|
||||
return pattern.sub(f"@{me.display_name}".replace("\\", r"\\"), self.prefix)
|
||||
|
||||
@property
|
||||
def me(self) -> Union[discord.ClientUser, discord.Member]:
|
||||
"""
|
||||
@@ -349,7 +339,7 @@ if TYPE_CHECKING or os.getenv("BUILDING_DOCS", False):
|
||||
...
|
||||
|
||||
@property
|
||||
def channel(self) -> discord.TextChannel:
|
||||
def channel(self) -> Union[discord.TextChannel, discord.Thread]:
|
||||
...
|
||||
|
||||
@property
|
||||
|
||||
@@ -19,7 +19,6 @@ from typing import (
|
||||
Dict,
|
||||
Type,
|
||||
TypeVar,
|
||||
Literal as Literal,
|
||||
Union as UserInputOptional,
|
||||
)
|
||||
|
||||
@@ -44,7 +43,6 @@ __all__ = [
|
||||
"get_timedelta_converter",
|
||||
"parse_relativedelta",
|
||||
"parse_timedelta",
|
||||
"Literal",
|
||||
"CommandConverter",
|
||||
"CogConverter",
|
||||
]
|
||||
@@ -281,7 +279,7 @@ else:
|
||||
Returns a typechecking safe `DictConverter` suitable for use with discord.py
|
||||
"""
|
||||
|
||||
class PartialMeta(type):
|
||||
class PartialMeta(type(DictConverter)):
|
||||
__call__ = functools.partialmethod(
|
||||
type(DictConverter).__call__, *expected_keys, delims=delims
|
||||
)
|
||||
@@ -389,7 +387,7 @@ else:
|
||||
The converter class, which will be a subclass of `TimedeltaConverter`
|
||||
"""
|
||||
|
||||
class PartialMeta(type):
|
||||
class PartialMeta(type(DictConverter)):
|
||||
__call__ = functools.partialmethod(
|
||||
type(DictConverter).__call__,
|
||||
allowed_units=allowed_units,
|
||||
@@ -475,44 +473,6 @@ if not TYPE_CHECKING:
|
||||
#: This converter class is still provisional.
|
||||
UserInputOptional = Optional
|
||||
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
|
||||
class Literal(dpy_commands.Converter):
|
||||
"""
|
||||
This can be used as a converter for `typing.Literal`.
|
||||
|
||||
In a type checking context it is `typing.Literal`.
|
||||
In a runtime context, it's a converter which only matches the literals it was given.
|
||||
|
||||
|
||||
.. warning::
|
||||
This converter class is still provisional.
|
||||
"""
|
||||
|
||||
def __init__(self, valid_names: Tuple[str]):
|
||||
self.valid_names = valid_names
|
||||
|
||||
def __call__(self, ctx, arg):
|
||||
# Callable's are treated as valid types:
|
||||
# https://github.com/python/cpython/blob/3.8/Lib/typing.py#L148
|
||||
# Without this, ``typing.Union[Literal["clear"], bool]`` would fail
|
||||
return self.convert(ctx, arg)
|
||||
|
||||
async def convert(self, ctx, arg):
|
||||
if arg in self.valid_names:
|
||||
return arg
|
||||
raise BadArgument(_("Expected one of: {}").format(humanize_list(self.valid_names)))
|
||||
|
||||
def __class_getitem__(cls, k):
|
||||
if not k:
|
||||
raise ValueError("Need at least one value for Literal")
|
||||
if isinstance(k, tuple):
|
||||
return cls(k)
|
||||
else:
|
||||
return cls((k,))
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
CommandConverter = dpy_commands.Command
|
||||
CogConverter = dpy_commands.Cog
|
||||
|
||||
@@ -3,12 +3,11 @@ import inspect
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
__all__ = [
|
||||
"ConversionFailure",
|
||||
__all__ = (
|
||||
"BotMissingPermissions",
|
||||
"UserFeedbackCheckFailure",
|
||||
"ArgParserFailure",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ConversionFailure(commands.BadArgument):
|
||||
|
||||
@@ -39,7 +39,7 @@ from discord.ext import commands as dpy_commands
|
||||
from . import commands
|
||||
from .context import Context
|
||||
from ..i18n import Translator
|
||||
from ..utils import menus
|
||||
from ..utils import can_user_react_in, menus
|
||||
from ..utils.mod import mass_purge
|
||||
from ..utils._internal_utils import fuzzy_command_search, format_fuzzy_results
|
||||
from ..utils.chat_formatting import (
|
||||
@@ -478,7 +478,7 @@ class RedHelpFormatter(HelpFormatterABC):
|
||||
|
||||
author_info = {
|
||||
"name": _("{ctx.me.display_name} Help Menu").format(ctx=ctx),
|
||||
"icon_url": ctx.me.avatar_url,
|
||||
"icon_url": ctx.me.display_avatar,
|
||||
}
|
||||
|
||||
# Offset calculation here is for total embed size limit
|
||||
@@ -733,7 +733,7 @@ class RedHelpFormatter(HelpFormatterABC):
|
||||
if use_embeds:
|
||||
ret.set_author(
|
||||
name=_("{ctx.me.display_name} Help Menu").format(ctx=ctx),
|
||||
icon_url=ctx.me.avatar_url,
|
||||
icon_url=ctx.me.display_avatar,
|
||||
)
|
||||
tagline = help_settings.tagline or self.get_default_tagline(ctx)
|
||||
ret.set_footer(text=tagline)
|
||||
@@ -746,7 +746,7 @@ class RedHelpFormatter(HelpFormatterABC):
|
||||
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
||||
ret.set_author(
|
||||
name=_("{ctx.me.display_name} Help Menu").format(ctx=ctx),
|
||||
icon_url=ctx.me.avatar_url,
|
||||
icon_url=ctx.me.display_avatar,
|
||||
)
|
||||
tagline = help_settings.tagline or self.get_default_tagline(ctx)
|
||||
ret.set_footer(text=tagline)
|
||||
@@ -765,7 +765,7 @@ class RedHelpFormatter(HelpFormatterABC):
|
||||
ret = discord.Embed(color=(await ctx.embed_color()), description=ret)
|
||||
ret.set_author(
|
||||
name=_("{ctx.me.display_name} Help Menu").format(ctx=ctx),
|
||||
icon_url=ctx.me.avatar_url,
|
||||
icon_url=ctx.me.display_avatar,
|
||||
)
|
||||
tagline = help_settings.tagline or self.get_default_tagline(ctx)
|
||||
ret.set_footer(text=tagline)
|
||||
@@ -813,15 +813,7 @@ class RedHelpFormatter(HelpFormatterABC):
|
||||
"""
|
||||
Sends pages based on settings.
|
||||
"""
|
||||
|
||||
# save on config calls
|
||||
channel_permissions = ctx.channel.permissions_for(ctx.me)
|
||||
|
||||
if not (
|
||||
channel_permissions.add_reactions
|
||||
and channel_permissions.read_message_history
|
||||
and help_settings.use_menus
|
||||
):
|
||||
if not (can_user_react_in(ctx.me, ctx.channel) and help_settings.use_menus):
|
||||
max_pages_in_guild = help_settings.max_pages_in_guild
|
||||
use_DMs = len(pages) > max_pages_in_guild
|
||||
destination = ctx.author if use_DMs else ctx.channel
|
||||
@@ -846,17 +838,18 @@ class RedHelpFormatter(HelpFormatterABC):
|
||||
if use_DMs and help_settings.use_tick:
|
||||
await ctx.tick()
|
||||
# The if statement takes into account that 'destination' will be
|
||||
# the context channel in non-DM context, reusing 'channel_permissions' to avoid
|
||||
# computing the permissions twice.
|
||||
# the context channel in non-DM context.
|
||||
if (
|
||||
not use_DMs # we're not in DMs
|
||||
and delete_delay > 0 # delete delay is enabled
|
||||
and channel_permissions.manage_messages # we can manage messages here
|
||||
and ctx.channel.permissions_for(ctx.me).manage_messages # we can manage messages
|
||||
):
|
||||
# We need to wrap this in a task to not block after-sending-help interactions.
|
||||
# The channel has to be TextChannel as we can't bulk-delete from DMs
|
||||
# The channel has to be TextChannel or Thread as we can't bulk-delete from DMs
|
||||
async def _delete_delay_help(
|
||||
channel: discord.TextChannel, messages: List[discord.Message], delay: int
|
||||
channel: Union[discord.TextChannel, discord.Thread],
|
||||
messages: List[discord.Message],
|
||||
delay: int,
|
||||
):
|
||||
await asyncio.sleep(delay)
|
||||
await mass_purge(messages, channel)
|
||||
|
||||
@@ -30,6 +30,8 @@ import discord
|
||||
from discord.ext.commands import check
|
||||
from .errors import BotMissingPermissions
|
||||
|
||||
from redbot.core import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .commands import Command
|
||||
from .context import Context
|
||||
@@ -48,14 +50,20 @@ __all__ = [
|
||||
"permissions_check",
|
||||
"bot_has_permissions",
|
||||
"bot_in_a_guild",
|
||||
"bot_can_manage_channel",
|
||||
"bot_can_react",
|
||||
"has_permissions",
|
||||
"can_manage_channel",
|
||||
"has_guild_permissions",
|
||||
"is_owner",
|
||||
"guildowner",
|
||||
"guildowner_or_can_manage_channel",
|
||||
"guildowner_or_permissions",
|
||||
"admin",
|
||||
"admin_or_can_manage_channel",
|
||||
"admin_or_permissions",
|
||||
"mod",
|
||||
"mod_or_can_manage_channel",
|
||||
"mod_or_permissions",
|
||||
"transition_permstate_to",
|
||||
"PermStateTransitions",
|
||||
@@ -135,12 +143,11 @@ class PrivilegeLevel(enum.IntEnum):
|
||||
# admin or mod role.
|
||||
guild_settings = ctx.bot._config.guild(ctx.guild)
|
||||
|
||||
member_snowflakes = ctx.author._roles # DEP-WARN
|
||||
for snowflake in await guild_settings.admin_role():
|
||||
if member_snowflakes.has(snowflake): # DEP-WARN
|
||||
if ctx.author.get_role(snowflake):
|
||||
return cls.ADMIN
|
||||
for snowflake in await guild_settings.mod_role():
|
||||
if member_snowflakes.has(snowflake): # DEP-WARN
|
||||
if ctx.author.get_role(snowflake):
|
||||
return cls.MOD
|
||||
|
||||
return cls.NONE
|
||||
@@ -596,7 +603,10 @@ class Requires:
|
||||
channels = []
|
||||
if author.voice is not None:
|
||||
channels.append(author.voice.channel)
|
||||
channels.append(ctx.channel)
|
||||
if isinstance(ctx.channel, discord.Thread):
|
||||
channels.append(ctx.channel.parent)
|
||||
else:
|
||||
channels.append(ctx.channel)
|
||||
category = ctx.channel.category
|
||||
if category is not None:
|
||||
channels.append(category)
|
||||
@@ -731,6 +741,77 @@ def bot_in_a_guild():
|
||||
return check(predicate)
|
||||
|
||||
|
||||
def bot_can_manage_channel(*, allow_thread_owner: bool = False) -> Callable[[_T], _T]:
|
||||
"""
|
||||
Complain if the bot is missing permissions to manage channel.
|
||||
|
||||
This check properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
allow_thread_owner: bool
|
||||
If ``True``, the command will also be allowed to run if the bot is a thread owner.
|
||||
This can, for example, be useful to check if the bot can edit a channel/thread's name
|
||||
as that, in addition to members with manage channel/threads permission,
|
||||
can also be done by the thread owner.
|
||||
"""
|
||||
|
||||
def predicate(ctx: "Context") -> bool:
|
||||
if ctx.guild is None:
|
||||
return False
|
||||
|
||||
if not utils.can_manage_channel_in(
|
||||
ctx.channel, ctx.me, allow_thread_owner=allow_thread_owner
|
||||
):
|
||||
if isinstance(ctx.channel, discord.Thread):
|
||||
# This is a slight lie - thread owner *might* also be allowed
|
||||
# but we just say that bot is missing the Manage Threads permission.
|
||||
missing = discord.Permissions(manage_threads=True)
|
||||
else:
|
||||
missing = discord.Permissions(manage_channels=True)
|
||||
raise BotMissingPermissions(missing=missing)
|
||||
|
||||
return True
|
||||
|
||||
return check(predicate)
|
||||
|
||||
|
||||
def bot_can_react() -> Callable[[_T], _T]:
|
||||
"""
|
||||
Complain if the bot is missing permissions to react.
|
||||
|
||||
This check properly resolves the permissions for `discord.Thread` as well.
|
||||
"""
|
||||
|
||||
async def predicate(ctx: "Context") -> bool:
|
||||
return not (isinstance(ctx.channel, discord.Thread) and ctx.channel.archived)
|
||||
|
||||
def decorator(func: _T) -> _T:
|
||||
func = bot_has_permissions(read_message_history=True, add_reactions=True)(func)
|
||||
func = check(predicate)(func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _can_manage_channel_deco(
|
||||
privilege_level: Optional[PrivilegeLevel] = None, allow_thread_owner: bool = False
|
||||
) -> Callable[[_T], _T]:
|
||||
async def predicate(ctx: "Context") -> bool:
|
||||
if utils.can_manage_channel_in(
|
||||
ctx.channel, ctx.author, allow_thread_owner=allow_thread_owner
|
||||
):
|
||||
return True
|
||||
|
||||
if privilege_level is not None:
|
||||
if await PrivilegeLevel.from_ctx(ctx) >= privilege_level:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return permissions_check(predicate)
|
||||
|
||||
|
||||
def has_permissions(**perms: bool):
|
||||
"""Restrict the command to users with these permissions.
|
||||
|
||||
@@ -741,6 +822,24 @@ def has_permissions(**perms: bool):
|
||||
return Requires.get_decorator(None, perms)
|
||||
|
||||
|
||||
def can_manage_channel(*, allow_thread_owner: bool = False) -> Callable[[_T], _T]:
|
||||
"""Restrict the command to users with permissions to manage channel.
|
||||
|
||||
This check properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
This check can be overridden by rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
allow_thread_owner: bool
|
||||
If ``True``, the command will also be allowed to run if the author is a thread owner.
|
||||
This can, for example, be useful to check if the author can edit a channel/thread's name
|
||||
as that, in addition to members with manage channel/threads permission,
|
||||
can also be done by the thread owner.
|
||||
"""
|
||||
return _can_manage_channel_deco(allow_thread_owner)
|
||||
|
||||
|
||||
def is_owner():
|
||||
"""Restrict the command to bot owners.
|
||||
|
||||
@@ -757,6 +856,24 @@ def guildowner_or_permissions(**perms: bool):
|
||||
return Requires.get_decorator(PrivilegeLevel.GUILD_OWNER, perms)
|
||||
|
||||
|
||||
def guildowner_or_can_manage_channel(*, allow_thread_owner: bool = False) -> Callable[[_T], _T]:
|
||||
"""Restrict the command to the guild owner or user with permissions to manage channel.
|
||||
|
||||
This check properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
This check can be overridden by rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
allow_thread_owner: bool
|
||||
If ``True``, the command will also be allowed to run if the author is a thread owner.
|
||||
This can, for example, be useful to check if the author can edit a channel/thread's name
|
||||
as that, in addition to members with manage channel/threads permission,
|
||||
can also be done by the thread owner.
|
||||
"""
|
||||
return _can_manage_channel_deco(PrivilegeLevel.GUILD_OWNER, allow_thread_owner)
|
||||
|
||||
|
||||
def guildowner():
|
||||
"""Restrict the command to the guild owner.
|
||||
|
||||
@@ -773,6 +890,24 @@ def admin_or_permissions(**perms: bool):
|
||||
return Requires.get_decorator(PrivilegeLevel.ADMIN, perms)
|
||||
|
||||
|
||||
def admin_or_can_manage_channel(*, allow_thread_owner: bool = False) -> Callable[[_T], _T]:
|
||||
"""Restrict the command to users with the admin role or permissions to manage channel.
|
||||
|
||||
This check properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
This check can be overridden by rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
allow_thread_owner: bool
|
||||
If ``True``, the command will also be allowed to run if the author is a thread owner.
|
||||
This can, for example, be useful to check if the author can edit a channel/thread's name
|
||||
as that, in addition to members with manage channel/threads permission,
|
||||
can also be done by the thread owner.
|
||||
"""
|
||||
return _can_manage_channel_deco(PrivilegeLevel.ADMIN, allow_thread_owner)
|
||||
|
||||
|
||||
def admin():
|
||||
"""Restrict the command to users with the admin role.
|
||||
|
||||
@@ -789,6 +924,24 @@ def mod_or_permissions(**perms: bool):
|
||||
return Requires.get_decorator(PrivilegeLevel.MOD, perms)
|
||||
|
||||
|
||||
def mod_or_can_manage_channel(*, allow_thread_owner: bool = False) -> Callable[[_T], _T]:
|
||||
"""Restrict the command to users with the mod role or permissions to manage channel.
|
||||
|
||||
This check properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
This check can be overridden by rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
allow_thread_owner: bool
|
||||
If ``True``, the command will also be allowed to run if the author is a thread owner.
|
||||
This can, for example, be useful to check if the author can edit a channel/thread's name
|
||||
as that, in addition to members with manage channel/threads permission,
|
||||
can also be done by the thread owner.
|
||||
"""
|
||||
return _can_manage_channel_deco(PrivilegeLevel.MOD, allow_thread_owner)
|
||||
|
||||
|
||||
def mod():
|
||||
"""Restrict the command to users with the mod role.
|
||||
|
||||
|
||||
@@ -1009,14 +1009,14 @@ class Config(metaclass=ConfigMeta):
|
||||
)
|
||||
return self._get_base_group(self.CHANNEL, str(channel_id))
|
||||
|
||||
def channel(self, channel: discord.abc.GuildChannel) -> Group:
|
||||
def channel(self, channel: Union[discord.abc.GuildChannel, discord.Thread]) -> Group:
|
||||
"""Returns a `Group` for the given channel.
|
||||
|
||||
This does not discriminate between text and voice channels.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
channel : `discord.abc.GuildChannel`
|
||||
channel : `discord.abc.GuildChannel` or `discord.Thread`
|
||||
A channel object.
|
||||
|
||||
Returns
|
||||
|
||||
@@ -39,7 +39,7 @@ from . import (
|
||||
modlog,
|
||||
)
|
||||
from ._diagnoser import IssueDiagnoser
|
||||
from .utils import AsyncIter
|
||||
from .utils import AsyncIter, can_user_send_messages_in
|
||||
from .utils._internal_utils import fetch_latest_red_version_info
|
||||
from .utils.predicates import MessagePredicate
|
||||
from .utils.chat_formatting import (
|
||||
@@ -275,7 +275,7 @@ class CoreLogic:
|
||||
|
||||
for name in pkg_names:
|
||||
if name in bot.extensions:
|
||||
bot.unload_extension(name)
|
||||
await bot.unload_extension(name)
|
||||
await bot.remove_loaded_package(name)
|
||||
unloaded_packages.append(name)
|
||||
else:
|
||||
@@ -318,7 +318,7 @@ class CoreLogic:
|
||||
The current (or new) username of the bot.
|
||||
"""
|
||||
if name is not None:
|
||||
await self.bot.user.edit(username=name)
|
||||
return (await self.bot.user.edit(username=name)).name
|
||||
|
||||
return self.bot.user.name
|
||||
|
||||
@@ -535,7 +535,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
uptime_str = humanize_timedelta(timedelta=delta) or _("Less than one second.")
|
||||
await ctx.send(
|
||||
_("I have been up for: **{time_quantity}** (since {timestamp})").format(
|
||||
time_quantity=uptime_str, timestamp=f"<t:{int(uptime.timestamp())}:f>"
|
||||
time_quantity=uptime_str, timestamp=discord.utils.format_dt(uptime, "f")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1366,6 +1366,15 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
**Arguments:**
|
||||
- `[enabled]` - Whether to use embeds in this channel. Leave blank to reset to default.
|
||||
"""
|
||||
if isinstance(ctx.channel, discord.Thread):
|
||||
await ctx.send(
|
||||
_(
|
||||
"This setting cannot be set for threads. If you want to set this for"
|
||||
" the parent channel, send the command in that channel."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if enabled is None:
|
||||
await self.bot._config.channel(ctx.channel).embeds.clear()
|
||||
await ctx.send(_("Embeds will now fall back to the global setting."))
|
||||
@@ -2403,7 +2412,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
"must be a valid image in either JPG or PNG format."
|
||||
)
|
||||
)
|
||||
except discord.InvalidArgument:
|
||||
except ValueError:
|
||||
await ctx.send(_("JPG / PNG format only."))
|
||||
else:
|
||||
await ctx.send(_("Done."))
|
||||
@@ -3211,7 +3220,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
|
||||
@_set_ownernotifications.command(name="adddestination")
|
||||
async def _set_ownernotifications_adddestination(
|
||||
self, ctx: commands.Context, *, channel: Union[discord.TextChannel, int]
|
||||
self, ctx: commands.Context, *, channel: discord.TextChannel
|
||||
):
|
||||
"""
|
||||
Adds a destination text channel to receive owner notifications.
|
||||
@@ -3223,15 +3232,9 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
**Arguments:**
|
||||
- `<channel>` - The channel to send owner notifications to.
|
||||
"""
|
||||
|
||||
try:
|
||||
channel_id = channel.id
|
||||
except AttributeError:
|
||||
channel_id = channel
|
||||
|
||||
async with ctx.bot._config.extra_owner_destinations() as extras:
|
||||
if channel_id not in extras:
|
||||
extras.append(channel_id)
|
||||
if channel.id not in extras:
|
||||
extras.append(channel.id)
|
||||
|
||||
await ctx.tick()
|
||||
|
||||
@@ -3982,12 +3985,8 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
color = await ctx.bot.get_embed_color(destination)
|
||||
|
||||
e = discord.Embed(colour=color, description=message)
|
||||
if author.avatar_url:
|
||||
e.set_author(name=description, icon_url=author.avatar_url)
|
||||
else:
|
||||
e.set_author(name=description)
|
||||
|
||||
e.set_footer(text="{}\n{}".format(footer, content))
|
||||
e.set_author(name=description, icon_url=author.display_avatar)
|
||||
e.set_footer(text=f"{footer}\n{content}")
|
||||
|
||||
try:
|
||||
await destination.send(embed=e)
|
||||
@@ -4057,10 +4056,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
e = discord.Embed(colour=discord.Colour.red(), description=message)
|
||||
|
||||
e.set_footer(text=content)
|
||||
if ctx.bot.user.avatar_url:
|
||||
e.set_author(name=description, icon_url=ctx.bot.user.avatar_url)
|
||||
else:
|
||||
e.set_author(name=description)
|
||||
e.set_author(name=description, icon_url=ctx.bot.user.display_avatar)
|
||||
|
||||
try:
|
||||
await destination.send(embed=e)
|
||||
@@ -4206,7 +4202,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
async def diagnoseissues(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
channel: Optional[discord.TextChannel],
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread]],
|
||||
member: Union[discord.Member, discord.User],
|
||||
*,
|
||||
command_name: str,
|
||||
@@ -4227,8 +4223,13 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
"""
|
||||
if channel is None:
|
||||
channel = ctx.channel
|
||||
if not isinstance(channel, discord.TextChannel):
|
||||
await ctx.send(_("The channel needs to be passed when using this command in DMs."))
|
||||
if not isinstance(channel, (discord.TextChannel, discord.Thread)):
|
||||
await ctx.send(
|
||||
_(
|
||||
"The text channel or thread needs to be passed"
|
||||
" when using this command in DMs."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
command = self.bot.get_command(command_name)
|
||||
@@ -4245,7 +4246,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
return
|
||||
member = maybe_member
|
||||
|
||||
if not channel.permissions_for(member).send_messages:
|
||||
if not can_user_send_messages_in(member, channel):
|
||||
# Let's make Flame happy here
|
||||
await ctx.send(
|
||||
_(
|
||||
@@ -5156,7 +5157,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
async def rpc_unload(self, request):
|
||||
cog_name = request.params[0]
|
||||
|
||||
self.bot.unload_extension(cog_name)
|
||||
await self.bot.unload_extension(cog_name)
|
||||
|
||||
async def rpc_reload(self, request):
|
||||
await self.rpc_unload(request)
|
||||
@@ -5164,7 +5165,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_channels=True)
|
||||
@commands.admin_or_can_manage_channel()
|
||||
async def ignore(self, ctx: commands.Context):
|
||||
"""
|
||||
Commands to add servers or channels to the ignore list.
|
||||
@@ -5189,12 +5190,14 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
async def ignore_channel(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
channel: Optional[Union[discord.TextChannel, discord.CategoryChannel]] = None,
|
||||
channel: Optional[
|
||||
Union[discord.TextChannel, discord.CategoryChannel, discord.Thread]
|
||||
] = None,
|
||||
):
|
||||
"""
|
||||
Ignore commands in the channel or category.
|
||||
Ignore commands in the channel, thread, or category.
|
||||
|
||||
Defaults to the current channel.
|
||||
Defaults to the current thread or channel.
|
||||
|
||||
Note: Owners, Admins, and those with Manage Channel permissions override ignored channels.
|
||||
|
||||
@@ -5205,7 +5208,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
- `[p]ignore channel 356236713347252226` - Also accepts IDs.
|
||||
|
||||
**Arguments:**
|
||||
- `<channel>` - The channel to ignore. Can be a category channel.
|
||||
- `<channel>` - The channel to ignore. This can also be a thread or category channel.
|
||||
"""
|
||||
if not channel:
|
||||
channel = ctx.channel
|
||||
@@ -5235,7 +5238,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_channels=True)
|
||||
@commands.admin_or_can_manage_channel()
|
||||
async def unignore(self, ctx: commands.Context):
|
||||
"""Commands to remove servers or channels from the ignore list."""
|
||||
|
||||
@@ -5243,12 +5246,14 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
async def unignore_channel(
|
||||
self,
|
||||
ctx: commands.Context,
|
||||
channel: Optional[Union[discord.TextChannel, discord.CategoryChannel]] = None,
|
||||
channel: Optional[
|
||||
Union[discord.TextChannel, discord.CategoryChannel, discord.Thread]
|
||||
] = None,
|
||||
):
|
||||
"""
|
||||
Remove a channel or category from the ignore list.
|
||||
Remove a channel, thread, or category from the ignore list.
|
||||
|
||||
Defaults to the current channel.
|
||||
Defaults to the current thread or channel.
|
||||
|
||||
**Examples:**
|
||||
- `[p]unignore channel #general` - Unignores commands in the #general channel.
|
||||
@@ -5257,7 +5262,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
- `[p]unignore channel 356236713347252226` - Also accepts IDs. Use this method to unignore categories.
|
||||
|
||||
**Arguments:**
|
||||
- `<channel>` - The channel to unignore. This can be a category channel.
|
||||
- `<channel>` - The channel to unignore. This can also be a thread or category channel.
|
||||
"""
|
||||
if not channel:
|
||||
channel = ctx.channel
|
||||
@@ -5287,6 +5292,7 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
async def count_ignored(self, ctx: commands.Context):
|
||||
category_channels: List[discord.CategoryChannel] = []
|
||||
text_channels: List[discord.TextChannel] = []
|
||||
threads: List[discord.Thread] = []
|
||||
if await self.bot._ignored_cache.get_ignored_guild(ctx.guild):
|
||||
return _("This server is currently being ignored.")
|
||||
for channel in ctx.guild.text_channels:
|
||||
@@ -5295,14 +5301,22 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
|
||||
category_channels.append(channel.category)
|
||||
if await self.bot._ignored_cache.get_ignored_channel(channel, check_category=False):
|
||||
text_channels.append(channel)
|
||||
for thread in ctx.guild.threads:
|
||||
if await self.bot_ignored_cache.get_ignored_channel(thread, check_category=False):
|
||||
threads.append(thread)
|
||||
|
||||
cat_str = (
|
||||
humanize_list([c.name for c in category_channels]) if category_channels else "None"
|
||||
humanize_list([c.name for c in category_channels]) if category_channels else _("None")
|
||||
)
|
||||
chan_str = humanize_list([c.mention for c in text_channels]) if text_channels else "None"
|
||||
msg = _("Currently ignored categories: {categories}\nChannels: {channels}").format(
|
||||
categories=cat_str, channels=chan_str
|
||||
chan_str = (
|
||||
humanize_list([c.mention for c in text_channels]) if text_channels else _("None")
|
||||
)
|
||||
thread_str = humanize_list([c.mention for c in threads]) if threads else _("None")
|
||||
msg = _(
|
||||
"Currently ignored categories: {categories}\n"
|
||||
"Channels: {channels}\n"
|
||||
"Threads (excluding archived):{threads}"
|
||||
).format(categories=cat_str, channels=chan_str, threads=thread_str)
|
||||
return msg
|
||||
|
||||
# Removing this command from forks is a violation of the GPLv3 under which it is licensed.
|
||||
|
||||
@@ -354,8 +354,7 @@ class Dev(commands.Cog):
|
||||
or anything else that makes the message non-empty.
|
||||
"""
|
||||
msg = ctx.message
|
||||
if not content and not msg.embeds and not msg.attachments:
|
||||
# DEP-WARN: add `msg.stickers` when adding d.py 2.0
|
||||
if not content and not msg.embeds and not msg.attachments and not msg.stickers:
|
||||
await ctx.send_help()
|
||||
return
|
||||
msg = copy(msg)
|
||||
|
||||
@@ -5,7 +5,7 @@ import sys
|
||||
import codecs
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
@@ -30,6 +30,7 @@ from .utils._internal_utils import (
|
||||
expected_version,
|
||||
fetch_latest_red_version_info,
|
||||
send_to_owners_with_prefix_replaced,
|
||||
get_converter,
|
||||
)
|
||||
from .utils.chat_formatting import inline, bordered, format_perms_list, humanize_timedelta
|
||||
|
||||
@@ -70,7 +71,7 @@ def init_events(bot, cli_flags):
|
||||
guilds = len(bot.guilds)
|
||||
users = len(set([m for m in bot.get_all_members()]))
|
||||
|
||||
invite_url = discord.utils.oauth_url(bot._app_info.id)
|
||||
invite_url = discord.utils.oauth_url(bot._app_info.id, scopes=("bot",))
|
||||
|
||||
prefixes = cli_flags.prefix or (await bot._config.prefix())
|
||||
lang = await bot._config.locale()
|
||||
@@ -219,7 +220,16 @@ def init_events(bot, cli_flags):
|
||||
await ctx.send(msg)
|
||||
if error.send_cmd_help:
|
||||
await ctx.send_help()
|
||||
elif isinstance(error, commands.ConversionFailure):
|
||||
elif isinstance(error, commands.BadArgument):
|
||||
if isinstance(error.__cause__, ValueError):
|
||||
converter = get_converter(ctx.current_parameter)
|
||||
argument = ctx.current_argument
|
||||
if converter is int:
|
||||
await ctx.send(_('"{argument}" is not an integer.').format(argument=argument))
|
||||
return
|
||||
if converter is float:
|
||||
await ctx.send(_('"{argument}" is not a number.').format(argument=argument))
|
||||
return
|
||||
if error.args:
|
||||
await ctx.send(error.args[0])
|
||||
else:
|
||||
@@ -330,7 +340,7 @@ def init_events(bot, cli_flags):
|
||||
log.exception(type(error).__name__, exc_info=error)
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
async def on_message(message, /):
|
||||
await set_contextual_locales_from_guild(bot, message.guild)
|
||||
|
||||
await bot.process_commands(message)
|
||||
@@ -339,7 +349,7 @@ def init_events(bot, cli_flags):
|
||||
not bot._checked_time_accuracy
|
||||
or (discord_now - timedelta(minutes=60)) > bot._checked_time_accuracy
|
||||
):
|
||||
system_now = datetime.utcnow()
|
||||
system_now = datetime.now(timezone.utc)
|
||||
diff = abs((discord_now - system_now).total_seconds())
|
||||
if diff > 60:
|
||||
log.warning(
|
||||
|
||||
@@ -106,7 +106,7 @@ async def _init(bot: Red):
|
||||
except RuntimeError:
|
||||
return # No modlog channel so no point in continuing
|
||||
|
||||
when = datetime.utcnow()
|
||||
when = datetime.now(timezone.utc)
|
||||
before = when + timedelta(minutes=1)
|
||||
after = when - timedelta(minutes=1)
|
||||
await asyncio.sleep(10) # prevent small delays from causing a 5 minute delay on entry
|
||||
@@ -116,9 +116,12 @@ async def _init(bot: Red):
|
||||
while attempts < 12 and guild.me.guild_permissions.view_audit_log:
|
||||
attempts += 1
|
||||
try:
|
||||
entry = await guild.audit_logs(
|
||||
action=discord.AuditLogAction.ban, before=before, after=after
|
||||
).find(lambda e: e.target.id == member.id and after < e.created_at < before)
|
||||
entry = await discord.utils.find(
|
||||
lambda e: e.target.id == member.id and after < e.created_at < before,
|
||||
guild.audit_logs(
|
||||
action=discord.AuditLogAction.ban, before=before, after=after
|
||||
),
|
||||
)
|
||||
except discord.Forbidden:
|
||||
break
|
||||
except discord.HTTPException:
|
||||
@@ -128,7 +131,7 @@ async def _init(bot: Red):
|
||||
if entry.user.id != guild.me.id:
|
||||
# Don't create modlog entires for the bot's own bans, cogs do this.
|
||||
mod, reason = entry.user, entry.reason
|
||||
date = entry.created_at.replace(tzinfo=timezone.utc)
|
||||
date = entry.created_at
|
||||
await create_case(_bot_ref, guild, date, "ban", member, mod, reason)
|
||||
return
|
||||
|
||||
@@ -143,7 +146,7 @@ async def _init(bot: Red):
|
||||
except RuntimeError:
|
||||
return # No modlog channel so no point in continuing
|
||||
|
||||
when = datetime.utcnow()
|
||||
when = datetime.now(timezone.utc)
|
||||
before = when + timedelta(minutes=1)
|
||||
after = when - timedelta(minutes=1)
|
||||
await asyncio.sleep(10) # prevent small delays from causing a 5 minute delay on entry
|
||||
@@ -153,9 +156,12 @@ async def _init(bot: Red):
|
||||
while attempts < 12 and guild.me.guild_permissions.view_audit_log:
|
||||
attempts += 1
|
||||
try:
|
||||
entry = await guild.audit_logs(
|
||||
action=discord.AuditLogAction.unban, before=before, after=after
|
||||
).find(lambda e: e.target.id == user.id and after < e.created_at < before)
|
||||
entry = await discord.utils.find(
|
||||
lambda e: e.target.id == user.id and after < e.created_at < before,
|
||||
guild.audit_logs(
|
||||
action=discord.AuditLogAction.unban, before=before, after=after
|
||||
),
|
||||
)
|
||||
except discord.Forbidden:
|
||||
break
|
||||
except discord.HTTPException:
|
||||
@@ -165,7 +171,7 @@ async def _init(bot: Red):
|
||||
if entry.user.id != guild.me.id:
|
||||
# Don't create modlog entires for the bot's own unbans, cogs do this.
|
||||
mod, reason = entry.user, entry.reason
|
||||
date = entry.created_at.replace(tzinfo=timezone.utc)
|
||||
date = entry.created_at
|
||||
await create_case(_bot_ref, guild, date, "unban", user, mod, reason)
|
||||
return
|
||||
|
||||
@@ -268,13 +274,16 @@ class Case:
|
||||
until: Optional[int]
|
||||
The UNIX time the action is in effect until.
|
||||
`None` if the action is permanent.
|
||||
channel: Optional[Union[discord.abc.GuildChannel, int]]
|
||||
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread, int]]
|
||||
The channel the action was taken in.
|
||||
`None` if the action was not related to a channel.
|
||||
|
||||
.. note::
|
||||
This attribute will be of type `int`
|
||||
if the channel seems to no longer exist.
|
||||
parent_channel_id: Optional[int]
|
||||
The parent channel ID of the thread in ``channel``.
|
||||
`None` if the action was not done in a thread.
|
||||
amended_by: Optional[Union[discord.abc.User, int]]
|
||||
The moderator who made the last change to the case.
|
||||
`None` if the case was never edited.
|
||||
@@ -310,7 +319,8 @@ class Case:
|
||||
case_number: int,
|
||||
reason: Optional[str] = None,
|
||||
until: Optional[int] = None,
|
||||
channel: Optional[Union[discord.abc.GuildChannel, int]] = None,
|
||||
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread, int]] = None,
|
||||
parent_channel_id: Optional[int] = None,
|
||||
amended_by: Optional[Union[discord.Object, discord.abc.User, int]] = None,
|
||||
modified_at: Optional[float] = None,
|
||||
message: Optional[Union[discord.PartialMessage, discord.Message]] = None,
|
||||
@@ -330,6 +340,7 @@ class Case:
|
||||
self.reason = reason
|
||||
self.until = until
|
||||
self.channel = channel
|
||||
self.parent_channel_id = parent_channel_id
|
||||
self.amended_by = amended_by
|
||||
if isinstance(amended_by, discord.Object):
|
||||
self.amended_by = amended_by.id
|
||||
@@ -337,6 +348,18 @@ class Case:
|
||||
self.case_number = case_number
|
||||
self.message = message
|
||||
|
||||
@property
|
||||
def parent_channel(self) -> Optional[discord.TextChannel]:
|
||||
"""
|
||||
The parent text channel of the thread in `channel`.
|
||||
|
||||
This will be `None` if `channel` is not a thread
|
||||
and when the parent text channel is not in cache (probably due to removal).
|
||||
"""
|
||||
if self.parent_channel_id is None:
|
||||
return None
|
||||
return self.guild.get_channel(self.parent_channel_id)
|
||||
|
||||
async def _set_message(self, message: discord.Message, /) -> None:
|
||||
# This should only be used for setting the message right after case creation
|
||||
# in order to avoid making an API request to "edit" the message with changes.
|
||||
@@ -359,6 +382,8 @@ class Case:
|
||||
# last username is set based on passed user object
|
||||
data.pop("last_known_username", None)
|
||||
for item, value in data.items():
|
||||
if item == "channel" and isinstance(value, discord.PartialMessageable):
|
||||
raise TypeError("Can't use PartialMessageable as the channel for a modlog case.")
|
||||
if isinstance(value, discord.Object):
|
||||
# probably expensive to call but meh should capture all cases
|
||||
setattr(self, item, value.id)
|
||||
@@ -369,6 +394,9 @@ class Case:
|
||||
if not isinstance(self.user, int):
|
||||
self.last_known_username = f"{self.user.name}#{self.user.discriminator}"
|
||||
|
||||
if isinstance(self.channel, discord.Thread):
|
||||
self.parent_channel_id = self.channel.parent_id
|
||||
|
||||
await _config.custom(_CASES, str(self.guild.id), str(self.case_number)).set(self.to_json())
|
||||
self.bot.dispatch("modlog_case_edit", self)
|
||||
if not self.message:
|
||||
@@ -443,7 +471,7 @@ class Case:
|
||||
if self.until:
|
||||
start = datetime.fromtimestamp(self.created_at, tz=timezone.utc)
|
||||
end = datetime.fromtimestamp(self.until, tz=timezone.utc)
|
||||
end_fmt = f"<t:{int(end.timestamp())}>"
|
||||
end_fmt = discord.utils.format_dt(end)
|
||||
duration = end - start
|
||||
dur_fmt = _strfdelta(duration)
|
||||
until = end_fmt
|
||||
@@ -463,7 +491,9 @@ class Case:
|
||||
|
||||
last_modified = None
|
||||
if self.modified_at:
|
||||
last_modified = f"<t:{int(self.modified_at)}>"
|
||||
last_modified = discord.utils.format_dt(
|
||||
datetime.fromtimestamp(self.modified_at, tz=timezone.utc)
|
||||
)
|
||||
|
||||
if isinstance(self.user, int):
|
||||
if self.user == 0xDE1:
|
||||
@@ -490,6 +520,31 @@ class Case:
|
||||
)
|
||||
) # Invites and spoilers get rendered even in embeds.
|
||||
|
||||
channel_value = None
|
||||
if isinstance(self.channel, int):
|
||||
if self.parent_channel_id is not None:
|
||||
if (parent_channel := self.parent_channel) is not None:
|
||||
channel_value = _(
|
||||
"Deleted or archived thread ({thread_id}) in {channel_name}"
|
||||
).format(thread_id=self.channel, channel_name=parent_channel)
|
||||
else:
|
||||
channel_value = _("Thread {thread_id} in {channel_id} (deleted)").format(
|
||||
thread_id=self.channel, channel_id=self.parent_channel_id
|
||||
)
|
||||
else:
|
||||
channel_value = _("{channel_id} (deleted)").format(channel_id=self.channel)
|
||||
elif self.channel is not None:
|
||||
channel_value = self.channel.name
|
||||
if self.parent_channel_id is not None:
|
||||
if (parent_channel := self.parent_channel) is not None:
|
||||
channel_value = _("Thread {thread_name} in {channel_name}").format(
|
||||
thread_name=self.channel, channel_name=parent_channel
|
||||
)
|
||||
else:
|
||||
channel_value = _("Thread {thread_name} in {channel_id} (deleted)").format(
|
||||
thread_name=self.channel, channel_id=self.parent_channel_id
|
||||
)
|
||||
|
||||
if embed:
|
||||
if self.reason:
|
||||
reason = f"{bold(_('Reason:'))} {self.reason}"
|
||||
@@ -510,20 +565,13 @@ class Case:
|
||||
if until and duration:
|
||||
emb.add_field(name=_("Until"), value=until)
|
||||
emb.add_field(name=_("Duration"), value=duration)
|
||||
|
||||
if isinstance(self.channel, int):
|
||||
emb.add_field(
|
||||
name=_("Channel"),
|
||||
value=_("{channel} (deleted)").format(channel=self.channel),
|
||||
inline=False,
|
||||
)
|
||||
elif self.channel is not None:
|
||||
emb.add_field(name=_("Channel"), value=self.channel.name, inline=False)
|
||||
if channel_value:
|
||||
emb.add_field(name=_("Channel"), value=channel_value, inline=False)
|
||||
if amended_by:
|
||||
emb.add_field(name=_("Amended by"), value=amended_by)
|
||||
if last_modified:
|
||||
emb.add_field(name=_("Last modified at"), value=last_modified)
|
||||
emb.timestamp = datetime.utcfromtimestamp(self.created_at)
|
||||
emb.timestamp = datetime.fromtimestamp(self.created_at, tz=timezone.utc)
|
||||
return emb
|
||||
else:
|
||||
if self.reason:
|
||||
@@ -549,9 +597,9 @@ class Case:
|
||||
case_text += f"{bold(_('Until:'))} {until}\n{bold(_('Duration:'))} {duration}\n"
|
||||
if self.channel:
|
||||
if isinstance(self.channel, int):
|
||||
case_text += f"{bold(_('Channel:'))} {self.channel} {_('(Deleted)')}\n"
|
||||
case_text += f"{bold(_('Channel:'))} {channel_value}\n"
|
||||
else:
|
||||
case_text += f"{bold(_('Channel:'))} {self.channel.name}\n"
|
||||
case_text += f"{bold(_('Channel:'))} {channel_value}\n"
|
||||
if amended_by:
|
||||
case_text += f"{bold(_('Amended by:'))} {amended_by}\n"
|
||||
if last_modified:
|
||||
@@ -590,6 +638,7 @@ class Case:
|
||||
"reason": self.reason,
|
||||
"until": self.until,
|
||||
"channel": self.channel.id if hasattr(self.channel, "id") else None,
|
||||
"parent_channel": self.parent_channel_id,
|
||||
"amended_by": amended_by,
|
||||
"modified_at": self.modified_at,
|
||||
"message": self.message.id if hasattr(self.message, "id") else None,
|
||||
@@ -650,7 +699,11 @@ class Case:
|
||||
user_object = bot.get_user(user_id) or user_id
|
||||
user_objects[user_key] = user_object
|
||||
|
||||
channel = kwargs.get("channel") or guild.get_channel(data["channel"]) or data["channel"]
|
||||
channel = (
|
||||
kwargs.get("channel")
|
||||
or guild.get_channel_or_thread(data["channel"])
|
||||
or data["channel"]
|
||||
)
|
||||
case_guild = kwargs.get("guild") or bot.get_guild(data["guild"])
|
||||
return cls(
|
||||
bot=bot,
|
||||
@@ -661,6 +714,7 @@ class Case:
|
||||
reason=data["reason"],
|
||||
until=data["until"],
|
||||
channel=channel,
|
||||
parent_channel_id=data.get("parent_channel_id"),
|
||||
modified_at=data["modified_at"],
|
||||
message=message,
|
||||
last_known_username=data.get("last_known_username"),
|
||||
@@ -917,7 +971,7 @@ async def create_case(
|
||||
moderator: Optional[Union[discord.Object, discord.abc.User, int]] = None,
|
||||
reason: Optional[str] = None,
|
||||
until: Optional[datetime] = None,
|
||||
channel: Optional[discord.abc.GuildChannel] = None,
|
||||
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread]] = None,
|
||||
last_known_username: Optional[str] = None,
|
||||
) -> Optional[Case]:
|
||||
"""
|
||||
@@ -947,12 +1001,17 @@ async def create_case(
|
||||
The time the action is in effect until.
|
||||
If naive `datetime` object is passed, it's treated as a local time
|
||||
(similarly to how Python treats naive `datetime` objects).
|
||||
channel: Optional[discord.abc.GuildChannel]
|
||||
channel: Optional[Union[discord.abc.GuildChannel, discord.Thread]]
|
||||
The channel the action was taken in
|
||||
last_known_username: Optional[str]
|
||||
The last known username of the user
|
||||
Note: This is ignored if a Member or User object is provided
|
||||
in the user field
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If ``channel`` is of type `discord.PartialMessageable`.
|
||||
"""
|
||||
case_type = await get_casetype(action_type, guild)
|
||||
if case_type is None:
|
||||
@@ -964,6 +1023,11 @@ async def create_case(
|
||||
if user == bot.user:
|
||||
return
|
||||
|
||||
if isinstance(channel, discord.PartialMessageable):
|
||||
raise TypeError("Can't use PartialMessageable as the channel for a modlog case.")
|
||||
|
||||
parent_channel_id = channel.parent_id if isinstance(channel, discord.Thread) else None
|
||||
|
||||
async with _config.guild(guild).latest_case_number.get_lock():
|
||||
# We're getting the case number from config, incrementing it, awaiting something, then
|
||||
# setting it again. This warrants acquiring the lock.
|
||||
@@ -980,6 +1044,7 @@ async def create_case(
|
||||
reason,
|
||||
int(until.timestamp()) if until else None,
|
||||
channel,
|
||||
parent_channel_id,
|
||||
amended_by=None,
|
||||
modified_at=None,
|
||||
message=None,
|
||||
|
||||
@@ -150,7 +150,7 @@ class IgnoreManager:
|
||||
self._cached_guilds: Dict[int, bool] = {}
|
||||
|
||||
async def get_ignored_channel(
|
||||
self, channel: discord.TextChannel, check_category: bool = True
|
||||
self, channel: Union[discord.TextChannel, discord.Thread], check_category: bool = True
|
||||
) -> bool:
|
||||
ret: bool
|
||||
|
||||
@@ -176,7 +176,9 @@ class IgnoreManager:
|
||||
return ret
|
||||
|
||||
async def set_ignored_channel(
|
||||
self, channel: Union[discord.TextChannel, discord.CategoryChannel], set_to: bool
|
||||
self,
|
||||
channel: Union[discord.TextChannel, discord.Thread, discord.CategoryChannel],
|
||||
set_to: bool,
|
||||
):
|
||||
cid: int = channel.id
|
||||
self._cached_channels[cid] = set_to
|
||||
|
||||
@@ -7,6 +7,7 @@ from asyncio.futures import isfuture
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterator,
|
||||
AsyncIterable,
|
||||
@@ -15,16 +16,27 @@ from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Literal,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
Generator,
|
||||
Coroutine,
|
||||
overload,
|
||||
)
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as dpy_commands
|
||||
from discord.utils import maybe_coroutine
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
if TYPE_CHECKING:
|
||||
GuildMessageable = Union[commands.GuildContext, discord.abc.GuildChannel, discord.Thread]
|
||||
DMMessageable = Union[commands.DMContext, discord.Member, discord.User, discord.DMChannel]
|
||||
|
||||
__all__ = (
|
||||
"bounded_gather",
|
||||
"bounded_gather_iter",
|
||||
@@ -32,6 +44,9 @@ __all__ = (
|
||||
"AsyncIter",
|
||||
"get_end_user_data_statement",
|
||||
"get_end_user_data_statement_or_raise",
|
||||
"can_user_send_messages_in",
|
||||
"can_user_manage_channel",
|
||||
"can_user_react_in",
|
||||
)
|
||||
|
||||
log = logging.getLogger("red.core.utils")
|
||||
@@ -532,7 +547,7 @@ def get_end_user_data_statement(file: Union[Path, str]) -> Optional[str]:
|
||||
>>> # In cog's `__init__.py`
|
||||
>>> from redbot.core.utils import get_end_user_data_statement
|
||||
>>> __red_end_user_data_statement__ = get_end_user_data_statement(__file__)
|
||||
>>> def setup(bot):
|
||||
>>> async def setup(bot):
|
||||
... ...
|
||||
"""
|
||||
try:
|
||||
@@ -590,3 +605,209 @@ def get_end_user_data_statement_or_raise(file: Union[Path, str]) -> str:
|
||||
info_json = file / "info.json"
|
||||
with info_json.open(encoding="utf-8") as fp:
|
||||
return json.load(fp)["end_user_data_statement"]
|
||||
|
||||
|
||||
@overload
|
||||
def can_user_send_messages_in(
|
||||
obj: discord.abc.User, messageable: discord.PartialMessageable, /
|
||||
) -> NoReturn:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def can_user_send_messages_in(obj: discord.Member, messageable: GuildMessageable, /) -> bool:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def can_user_send_messages_in(obj: discord.User, messageable: DMMessageable, /) -> Literal[True]:
|
||||
...
|
||||
|
||||
|
||||
def can_user_send_messages_in(
|
||||
obj: discord.abc.User, messageable: discord.abc.Messageable, /
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if a user/member can send messages in the given messageable.
|
||||
|
||||
This function properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
.. note::
|
||||
|
||||
Without making an API request, it is not possible to reliably detect
|
||||
whether a guild member (who is NOT current bot user) can send messages in a private thread.
|
||||
|
||||
If it's essential for you to reliably detect this, you will need to
|
||||
try fetching the thread member:
|
||||
|
||||
.. code::
|
||||
|
||||
can_send_messages = can_user_send_messages_in(member, thread)
|
||||
if thread.is_private() and not thread.permissions_for(member).manage_threads:
|
||||
try:
|
||||
await thread.fetch_member(member.id)
|
||||
except discord.NotFound:
|
||||
can_send_messages = False
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: discord.abc.User
|
||||
The user or member to check permissions for.
|
||||
If passed ``messageable`` resolves to a guild channel/thread,
|
||||
this needs to be an instance of `discord.Member`.
|
||||
messageable: discord.abc.Messageable
|
||||
The messageable object to check permissions for.
|
||||
If this resolves to a DM/group channel, this function will return ``True``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the user can send messages in the given messageable.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
When the passed channel is of type `discord.PartialMessageable`.
|
||||
"""
|
||||
channel = messageable.channel if isinstance(messageable, dpy_commands.Context) else messageable
|
||||
if isinstance(channel, discord.PartialMessageable):
|
||||
# If we have a partial messageable, we sadly can't do much...
|
||||
raise TypeError("Can't check permissions for PartialMessageable.")
|
||||
|
||||
if isinstance(channel, discord.abc.User):
|
||||
# Unlike DMChannel, abc.User subclasses do not have `permissions_for()`.
|
||||
return True
|
||||
|
||||
perms = channel.permissions_for(obj)
|
||||
if isinstance(channel, discord.Thread):
|
||||
return (
|
||||
perms.send_messages_in_threads
|
||||
and (not channel.locked or perms.manage_threads)
|
||||
# For private threads, the only way to know if user can send messages would be to check
|
||||
# if they're a member of it which we cannot reliably do without an API request.
|
||||
#
|
||||
# and (not channel.is_private() or "obj is thread member" or perms.manage_threads)
|
||||
)
|
||||
|
||||
return perms.send_messages
|
||||
|
||||
|
||||
def can_user_manage_channel(
|
||||
obj: discord.Member,
|
||||
channel: Union[discord.abc.GuildChannel, discord.Thread],
|
||||
/,
|
||||
allow_thread_owner: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if a guild member can manage the given channel.
|
||||
|
||||
This function properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: discord.Member
|
||||
The guild member to check permissions for.
|
||||
If passed ``messageable`` resolves to a guild channel/thread,
|
||||
this needs to be an instance of `discord.Member`.
|
||||
channel: Union[discord.abc.GuildChannel, discord.Thread]
|
||||
The messageable object to check permissions for.
|
||||
If this resolves to a DM/group channel, this function will return ``True``.
|
||||
allow_thread_owner: bool
|
||||
If ``True``, the function will also return ``True`` if the given member is a thread owner.
|
||||
This can, for example, be useful to check if the member can edit a channel/thread's name
|
||||
as that, in addition to members with manage channel/threads permission,
|
||||
can also be done by the thread owner.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the user can manage the given channel.
|
||||
"""
|
||||
perms = channel.permissions_for(obj)
|
||||
if isinstance(channel, discord.Thread):
|
||||
return perms.manage_threads or (allow_thread_owner and channel.owner_id == obj.id)
|
||||
|
||||
return perms.manage_channels
|
||||
|
||||
|
||||
@overload
|
||||
def can_user_react_in(
|
||||
obj: discord.abc.User, messageable: discord.PartialMessageable, /
|
||||
) -> NoReturn:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def can_user_react_in(obj: discord.Member, messageable: GuildMessageable, /) -> bool:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def can_user_react_in(obj: discord.User, messageable: DMMessageable, /) -> Literal[True]:
|
||||
...
|
||||
|
||||
|
||||
def can_user_react_in(obj: discord.abc.User, messageable: discord.abc.Messageable, /) -> bool:
|
||||
"""
|
||||
Checks if a user/guild member can react in the given messageable.
|
||||
|
||||
This function properly resolves the permissions for `discord.Thread` as well.
|
||||
|
||||
.. note::
|
||||
|
||||
Without making an API request, it is not possible to reliably detect
|
||||
whether a guild member (who is NOT current bot user) can react in a private thread.
|
||||
|
||||
If it's essential for you to reliably detect this, you will need to
|
||||
try fetching the thread member:
|
||||
|
||||
.. code::
|
||||
|
||||
can_react = can_user_react_in(member, thread)
|
||||
if thread.is_private() and not thread.permissions_for(member).manage_threads:
|
||||
try:
|
||||
await thread.fetch_member(member.id)
|
||||
except discord.NotFound:
|
||||
can_react = False
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: discord.abc.User
|
||||
The user or member to check permissions for.
|
||||
If passed ``messageable`` resolves to a guild channel/thread,
|
||||
this needs to be an instance of `discord.Member`.
|
||||
messageable: discord.abc.Messageable
|
||||
The messageable object to check permissions for.
|
||||
If this resolves to a DM/group channel, this function will return ``True``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the user can send messages in the given messageable.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
When the passed channel is of type `discord.PartialMessageable`.
|
||||
"""
|
||||
channel = messageable.channel if isinstance(messageable, dpy_commands.Context) else messageable
|
||||
if isinstance(channel, discord.PartialMessageable):
|
||||
# If we have a partial messageable, we sadly can't do much...
|
||||
raise TypeError("Can't check permissions for PartialMessageable.")
|
||||
|
||||
if isinstance(channel, discord.abc.User):
|
||||
# Unlike DMChannel, abc.User subclasses do not have `permissions_for()`.
|
||||
return True
|
||||
|
||||
perms = channel.permissions_for(obj)
|
||||
if isinstance(channel, discord.Thread):
|
||||
return (
|
||||
(perms.read_message_history and perms.add_reactions)
|
||||
and not channel.archived
|
||||
# For private threads, the only way to know if user can send messages would be to check
|
||||
# if they're a member of it which we cannot reliably do without an API request.
|
||||
#
|
||||
# and (not channel.is_private() or perms.manage_threads or "obj is thread member")
|
||||
)
|
||||
|
||||
return perms.read_message_history and perms.add_reactions
|
||||
|
||||
@@ -32,6 +32,7 @@ from typing import (
|
||||
import aiohttp
|
||||
import discord
|
||||
import pkg_resources
|
||||
from discord.ext.commands.converter import get_converter # DEP-WARN
|
||||
from fuzzywuzzy import fuzz, process
|
||||
from rich.progress import ProgressColumn
|
||||
from rich.progress_bar import ProgressBar
|
||||
@@ -59,6 +60,7 @@ __all__ = (
|
||||
"deprecated_removed",
|
||||
"RichIndefiniteBarColumn",
|
||||
"cli_level_to_log_level",
|
||||
"get_converter",
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
@@ -106,7 +106,10 @@ async def menu(
|
||||
if not ctx.me:
|
||||
return
|
||||
try:
|
||||
if message.channel.permissions_for(ctx.me).manage_messages:
|
||||
if (
|
||||
isinstance(message.channel, discord.PartialMessageable)
|
||||
or message.channel.permissions_for(ctx.me).manage_messages
|
||||
):
|
||||
await message.clear_reactions()
|
||||
else:
|
||||
raise RuntimeError
|
||||
|
||||
@@ -9,7 +9,9 @@ if TYPE_CHECKING:
|
||||
from ..commands import Context
|
||||
|
||||
|
||||
async def mass_purge(messages: List[discord.Message], channel: discord.TextChannel):
|
||||
async def mass_purge(
|
||||
messages: List[discord.Message], channel: Union[discord.TextChannel, discord.Thread]
|
||||
):
|
||||
"""Bulk delete messages from a channel.
|
||||
|
||||
If more than 100 messages are supplied, the bot will delete 100 messages at
|
||||
@@ -24,7 +26,7 @@ async def mass_purge(messages: List[discord.Message], channel: discord.TextChann
|
||||
----------
|
||||
messages : `list` of `discord.Message`
|
||||
The messages to bulk delete.
|
||||
channel : discord.TextChannel
|
||||
channel : `discord.TextChannel` or `discord.Thread`
|
||||
The channel to delete messages from.
|
||||
|
||||
Raises
|
||||
|
||||
@@ -67,7 +67,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def same_context(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the message fits the described context.
|
||||
@@ -76,7 +76,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
The current invocation context.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
The channel we expect a message in. If unspecified,
|
||||
defaults to ``ctx.channel``. If ``ctx`` is unspecified
|
||||
too, the message's channel will be ignored.
|
||||
@@ -104,7 +104,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def cancelled(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the message is ``[p]cancel``.
|
||||
@@ -113,7 +113,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -133,7 +133,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def yes_or_no(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the message is "yes"/"y" or "no"/"n".
|
||||
@@ -145,7 +145,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -176,7 +176,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def valid_int(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response is an integer.
|
||||
@@ -187,7 +187,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -216,7 +216,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def valid_float(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response is a float.
|
||||
@@ -227,7 +227,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -256,7 +256,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def positive(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response is a positive number.
|
||||
@@ -267,7 +267,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -300,7 +300,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def valid_role(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[discord.TextChannel] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response refers to a role in the current guild.
|
||||
@@ -313,7 +313,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -344,7 +344,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def valid_member(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[discord.TextChannel] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response refers to a member in the current guild.
|
||||
@@ -357,7 +357,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -392,7 +392,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def valid_text_channel(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[discord.TextChannel] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response refers to a text channel in the current guild.
|
||||
@@ -405,7 +405,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -440,7 +440,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
def has_role(
|
||||
cls,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[discord.TextChannel] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response refers to a role which the author has.
|
||||
@@ -454,7 +454,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
----------
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -492,7 +492,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
value: str,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response is equal to the specified value.
|
||||
@@ -503,7 +503,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The value to compare the response with.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -522,7 +522,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
value: str,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response *as lowercase* is equal to the specified value.
|
||||
@@ -533,7 +533,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The value to compare the response with.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -552,7 +552,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
value: Union[int, float],
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response is less than the specified value.
|
||||
@@ -563,7 +563,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The value to compare the response with.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -583,7 +583,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
value: Union[int, float],
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response is greater than the specified value.
|
||||
@@ -594,7 +594,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The value to compare the response with.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -614,7 +614,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
length: int,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response's length is less than the specified length.
|
||||
@@ -625,7 +625,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The value to compare the response's length with.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -644,7 +644,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
length: int,
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response's length is greater than the specified length.
|
||||
@@ -655,7 +655,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The value to compare the response's length with.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -674,7 +674,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
collection: Sequence[str],
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response is contained in the specified collection.
|
||||
@@ -688,7 +688,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The collection containing valid responses.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -718,7 +718,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
collection: Sequence[str],
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Same as :meth:`contained_in`, but the response is set to lowercase before matching.
|
||||
@@ -729,7 +729,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The collection containing valid lowercase responses.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -759,7 +759,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
cls,
|
||||
pattern: Union[Pattern[str], str],
|
||||
ctx: Optional[commands.Context] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.DMChannel]] = None,
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread, discord.DMChannel]] = None,
|
||||
user: Optional[discord.abc.User] = None,
|
||||
) -> "MessagePredicate":
|
||||
"""Match if the response matches the specified regex pattern.
|
||||
@@ -774,7 +774,7 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
The pattern to search for in the response.
|
||||
ctx : Optional[Context]
|
||||
Same as ``ctx`` in :meth:`same_context`.
|
||||
channel : Optional[discord.TextChannel]
|
||||
channel : Optional[Union[`discord.TextChannel`, `discord.Thread`, `discord.DMChannel`]]
|
||||
Same as ``channel`` in :meth:`same_context`.
|
||||
user : Optional[discord.abc.User]
|
||||
Same as ``user`` in :meth:`same_context`.
|
||||
@@ -815,7 +815,9 @@ class MessagePredicate(Callable[[discord.Message], bool]):
|
||||
|
||||
@staticmethod
|
||||
def _get_guild(
|
||||
ctx: commands.Context, channel: discord.TextChannel, user: discord.Member
|
||||
ctx: Optional[commands.Context],
|
||||
channel: Optional[Union[discord.TextChannel, discord.Thread]],
|
||||
user: Optional[discord.Member],
|
||||
) -> discord.Guild:
|
||||
if ctx is not None:
|
||||
return ctx.guild
|
||||
@@ -930,6 +932,7 @@ class ReactionPredicate(Callable[[discord.Reaction, discord.abc.User], bool]):
|
||||
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
# DEP-WARN
|
||||
me_id = message._state.self_id
|
||||
return cls(
|
||||
lambda self, r, u: u.id != me_id
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import datetime
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
import io
|
||||
import weakref
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Union
|
||||
from .common_filters import filter_mass_mentions
|
||||
|
||||
_instances = weakref.WeakValueDictionary({})
|
||||
@@ -57,14 +57,18 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
----------
|
||||
sender: `discord.Member`
|
||||
The person who opened the tunnel
|
||||
origin: `discord.TextChannel`
|
||||
origin: `discord.TextChannel` or `discord.Thread`
|
||||
The channel in which it was opened
|
||||
recipient: `discord.User`
|
||||
The user on the other end of the tunnel
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *, sender: discord.Member, origin: discord.TextChannel, recipient: discord.User
|
||||
self,
|
||||
*,
|
||||
sender: discord.Member,
|
||||
origin: Union[discord.TextChannel, discord.Thread],
|
||||
recipient: discord.User,
|
||||
):
|
||||
self.sender = sender
|
||||
self.origin = origin
|
||||
@@ -219,9 +223,9 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
the bot can't upload at the origin channel
|
||||
or can't add reactions there.
|
||||
"""
|
||||
if message.channel == self.origin and message.author == self.sender:
|
||||
if message.channel.id == self.origin.id and message.author == self.sender:
|
||||
send_to = self.recipient
|
||||
elif message.author == self.recipient and isinstance(message.channel, discord.DMChannel):
|
||||
elif message.author == self.recipient and message.guild is None:
|
||||
send_to = self.origin
|
||||
else:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user