diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index fb0e8cba2..8c17c63b1 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -164,17 +164,6 @@ class BotBase(GroupMixin): # internal helpers - @asyncio.coroutine - def _get_prefix(self, message): - prefix = self.command_prefix - if callable(prefix): - ret = prefix(self, message) - if asyncio.iscoroutine(ret): - ret = yield from ret - return ret - else: - return prefix - @asyncio.coroutine def _run_extra(self, coro, event_name, *args, **kwargs): try: @@ -552,72 +541,127 @@ class BotBase(GroupMixin): # command processing @asyncio.coroutine - def process_commands(self, message): + def get_prefix(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. + Retrieves the prefix the bot is listening to + with the message as a context. - 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. + Parameters + ----------- + message: :class:`discord.Message` + The message context to get the prefix of. - Warning + Returns -------- - 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. It is also required for the :func:`on_command` - and :func:`on_command_completion` events. + Union[List[str], str] + A list of prefixes or a single prefix that the bot is + listening for. + """ + prefix = self.command_prefix + if callable(prefix): + ret = prefix(self, message) + if asyncio.iscoroutine(ret): + ret = yield from ret + return ret + else: + return prefix + + @asyncio.coroutine + def get_context(self, message): + """|coro| + + Returns the invocation context from the message. + + This is a more low-level counter-part for :meth:`process_message` + to allow users more fine grained control over the processing. + + The returned context is not guaranteed to be a valid invocation + context, :attr:`Context.valid` must be checked to make sure it is. + If the context is not valid then it is not a valid candidate to be + invoked under :meth:`invoke`. Parameters ----------- - message : discord.Message - The message to process commands for. + message: :class:`discord.Message` + The message to get the invocation context from. + + Returns + -------- + :class:`Context` + The invocation context. """ - _internal_channel = message.channel - _internal_author = message.author view = StringView(message.content) + ctx = Context(prefix=None, view=view, bot=self, message=message) + if self._skip_check(message.author.id, self.user.id): - return + return ctx - prefix = yield from self._get_prefix(message) + prefix = yield from self.get_prefix(message) invoked_prefix = prefix if not isinstance(prefix, (tuple, list)): if not view.skip_string(prefix): - return + return ctx else: invoked_prefix = discord.utils.find(view.skip_string, prefix) if invoked_prefix is None: - return - + return ctx invoker = view.get_word() - tmp = { - 'bot': self, - 'invoked_with': invoker, - 'message': message, - 'view': view, - 'prefix': invoked_prefix - } - ctx = Context(**tmp) - del tmp - - if invoker in self.commands: - command = self.commands[invoker] - self.dispatch('command', command, ctx) + ctx.invoked_with = invoker + ctx.prefix = invoked_prefix + ctx.command = self.commands.get(invoker) + return ctx + + @asyncio.coroutine + def invoke(self, ctx): + """|coro| + + Invokes the command given under the invocation context and + handles all the internal event dispatch mechanisms. + + Parameters + ----------- + ctx: :class:`Context` + The invocation context to invoke. + """ + if ctx.command is not None: + self.dispatch('command', ctx) try: - yield from command.invoke(ctx) + yield from ctx.command.invoke(ctx) except CommandError as e: ctx.command.dispatch_error(e, ctx) else: - self.dispatch('command_completion', command, ctx) - elif invoker: - exc = CommandNotFound('Command "{}" is not found'.format(invoker)) + self.dispatch('command_completion', ctx) + elif ctx.invoked_with: + exc = CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) self.dispatch('command_error', exc, ctx) + @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. + + This is built using other low level tools, and is equivalent to a + call to :meth:`get_context` followed by a call to :meth:`invoke`. + + Parameters + ----------- + message : discord.Message + The message to process commands for. + """ + ctx = yield from self.get_context(message) + yield from self.invoke(ctx) + @asyncio.coroutine def on_message(self, message): yield from self.process_commands(message) diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 59f091179..c1d120f32 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -117,6 +117,11 @@ class Context(discord.abc.Messageable): ret = yield from command.callback(*arguments, **kwargs) return ret + @property + def valid(self): + """Checks if the invocation context is valid to be invoked with.""" + return self.prefix is not None and self.command is not None + @asyncio.coroutine def _get_channel(self): return self.channel