fix conflicts
This commit is contained in:
@@ -6,7 +6,7 @@ discord.ext.commands
|
||||
|
||||
An extension module to facilitate creation of bot commands.
|
||||
|
||||
:copyright: (c) 2015-2020 Rapptz
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -110,6 +110,7 @@ class BotBase(GroupMixin):
|
||||
self.description = inspect.cleandoc(description) if description else ''
|
||||
self.owner_id = options.get('owner_id')
|
||||
self.owner_ids = options.get('owner_ids', set())
|
||||
self.strip_after_prefix = options.get('strip_after_prefix', False)
|
||||
|
||||
if self.owner_id and self.owner_ids:
|
||||
raise TypeError('Both owner_id and owner_ids are set.')
|
||||
@@ -190,9 +191,8 @@ class BotBase(GroupMixin):
|
||||
return
|
||||
|
||||
cog = context.cog
|
||||
if cog:
|
||||
if Cog._get_overridden_method(cog.cog_command_error) is not None:
|
||||
return
|
||||
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
|
||||
return
|
||||
|
||||
print('Ignoring exception in command {}:'.format(context.command), file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
@@ -656,7 +656,13 @@ class BotBase(GroupMixin):
|
||||
else:
|
||||
self.__extensions[key] = lib
|
||||
|
||||
def load_extension(self, name):
|
||||
def _resolve_name(self, name, package):
|
||||
try:
|
||||
return importlib.util.resolve_name(name, package)
|
||||
except ImportError:
|
||||
raise errors.ExtensionNotFound(name)
|
||||
|
||||
def load_extension(self, name, *, package=None):
|
||||
"""Loads an extension.
|
||||
|
||||
An extension is a python module that contains commands, cogs, or
|
||||
@@ -672,11 +678,19 @@ class BotBase(GroupMixin):
|
||||
The extension name to load. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when loading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
--------
|
||||
ExtensionNotFound
|
||||
The extension could not be imported.
|
||||
This is also raised if the name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
ExtensionAlreadyLoaded
|
||||
The extension is already loaded.
|
||||
NoEntryPointError
|
||||
@@ -685,6 +699,7 @@ class BotBase(GroupMixin):
|
||||
The extension or its setup function had an execution error.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
if name in self.__extensions:
|
||||
raise errors.ExtensionAlreadyLoaded(name)
|
||||
|
||||
@@ -694,7 +709,7 @@ class BotBase(GroupMixin):
|
||||
|
||||
self._load_from_module_spec(spec, name)
|
||||
|
||||
def unload_extension(self, name):
|
||||
def unload_extension(self, name, *, package=None):
|
||||
"""Unloads an extension.
|
||||
|
||||
When the extension is unloaded, all commands, listeners, and cogs are
|
||||
@@ -711,13 +726,23 @@ class BotBase(GroupMixin):
|
||||
The extension name to unload. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when unloading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
ExtensionNotFound
|
||||
The name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
ExtensionNotLoaded
|
||||
The extension was not loaded.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
lib = self.__extensions.get(name)
|
||||
if lib is None:
|
||||
raise errors.ExtensionNotLoaded(name)
|
||||
@@ -725,7 +750,7 @@ class BotBase(GroupMixin):
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
|
||||
def reload_extension(self, name):
|
||||
def reload_extension(self, name, *, package=None):
|
||||
"""Atomically reloads an extension.
|
||||
|
||||
This replaces the extension with the same extension, only refreshed. This is
|
||||
@@ -739,6 +764,12 @@ class BotBase(GroupMixin):
|
||||
The extension name to reload. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
package: Optional[:class:`str`]
|
||||
The package name to resolve relative imports with.
|
||||
This is required when reloading an extension using a relative path, e.g ``.foo.test``.
|
||||
Defaults to ``None``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Raises
|
||||
-------
|
||||
@@ -746,12 +777,15 @@ class BotBase(GroupMixin):
|
||||
The extension was not loaded.
|
||||
ExtensionNotFound
|
||||
The extension could not be imported.
|
||||
This is also raised if the name of the extension could not
|
||||
be resolved using the provided ``package`` parameter.
|
||||
NoEntryPointError
|
||||
The extension does not have a setup function.
|
||||
ExtensionFailed
|
||||
The extension setup function had an execution error.
|
||||
"""
|
||||
|
||||
name = self._resolve_name(name, package)
|
||||
lib = self.__extensions.get(name)
|
||||
if lib is None:
|
||||
raise errors.ExtensionNotLoaded(name)
|
||||
@@ -768,7 +802,7 @@ class BotBase(GroupMixin):
|
||||
self._remove_module_references(lib.__name__)
|
||||
self._call_module_finalizers(lib, name)
|
||||
self.load_extension(name)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# if the load failed, the remnants should have been
|
||||
# cleaned from the load_extension function call
|
||||
# so let's load it from our old compiled library.
|
||||
@@ -920,6 +954,9 @@ class BotBase(GroupMixin):
|
||||
# Getting here shouldn't happen
|
||||
raise
|
||||
|
||||
if self.strip_after_prefix:
|
||||
view.skip_ws()
|
||||
|
||||
invoker = view.get_word()
|
||||
ctx.invoked_with = invoker
|
||||
ctx.prefix = invoked_prefix
|
||||
@@ -1054,6 +1091,12 @@ class Bot(BotBase, discord.Client):
|
||||
for the collection. You cannot set both ``owner_id`` and ``owner_ids``.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
strip_after_prefix: :class:`bool`
|
||||
Whether to strip whitespace characters after encountering the command
|
||||
prefix. This allows for ``! hello`` and ``!hello`` to both work if
|
||||
the ``command_prefix`` is set to ``!``. Defaults to ``False``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -101,7 +101,7 @@ class CogMeta(type):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
name, bases, attrs = args
|
||||
attrs['__cog_name__'] = kwargs.pop('name', name)
|
||||
attrs['__cog_settings__'] = command_attrs = kwargs.pop('command_attrs', {})
|
||||
attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
|
||||
|
||||
aliases = kwargs.pop('aliases', [])
|
||||
if not isinstance(aliases, list):
|
||||
@@ -136,7 +136,7 @@ class CogMeta(type):
|
||||
commands[elem] = value
|
||||
elif inspect.iscoroutinefunction(value):
|
||||
try:
|
||||
is_listener = getattr(value, '__cog_listener__')
|
||||
getattr(value, '__cog_listener__')
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
@@ -202,7 +202,7 @@ class Cog(metaclass=CogMeta):
|
||||
parent = lookup[parent.qualified_name]
|
||||
|
||||
# Update our parent's reference to our self
|
||||
removed = parent.remove_command(command.name)
|
||||
parent.remove_command(command.name)
|
||||
parent.add_command(command)
|
||||
|
||||
return self
|
||||
@@ -306,6 +306,13 @@ class Cog(metaclass=CogMeta):
|
||||
return func
|
||||
return decorator
|
||||
|
||||
def has_error_handler(self):
|
||||
""":class:`bool`: Checks whether the cog has an error handler.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return not hasattr(self.cog_command_error.__func__, '__cog_special_method__')
|
||||
|
||||
@_cog_special_method
|
||||
def cog_unload(self):
|
||||
"""A special method that is called when the cog gets removed.
|
||||
@@ -411,7 +418,8 @@ class Cog(metaclass=CogMeta):
|
||||
except Exception as e:
|
||||
# undo our additions
|
||||
for to_undo in self.__cog_commands__[:index]:
|
||||
bot.remove_command(to_undo.name)
|
||||
if to_undo.parent is None:
|
||||
bot.remove_command(to_undo.name)
|
||||
raise e
|
||||
|
||||
# check if we're overriding the default
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -58,6 +58,14 @@ class Context(discord.abc.Messageable):
|
||||
invoked_with: :class:`str`
|
||||
The command name that triggered this invocation. Useful for finding out
|
||||
which alias called the command.
|
||||
invoked_parents: List[:class:`str`]
|
||||
The command names of the parents that triggered this invocation. Useful for
|
||||
finding out which aliases called the command.
|
||||
|
||||
For example in commands ``?a b c test``, the invoked parents are ``['a', 'b', 'c']``.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
invoked_subcommand: :class:`Command`
|
||||
The subcommand that was invoked.
|
||||
If no valid subcommand was invoked then this is equal to ``None``.
|
||||
@@ -80,6 +88,7 @@ class Context(discord.abc.Messageable):
|
||||
self.command = attrs.pop('command', None)
|
||||
self.view = attrs.pop('view', None)
|
||||
self.invoked_with = attrs.pop('invoked_with', None)
|
||||
self.invoked_parents = attrs.pop('invoked_parents', [])
|
||||
self.invoked_subcommand = attrs.pop('invoked_subcommand', None)
|
||||
self.subcommand_passed = attrs.pop('subcommand_passed', None)
|
||||
self.command_failed = attrs.pop('command_failed', False)
|
||||
@@ -184,13 +193,15 @@ class Context(discord.abc.Messageable):
|
||||
index, previous = view.index, view.previous
|
||||
invoked_with = self.invoked_with
|
||||
invoked_subcommand = self.invoked_subcommand
|
||||
invoked_parents = self.invoked_parents
|
||||
subcommand_passed = self.subcommand_passed
|
||||
|
||||
if restart:
|
||||
to_call = cmd.root_parent or cmd
|
||||
view.index = len(self.prefix)
|
||||
view.previous = 0
|
||||
view.get_word() # advance to get the root command
|
||||
self.invoked_parents = []
|
||||
self.invoked_with = view.get_word() # advance to get the root command
|
||||
else:
|
||||
to_call = cmd
|
||||
|
||||
@@ -202,6 +213,7 @@ class Context(discord.abc.Messageable):
|
||||
view.previous = previous
|
||||
self.invoked_with = invoked_with
|
||||
self.invoked_subcommand = invoked_subcommand
|
||||
self.invoked_parents = invoked_parents
|
||||
self.subcommand_passed = subcommand_passed
|
||||
|
||||
@property
|
||||
@@ -214,7 +226,7 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
""":class:`.Cog`: Returns the cog associated with this context's command. None if it does not exist."""
|
||||
"""Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist."""
|
||||
|
||||
if self.command is None:
|
||||
return None
|
||||
@@ -227,8 +239,8 @@ class Context(discord.abc.Messageable):
|
||||
|
||||
@discord.utils.cached_property
|
||||
def channel(self):
|
||||
""":class:`.TextChannel`:
|
||||
Returns the channel associated with this context's command. Shorthand for :attr:`.Message.channel`.
|
||||
"""Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command.
|
||||
Shorthand for :attr:`.Message.channel`.
|
||||
"""
|
||||
return self.message.channel
|
||||
|
||||
@@ -311,7 +323,7 @@ class Context(discord.abc.Messageable):
|
||||
entity = bot.get_cog(entity) or bot.get_command(entity)
|
||||
|
||||
try:
|
||||
qualified_name = entity.qualified_name
|
||||
entity.qualified_name
|
||||
except AttributeError:
|
||||
# if we're here then it's not a cog, group, or command.
|
||||
return None
|
||||
@@ -333,7 +345,6 @@ class Context(discord.abc.Messageable):
|
||||
except CommandError as e:
|
||||
await cmd.on_help_command_error(self, e)
|
||||
|
||||
@discord.utils.copy_doc(discord.Message.reply)
|
||||
async def reply(self, content=None, **kwargs):
|
||||
return await self.message.reply(content, **kwargs)
|
||||
|
||||
reply.__doc__ = discord.Message.reply.__doc__
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -37,17 +37,21 @@ __all__ = (
|
||||
'MemberConverter',
|
||||
'UserConverter',
|
||||
'MessageConverter',
|
||||
'PartialMessageConverter',
|
||||
'TextChannelConverter',
|
||||
'InviteConverter',
|
||||
'GuildConverter',
|
||||
'RoleConverter',
|
||||
'GameConverter',
|
||||
'ColourConverter',
|
||||
'ColorConverter',
|
||||
'VoiceChannelConverter',
|
||||
'StageChannelConverter',
|
||||
'EmojiConverter',
|
||||
'PartialEmojiConverter',
|
||||
'CategoryChannelConverter',
|
||||
'IDConverter',
|
||||
'StoreChannelConverter',
|
||||
'clean_content',
|
||||
'Greedy',
|
||||
)
|
||||
@@ -100,7 +104,7 @@ class Converter:
|
||||
|
||||
class IDConverter(Converter):
|
||||
def __init__(self):
|
||||
self._id_regex = re.compile(r'([0-9]{15,21})$')
|
||||
self._id_regex = re.compile(r'([0-9]{15,20})$')
|
||||
super().__init__()
|
||||
|
||||
def _get_id_match(self, argument):
|
||||
@@ -251,7 +255,38 @@ class UserConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class MessageConverter(Converter):
|
||||
class PartialMessageConverter(Converter):
|
||||
"""Converts to a :class:`discord.PartialMessage`.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
The creation strategy is as follows (in order):
|
||||
|
||||
1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
|
||||
2. By message ID (The message is assumed to be in the context channel.)
|
||||
3. By message URL
|
||||
"""
|
||||
def _get_id_matches(self, argument):
|
||||
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
|
||||
link_regex = re.compile(
|
||||
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
|
||||
r'(?:[0-9]{15,20}|@me)'
|
||||
r'/(?P<channel_id>[0-9]{15,20})/(?P<message_id>[0-9]{15,20})/?$'
|
||||
)
|
||||
match = id_regex.match(argument) or link_regex.match(argument)
|
||||
if not match:
|
||||
raise MessageNotFound(argument)
|
||||
channel_id = match.group("channel_id")
|
||||
return int(match.group("message_id")), int(channel_id) if channel_id else None
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
message_id, channel_id = self._get_id_matches(argument)
|
||||
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||
if not channel:
|
||||
raise ChannelNotFound(channel_id)
|
||||
return discord.PartialMessage(channel=channel, id=message_id)
|
||||
|
||||
class MessageConverter(PartialMessageConverter):
|
||||
"""Converts to a :class:`discord.Message`.
|
||||
|
||||
.. versionadded:: 1.1
|
||||
@@ -266,21 +301,11 @@ class MessageConverter(Converter):
|
||||
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,21})-)?(?P<message_id>[0-9]{15,21})$')
|
||||
link_regex = re.compile(
|
||||
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
|
||||
r'(?:[0-9]{15,21}|@me)'
|
||||
r'/(?P<channel_id>[0-9]{15,21})/(?P<message_id>[0-9]{15,21})/?$'
|
||||
)
|
||||
match = id_regex.match(argument) or link_regex.match(argument)
|
||||
if not match:
|
||||
raise MessageNotFound(argument)
|
||||
message_id = int(match.group("message_id"))
|
||||
channel_id = match.group("channel_id")
|
||||
message_id, channel_id = self._get_id_matches(argument)
|
||||
message = ctx.bot._connection._get_message(message_id)
|
||||
if message:
|
||||
return message
|
||||
channel = ctx.bot.get_channel(int(channel_id)) if channel_id else ctx.channel
|
||||
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
|
||||
if not channel:
|
||||
raise ChannelNotFound(channel_id)
|
||||
try:
|
||||
@@ -373,6 +398,46 @@ class VoiceChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class StageChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.StageChannel`.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.stage_channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.StageChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.StageChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class CategoryChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.CategoryChannel`.
|
||||
|
||||
@@ -415,6 +480,47 @@ class CategoryChannelConverter(IDConverter):
|
||||
|
||||
return result
|
||||
|
||||
class StoreChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.StoreChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.StoreChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.StoreChannel):
|
||||
raise ChannelNotFound(argument)
|
||||
|
||||
return result
|
||||
|
||||
class ColourConverter(Converter):
|
||||
"""Converts to a :class:`~discord.Colour`.
|
||||
|
||||
@@ -426,37 +532,84 @@ class ColourConverter(Converter):
|
||||
- ``0x<hex>``
|
||||
- ``#<hex>``
|
||||
- ``0x#<hex>``
|
||||
- ``rgb(<number>, <number>, <number>)``
|
||||
- Any of the ``classmethod`` in :class:`Colour`
|
||||
|
||||
- The ``_`` in the name can be optionally replaced with spaces.
|
||||
|
||||
Like CSS, ``<number>`` can be either 0-255 or 0-100% and ``<hex>`` can be
|
||||
either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff).
|
||||
|
||||
.. versionchanged:: 1.5
|
||||
Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument`
|
||||
"""
|
||||
async def convert(self, ctx, argument):
|
||||
arg = argument.replace('0x', '').lower()
|
||||
|
||||
if arg[0] == '#':
|
||||
arg = arg[1:]
|
||||
.. versionchanged:: 1.7
|
||||
Added support for ``rgb`` function and 3-digit hex shortcuts
|
||||
"""
|
||||
|
||||
RGB_REGEX = re.compile(r'rgb\s*\((?P<r>[0-9]{1,3}%?)\s*,\s*(?P<g>[0-9]{1,3}%?)\s*,\s*(?P<b>[0-9]{1,3}%?)\s*\)')
|
||||
|
||||
def parse_hex_number(self, argument):
|
||||
arg = ''.join(i * 2 for i in argument) if len(argument) == 3 else argument
|
||||
try:
|
||||
value = int(arg, base=16)
|
||||
if not (0 <= value <= 0xFFFFFF):
|
||||
raise BadColourArgument(arg)
|
||||
return discord.Colour(value=value)
|
||||
raise BadColourArgument(argument)
|
||||
except ValueError:
|
||||
arg = arg.replace(' ', '_')
|
||||
method = getattr(discord.Colour, arg, None)
|
||||
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||
raise BadColourArgument(arg)
|
||||
return method()
|
||||
raise BadColourArgument(argument)
|
||||
else:
|
||||
return discord.Color(value=value)
|
||||
|
||||
def parse_rgb_number(self, argument, number):
|
||||
if number[-1] == '%':
|
||||
value = int(number[:-1])
|
||||
if not (0 <= value <= 100):
|
||||
raise BadColourArgument(argument)
|
||||
return round(255 * (value / 100))
|
||||
|
||||
value = int(number)
|
||||
if not (0 <= value <= 255):
|
||||
raise BadColourArgument(argument)
|
||||
return value
|
||||
|
||||
def parse_rgb(self, argument, *, regex=RGB_REGEX):
|
||||
match = regex.match(argument)
|
||||
if match is None:
|
||||
raise BadColourArgument(argument)
|
||||
|
||||
red = self.parse_rgb_number(argument, match.group('r'))
|
||||
green = self.parse_rgb_number(argument, match.group('g'))
|
||||
blue = self.parse_rgb_number(argument, match.group('b'))
|
||||
return discord.Color.from_rgb(red, green, blue)
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
if argument[0] == '#':
|
||||
return self.parse_hex_number(argument[1:])
|
||||
|
||||
if argument[0:2] == '0x':
|
||||
rest = argument[2:]
|
||||
# Legacy backwards compatible syntax
|
||||
if rest.startswith('#'):
|
||||
return self.parse_hex_number(rest[1:])
|
||||
return self.parse_hex_number(rest)
|
||||
|
||||
arg = argument.lower()
|
||||
if arg[0:3] == 'rgb':
|
||||
return self.parse_rgb(arg)
|
||||
|
||||
arg = arg.replace(' ', '_')
|
||||
method = getattr(discord.Colour, arg, None)
|
||||
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||
raise BadColourArgument(arg)
|
||||
return method()
|
||||
|
||||
ColorConverter = ColourConverter
|
||||
|
||||
class RoleConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.Role`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
All lookups are via the local guild. If in a DM context, the converter raises
|
||||
:exc:`.NoPrivateMessage` exception.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
@@ -502,6 +655,32 @@ class InviteConverter(Converter):
|
||||
except Exception as exc:
|
||||
raise BadInviteArgument() from exc
|
||||
|
||||
class GuildConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.Guild`.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names).
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument)
|
||||
result = None
|
||||
|
||||
if match is not None:
|
||||
guild_id = int(match.group(1))
|
||||
result = ctx.bot.get_guild(guild_id)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(ctx.bot.guilds, name=argument)
|
||||
|
||||
if result is None:
|
||||
raise GuildNotFound(argument)
|
||||
return result
|
||||
|
||||
class EmojiConverter(IDConverter):
|
||||
"""Converts to a :class:`~discord.Emoji`.
|
||||
|
||||
@@ -580,11 +759,16 @@ class clean_content(Converter):
|
||||
Whether to use nicknames when transforming mentions.
|
||||
escape_markdown: :class:`bool`
|
||||
Whether to also escape special markdown characters.
|
||||
remove_markdown: :class:`bool`
|
||||
Whether to also remove special markdown characters. This option is not supported with ``escape_markdown``
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False):
|
||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False, remove_markdown=False):
|
||||
self.fix_channel_mentions = fix_channel_mentions
|
||||
self.use_nicknames = use_nicknames
|
||||
self.escape_markdown = escape_markdown
|
||||
self.remove_markdown = remove_markdown
|
||||
|
||||
async def convert(self, ctx, argument):
|
||||
message = ctx.message
|
||||
@@ -635,6 +819,8 @@ class clean_content(Converter):
|
||||
|
||||
if self.escape_markdown:
|
||||
result = discord.utils.escape_markdown(result)
|
||||
elif self.remove_markdown:
|
||||
result = discord.utils.remove_markdown(result)
|
||||
|
||||
# Completely ensure no mentions escape:
|
||||
return discord.utils.escape_mentions(result)
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -66,6 +66,9 @@ class BucketType(Enum):
|
||||
# recieving a DMChannel or GroupChannel which inherit from PrivateChannel and do
|
||||
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id
|
||||
|
||||
def __call__(self, msg):
|
||||
return self.get_key(msg)
|
||||
|
||||
|
||||
class Cooldown:
|
||||
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
|
||||
@@ -78,8 +81,8 @@ class Cooldown:
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
if not isinstance(self.type, BucketType):
|
||||
raise TypeError('Cooldown type must be a BucketType')
|
||||
if not callable(self.type):
|
||||
raise TypeError('Cooldown type must be a BucketType or callable')
|
||||
|
||||
def get_tokens(self, current=None):
|
||||
if not current:
|
||||
@@ -151,7 +154,7 @@ class CooldownMapping:
|
||||
return cls(Cooldown(rate, per, type))
|
||||
|
||||
def _bucket_key(self, msg):
|
||||
return self._cooldown.type.get_key(msg)
|
||||
return self._cooldown.type(msg)
|
||||
|
||||
def _verify_cache_integrity(self, current=None):
|
||||
# we want to delete all cache objects that haven't been used
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -158,7 +158,7 @@ class Command(_BaseCommand):
|
||||
isn't one.
|
||||
cog: Optional[:class:`Cog`]
|
||||
The cog that this command belongs to. ``None`` if there isn't one.
|
||||
checks: List[Callable[..., :class:`bool`]]
|
||||
checks: List[Callable[[:class:`.Context`], :class:`bool`]]
|
||||
A list of predicates that verifies if the command could be executed
|
||||
with the given :class:`.Context` as the sole parameter. If an exception
|
||||
is necessary to be thrown to signal failure, then one inherited from
|
||||
@@ -523,7 +523,7 @@ class Command(_BaseCommand):
|
||||
# The greedy converter is simple -- it keeps going until it fails in which case,
|
||||
# it undos the view ready for the next parameter to use instead
|
||||
if type(converter) is converters._Greedy:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
||||
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
return await self._transform_greedy_var_pos(ctx, param, converter.converter)
|
||||
@@ -693,7 +693,7 @@ class Command(_BaseCommand):
|
||||
raise discord.ClientException(fmt.format(self))
|
||||
|
||||
for name, param in iterator:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD:
|
||||
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
|
||||
transformed = await self.transform(ctx, param)
|
||||
args.append(transformed)
|
||||
elif param.kind == param.KEYWORD_ONLY:
|
||||
@@ -715,9 +715,8 @@ class Command(_BaseCommand):
|
||||
except RuntimeError:
|
||||
break
|
||||
|
||||
if not self.ignore_extra:
|
||||
if not view.eof:
|
||||
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
|
||||
if not self.ignore_extra and not view.eof:
|
||||
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
|
||||
|
||||
async def call_before_hooks(self, ctx):
|
||||
# now that we're done preparing we can call the pre-command hooks
|
||||
@@ -904,6 +903,13 @@ class Command(_BaseCommand):
|
||||
self.on_error = coro
|
||||
return coro
|
||||
|
||||
def has_error_handler(self):
|
||||
""":class:`bool`: Checks whether the command has an error handler registered.
|
||||
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
return hasattr(self, 'on_error')
|
||||
|
||||
def before_invoke(self, coro):
|
||||
"""A decorator that registers a coroutine as a pre-invoke hook.
|
||||
|
||||
@@ -1335,6 +1341,8 @@ class Group(GroupMixin, Command):
|
||||
injected = hooked_wrapped_callback(self, ctx, self.callback)
|
||||
await injected(*ctx.args, **ctx.kwargs)
|
||||
|
||||
ctx.invoked_parents.append(ctx.invoked_with)
|
||||
|
||||
if trigger and ctx.invoked_subcommand:
|
||||
ctx.invoked_with = trigger
|
||||
await ctx.invoked_subcommand.invoke(ctx)
|
||||
@@ -1373,6 +1381,8 @@ class Group(GroupMixin, Command):
|
||||
if call_hooks:
|
||||
await self.call_after_hooks(ctx)
|
||||
|
||||
ctx.invoked_parents.append(ctx.invoked_with)
|
||||
|
||||
if trigger and ctx.invoked_subcommand:
|
||||
ctx.invoked_with = trigger
|
||||
await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks)
|
||||
@@ -1949,8 +1959,11 @@ def cooldown(rate, per, type=BucketType.default):
|
||||
The number of times a command can be used before triggering a cooldown.
|
||||
per: :class:`float`
|
||||
The amount of seconds to wait for a cooldown when it's been triggered.
|
||||
type: :class:`.BucketType`
|
||||
The type of cooldown to have.
|
||||
type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]]
|
||||
The type of cooldown to have. If callable, should return a key for the mapping.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
Callables are now supported for custom bucket types.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -45,6 +45,7 @@ __all__ = (
|
||||
'NotOwner',
|
||||
'MessageNotFound',
|
||||
'MemberNotFound',
|
||||
'GuildNotFound',
|
||||
'UserNotFound',
|
||||
'ChannelNotFound',
|
||||
'ChannelNotReadable',
|
||||
@@ -230,6 +231,22 @@ class MemberNotFound(BadArgument):
|
||||
self.argument = argument
|
||||
super().__init__('Member "{}" not found.'.format(argument))
|
||||
|
||||
class GuildNotFound(BadArgument):
|
||||
"""Exception raised when the guild provided was not found in the bot's cache.
|
||||
|
||||
This inherits from :exc:`BadArgument`
|
||||
|
||||
.. versionadded:: 1.7
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
argument: :class:`str`
|
||||
The guild supplied by the called that was not found
|
||||
"""
|
||||
def __init__(self, argument):
|
||||
self.argument = argument
|
||||
super().__init__('Guild "{}" not found.'.format(argument))
|
||||
|
||||
class UserNotFound(BadArgument):
|
||||
"""Exception raised when the user provided was not found in the bot's
|
||||
cache.
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
@@ -79,18 +79,22 @@ class Paginator:
|
||||
The suffix appended at the end of every page. e.g. three backticks.
|
||||
max_size: :class:`int`
|
||||
The maximum amount of codepoints allowed in a page.
|
||||
linesep: :class:`str`
|
||||
The character string inserted between lines. e.g. a newline character.
|
||||
.. versionadded:: 1.7
|
||||
"""
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000):
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
self.max_size = max_size
|
||||
self.linesep = linesep
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
"""Clears the paginator to have no pages."""
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + newline
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@@ -104,6 +108,10 @@ class Paginator:
|
||||
def _suffix_len(self):
|
||||
return len(self.suffix) if self.suffix else 0
|
||||
|
||||
@property
|
||||
def _linesep_len(self):
|
||||
return len(self.linesep)
|
||||
|
||||
def add_line(self, line='', *, empty=False):
|
||||
"""Adds a line to the current page.
|
||||
|
||||
@@ -122,29 +130,29 @@ class Paginator:
|
||||
RuntimeError
|
||||
The line was too big for the current :attr:`max_size`.
|
||||
"""
|
||||
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2
|
||||
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len
|
||||
if len(line) > max_page_size:
|
||||
raise RuntimeError('Line exceeds maximum page size %s' % (max_page_size))
|
||||
|
||||
if self._count + len(line) + 1 > self.max_size - self._suffix_len:
|
||||
if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len:
|
||||
self.close_page()
|
||||
|
||||
self._count += len(line) + 1
|
||||
self._count += len(line) + self._linesep_len
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append('')
|
||||
self._count += 1
|
||||
self._count += self._linesep_len
|
||||
|
||||
def close_page(self):
|
||||
"""Prematurely terminate a page."""
|
||||
if self.suffix is not None:
|
||||
self._current_page.append(self.suffix)
|
||||
self._pages.append('\n'.join(self._current_page))
|
||||
self._pages.append(self.linesep.join(self._current_page))
|
||||
|
||||
if self.prefix is not None:
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
|
||||
else:
|
||||
self._current_page = []
|
||||
self._count = 0
|
||||
@@ -162,7 +170,7 @@ class Paginator:
|
||||
return self._pages
|
||||
|
||||
def __repr__(self):
|
||||
fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
|
||||
fmt = '<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>'
|
||||
return fmt.format(self)
|
||||
|
||||
def _not_overriden(f):
|
||||
@@ -264,9 +272,13 @@ class HelpCommand:
|
||||
show_hidden: :class:`bool`
|
||||
Specifies if hidden commands should be shown in the output.
|
||||
Defaults to ``False``.
|
||||
verify_checks: :class:`bool`
|
||||
verify_checks: Optional[:class:`bool`]
|
||||
Specifies if commands should have their :attr:`.Command.checks` called
|
||||
and verified. Defaults to ``True``.
|
||||
and verified. If ``True``, always calls :attr:`.Commands.checks`.
|
||||
If ``None``, only calls :attr:`.Commands.checks` in a guild setting.
|
||||
If ``False``, never calls :attr:`.Commands.checks`. Defaults to ``True``.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
command_attrs: :class:`dict`
|
||||
A dictionary of options to pass in for the construction of the help command.
|
||||
This allows you to change the command behaviour without actually changing
|
||||
@@ -309,7 +321,7 @@ class HelpCommand:
|
||||
attrs.setdefault('name', 'help')
|
||||
attrs.setdefault('help', 'Shows this message')
|
||||
self.context = None
|
||||
self._command_impl = None
|
||||
self._command_impl = _HelpCommandImpl(self, **self.command_attrs)
|
||||
|
||||
def copy(self):
|
||||
obj = self.__class__(*self.__original_args__, **self.__original_kwargs__)
|
||||
@@ -324,7 +336,6 @@ class HelpCommand:
|
||||
def _remove_from_bot(self, bot):
|
||||
bot.remove_command(self._command_impl.name)
|
||||
self._command_impl._eject_cog()
|
||||
self._command_impl = None
|
||||
|
||||
def add_check(self, func):
|
||||
"""
|
||||
@@ -338,13 +349,7 @@ class HelpCommand:
|
||||
The function that will be used as a check.
|
||||
"""
|
||||
|
||||
if self._command_impl is not None:
|
||||
self._command_impl.add_check(func)
|
||||
else:
|
||||
try:
|
||||
self.command_attrs["checks"].append(func)
|
||||
except KeyError:
|
||||
self.command_attrs["checks"] = [func]
|
||||
self._command_impl.add_check(func)
|
||||
|
||||
def remove_check(self, func):
|
||||
"""
|
||||
@@ -361,13 +366,7 @@ class HelpCommand:
|
||||
The function to remove from the checks.
|
||||
"""
|
||||
|
||||
if self._command_impl is not None:
|
||||
self._command_impl.remove_check(func)
|
||||
else:
|
||||
try:
|
||||
self.command_attrs["checks"].remove(func)
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
self._command_impl.remove_check(func)
|
||||
|
||||
def get_bot_mapping(self):
|
||||
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
|
||||
@@ -376,7 +375,7 @@ class HelpCommand:
|
||||
cog: cog.get_commands()
|
||||
for cog in bot.cogs.values()
|
||||
}
|
||||
mapping[None] = [c for c in bot.all_commands.values() if c.cog is None]
|
||||
mapping[None] = [c for c in bot.commands if c.cog is None]
|
||||
return mapping
|
||||
|
||||
@property
|
||||
@@ -425,15 +424,24 @@ class HelpCommand:
|
||||
The signature for the command.
|
||||
"""
|
||||
|
||||
parent = command.full_parent_name
|
||||
parent = command.parent
|
||||
entries = []
|
||||
while parent is not None:
|
||||
if not parent.signature or parent.invoke_without_command:
|
||||
entries.append(parent.name)
|
||||
else:
|
||||
entries.append(parent.name + ' ' + parent.signature)
|
||||
parent = parent.parent
|
||||
parent_sig = ' '.join(reversed(entries))
|
||||
|
||||
if len(command.aliases) > 0:
|
||||
aliases = '|'.join(command.aliases)
|
||||
fmt = '[%s|%s]' % (command.name, aliases)
|
||||
if parent:
|
||||
fmt = parent + ' ' + fmt
|
||||
if parent_sig:
|
||||
fmt = parent_sig + ' ' + fmt
|
||||
alias = fmt
|
||||
else:
|
||||
alias = command.name if not parent else parent + ' ' + command.name
|
||||
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
|
||||
|
||||
return '%s%s %s' % (self.clean_prefix, alias, command.signature)
|
||||
|
||||
@@ -560,11 +568,15 @@ class HelpCommand:
|
||||
|
||||
iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
|
||||
|
||||
if not self.verify_checks:
|
||||
if self.verify_checks is False:
|
||||
# if we do not need to verify the checks then we can just
|
||||
# run it straight through normally without using await.
|
||||
return sorted(iterator, key=key) if sort else list(iterator)
|
||||
|
||||
if self.verify_checks is None and not self.context.guild:
|
||||
# if verify_checks is None and we're in a DM, don't verify
|
||||
return sorted(iterator, key=key) if sort else list(iterator)
|
||||
|
||||
# if we're here then we need to check every command if it can run
|
||||
async def predicate(cmd):
|
||||
try:
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Rapptz
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
|
Reference in New Issue
Block a user