7 changed files with 1069 additions and 0 deletions
@ -0,0 +1,12 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
""" |
||||
|
discord.py extensions |
||||
|
~~~~~~~~~~~~~~~~~~~~~~ |
||||
|
|
||||
|
Extensions for the discord.py library live in this namespace. |
||||
|
|
||||
|
:copyright: (c) 2016 Rapptz |
||||
|
:license: MIT, see LICENSE for more details. |
||||
|
|
||||
|
""" |
@ -0,0 +1,16 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
""" |
||||
|
discord.ext.commands |
||||
|
~~~~~~~~~~~~~~~~~~~~~ |
||||
|
|
||||
|
An extension module to facilitate creation of bot commands. |
||||
|
|
||||
|
:copyright: (c) 2016 Rapptz |
||||
|
:license: MIT, see LICENSE for more details. |
||||
|
""" |
||||
|
|
||||
|
from .bot import Bot |
||||
|
from .context import Context |
||||
|
from .core import * |
||||
|
from .errors import * |
@ -0,0 +1,221 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-2016 Rapptz |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a |
||||
|
copy of this software and associated documentation files (the "Software"), |
||||
|
to deal in the Software without restriction, including without limitation |
||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
|
and/or sell copies of the Software, and to permit persons to whom the |
||||
|
Software is furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
|
DEALINGS IN THE SOFTWARE. |
||||
|
""" |
||||
|
|
||||
|
import asyncio |
||||
|
import discord |
||||
|
import inspect |
||||
|
|
||||
|
from .core import GroupMixin |
||||
|
from .view import StringView |
||||
|
from .context import Context |
||||
|
|
||||
|
class Bot(GroupMixin, discord.Client): |
||||
|
"""Represents a discord bot. |
||||
|
|
||||
|
This class is a subclass of :class:`discord.Client` and as a result |
||||
|
anything that you can do with a :class:`discord.Client` you can do with |
||||
|
this bot. |
||||
|
|
||||
|
This class also subclasses :class:`GroupMixin` to provide the functionality |
||||
|
to manage commands. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
command_prefix |
||||
|
The command prefix is what the message content must contain initially |
||||
|
to have a command invoked. This prefix could either be a string to |
||||
|
indicate what the prefix should be, or a callable that takes in a |
||||
|
:class:`discord.Message` as its first parameter and returns the prefix. |
||||
|
This is to facilitate "dynamic" command prefixes. |
||||
|
""" |
||||
|
def __init__(self, command_prefix, **options): |
||||
|
super().__init__(**options) |
||||
|
self.command_prefix = command_prefix |
||||
|
|
||||
|
def _get_variable(self, name): |
||||
|
stack = inspect.stack() |
||||
|
for frames in stack: |
||||
|
current_locals = frames[0].f_locals |
||||
|
if name in current_locals: |
||||
|
return current_locals[name] |
||||
|
|
||||
|
def _get_prefix(self, message): |
||||
|
prefix = self.command_prefix |
||||
|
if callable(prefix): |
||||
|
return prefix(message) |
||||
|
else: |
||||
|
return prefix |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def say(self, content): |
||||
|
"""|coro| |
||||
|
|
||||
|
A helper function that is equivalent to doing |
||||
|
|
||||
|
.. code-block:: python |
||||
|
|
||||
|
self.send_message(message.channel, content) |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
content : str |
||||
|
The content to pass to :class:`Client.send_message` |
||||
|
""" |
||||
|
destination = self._get_variable('_internal_channel') |
||||
|
result = yield from self.send_message(destination, content) |
||||
|
return result |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def whisper(self, content): |
||||
|
"""|coro| |
||||
|
|
||||
|
A helper function that is equivalent to doing |
||||
|
|
||||
|
.. code-block:: python |
||||
|
|
||||
|
self.send_message(message.author, content) |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
content : str |
||||
|
The content to pass to :class:`Client.send_message` |
||||
|
""" |
||||
|
destination = self._get_variable('_internal_author') |
||||
|
result = yield from self.send_message(destination, content) |
||||
|
return result |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def reply(self, content): |
||||
|
"""|coro| |
||||
|
|
||||
|
A helper function that is equivalent to doing |
||||
|
|
||||
|
.. code-block:: python |
||||
|
|
||||
|
msg = '{0.mention}, {1}'.format(message.author, content) |
||||
|
self.send_message(message.channel, msg) |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
content : str |
||||
|
The content to pass to :class:`Client.send_message` |
||||
|
""" |
||||
|
author = self._get_variable('_internal_author') |
||||
|
destination = self._get_variable('_internal_channel') |
||||
|
fmt = '{0.mention}, {1}'.format(author, str(content)) |
||||
|
result = yield from self.send_message(destination, fmt) |
||||
|
return result |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def upload(self, fp, name=None): |
||||
|
"""|coro| |
||||
|
|
||||
|
A helper function that is equivalent to doing |
||||
|
|
||||
|
.. code-block:: python |
||||
|
|
||||
|
self.send_file(message.channel, fp, name) |
||||
|
|
||||
|
Parameters |
||||
|
---------- |
||||
|
fp |
||||
|
The first parameter to pass to :meth:`Client.send_file` |
||||
|
name |
||||
|
The second parameter to pass to :meth:`Client.send_file` |
||||
|
""" |
||||
|
destination = self._get_variable('_internal_channel') |
||||
|
result = yield from self.send_file(destination, fp, name) |
||||
|
return result |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def type(self): |
||||
|
"""|coro| |
||||
|
|
||||
|
A helper function that is equivalent to doing |
||||
|
|
||||
|
.. code-block:: python |
||||
|
|
||||
|
self.send_typing(message.channel) |
||||
|
|
||||
|
See Also |
||||
|
--------- |
||||
|
The :meth:`Client.send_typing` function. |
||||
|
""" |
||||
|
destination = self._get_variable('_internal_channel') |
||||
|
yield from self.send_typing(destination) |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def process_commands(self, message): |
||||
|
"""|coro| |
||||
|
|
||||
|
This function processes the commands that have been registered |
||||
|
to the bot and other groups. Without this coroutine, none of the |
||||
|
commands will be triggered. |
||||
|
|
||||
|
By default, this coroutine is called inside the :func:`on_message` |
||||
|
event. If you choose to override the :func:`on_message` event, then |
||||
|
you should invoke this coroutine as well. |
||||
|
|
||||
|
Warning |
||||
|
-------- |
||||
|
This function is necessary for :meth:`say`, :meth:`whisper`, |
||||
|
:meth:`type`, :meth:`reply`, and :meth:`upload` to work due to the |
||||
|
way they are written. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
message : discord.Message |
||||
|
The message to process commands for. |
||||
|
""" |
||||
|
_internal_channel = message.channel |
||||
|
_internal_author = message.author |
||||
|
|
||||
|
view = StringView(message.content) |
||||
|
if message.author == self.user: |
||||
|
return |
||||
|
|
||||
|
prefix = self._get_prefix(message) |
||||
|
if not view.skip_string(prefix): |
||||
|
return |
||||
|
|
||||
|
view.skip_ws() |
||||
|
invoker = view.get_word() |
||||
|
if invoker in self.commands: |
||||
|
command = self.commands[invoker] |
||||
|
tmp = { |
||||
|
'bot': self, |
||||
|
'invoked_with': invoker, |
||||
|
'message': message, |
||||
|
'view': view, |
||||
|
'command': command |
||||
|
} |
||||
|
ctx = Context(**tmp) |
||||
|
del tmp |
||||
|
yield from command.invoke(ctx) |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def on_message(self, message): |
||||
|
yield from self.process_commands(message) |
@ -0,0 +1,84 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-2016 Rapptz |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a |
||||
|
copy of this software and associated documentation files (the "Software"), |
||||
|
to deal in the Software without restriction, including without limitation |
||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
|
and/or sell copies of the Software, and to permit persons to whom the |
||||
|
Software is furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
|
DEALINGS IN THE SOFTWARE. |
||||
|
""" |
||||
|
|
||||
|
import asyncio |
||||
|
|
||||
|
class Context: |
||||
|
"""Represents the context in which a command is being invoked under. |
||||
|
|
||||
|
This class contains a lot of meta data to help you understand more about |
||||
|
the invocation context. This class is not created manually and is instead |
||||
|
passed around to commands by passing in :attr:`Command.pass_context`. |
||||
|
|
||||
|
Attributes |
||||
|
----------- |
||||
|
message : :class:`discord.Message` |
||||
|
The message that triggered the command being executed. |
||||
|
bot : :class:`Bot` |
||||
|
The bot that contains the command being executed. |
||||
|
args : list |
||||
|
The list of transformed arguments that were passed into the command. |
||||
|
If this is accessed during the :func:`on_command_error` event |
||||
|
then this list could be incomplete. |
||||
|
kwargs : dict |
||||
|
A dictionary of transformed arguments that were passed into the command. |
||||
|
Similar to :attr:`args`\, if this is accessed in the |
||||
|
:func:`on_command_error` event then this dict could be incomplete. |
||||
|
command |
||||
|
The command (i.e. :class:`Command` or its superclasses) that is being |
||||
|
invoked currently. |
||||
|
invoked_with : str |
||||
|
The command name that triggered this invocation. Useful for finding out |
||||
|
which alias called the command. |
||||
|
invoked_subcommand |
||||
|
The subcommand (i.e. :class:`Command` or its superclasses) that was |
||||
|
invoked. If no valid subcommand was invoked then this is equal to |
||||
|
`None`. |
||||
|
subcommand_passed : Optional[str] |
||||
|
The string that was attempted to call a subcommand. This does not have |
||||
|
to point to a valid registered subcommand and could just point to a |
||||
|
nonsense string. If nothing was passed to attempt a call to a |
||||
|
subcommand then this is set to `None`. |
||||
|
""" |
||||
|
__slots__ = ['message', 'bot', 'args', 'kwargs', 'command', 'view', |
||||
|
'invoked_with', 'invoked_subcommand', 'subcommand_passed'] |
||||
|
|
||||
|
def __init__(self, **attrs): |
||||
|
self.message = attrs.pop('message', None) |
||||
|
self.bot = attrs.pop('bot', None) |
||||
|
self.args = attrs.pop('args', []) |
||||
|
self.kwargs = attrs.pop('kwargs', {}) |
||||
|
self.command = attrs.pop('command', None) |
||||
|
self.view = attrs.pop('view', None) |
||||
|
self.invoked_with = attrs.pop('invoked_with', None) |
||||
|
self.invoked_subcommand = attrs.pop('invoked_subcommand', None) |
||||
|
self.subcommand_passed = attrs.pop('subcommand_passed', None) |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def invoke(self, command, **kwargs): |
||||
|
if len(kwargs) == 0: |
||||
|
yield from command.invoke(self) |
||||
|
else: |
||||
|
yield from command.callback(**kwargs) |
@ -0,0 +1,506 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-2016 Rapptz |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a |
||||
|
copy of this software and associated documentation files (the "Software"), |
||||
|
to deal in the Software without restriction, including without limitation |
||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
|
and/or sell copies of the Software, and to permit persons to whom the |
||||
|
Software is furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
|
DEALINGS IN THE SOFTWARE. |
||||
|
""" |
||||
|
|
||||
|
import inspect |
||||
|
import re |
||||
|
import discord |
||||
|
|
||||
|
from .errors import * |
||||
|
from .view import quoted_word |
||||
|
|
||||
|
__all__ = [ 'Command', 'Group', 'GroupMixin', 'command', 'group', |
||||
|
'has_role', 'has_permissions', 'has_any_role', 'check' ] |
||||
|
|
||||
|
class Command: |
||||
|
"""A class that implements the protocol for a bot text command. |
||||
|
|
||||
|
These are not created manually, instead they are created via the |
||||
|
decorator or functional interface. |
||||
|
|
||||
|
Attributes |
||||
|
----------- |
||||
|
name : str |
||||
|
The name of the command. |
||||
|
callback : coroutine |
||||
|
The coroutine that is executed when the command is called. |
||||
|
help : str |
||||
|
The long help text for the command. |
||||
|
brief : str |
||||
|
The short help text for the command. |
||||
|
aliases : list |
||||
|
The list of aliases the command can be invoked under. |
||||
|
pass_context : bool |
||||
|
A boolean that indicates that the current :class:`Context` should |
||||
|
be passed as the **first parameter**. Defaults to `False`. |
||||
|
checks |
||||
|
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 derived from |
||||
|
:exc:`CommandError` should be used. Note that if the checks fail then |
||||
|
:exc:`CheckFailure` exception is raised to the :func:`on_command_error` |
||||
|
event. |
||||
|
""" |
||||
|
def __init__(self, name, callback, **kwargs): |
||||
|
self.name = name |
||||
|
self.callback = callback |
||||
|
self.help = kwargs.get('help') |
||||
|
self.brief = kwargs.get('brief') |
||||
|
self.aliases = kwargs.get('aliases', []) |
||||
|
self.pass_context = kwargs.get('pass_context', False) |
||||
|
signature = inspect.signature(callback) |
||||
|
self.params = signature.parameters.copy() |
||||
|
self.checks = kwargs.get('checks', []) |
||||
|
|
||||
|
def _receive_item(self, message, argument, regex, receiver, generator): |
||||
|
match = re.match(regex, argument) |
||||
|
result = None |
||||
|
private = message.channel.is_private |
||||
|
receiver = getattr(message.server, receiver, ()) |
||||
|
if match is None: |
||||
|
if not private: |
||||
|
result = discord.utils.get(receiver, name=argument) |
||||
|
else: |
||||
|
iterable = receiver if not private else generator |
||||
|
result = discord.utils.get(iterable, id=match.group(1)) |
||||
|
return result |
||||
|
|
||||
|
def do_conversion(self, bot, message, converter, argument): |
||||
|
if converter.__module__.split('.')[0] != 'discord': |
||||
|
return converter(argument) |
||||
|
|
||||
|
# special handling for discord.py related classes |
||||
|
if converter is discord.User or converter is discord.Member: |
||||
|
member = self._receive_item(message, argument, r'<@([0-9]+)>', 'members', bot.get_all_members()) |
||||
|
if member is None: |
||||
|
raise BadArgument('User/Member not found.') |
||||
|
return member |
||||
|
elif converter is discord.Channel: |
||||
|
channel = self._receive_item(message, argument, r'<#([0-9]+)>', 'channels', bot.get_all_channels()) |
||||
|
if channel is None: |
||||
|
raise BadArgument('Channel not found.') |
||||
|
return channel |
||||
|
elif converter is discord.Colour: |
||||
|
arg = argument.replace('0x', '').lower() |
||||
|
try: |
||||
|
value = int(arg, base=16) |
||||
|
return discord.Colour(value=value) |
||||
|
except ValueError: |
||||
|
method = getattr(discord.Colour, arg, None) |
||||
|
if method is None or not inspect.ismethod(method): |
||||
|
raise BadArgument('Colour passed is invalid.') |
||||
|
return method() |
||||
|
elif converter is discord.Role: |
||||
|
if message.channel.is_private: |
||||
|
raise NoPrivateMessage() |
||||
|
|
||||
|
role = discord.utils.get(message.server.roles, name=argument) |
||||
|
if role is None: |
||||
|
raise BadArgument('Role not found') |
||||
|
return role |
||||
|
elif converter is discord.Game: |
||||
|
return discord.Game(name=argument) |
||||
|
elif converter is discord.Invite: |
||||
|
try: |
||||
|
return bot.get_invite(argument) |
||||
|
except: |
||||
|
raise BadArgument('Invite is invalid') |
||||
|
|
||||
|
def transform(self, ctx, param): |
||||
|
required = param.default is param.empty |
||||
|
converter = param.annotation |
||||
|
view = ctx.view |
||||
|
|
||||
|
if converter is param.empty: |
||||
|
if not required: |
||||
|
converter = type(param.default) |
||||
|
else: |
||||
|
converter = str |
||||
|
elif not inspect.isclass(type(converter)): |
||||
|
raise discord.ClientException('Function annotation must be a type') |
||||
|
|
||||
|
view.skip_ws() |
||||
|
|
||||
|
if view.eof: |
||||
|
if param.kind == param.VAR_POSITIONAL: |
||||
|
raise StopIteration() # break the loop |
||||
|
if required: |
||||
|
raise MissingRequiredArgument('{0.name} is a required argument that is missing.'.format(param)) |
||||
|
return param.default |
||||
|
|
||||
|
argument = quoted_word(view) |
||||
|
|
||||
|
try: |
||||
|
return self.do_conversion(ctx.bot, ctx.message, converter, argument) |
||||
|
except CommandError as e: |
||||
|
raise e |
||||
|
except Exception: |
||||
|
raise BadArgument('Converting to "{0.__name__}" failed.'.format(converter)) |
||||
|
|
||||
|
def _parse_arguments(self, ctx): |
||||
|
try: |
||||
|
ctx.args = [] |
||||
|
ctx.kwargs = {} |
||||
|
args = ctx.args |
||||
|
kwargs = ctx.kwargs |
||||
|
|
||||
|
first = True |
||||
|
view = ctx.view |
||||
|
for name, param in self.params.items(): |
||||
|
if first and self.pass_context: |
||||
|
args.append(ctx) |
||||
|
first = False |
||||
|
continue |
||||
|
|
||||
|
if param.kind == param.POSITIONAL_OR_KEYWORD: |
||||
|
args.append(self.transform(ctx, param)) |
||||
|
elif param.kind == param.KEYWORD_ONLY: |
||||
|
# kwarg only param denotes "consume rest" semantics |
||||
|
kwargs[name] = view.read_rest() |
||||
|
break |
||||
|
elif param.kind == param.VAR_POSITIONAL: |
||||
|
while not view.eof: |
||||
|
try: |
||||
|
args.append(self.transform(ctx, param)) |
||||
|
except StopIteration: |
||||
|
break |
||||
|
except CommandError as e: |
||||
|
ctx.bot.dispatch('command_error', e, ctx) |
||||
|
return False |
||||
|
return True |
||||
|
|
||||
|
def _verify_checks(self, ctx): |
||||
|
predicates = self.checks |
||||
|
if predicates: |
||||
|
try: |
||||
|
check = all(predicate(ctx) for predicate in predicates) |
||||
|
if not check: |
||||
|
raise CheckFailure('The check functions for command {0.name} failed.'.format(self)) |
||||
|
except CommandError as exc: |
||||
|
ctx.bot.dispatch('command_error', exc, ctx) |
||||
|
return False |
||||
|
|
||||
|
return True |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def invoke(self, ctx): |
||||
|
if not self._verify_checks(ctx): |
||||
|
return |
||||
|
|
||||
|
if self._parse_arguments(ctx): |
||||
|
yield from self.callback(*ctx.args, **ctx.kwargs) |
||||
|
|
||||
|
class GroupMixin: |
||||
|
"""A mixin that implements common functionality for classes that behave |
||||
|
similar to :class:`Group` and are allowed to register commands. |
||||
|
|
||||
|
Attributes |
||||
|
----------- |
||||
|
commands : dict |
||||
|
A mapping of command name to :class:`Command` or superclass |
||||
|
objects. |
||||
|
""" |
||||
|
def __init__(self, **kwargs): |
||||
|
self.commands = {} |
||||
|
super().__init__(**kwargs) |
||||
|
|
||||
|
def add_command(self, command): |
||||
|
"""Adds a :class:`Command` or its superclasses into the internal list |
||||
|
of commands. |
||||
|
|
||||
|
This is usually not called, instead the :meth:`command` or |
||||
|
:meth:`group` shortcut decorators are used instead. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
command |
||||
|
The command to add. |
||||
|
|
||||
|
Raises |
||||
|
------- |
||||
|
discord.ClientException |
||||
|
If the command is already registered. |
||||
|
TypeError |
||||
|
If the command passed is not a subclass of :class:`Command`. |
||||
|
""" |
||||
|
|
||||
|
if not isinstance(command, Command): |
||||
|
raise TypeError('The command passed must be a subclass of Command') |
||||
|
|
||||
|
if command.name in self.commands: |
||||
|
raise discord.ClientException('Command {0.name} is already registered.'.format(command)) |
||||
|
|
||||
|
self.commands[command.name] = command |
||||
|
for alias in command.aliases: |
||||
|
if alias in self.commands: |
||||
|
raise discord.ClientException('The alias {} is already an existing command or alias.'.format(alias)) |
||||
|
self.commands[alias] = command |
||||
|
|
||||
|
def command(self, *args, **kwargs): |
||||
|
"""A shortcut decorator that invokes :func:`command` and adds it to |
||||
|
the internal command list via :meth:`add_command`. |
||||
|
""" |
||||
|
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 via :meth:`add_command`. |
||||
|
""" |
||||
|
def decorator(func): |
||||
|
result = group(*args, **kwargs)(func) |
||||
|
self.add_command(result) |
||||
|
return result |
||||
|
|
||||
|
return decorator |
||||
|
|
||||
|
class Group(GroupMixin, Command): |
||||
|
"""A class that implements a grouping protocol for commands to be |
||||
|
executed as subcommands. |
||||
|
|
||||
|
This class is a subclass of :class:`Command` and thus all options |
||||
|
valid in :class:`Command` are valid in here as well. |
||||
|
""" |
||||
|
def __init__(self, **attrs): |
||||
|
super().__init__(**attrs) |
||||
|
|
||||
|
@asyncio.coroutine |
||||
|
def invoke(self, ctx): |
||||
|
if not self._verify_checks(ctx): |
||||
|
return |
||||
|
|
||||
|
if not self._parse_arguments(ctx): |
||||
|
return |
||||
|
|
||||
|
view = ctx.view |
||||
|
|
||||
|
view.skip_ws() |
||||
|
trigger = view.get_word() |
||||
|
|
||||
|
if trigger: |
||||
|
ctx.subcommand_passed = trigger |
||||
|
if trigger in self.commands: |
||||
|
ctx.invoked_subcommand = self.commands[trigger] |
||||
|
|
||||
|
yield from self.callback(*ctx.args, **ctx.kwargs) |
||||
|
|
||||
|
if ctx.invoked_subcommand: |
||||
|
yield from ctx.invoked_subcommand.invoke(ctx) |
||||
|
|
||||
|
|
||||
|
# Decorators |
||||
|
|
||||
|
def command(name=None, cls=None, **attrs): |
||||
|
"""A decorator that transforms a function into a :class:`Command`. |
||||
|
|
||||
|
By default the ``help`` attribute is received automatically from the |
||||
|
docstring of the function and is cleaned up with the use of |
||||
|
``inspect.cleandoc``. If the docstring is ``bytes``, then it is decoded |
||||
|
into ``str`` using utf-8 encoding. |
||||
|
|
||||
|
All checks added using the :func:`check` & co. decorators are added into |
||||
|
the function. There is no way to supply your own checks through this |
||||
|
decorator. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
name : str |
||||
|
The name to create the command with. By default this uses the |
||||
|
function named unchanged. |
||||
|
cls |
||||
|
The class to construct with. By default this is :class:`Command`. |
||||
|
You usually do not change this. |
||||
|
attrs |
||||
|
Keyword arguments to pass into the construction of :class:`Command`. |
||||
|
|
||||
|
Raises |
||||
|
------- |
||||
|
TypeError |
||||
|
If the function is not a coroutine or is already a command. |
||||
|
""" |
||||
|
if cls is None: |
||||
|
cls = Command |
||||
|
|
||||
|
def decorator(func): |
||||
|
if isinstance(func, Command): |
||||
|
raise TypeError('Callback is already a command.') |
||||
|
if not asyncio.iscoroutinefunction(func): |
||||
|
raise TypeError('Callback must be a coroutine.') |
||||
|
|
||||
|
try: |
||||
|
checks = func.__commands_checks__ |
||||
|
checks.reverse() |
||||
|
del func.__commands_checks__ |
||||
|
except AttributeError: |
||||
|
checks = [] |
||||
|
|
||||
|
help_doc = attrs.get('help') |
||||
|
if help_doc is not None: |
||||
|
help_doc = inspect.cleandoc(help_doc) |
||||
|
else: |
||||
|
help_doc = inspect.getdoc(func) |
||||
|
if isinstance(help_doc, bytes): |
||||
|
help_doc = help_doc.decode('utf-8') |
||||
|
|
||||
|
attrs['help'] = help_doc |
||||
|
fname = name or func.__name__.lower() |
||||
|
return cls(name=fname, callback=func, checks=checks, **attrs) |
||||
|
|
||||
|
return decorator |
||||
|
|
||||
|
def group(name=None, **attrs): |
||||
|
"""A decorator that transforms a function into a :class:`Group`. |
||||
|
|
||||
|
This is similar to the :func:`command` decorator but creates a |
||||
|
:class:`Group` instead of a :class:`Command`. |
||||
|
""" |
||||
|
return command(name=name, cls=Group, **attrs) |
||||
|
|
||||
|
def check(predicate): |
||||
|
"""A decorator that adds a check to the :class:`Command` or its |
||||
|
subclasses. These checks could be accessed via :attr:`Command.checks`. |
||||
|
|
||||
|
These checks should be predicates that take in a single parameter taking |
||||
|
a :class:`Context`. If the check returns a ``False``\-like value then |
||||
|
during invocation a :exc:`CheckFailure` exception is raised and sent to |
||||
|
the :func:`on_command_error` event. |
||||
|
|
||||
|
If an exception should be thrown in the predicate then it should be a |
||||
|
subclass of :exc:`CommandError`. Any exception not subclassed from it |
||||
|
will be propagated while those subclassed will be sent to |
||||
|
:func:`on_command_error`. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
predicate |
||||
|
The predicate to check if the command should be invoked. |
||||
|
""" |
||||
|
|
||||
|
def decorator(func): |
||||
|
if isinstance(func, Command): |
||||
|
func.checks.append(predicate) |
||||
|
else: |
||||
|
if not hasattr(func, '__commands_checks__'): |
||||
|
func.__commands_checks__ = [] |
||||
|
|
||||
|
func.__commands_checks__.append(predicate) |
||||
|
|
||||
|
return func |
||||
|
return decorator |
||||
|
|
||||
|
def has_role(name): |
||||
|
"""A :func:`check` that is added that checks if the member invoking the |
||||
|
command has the role specified via the name specified. |
||||
|
|
||||
|
The name is case sensitive and must be exact. No normalisation is done in |
||||
|
the input. |
||||
|
|
||||
|
If the message is invoked in a private message context then the check will |
||||
|
return ``False``. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
name : str |
||||
|
The name of the role to check. |
||||
|
""" |
||||
|
|
||||
|
def predicate(ctx): |
||||
|
msg = ctx.message |
||||
|
ch = msg.channel |
||||
|
if ch.is_private: |
||||
|
return False |
||||
|
|
||||
|
role = discord.utils.get(msg.author.roles, name=name) |
||||
|
return role is not None |
||||
|
|
||||
|
return check(predicate) |
||||
|
|
||||
|
def has_any_role(*names): |
||||
|
"""A :func:`check` that is added that checks if the member invoking the |
||||
|
command has **any** of the roles specified. This means that if they have |
||||
|
one out of the three roles specified, then this check will return `True`. |
||||
|
|
||||
|
Similar to :func:`has_role`\, the names passed in must be exact. |
||||
|
|
||||
|
Parameters |
||||
|
----------- |
||||
|
names |
||||
|
An argument list of names to check that the member has roles wise. |
||||
|
|
||||
|
Example |
||||
|
-------- |
||||
|
|
||||
|
.. code-block:: python |
||||
|
|
||||
|
@bot.command() |
||||
|
@has_any_role('Library Devs', 'Moderators') |
||||
|
async def cool(): |
||||
|
await bot.say('You are cool indeed') |
||||
|
""" |
||||
|
def predicate(ctx): |
||||
|
msg = ctx.message |
||||
|
ch = msg.channel |
||||
|
if ch.is_private: |
||||
|
return False |
||||
|
|
||||
|
getter = partial(discord.utils.get, msg.author.roles) |
||||
|
return any(getter(name=name) is not None for name in names) |
||||
|
return check(predicate) |
||||
|
|
||||
|
def has_permissions(**perms): |
||||
|
"""A :func:`check` that is added that checks if the member has any of |
||||
|
the permissions necessary. |
||||
|
|
||||
|
The permissions passed in must be exactly like the properties shown under |
||||
|
:class:`discord.Permissions`. |
||||
|
|
||||
|
Parameters |
||||
|
------------ |
||||
|
perms |
||||
|
An argument list of permissions to check for. |
||||
|
|
||||
|
Example |
||||
|
--------- |
||||
|
|
||||
|
.. code-block:: python |
||||
|
|
||||
|
@bot.command() |
||||
|
@has_permissions(manage_messages=True) |
||||
|
async def test(): |
||||
|
await bot.say('You can manage messages.') |
||||
|
|
||||
|
""" |
||||
|
def predicate(ctx): |
||||
|
msg = ctx.message |
||||
|
ch = msg.channel |
||||
|
me = msg.server.me if not ch.is_private else ctx.bot.user |
||||
|
permissions = ch.permissions_for(me) |
||||
|
return all(getattr(permissions, perm, None) == value for perm, value in perms.items()) |
||||
|
|
||||
|
return check(predicate) |
@ -0,0 +1,63 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-2016 Rapptz |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a |
||||
|
copy of this software and associated documentation files (the "Software"), |
||||
|
to deal in the Software without restriction, including without limitation |
||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
|
and/or sell copies of the Software, and to permit persons to whom the |
||||
|
Software is furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
|
DEALINGS IN THE SOFTWARE. |
||||
|
""" |
||||
|
|
||||
|
from discord.errors import DiscordException |
||||
|
|
||||
|
|
||||
|
__all__ = [ 'CommandError', 'MissingRequiredArgument', 'BadArgument', |
||||
|
'NoPrivateMessage', 'CheckFailure' ] |
||||
|
|
||||
|
class CommandError(DiscordException): |
||||
|
"""The base exception type for all command related errors. |
||||
|
|
||||
|
This inherits from :exc:`discord.DiscordException`. |
||||
|
|
||||
|
This exception and exceptions derived from it are handled |
||||
|
in a special way as they are caught and passed into a special event |
||||
|
from :class:`Bot`\, :func:`on_command_error`. |
||||
|
""" |
||||
|
pass |
||||
|
|
||||
|
class MissingRequiredArgument(CommandError): |
||||
|
"""Exception raised when parsing a command and a parameter |
||||
|
that is required is not encountered. |
||||
|
""" |
||||
|
pass |
||||
|
|
||||
|
class BadArgument(CommandError): |
||||
|
"""Exception raised when a parsing or conversion failure is encountered |
||||
|
on an argument to pass into a command. |
||||
|
""" |
||||
|
pass |
||||
|
|
||||
|
class NoPrivateMessage(CommandError): |
||||
|
"""Exception raised when an operation does not work in private message |
||||
|
contexts. |
||||
|
""" |
||||
|
pass |
||||
|
|
||||
|
class CheckFailure(CommandError): |
||||
|
"""Exception raised when the predicates in :attr:`Command.checks` have failed.""" |
||||
|
pass |
@ -0,0 +1,167 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
""" |
||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2015-2016 Rapptz |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a |
||||
|
copy of this software and associated documentation files (the "Software"), |
||||
|
to deal in the Software without restriction, including without limitation |
||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
|
and/or sell copies of the Software, and to permit persons to whom the |
||||
|
Software is furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
|
DEALINGS IN THE SOFTWARE. |
||||
|
""" |
||||
|
|
||||
|
from .errors import BadArgument |
||||
|
|
||||
|
class StringView: |
||||
|
def __init__(self, buffer): |
||||
|
self.index = 0 |
||||
|
self.buffer = buffer |
||||
|
self.end = len(buffer) |
||||
|
self.previous = 0 |
||||
|
|
||||
|
@property |
||||
|
def current(self): |
||||
|
return None if self.eof else self.buffer[self.index] |
||||
|
|
||||
|
@property |
||||
|
def eof(self): |
||||
|
return self.index >= self.end |
||||
|
|
||||
|
def undo(self): |
||||
|
self.index = self.previous |
||||
|
|
||||
|
def skip_ws(self): |
||||
|
pos = 0 |
||||
|
while not self.eof: |
||||
|
try: |
||||
|
current = self.buffer[self.index + pos] |
||||
|
if not current.isspace(): |
||||
|
break |
||||
|
pos += 1 |
||||
|
except IndexError: |
||||
|
break |
||||
|
|
||||
|
self.previous = self.index |
||||
|
self.index += pos |
||||
|
return self.previous != self.index |
||||
|
|
||||
|
def skip_string(self, string): |
||||
|
strlen = len(string) |
||||
|
if self.buffer[self.index:self.index + strlen] == string: |
||||
|
self.previous = self.index |
||||
|
self.index += strlen |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
def read_rest(self): |
||||
|
result = self.buffer[self.index:] |
||||
|
self.previous = self.index |
||||
|
self.index = self.end |
||||
|
return result |
||||
|
|
||||
|
def read(self, n): |
||||
|
result = self.buffer[self.index:self.index + n] |
||||
|
self.previous = self.index |
||||
|
self.index += n |
||||
|
return result |
||||
|
|
||||
|
def get(self): |
||||
|
try: |
||||
|
result = self.buffer[self.index + 1] |
||||
|
except IndexError: |
||||
|
result = None |
||||
|
|
||||
|
self.previous = self.index |
||||
|
self.index += 1 |
||||
|
return result |
||||
|
|
||||
|
def get_word(self): |
||||
|
pos = 0 |
||||
|
while not self.eof: |
||||
|
try: |
||||
|
current = self.buffer[self.index + pos] |
||||
|
if current.isspace(): |
||||
|
break |
||||
|
pos += 1 |
||||
|
except IndexError: |
||||
|
break |
||||
|
self.previous = self.index |
||||
|
result = self.buffer[self.index:self.index + pos] |
||||
|
self.index += pos |
||||
|
return result |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return '<StringView pos: {0.index} prev: {0.previous} end: {0.end} eof: {0.eof}>'.format(self) |
||||
|
|
||||
|
# Parser |
||||
|
|
||||
|
def quoted_word(view): |
||||
|
current = view.current |
||||
|
|
||||
|
if current is None: |
||||
|
return None |
||||
|
|
||||
|
is_quoted = current == '"' |
||||
|
result = [] if is_quoted else [current] |
||||
|
|
||||
|
while not view.eof: |
||||
|
current = view.get() |
||||
|
if not current: |
||||
|
if is_quoted: |
||||
|
# unexpected EOF |
||||
|
raise BadArgument('Expected closing "') |
||||
|
return ''.join(result) |
||||
|
|
||||
|
# currently we accept strings in the format of "hello world" |
||||
|
# to embed a quote inside the string you must escape it: "a \"world\"" |
||||
|
if current == '\\': |
||||
|
next_char = view.get() |
||||
|
if not next_char: |
||||
|
# string ends with \ and no character after it |
||||
|
if is_quoted: |
||||
|
# if we're quoted then we're expecting a closing quote |
||||
|
raise BadArgument('Expected closing "') |
||||
|
# if we aren't then we just let it through |
||||
|
return ''.join(result) |
||||
|
|
||||
|
if next_char == '"': |
||||
|
# escaped quote |
||||
|
result.append('"') |
||||
|
else: |
||||
|
# different escape character, ignore it |
||||
|
view.undo() |
||||
|
result.append(current) |
||||
|
continue |
||||
|
|
||||
|
# closing quote |
||||
|
if current == '"': |
||||
|
next_char = view.get() |
||||
|
valid_eof = not next_char or next_char.isspace() |
||||
|
if is_quoted: |
||||
|
if not valid_eof: |
||||
|
raise BadArgument('Expected space after closing quotation') |
||||
|
|
||||
|
# we're quoted so it's okay |
||||
|
return ''.join(result) |
||||
|
else: |
||||
|
# we aren't quoted |
||||
|
raise BadArgument('Unexpected quote mark in non-quoted string') |
||||
|
|
||||
|
if current.isspace() and not is_quoted: |
||||
|
# end of word found |
||||
|
return ''.join(result) |
||||
|
|
||||
|
result.append(current) |
Loading…
Reference in new issue