Initial implementation of commands extension module.
This commit is contained in:
		
							
								
								
									
										12
									
								
								discord/ext/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								discord/ext/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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. | ||||
|  | ||||
| """ | ||||
							
								
								
									
										16
									
								
								discord/ext/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								discord/ext/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 * | ||||
							
								
								
									
										221
									
								
								discord/ext/commands/bot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								discord/ext/commands/bot.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										84
									
								
								discord/ext/commands/context.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								discord/ext/commands/context.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										506
									
								
								discord/ext/commands/core.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										506
									
								
								discord/ext/commands/core.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										63
									
								
								discord/ext/commands/errors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								discord/ext/commands/errors.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										167
									
								
								discord/ext/commands/view.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								discord/ext/commands/view.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
		Reference in New Issue
	
	Block a user