mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
What's changed: - Fixed issues mentioned on #2031 - Fuzzy help displays more like help manual - Fuzzy help is easier and more flexible to use - Fuzzy help string-matching ratio lowered to 80 - Help formatter is more extendable - Help command has been optimized, cleaned up and better incorporates fuzzy help - Added async_filter and async_enumerate utility functions because I was using them for this PR, then no longer needed them, but decided they might be useful anyway. - Added `Context.me` property which is a shortcut to `Context.guild.me` or `Context.bot.user`, depending on the channel type.
290 lines
8.4 KiB
Python
290 lines
8.4 KiB
Python
"""Module for command helpers and classes.
|
|
|
|
This module contains extended classes and functions which are intended to
|
|
replace those from the `discord.ext.commands` module.
|
|
"""
|
|
import inspect
|
|
import weakref
|
|
from typing import Awaitable, Callable, TYPE_CHECKING
|
|
|
|
import discord
|
|
from discord.ext import commands
|
|
|
|
from .errors import ConversionFailure
|
|
from ..i18n import Translator
|
|
|
|
if TYPE_CHECKING:
|
|
from .context import Context
|
|
|
|
__all__ = ["Command", "GroupMixin", "Group", "command", "group"]
|
|
|
|
_ = Translator("commands.commands", __file__)
|
|
|
|
|
|
class Command(commands.Command):
|
|
"""Command class for Red.
|
|
|
|
This should not be created directly, and instead via the decorator.
|
|
|
|
This class inherits from `discord.ext.commands.Command`.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._help_override = kwargs.pop("help_override", None)
|
|
super().__init__(*args, **kwargs)
|
|
self.translator = kwargs.pop("i18n", None)
|
|
|
|
@property
|
|
def help(self):
|
|
"""Help string for this command.
|
|
|
|
If the :code:`help` kwarg was passed into the decorator, it will
|
|
default to that. If not, it will attempt to translate the docstring
|
|
of the command's callback function.
|
|
"""
|
|
if self._help_override is not None:
|
|
return self._help_override
|
|
if self.translator is None:
|
|
translator = lambda s: s
|
|
else:
|
|
translator = self.translator
|
|
command_doc = self.callback.__doc__
|
|
if command_doc is None:
|
|
return ""
|
|
return inspect.cleandoc(translator(command_doc))
|
|
|
|
@help.setter
|
|
def help(self, value):
|
|
# We don't want our help property to be overwritten, namely by super()
|
|
pass
|
|
|
|
@property
|
|
def parents(self):
|
|
"""
|
|
Returns all parent commands of this command.
|
|
|
|
This is a list, sorted by the length of :attr:`.qualified_name` from highest to lowest.
|
|
If the command has no parents, this will be an empty list.
|
|
"""
|
|
cmd = self.parent
|
|
entries = []
|
|
while cmd is not None:
|
|
entries.append(cmd)
|
|
cmd = cmd.parent
|
|
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
|
|
|
|
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 commands.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.
|
|
|
|
In short, this will verify whether the user can run the
|
|
command, and also whether the command is hidden or not.
|
|
|
|
Parameters
|
|
----------
|
|
ctx : `Context`
|
|
The invocation context to check with.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if this command is visible in the given context.
|
|
|
|
"""
|
|
for cmd in (self, *self.parents):
|
|
if cmd.hidden:
|
|
return False
|
|
try:
|
|
can_run = await self.can_run(ctx)
|
|
except commands.CheckFailure:
|
|
return False
|
|
else:
|
|
if can_run is False:
|
|
return False
|
|
|
|
return True
|
|
|
|
def disable_in(self, guild: discord.Guild) -> bool:
|
|
"""Disable this command in the given guild.
|
|
|
|
Parameters
|
|
----------
|
|
guild : discord.Guild
|
|
The guild to disable the command in.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if the command wasn't already disabled.
|
|
|
|
"""
|
|
disabler = get_command_disabler(guild)
|
|
if disabler in self.checks:
|
|
return False
|
|
else:
|
|
self.checks.append(disabler)
|
|
return True
|
|
|
|
def enable_in(self, guild: discord.Guild) -> bool:
|
|
"""Enable this command in the given guild.
|
|
|
|
Parameters
|
|
----------
|
|
guild : discord.Guild
|
|
The guild to enable the command in.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if the command wasn't already enabled.
|
|
|
|
"""
|
|
disabler = get_command_disabler(guild)
|
|
try:
|
|
self.checks.remove(disabler)
|
|
except ValueError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
class GroupMixin(commands.GroupMixin):
|
|
"""Mixin for `Group` and `Red` classes.
|
|
|
|
This class inherits from :class:`discord.ext.commands.GroupMixin`.
|
|
"""
|
|
|
|
def command(self, *args, **kwargs):
|
|
"""A shortcut decorator that invokes :func:`.command` and adds it to
|
|
the internal command list.
|
|
"""
|
|
|
|
def decorator(func):
|
|
result = command(*args, **kwargs)(func)
|
|
self.add_command(result)
|
|
return result
|
|
|
|
return decorator
|
|
|
|
def group(self, *args, **kwargs):
|
|
"""A shortcut decorator that invokes :func:`.group` and adds it to
|
|
the internal command list.
|
|
"""
|
|
|
|
def decorator(func):
|
|
result = group(*args, **kwargs)(func)
|
|
self.add_command(result)
|
|
return result
|
|
|
|
return decorator
|
|
|
|
|
|
class Group(GroupMixin, Command, commands.Group):
|
|
"""Group command class for Red.
|
|
|
|
This class inherits from `Command`, with :class:`GroupMixin` and
|
|
`discord.ext.commands.Group` mixed in.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.autohelp = kwargs.pop("autohelp", True)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
async def invoke(self, ctx):
|
|
view = ctx.view
|
|
previous = view.index
|
|
view.skip_ws()
|
|
trigger = view.get_word()
|
|
if trigger:
|
|
ctx.subcommand_passed = trigger
|
|
ctx.invoked_subcommand = self.all_commands.get(trigger, None)
|
|
view.index = previous
|
|
view.previous = previous
|
|
|
|
if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand:
|
|
if self.autohelp and not self.invoke_without_command:
|
|
await self._verify_checks(ctx)
|
|
await ctx.send_help()
|
|
elif self.invoke_without_command:
|
|
# So invoke_without_command when a subcommand of this group is invoked
|
|
# will skip the the invokation of *this* command. However, because of
|
|
# how our permissions system works, we don't want it to skip the checks
|
|
# as well.
|
|
await self._verify_checks(ctx)
|
|
|
|
await super().invoke(ctx)
|
|
|
|
|
|
# decorators
|
|
|
|
|
|
def command(name=None, cls=Command, **attrs):
|
|
"""A decorator which transforms an async function into a `Command`.
|
|
|
|
Same interface as `discord.ext.commands.command`.
|
|
"""
|
|
attrs["help_override"] = attrs.pop("help", None)
|
|
return commands.command(name, cls, **attrs)
|
|
|
|
|
|
def group(name=None, **attrs):
|
|
"""A decorator which transforms an async function into a `Group`.
|
|
|
|
Same interface as `discord.ext.commands.group`.
|
|
"""
|
|
return command(name, cls=Group, **attrs)
|
|
|
|
|
|
__command_disablers = weakref.WeakValueDictionary()
|
|
|
|
|
|
def get_command_disabler(guild: discord.Guild) -> Callable[["Context"], Awaitable[bool]]:
|
|
"""Get the command disabler for a guild.
|
|
|
|
A command disabler is a simple check predicate which returns
|
|
``False`` if the context is within the given guild.
|
|
"""
|
|
try:
|
|
return __command_disablers[guild]
|
|
except KeyError:
|
|
|
|
async def disabler(ctx: "Context") -> bool:
|
|
if ctx.guild == guild:
|
|
raise commands.DisabledCommand()
|
|
return True
|
|
|
|
__command_disablers[guild] = disabler
|
|
return disabler
|