Migration to discord.py 2.0 (#5600)

* Temporarily set d.py to use latest git revision

* Remove `bot` param to Client.start

* Switch to aware datetimes

A lot of this is removing `.replace(...)` which while not technically
needed, simplifies the code base. There's only a few changes that are
actually necessary here.

* Update to work with new Asset design

* [threads] Update core ModLog API to support threads

- Added proper support for passing `Thread` to `channel`
  when creating/editing case
- Added `parent_channel_id` attribute to Modlog API's Case
    - Added `parent_channel` property that tries to get parent channel
- Updated case's content to show both thread and parent information

* [threads] Disallow usage of threads in some of the commands

- announceset channel
- filter channel clear
- filter channel add
- filter channel remove
- GlobalUniqueObjectFinder converter
    - permissions addglobalrule
    - permissions removeglobalrule
    - permissions removeserverrule
    - Permissions cog does not perform any validation for IDs
      when setting through YAML so that has not been touched
- streamalert twitch/youtube/picarto
- embedset channel
- set ownernotifications adddestination

* [threads] Handle threads in Red's permissions system (Requires)

- Made permissions system apply rules of (only) parent in threads

* [threads] Update embed_requested to support threads

- Threads don't have their own embed settings and inherit from parent

* [threads] Update Red.message_eligible_as_command to support threads

* [threads] Properly handle invocation of [p](un)mutechannel in threads

Usage of a (un)mutechannel will mute/unmute user in the parent channel
if it's invoked in a thread.

* [threads] Update Filter cog to properly handle threads

- `[p]filter channel list` in a threads sends list for parent channel
- Checking for filter hits for a message in a thread checks its parent
  channel's word list. There's no separate word list for threads.

* [threads] Support threads in Audio cog

- Handle threads being notify channels
- Update type hint for `is_query_allowed()`

* [threads] Update type hints and documentation to reflect thread support

- Documented that `{channel}` in CCs might be a thread
- Allowed (documented) usage of threads with `Config.channel()`
    - Separate thread scope is still in the picture though
      if it were to be done, it's going to be in separate in PR
- GuildContext.channel might be Thread

* Use less costy channel check in customcom's on_message_without_command

This isn't needed for d.py 2.0 but whatever...

* Update for in-place edits

* Embed's bool changed behavior, I'm hoping it doesn't affect us

* Address User.permissions_in() removal

* Swap VerificationLevel.extreme with VerificationLevel.highest

* Change to keyword-only parameters

* Change of `Guild.vanity_invite()` return type

* avatar -> display_avatar

* Fix metaclass shenanigans with Converter

* Update Red.add_cog() to be inline with `dpy_commands.Bot.add_cog()`

This means adding `override` keyword-only parameter and causing
small breakage by swapping RuntimeError with discord.ClientException.

* Address all DEP-WARNs

* Remove Context.clean_prefix and use upstream implementation instead

* Remove commands.Literal and use upstream implementation instead

Honestly, this was a rather bad implementation anyway...

Breaking but actually not really - it was provisional.

* Update Command.callback's setter

Support for functools.partial is now built into d.py

* Add new perms in HUMANIZED_PERM mapping (some from d.py 1.7 it seems)

BTW, that should really be in core instead of what we have now...

* Remove the part of do_conversion that has not worked for a long while

* Stop wrapping BadArgument in ConversionFailure

This is breaking but it's best to resolve it like this.

The functionality of ConversionFailure can be replicated with
Context.current_parameter and Context.current_argument.

* Add custom errors for int and float converters

* Remove Command.__call__ as it's now implemented in d.py

* Get rid of _dpy_reimplements

These were reimplemented for the purpose of typing
so it is no longer needed now that d.py is type hinted.

* Add return to Red.remove_cog

* Ensure we don't delete messages that differ only by used sticker

* discord.InvalidArgument->ValueError

* Move from raw <t:...> syntax to discord.utils.format_dt()

* Address AsyncIter removal

* Swap to pos-only for params that are pos-only in upstream

* Update for changes to Command.params

* [threads] Support threads in ignore checks and allow ignoring them

- Updated `[p](un)ignore channel` to accept threads
- Updated `[p]ignore list` to list ignored threads
- Updated logic in `Red.ignored_channel_or_guild()`

Ignores for guild channels now work as follows (only changes for threads):
- if channel is not a thread:
    - check if user has manage channels perm in channel
      and allow command usage if so
    - check if channel is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened
- if channel is a thread:
    - check if user has manage channels perm in parent channel
      and allow command usage if so
    - check if parent channel is ignored and disallow command usage
      if so
    - check if user has manage thread perm in parent channel
      and allow command usage if so
    - check if thread is ignored and disallow command usage if so
    - allow command usage if none of the conditions above happened

* [partial] Raise TypeError when channel is of PartialMessageable type

- Red.embed_requested
- Red.ignored_channel_or_guild

* [partial] Discard command messages when channel is PartialMessageable

* [threads] Add utilities for checking appropriate perms in both channels & threads

* [threads] Update code to use can_react_in() and @bot_can_react()

* [threads] Update code to use can_send_messages_in

* [threads] Add send_messages_in_threads perm to mute role and overrides

* [threads] Update code to use (bot/user)_can_manage_channel

* [threads] Update [p]diagnoseissues to work with threads

* Type hint fix

* [threads] Patch vendored discord.ext.menus to check proper perms in threads

I guess we've reached time when we have to patch the lib we vendor...

* Make docs generation work with non-final d.py releases

* Update discord.utils.oauth_url() usage

* Swap usage of discord.Embed.Empty/discord.embeds.EmptyEmbed to None

* Update usage of Guild.member_count to work with `None`

* Switch from Guild.vanity_invite() to Guild.vanity_url

* Update startup process to work with d.py's new asynchronous startup

* Use setup_hook() for pre-connect actions

* Update core's add_cog, remove_cog, and load_extension methods

* Update all setup functions to async and add awaits to bot.add_cog calls

* Modernize cogs by using async cog_load and cog_unload

* Address StoreChannel removal

* [partial] Disallow passing PartialMessageable to Case.channel

* [partial] Update cogs and utils to work better with PartialMessageable

- Ignore messages with PartialMessageable channel in CustomCommands cog
- In Filter cog, don't pass channel to modlog.create_case()
  if it's PartialMessageable
- In Trivia cog, only compare channel IDs
- Make `.utils.menus.menu()` work for messages
  with PartialMessageable channel
- Make checks in `.utils.tunnel.Tunnel.communicate()` more rigid

* Add few missing DEP-WARNs
This commit is contained in:
jack1142
2022-04-03 03:21:20 +02:00
committed by GitHub
parent c9a0971945
commit febca8ccbb
104 changed files with 1427 additions and 999 deletions

View File

@@ -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,
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,11 @@ import inspect
import discord
from discord.ext import commands
__all__ = [
"ConversionFailure",
__all__ = (
"BotMissingPermissions",
"UserFeedbackCheckFailure",
"ArgParserFailure",
]
)
class ConversionFailure(commands.BadArgument):

View File

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

View File

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