updated after a while because local stuff
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
discord.ext.commands
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An extension module to facilitate creation of bot commands.
|
||||
|
||||
:copyright: (c) 2017 Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or
|
||||
from .context import Context
|
||||
from .core import *
|
||||
from .errors import *
|
||||
from .formatter import HelpFormatter, Paginator
|
||||
from .converter import *
|
||||
from .cooldowns import *
|
||||
@@ -0,0 +1,995 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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
|
||||
import importlib
|
||||
import sys
|
||||
import traceback
|
||||
import re
|
||||
|
||||
from .core import GroupMixin, Command, command
|
||||
from .view import StringView
|
||||
from .context import Context
|
||||
from .errors import CommandNotFound, CommandError
|
||||
from .formatter import HelpFormatter
|
||||
|
||||
def when_mentioned(bot, msg):
|
||||
"""A callable that implements a command prefix equivalent to being mentioned.
|
||||
|
||||
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
|
||||
"""
|
||||
return [bot.user.mention + ' ', '<@!%s> ' % bot.user.id]
|
||||
|
||||
def when_mentioned_or(*prefixes):
|
||||
"""A callable that implements when mentioned or other prefixes provided.
|
||||
|
||||
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
|
||||
|
||||
Example
|
||||
--------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
bot = commands.Bot(command_prefix=commands.when_mentioned_or('!'))
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This callable returns another callable, so if this is done inside a custom
|
||||
callable, you must call the returned callable, for example:
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
async def get_prefix(bot, message):
|
||||
extras = await prefixes_for(message.guild) # returns a list
|
||||
return commands.when_mentioned_or(*extras)(bot, message)
|
||||
|
||||
|
||||
See Also
|
||||
----------
|
||||
:func:`.when_mentioned`
|
||||
"""
|
||||
def inner(bot, msg):
|
||||
r = list(prefixes)
|
||||
r.extend(when_mentioned(bot, msg))
|
||||
return r
|
||||
|
||||
return inner
|
||||
|
||||
_mentions_transforms = {
|
||||
'@everyone': '@\u200beveryone',
|
||||
'@here': '@\u200bhere'
|
||||
}
|
||||
|
||||
_mention_pattern = re.compile('|'.join(_mentions_transforms.keys()))
|
||||
|
||||
def _is_submodule(parent, child):
|
||||
return parent == child or child.startswith(parent + ".")
|
||||
|
||||
@asyncio.coroutine
|
||||
def _default_help_command(ctx, *commands : str):
|
||||
"""Shows this message."""
|
||||
bot = ctx.bot
|
||||
destination = ctx.message.author if bot.pm_help else ctx.message.channel
|
||||
|
||||
def repl(obj):
|
||||
return _mentions_transforms.get(obj.group(0), '')
|
||||
|
||||
# help by itself just lists our own commands.
|
||||
if len(commands) == 0:
|
||||
pages = yield from bot.formatter.format_help_for(ctx, bot)
|
||||
elif len(commands) == 1:
|
||||
# try to see if it is a cog name
|
||||
name = _mention_pattern.sub(repl, commands[0])
|
||||
command = None
|
||||
if name in bot.cogs:
|
||||
command = bot.cogs[name]
|
||||
else:
|
||||
command = bot.all_commands.get(name)
|
||||
if command is None:
|
||||
yield from destination.send(bot.command_not_found.format(name))
|
||||
return
|
||||
|
||||
pages = yield from bot.formatter.format_help_for(ctx, command)
|
||||
else:
|
||||
name = _mention_pattern.sub(repl, commands[0])
|
||||
command = bot.all_commands.get(name)
|
||||
if command is None:
|
||||
yield from destination.send(bot.command_not_found.format(name))
|
||||
return
|
||||
|
||||
for key in commands[1:]:
|
||||
try:
|
||||
key = _mention_pattern.sub(repl, key)
|
||||
command = command.all_commands.get(key)
|
||||
if command is None:
|
||||
yield from destination.send(bot.command_not_found.format(key))
|
||||
return
|
||||
except AttributeError:
|
||||
yield from destination.send(bot.command_has_no_subcommands.format(command, key))
|
||||
return
|
||||
|
||||
pages = yield from bot.formatter.format_help_for(ctx, command)
|
||||
|
||||
if bot.pm_help is None:
|
||||
characters = sum(map(lambda l: len(l), pages))
|
||||
# modify destination based on length of pages.
|
||||
if characters > 1000:
|
||||
destination = ctx.message.author
|
||||
|
||||
for page in pages:
|
||||
yield from destination.send(page)
|
||||
|
||||
class BotBase(GroupMixin):
|
||||
def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options):
|
||||
super().__init__(**options)
|
||||
self.command_prefix = command_prefix
|
||||
self.extra_events = {}
|
||||
self.cogs = {}
|
||||
self.extensions = {}
|
||||
self._checks = []
|
||||
self._check_once = []
|
||||
self._before_invoke = None
|
||||
self._after_invoke = None
|
||||
self.description = inspect.cleandoc(description) if description else ''
|
||||
self.pm_help = pm_help
|
||||
self.owner_id = options.get('owner_id')
|
||||
self.command_not_found = options.pop('command_not_found', 'No command called "{}" found.')
|
||||
self.command_has_no_subcommands = options.pop('command_has_no_subcommands', 'Command {0.name} has no subcommands.')
|
||||
|
||||
if options.pop('self_bot', False):
|
||||
self._skip_check = lambda x, y: x != y
|
||||
else:
|
||||
self._skip_check = lambda x, y: x == y
|
||||
|
||||
self.help_attrs = options.pop('help_attrs', {})
|
||||
|
||||
if 'name' not in self.help_attrs:
|
||||
self.help_attrs['name'] = 'help'
|
||||
|
||||
if formatter is not None:
|
||||
if not isinstance(formatter, HelpFormatter):
|
||||
raise discord.ClientException('Formatter must be a subclass of HelpFormatter')
|
||||
self.formatter = formatter
|
||||
else:
|
||||
self.formatter = HelpFormatter()
|
||||
|
||||
# pay no mind to this ugliness.
|
||||
self.command(**self.help_attrs)(_default_help_command)
|
||||
|
||||
# internal helpers
|
||||
|
||||
def dispatch(self, event_name, *args, **kwargs):
|
||||
super().dispatch(event_name, *args, **kwargs)
|
||||
ev = 'on_' + event_name
|
||||
for event in self.extra_events.get(ev, []):
|
||||
coro = self._run_event(event, event_name, *args, **kwargs)
|
||||
discord.compat.create_task(coro, loop=self.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def close(self):
|
||||
for extension in tuple(self.extensions):
|
||||
try:
|
||||
self.unload_extension(extension)
|
||||
except:
|
||||
pass
|
||||
|
||||
for cog in tuple(self.cogs):
|
||||
try:
|
||||
self.remove_cog(cog)
|
||||
except:
|
||||
pass
|
||||
|
||||
yield from super().close()
|
||||
|
||||
@asyncio.coroutine
|
||||
def on_command_error(self, context, exception):
|
||||
"""|coro|
|
||||
|
||||
The default command error handler provided by the bot.
|
||||
|
||||
By default this prints to ``sys.stderr`` however it could be
|
||||
overridden to have a different implementation.
|
||||
|
||||
This only fires if you do not specify any listeners for command error.
|
||||
"""
|
||||
if self.extra_events.get('on_command_error', None):
|
||||
return
|
||||
|
||||
if hasattr(context.command, 'on_error'):
|
||||
return
|
||||
|
||||
cog = context.cog
|
||||
if cog:
|
||||
attr = '_{0.__class__.__name__}__error'.format(cog)
|
||||
if hasattr(cog, attr):
|
||||
return
|
||||
|
||||
print('Ignoring exception in command {}:'.format(context.command), file=sys.stderr)
|
||||
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
|
||||
|
||||
# global check registration
|
||||
|
||||
def check(self, func):
|
||||
"""A decorator that adds a global check to the bot.
|
||||
|
||||
A global check is similar to a :func:`.check` that is applied
|
||||
on a per command basis except it is run before any command checks
|
||||
have been verified and applies to every command the bot has.
|
||||
|
||||
.. note::
|
||||
|
||||
This function can either be a regular function or a coroutine.
|
||||
|
||||
Similar to a command :func:`.check`\, this takes a single parameter
|
||||
of type :class:`.Context` and can only raise exceptions derived from
|
||||
:exc:`.CommandError`.
|
||||
|
||||
Example
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@bot.check
|
||||
def check_commands(ctx):
|
||||
return ctx.command.qualified_name in allowed_commands
|
||||
|
||||
"""
|
||||
self.add_check(func)
|
||||
return func
|
||||
|
||||
def add_check(self, func, *, call_once=False):
|
||||
"""Adds a global check to the bot.
|
||||
|
||||
This is the non-decorator interface to :meth:`.check`
|
||||
and :meth:`.check_once`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
func
|
||||
The function that was used as a global check.
|
||||
call_once: bool
|
||||
If the function should only be called once per
|
||||
:meth:`.Command.invoke` call.
|
||||
"""
|
||||
|
||||
if call_once:
|
||||
self._check_once.append(func)
|
||||
else:
|
||||
self._checks.append(func)
|
||||
|
||||
def remove_check(self, func):
|
||||
"""Removes a global check from the bot.
|
||||
|
||||
This function is idempotent and will not raise an exception
|
||||
if the function is not in the global checks.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
func
|
||||
The function to remove from the global checks.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._checks.remove(func)
|
||||
except ValueError:
|
||||
try:
|
||||
self._check_once.remove(func)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def check_once(self, func):
|
||||
"""A decorator that adds a "call once" global check to the bot.
|
||||
|
||||
Unlike regular global checks, this one is called only once
|
||||
per :meth:`.Command.invoke` call.
|
||||
|
||||
Regular global checks are called whenever a command is called
|
||||
or :meth:`.Command.can_run` is called. This type of check
|
||||
bypasses that and ensures that it's called only once, even inside
|
||||
the default help command.
|
||||
|
||||
.. note::
|
||||
|
||||
This function can either be a regular function or a coroutine.
|
||||
|
||||
Similar to a command :func:`.check`\, this takes a single parameter
|
||||
of type :class:`.Context` and can only raise exceptions derived from
|
||||
:exc:`.CommandError`.
|
||||
|
||||
Example
|
||||
---------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@bot.check_once
|
||||
def whitelist(ctx):
|
||||
return ctx.message.author.id in my_whitelist
|
||||
|
||||
"""
|
||||
self.add_check(func, call_once=True)
|
||||
return func
|
||||
|
||||
@asyncio.coroutine
|
||||
def can_run(self, ctx, *, call_once=False):
|
||||
data = self._check_once if call_once else self._checks
|
||||
|
||||
if len(data) == 0:
|
||||
return True
|
||||
|
||||
return (yield from discord.utils.async_all(f(ctx) for f in data))
|
||||
|
||||
@asyncio.coroutine
|
||||
def is_owner(self, user):
|
||||
"""Checks if a :class:`.User` or :class:`.Member` is the owner of
|
||||
this bot.
|
||||
|
||||
If an :attr:`owner_id` is not set, it is fetched automatically
|
||||
through the use of :meth:`~.Bot.application_info`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
user: :class:`.abc.User`
|
||||
The user to check for.
|
||||
"""
|
||||
|
||||
if self.owner_id is None:
|
||||
app = yield from self.application_info()
|
||||
self.owner_id = owner_id = app.owner.id
|
||||
return user.id == owner_id
|
||||
return user.id == self.owner_id
|
||||
|
||||
def before_invoke(self, coro):
|
||||
"""A decorator that registers a coroutine as a pre-invoke hook.
|
||||
|
||||
A pre-invoke hook is called directly before the command is
|
||||
called. This makes it a useful function to set up database
|
||||
connections or any type of set up required.
|
||||
|
||||
This pre-invoke hook takes a sole parameter, a :class:`.Context`.
|
||||
|
||||
.. note::
|
||||
|
||||
The :meth:`~.Bot.before_invoke` and :meth:`~.Bot.after_invoke` hooks are
|
||||
only called if all checks and argument parsing procedures pass
|
||||
without error. If any check or argument parsing procedures fail
|
||||
then the hooks are not called.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
coro
|
||||
The coroutine to register as the pre-invoke hook.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.ClientException`
|
||||
The coroutine is not actually a coroutine.
|
||||
"""
|
||||
if not asyncio.iscoroutinefunction(coro):
|
||||
raise discord.ClientException('The error handler must be a coroutine.')
|
||||
|
||||
self._before_invoke = coro
|
||||
return coro
|
||||
|
||||
def after_invoke(self, coro):
|
||||
"""A decorator that registers a coroutine as a post-invoke hook.
|
||||
|
||||
A post-invoke hook is called directly after the command is
|
||||
called. This makes it a useful function to clean-up database
|
||||
connections or any type of clean up required.
|
||||
|
||||
This post-invoke hook takes a sole parameter, a :class:`.Context`.
|
||||
|
||||
.. note::
|
||||
|
||||
Similar to :meth:`~.Bot.before_invoke`\, this is not called unless
|
||||
checks and argument parsing procedures succeed. This hook is,
|
||||
however, **always** called regardless of the internal command
|
||||
callback raising an error (i.e. :exc:`.CommandInvokeError`\).
|
||||
This makes it ideal for clean-up scenarios.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
coro
|
||||
The coroutine to register as the post-invoke hook.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.ClientException`
|
||||
The coroutine is not actually a coroutine.
|
||||
"""
|
||||
if not asyncio.iscoroutinefunction(coro):
|
||||
raise discord.ClientException('The error handler must be a coroutine.')
|
||||
|
||||
self._after_invoke = coro
|
||||
return coro
|
||||
|
||||
# listener registration
|
||||
|
||||
def add_listener(self, func, name=None):
|
||||
"""The non decorator alternative to :meth:`.listen`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
func : coroutine
|
||||
The extra event to listen to.
|
||||
name : Optional[str]
|
||||
The name of the command to use. Defaults to ``func.__name__``.
|
||||
|
||||
Example
|
||||
--------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
async def on_ready(): pass
|
||||
async def my_message(message): pass
|
||||
|
||||
bot.add_listener(on_ready)
|
||||
bot.add_listener(my_message, 'on_message')
|
||||
|
||||
"""
|
||||
name = func.__name__ if name is None else name
|
||||
|
||||
if not asyncio.iscoroutinefunction(func):
|
||||
raise discord.ClientException('Listeners must be coroutines')
|
||||
|
||||
if name in self.extra_events:
|
||||
self.extra_events[name].append(func)
|
||||
else:
|
||||
self.extra_events[name] = [func]
|
||||
|
||||
def remove_listener(self, func, name=None):
|
||||
"""Removes a listener from the pool of listeners.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
func
|
||||
The function that was used as a listener to remove.
|
||||
name
|
||||
The name of the event we want to remove. Defaults to
|
||||
``func.__name__``.
|
||||
"""
|
||||
|
||||
name = func.__name__ if name is None else name
|
||||
|
||||
if name in self.extra_events:
|
||||
try:
|
||||
self.extra_events[name].remove(func)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def listen(self, name=None):
|
||||
"""A decorator that registers another function as an external
|
||||
event listener. Basically this allows you to listen to multiple
|
||||
events from different places e.g. such as :func:`.on_ready`
|
||||
|
||||
The functions being listened to must be a coroutine.
|
||||
|
||||
Example
|
||||
--------
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@bot.listen()
|
||||
async def on_message(message):
|
||||
print('one')
|
||||
|
||||
# in some other file...
|
||||
|
||||
@bot.listen('on_message')
|
||||
async def my_message(message):
|
||||
print('two')
|
||||
|
||||
Would print one and two in an unspecified order.
|
||||
|
||||
Raises
|
||||
-------
|
||||
:exc:`.ClientException`
|
||||
The function being listened to is not a coroutine.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
self.add_listener(func, name)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
# cogs
|
||||
|
||||
def add_cog(self, cog):
|
||||
"""Adds a "cog" to the bot.
|
||||
|
||||
A cog is a class that has its own event listeners and commands.
|
||||
|
||||
They are meant as a way to organize multiple relevant commands
|
||||
into a singular class that shares some state or no state at all.
|
||||
|
||||
The cog can also have a ``__global_check`` member function that allows
|
||||
you to define a global check. See :meth:`.check` for more info. If
|
||||
the name is ``__global_check_once`` then it's equivalent to the
|
||||
:meth:`.check_once` decorator.
|
||||
|
||||
More information will be documented soon.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
cog
|
||||
The cog to register to the bot.
|
||||
"""
|
||||
|
||||
self.cogs[type(cog).__name__] = cog
|
||||
|
||||
try:
|
||||
check = getattr(cog, '_{.__class__.__name__}__global_check'.format(cog))
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.add_check(check)
|
||||
|
||||
try:
|
||||
check = getattr(cog, '_{.__class__.__name__}__global_check_once'.format(cog))
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.add_check(check, call_once=True)
|
||||
|
||||
members = inspect.getmembers(cog)
|
||||
for name, member in members:
|
||||
# register commands the cog has
|
||||
if isinstance(member, Command):
|
||||
if member.parent is None:
|
||||
self.add_command(member)
|
||||
continue
|
||||
|
||||
# register event listeners the cog has
|
||||
if name.startswith('on_'):
|
||||
self.add_listener(member, name)
|
||||
|
||||
def get_cog(self, name):
|
||||
"""Gets the cog instance requested.
|
||||
|
||||
If the cog is not found, ``None`` is returned instead.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name : str
|
||||
The name of the cog you are requesting.
|
||||
"""
|
||||
return self.cogs.get(name)
|
||||
|
||||
def get_cog_commands(self, name):
|
||||
"""Gets a unique set of the cog's registered commands
|
||||
without aliases.
|
||||
|
||||
If the cog is not found, an empty set is returned.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: str
|
||||
The name of the cog whose commands you are requesting.
|
||||
|
||||
Returns
|
||||
---------
|
||||
Set[:class:`.Command`]
|
||||
A unique set of commands without aliases that belong
|
||||
to the cog.
|
||||
"""
|
||||
|
||||
try:
|
||||
cog = self.cogs[name]
|
||||
except KeyError:
|
||||
return set()
|
||||
|
||||
return {c for c in self.all_commands.values() if c.instance is cog}
|
||||
|
||||
def remove_cog(self, name):
|
||||
"""Removes a cog from the bot.
|
||||
|
||||
All registered commands and event listeners that the
|
||||
cog has registered will be removed as well.
|
||||
|
||||
If no cog is found then this method has no effect.
|
||||
|
||||
If the cog defines a special member function named ``__unload``
|
||||
then it is called when removal has completed. This function
|
||||
**cannot** be a coroutine. It must be a regular function.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name : str
|
||||
The name of the cog to remove.
|
||||
"""
|
||||
|
||||
cog = self.cogs.pop(name, None)
|
||||
if cog is None:
|
||||
return
|
||||
|
||||
members = inspect.getmembers(cog)
|
||||
for name, member in members:
|
||||
# remove commands the cog has
|
||||
if isinstance(member, Command):
|
||||
if member.parent is None:
|
||||
self.remove_command(member.name)
|
||||
continue
|
||||
|
||||
# remove event listeners the cog has
|
||||
if name.startswith('on_'):
|
||||
self.remove_listener(member)
|
||||
|
||||
try:
|
||||
check = getattr(cog, '_{0.__class__.__name__}__global_check'.format(cog))
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.remove_check(check)
|
||||
|
||||
try:
|
||||
check = getattr(cog, '_{0.__class__.__name__}__global_check_once'.format(cog))
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
self.remove_check(check)
|
||||
|
||||
unloader_name = '_{0.__class__.__name__}__unload'.format(cog)
|
||||
try:
|
||||
unloader = getattr(cog, unloader_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
unloader()
|
||||
|
||||
del cog
|
||||
|
||||
# extensions
|
||||
|
||||
def load_extension(self, name):
|
||||
"""Loads an extension.
|
||||
|
||||
An extension is a python module that contains commands, cogs, or
|
||||
listeners.
|
||||
|
||||
An extension must have a global function, ``setup`` defined as
|
||||
the entry point on what to do when the extension is loaded. This entry
|
||||
point must have a single argument, the ``bot``.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: str
|
||||
The extension name to load. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
|
||||
Raises
|
||||
--------
|
||||
ClientException
|
||||
The extension does not have a setup function.
|
||||
ImportError
|
||||
The extension could not be imported.
|
||||
"""
|
||||
|
||||
if name in self.extensions:
|
||||
return
|
||||
|
||||
lib = importlib.import_module(name)
|
||||
if not hasattr(lib, 'setup'):
|
||||
del lib
|
||||
del sys.modules[name]
|
||||
raise discord.ClientException('extension does not have a setup function')
|
||||
|
||||
lib.setup(self)
|
||||
self.extensions[name] = lib
|
||||
|
||||
def unload_extension(self, name):
|
||||
"""Unloads an extension.
|
||||
|
||||
When the extension is unloaded, all commands, listeners, and cogs are
|
||||
removed from the bot and the module is un-imported.
|
||||
|
||||
The extension can provide an optional global function, ``teardown``,
|
||||
to do miscellaneous clean-up if necessary. This function takes a single
|
||||
parameter, the ``bot``, similar to ``setup`` from
|
||||
:func:`~.Bot.load_extension`.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: str
|
||||
The extension name to unload. It must be dot separated like
|
||||
regular Python imports if accessing a sub-module. e.g.
|
||||
``foo.test`` if you want to import ``foo/test.py``.
|
||||
"""
|
||||
|
||||
lib = self.extensions.get(name)
|
||||
if lib is None:
|
||||
return
|
||||
|
||||
lib_name = lib.__name__
|
||||
|
||||
# find all references to the module
|
||||
|
||||
# remove the cogs registered from the module
|
||||
for cogname, cog in self.cogs.copy().items():
|
||||
if _is_submodule(lib_name, cog.__module__):
|
||||
self.remove_cog(cogname)
|
||||
|
||||
# first remove all the commands from the module
|
||||
for cmd in self.all_commands.copy().values():
|
||||
if _is_submodule(lib_name, cmd.module):
|
||||
if isinstance(cmd, GroupMixin):
|
||||
cmd.recursively_remove_all_commands()
|
||||
self.remove_command(cmd.name)
|
||||
|
||||
# then remove all the listeners from the module
|
||||
for event_list in self.extra_events.copy().values():
|
||||
remove = []
|
||||
for index, event in enumerate(event_list):
|
||||
if _is_submodule(lib_name, event.__module__):
|
||||
remove.append(index)
|
||||
|
||||
for index in reversed(remove):
|
||||
del event_list[index]
|
||||
|
||||
try:
|
||||
func = getattr(lib, 'teardown')
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
func(self)
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
# finally remove the import..
|
||||
del lib
|
||||
del self.extensions[name]
|
||||
del sys.modules[name]
|
||||
for module in list(sys.modules.keys()):
|
||||
if _is_submodule(lib_name, module):
|
||||
del sys.modules[module]
|
||||
|
||||
# command processing
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_prefix(self, message):
|
||||
"""|coro|
|
||||
|
||||
Retrieves the prefix the bot is listening to
|
||||
with the message as a context.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
message: :class:`discord.Message`
|
||||
The message context to get the prefix of.
|
||||
|
||||
Raises
|
||||
--------
|
||||
:exc:`.ClientException`
|
||||
The prefix was invalid. This could be if the prefix
|
||||
function returned None, the prefix list returned no
|
||||
elements that aren't None, or the prefix string is
|
||||
empty.
|
||||
|
||||
Returns
|
||||
--------
|
||||
Union[List[str], str]
|
||||
A list of prefixes or a single prefix that the bot is
|
||||
listening for.
|
||||
"""
|
||||
prefix = ret = self.command_prefix
|
||||
if callable(prefix):
|
||||
ret = prefix(self, message)
|
||||
if asyncio.iscoroutine(ret):
|
||||
ret = yield from ret
|
||||
|
||||
if isinstance(ret, (list, tuple)):
|
||||
ret = [p for p in ret if p]
|
||||
|
||||
if not ret:
|
||||
raise discord.ClientException('invalid prefix (could be an empty string, empty list, or None)')
|
||||
|
||||
return ret
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_context(self, message, *, cls=Context):
|
||||
"""|coro|
|
||||
|
||||
Returns the invocation context from the message.
|
||||
|
||||
This is a more low-level counter-part for :meth:`.process_commands`
|
||||
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:`~.Bot.invoke`.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
message: :class:`discord.Message`
|
||||
The message to get the invocation context from.
|
||||
cls
|
||||
The factory class that will be used to create the context.
|
||||
By default, this is :class:`.Context`. Should a custom
|
||||
class be provided, it must be similar enough to :class:`.Context`\'s
|
||||
interface.
|
||||
|
||||
Returns
|
||||
--------
|
||||
:class:`.Context`
|
||||
The invocation context. The type of this can change via the
|
||||
``cls`` parameter.
|
||||
"""
|
||||
|
||||
view = StringView(message.content)
|
||||
ctx = cls(prefix=None, view=view, bot=self, message=message)
|
||||
|
||||
if self._skip_check(message.author.id, self.user.id):
|
||||
return ctx
|
||||
|
||||
prefix = yield from self.get_prefix(message)
|
||||
invoked_prefix = prefix
|
||||
|
||||
if isinstance(prefix, str):
|
||||
if not view.skip_string(prefix):
|
||||
return ctx
|
||||
else:
|
||||
invoked_prefix = discord.utils.find(view.skip_string, prefix)
|
||||
if invoked_prefix is None:
|
||||
return ctx
|
||||
|
||||
invoker = view.get_word()
|
||||
ctx.invoked_with = invoker
|
||||
ctx.prefix = invoked_prefix
|
||||
ctx.command = self.all_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:
|
||||
if (yield from self.can_run(ctx, call_once=True)):
|
||||
yield from ctx.command.invoke(ctx)
|
||||
except CommandError as e:
|
||||
yield from ctx.command.dispatch_error(ctx, e)
|
||||
else:
|
||||
self.dispatch('command_completion', ctx)
|
||||
elif ctx.invoked_with:
|
||||
exc = CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with))
|
||||
self.dispatch('command_error', ctx, exc)
|
||||
|
||||
@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:`~.Bot.get_context` followed by a call to :meth:`~.Bot.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)
|
||||
|
||||
class Bot(BotBase, 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.
|
||||
|
||||
.. _deque: https://docs.python.org/3.4/library/collections.html#collections.deque
|
||||
.. _event loop: https://docs.python.org/3/library/asyncio-eventloops.html
|
||||
|
||||
This class also subclasses :class:`.GroupMixin` to provide the functionality
|
||||
to manage commands.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
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 the bot
|
||||
as its first parameter and :class:`discord.Message` as its second
|
||||
parameter and returns the prefix. This is to facilitate "dynamic"
|
||||
command prefixes. This callable can be either a regular function or
|
||||
a coroutine.
|
||||
|
||||
The command prefix could also be a list or a tuple indicating that
|
||||
multiple checks for the prefix should be used and the first one to
|
||||
match will be the invocation prefix. You can get this prefix via
|
||||
:attr:`.Context.prefix`.
|
||||
description : str
|
||||
The content prefixed into the default help message.
|
||||
self_bot : bool
|
||||
If ``True``, the bot will only listen to commands invoked by itself rather
|
||||
than ignoring itself. If ``False`` (the default) then the bot will ignore
|
||||
itself. This cannot be changed once initialised.
|
||||
formatter : :class:`.HelpFormatter`
|
||||
The formatter used to format the help message. By default, it uses a
|
||||
the :class:`.HelpFormatter`. Check it for more info on how to override it.
|
||||
If you want to change the help command completely (add aliases, etc) then
|
||||
a call to :meth:`~.Bot.remove_command` with 'help' as the argument would do the
|
||||
trick.
|
||||
pm_help : Optional[bool]
|
||||
A tribool that indicates if the help command should PM the user instead of
|
||||
sending it to the channel it received it from. If the boolean is set to
|
||||
``True``, then all help output is PM'd. If ``False``, none of the help
|
||||
output is PM'd. If ``None``, then the bot will only PM when the help
|
||||
message becomes too long (dictated by more than 1000 characters).
|
||||
Defaults to ``False``.
|
||||
help_attrs : dict
|
||||
A dictionary of options to pass in for the construction of the help command.
|
||||
This allows you to change the command behaviour without actually changing
|
||||
the implementation of the command. The attributes will be the same as the
|
||||
ones passed in the :class:`.Command` constructor. Note that ``pass_context``
|
||||
will always be set to ``True`` regardless of what you pass in.
|
||||
command_not_found : str
|
||||
The format string used when the help command is invoked with a command that
|
||||
is not found. Useful for i18n. Defaults to ``"No command called {} found."``.
|
||||
The only format argument is the name of the command passed.
|
||||
command_has_no_subcommands : str
|
||||
The format string used when the help command is invoked with requests for a
|
||||
subcommand but the command does not have any subcommands. Defaults to
|
||||
``"Command {0.name} has no subcommands."``. The first format argument is the
|
||||
:class:`.Command` attempted to get a subcommand and the second is the name.
|
||||
owner_id: Optional[int]
|
||||
The ID that owns the bot. If this is not set and is then queried via
|
||||
:meth:`.is_owner` then it is fetched automatically using
|
||||
:meth:`~.Bot.application_info`.
|
||||
"""
|
||||
pass
|
||||
|
||||
class AutoShardedBot(BotBase, discord.AutoShardedClient):
|
||||
"""This is similar to :class:`.Bot` except that it is derived from
|
||||
:class:`discord.AutoShardedClient` instead.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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.abc
|
||||
import discord.utils
|
||||
|
||||
class Context(discord.abc.Messageable):
|
||||
"""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 as the first parameter.
|
||||
|
||||
This class implements the :class:`abc.Messageable` ABC.
|
||||
|
||||
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.
|
||||
prefix: str
|
||||
The prefix that was used to invoke the command.
|
||||
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`.
|
||||
command_failed: bool
|
||||
A boolean that indicates if the command failed to be parsed, checked,
|
||||
or invoked.
|
||||
"""
|
||||
|
||||
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.prefix = attrs.pop('prefix')
|
||||
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)
|
||||
self.command_failed = attrs.pop('command_failed', False)
|
||||
self._state = self.message._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def invoke(self, *args, **kwargs):
|
||||
"""|coro|
|
||||
|
||||
Calls a command with the arguments given.
|
||||
|
||||
This is useful if you want to just call the callback that a
|
||||
:class:`.Command` holds internally.
|
||||
|
||||
Note
|
||||
------
|
||||
You do not pass in the context as it is done for you.
|
||||
|
||||
Warning
|
||||
---------
|
||||
The first parameter passed **must** be the command being invoked.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
command: :class:`.Command`
|
||||
A command or superclass of a command that is going to be called.
|
||||
\*args
|
||||
The arguments to to use.
|
||||
\*\*kwargs
|
||||
The keyword arguments to use.
|
||||
"""
|
||||
|
||||
try:
|
||||
command = args[0]
|
||||
except IndexError:
|
||||
raise TypeError('Missing command to invoke.') from None
|
||||
|
||||
arguments = []
|
||||
if command.instance is not None:
|
||||
arguments.append(command.instance)
|
||||
|
||||
arguments.append(self)
|
||||
arguments.extend(args[1:])
|
||||
|
||||
ret = yield from command.callback(*arguments, **kwargs)
|
||||
return ret
|
||||
|
||||
@asyncio.coroutine
|
||||
def reinvoke(self, *, call_hooks=False, restart=True):
|
||||
"""|coro|
|
||||
|
||||
Calls the command again.
|
||||
|
||||
This is similar to :meth:`~.Context.invoke` except that it bypasses
|
||||
checks, cooldowns, and error handlers.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to bypass :exc:`.UserInputError` derived exceptions,
|
||||
it is recommended to use the regular :meth:`~.Context.invoke`
|
||||
as it will work more naturally. After all, this will end up
|
||||
using the old arguments the user has used and will thus just
|
||||
fail again.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
call_hooks: bool
|
||||
Whether to call the before and after invoke hooks.
|
||||
restart: bool
|
||||
Whether to start the call chain from the very beginning
|
||||
or where we left off (i.e. the command that caused the error).
|
||||
The default is to start where we left off.
|
||||
"""
|
||||
cmd = self.command
|
||||
view = self.view
|
||||
if cmd is None:
|
||||
raise ValueError('This context is not valid.')
|
||||
|
||||
# some state to revert to when we're done
|
||||
index, previous = view.index, view.previous
|
||||
invoked_with = self.invoked_with
|
||||
invoked_subcommand = self.invoked_subcommand
|
||||
subcommand_passed = self.subcommand_passed
|
||||
|
||||
if restart:
|
||||
to_call = cmd.root_parent or cmd
|
||||
view.index = len(self.prefix)
|
||||
view.previous = 0
|
||||
view.get_word() # advance to get the root command
|
||||
else:
|
||||
to_call = cmd
|
||||
|
||||
try:
|
||||
yield from to_call.reinvoke(self, call_hooks=call_hooks)
|
||||
finally:
|
||||
self.command = cmd
|
||||
view.index = index
|
||||
view.previous = previous
|
||||
self.invoked_with = invoked_with
|
||||
self.invoked_subcommand = invoked_subcommand
|
||||
self.subcommand_passed = subcommand_passed
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def cog(self):
|
||||
"""Returns the cog associated with this context's command. None if it does not exist."""
|
||||
|
||||
if self.command is None:
|
||||
return None
|
||||
return self.command.instance
|
||||
|
||||
@discord.utils.cached_property
|
||||
def guild(self):
|
||||
"""Returns the guild associated with this context's command. None if not available."""
|
||||
return self.message.guild
|
||||
|
||||
@discord.utils.cached_property
|
||||
def channel(self):
|
||||
"""Returns the channel associated with this context's command. Shorthand for :attr:`Message.channel`."""
|
||||
return self.message.channel
|
||||
|
||||
@discord.utils.cached_property
|
||||
def author(self):
|
||||
"""Returns the author associated with this context's command. Shorthand for :attr:`Message.author`"""
|
||||
return self.message.author
|
||||
|
||||
@discord.utils.cached_property
|
||||
def me(self):
|
||||
"""Similar to :attr:`Guild.me` except it may return the :class:`ClientUser` in private message contexts."""
|
||||
return self.guild.me if self.guild is not None else self.bot.user
|
||||
|
||||
@property
|
||||
def voice_client(self):
|
||||
"""Optional[:class:`VoiceClient`]: A shortcut to :attr:`Guild.voice_client`\, if applicable."""
|
||||
g = self.guild
|
||||
return g.voice_client if g else None
|
||||
@@ -0,0 +1,481 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 discord
|
||||
import asyncio
|
||||
import re
|
||||
import inspect
|
||||
|
||||
from .errors import BadArgument, NoPrivateMessage
|
||||
from .view import StringView
|
||||
|
||||
__all__ = [ 'Converter', 'MemberConverter', 'UserConverter',
|
||||
'TextChannelConverter', 'InviteConverter', 'RoleConverter',
|
||||
'GameConverter', 'ColourConverter', 'VoiceChannelConverter',
|
||||
'EmojiConverter','CategoryChannelConverter', 'IDConverter',
|
||||
'clean_content' ]
|
||||
|
||||
def _get_from_guilds(bot, getter, argument):
|
||||
result = None
|
||||
for guild in bot.guilds:
|
||||
result = getattr(guild, getter)(argument)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
|
||||
class Converter:
|
||||
"""The base class of custom converters that require the :class:`.Context`
|
||||
to be passed to be useful.
|
||||
|
||||
This allows you to implement converters that function similar to the
|
||||
special cased ``discord`` classes.
|
||||
|
||||
Classes that derive from this should override the :meth:`~.Converter.convert`
|
||||
method to do its conversion logic. This method must be a coroutine.
|
||||
"""
|
||||
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
"""|coro|
|
||||
|
||||
The method to override to do conversion logic.
|
||||
|
||||
If an error is found while converting, it is recommended to
|
||||
raise a :exc:`.CommandError` derived exception as it will
|
||||
properly propagate to the error handlers.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
ctx: :class:`.Context`
|
||||
The invocation context that the argument is being used in.
|
||||
argument: str
|
||||
The argument that is being converted.
|
||||
"""
|
||||
raise NotImplementedError('Derived classes need to implement this.')
|
||||
|
||||
class IDConverter(Converter):
|
||||
def __init__(self):
|
||||
self._id_regex = re.compile(r'([0-9]{15,21})$')
|
||||
super().__init__()
|
||||
|
||||
def _get_id_match(self, argument):
|
||||
return self._id_regex.match(argument)
|
||||
|
||||
class MemberConverter(IDConverter):
|
||||
"""Converts to a :class:`Member`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name#discrim
|
||||
4. Lookup by name
|
||||
5. Lookup by nickname
|
||||
"""
|
||||
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
message = ctx.message
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
guild = message.guild
|
||||
result = None
|
||||
if match is None:
|
||||
# not a mention...
|
||||
if guild:
|
||||
result = guild.get_member_named(argument)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_member_named', argument)
|
||||
else:
|
||||
user_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_member(user_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_member', user_id)
|
||||
|
||||
if result is None:
|
||||
raise BadArgument('Member "{}" not found'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
class UserConverter(IDConverter):
|
||||
"""Converts to a :class:`User`.
|
||||
|
||||
All lookups are via the global user cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name#discrim
|
||||
4. Lookup by name
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
|
||||
result = None
|
||||
state = ctx._state
|
||||
|
||||
if match is not None:
|
||||
user_id = int(match.group(1))
|
||||
result = ctx.bot.get_user(user_id)
|
||||
else:
|
||||
arg = argument
|
||||
# check for discriminator if it exists
|
||||
if len(arg) > 5 and arg[-5] == '#':
|
||||
discrim = arg[-4:]
|
||||
name = arg[:-5]
|
||||
predicate = lambda u: u.name == name and u.discriminator == discrim
|
||||
result = discord.utils.find(predicate, state._users.values())
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
predicate = lambda u: u.name == arg
|
||||
result = discord.utils.find(predicate, state._users.values())
|
||||
|
||||
if result is None:
|
||||
raise BadArgument('User "{}" not found'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
class TextChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`TextChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.text_channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.TextChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.TextChannel):
|
||||
raise BadArgument('Channel "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
class VoiceChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`VoiceChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.voice_channels, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.VoiceChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.VoiceChannel):
|
||||
raise BadArgument('Channel "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
class CategoryChannelConverter(IDConverter):
|
||||
"""Converts to a :class:`CategoryChannel`.
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
bot = ctx.bot
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
|
||||
result = None
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# not a mention
|
||||
if guild:
|
||||
result = discord.utils.get(guild.categories, name=argument)
|
||||
else:
|
||||
def check(c):
|
||||
return isinstance(c, discord.CategoryChannel) and c.name == argument
|
||||
result = discord.utils.find(check, bot.get_all_channels())
|
||||
else:
|
||||
channel_id = int(match.group(1))
|
||||
if guild:
|
||||
result = guild.get_channel(channel_id)
|
||||
else:
|
||||
result = _get_from_guilds(bot, 'get_channel', channel_id)
|
||||
|
||||
if not isinstance(result, discord.CategoryChannel):
|
||||
raise BadArgument('Channel "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
class ColourConverter(Converter):
|
||||
"""Converts to a :class:`Colour`.
|
||||
|
||||
The following formats are accepted:
|
||||
|
||||
- ``0x<hex>``
|
||||
- ``#<hex>``
|
||||
- ``0x#<hex>``
|
||||
- Any of the ``classmethod`` in :class:`Colour`
|
||||
|
||||
- The ``_`` in the name can be optionally replaced with spaces.
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
arg = argument.replace('0x', '').lower()
|
||||
|
||||
if arg[0] == '#':
|
||||
arg = arg[1:]
|
||||
try:
|
||||
value = int(arg, base=16)
|
||||
return discord.Colour(value=value)
|
||||
except ValueError:
|
||||
method = getattr(discord.Colour, arg.replace(' ', '_'), None)
|
||||
if method is None or not inspect.ismethod(method):
|
||||
raise BadArgument('Colour "{}" is invalid.'.format(arg))
|
||||
return method()
|
||||
|
||||
class RoleConverter(IDConverter):
|
||||
"""Converts to a :class:`Role`.
|
||||
|
||||
|
||||
All lookups are via the local guild. If in a DM context, then the lookup
|
||||
is done by the global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by mention.
|
||||
3. Lookup by name
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
guild = ctx.message.guild
|
||||
if not guild:
|
||||
raise NoPrivateMessage()
|
||||
|
||||
match = self._get_id_match(argument) or re.match(r'<@&([0-9]+)>$', argument)
|
||||
params = dict(id=int(match.group(1))) if match else dict(name=argument)
|
||||
result = discord.utils.get(guild.roles, **params)
|
||||
if result is None:
|
||||
raise BadArgument('Role "{}" not found.'.format(argument))
|
||||
return result
|
||||
|
||||
class GameConverter(Converter):
|
||||
"""Converts to :class:`Game`."""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
return discord.Game(name=argument)
|
||||
|
||||
class InviteConverter(Converter):
|
||||
"""Converts to a :class:`Invite`.
|
||||
|
||||
This is done via an HTTP request using :meth:`.Bot.get_invite`.
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
try:
|
||||
invite = yield from ctx.bot.get_invite(argument)
|
||||
return invite
|
||||
except Exception as e:
|
||||
raise BadArgument('Invite is invalid or expired') from e
|
||||
|
||||
class EmojiConverter(IDConverter):
|
||||
"""Converts to a :class:`Emoji`.
|
||||
|
||||
|
||||
All lookups are done for the local guild first, if available. If that lookup
|
||||
fails, then it checks the client's global cache.
|
||||
|
||||
The lookup strategy is as follows (in order):
|
||||
|
||||
1. Lookup by ID.
|
||||
2. Lookup by extracting ID from the emoji.
|
||||
3. Lookup by name
|
||||
"""
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
match = self._get_id_match(argument) or re.match(r'<:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
|
||||
result = None
|
||||
bot = ctx.bot
|
||||
guild = ctx.guild
|
||||
|
||||
if match is None:
|
||||
# Try to get the emoji by name. Try local guild first.
|
||||
if guild:
|
||||
result = discord.utils.get(guild.emojis, name=argument)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(bot.emojis, name=argument)
|
||||
else:
|
||||
emoji_id = int(match.group(1))
|
||||
|
||||
# Try to look up emoji by id.
|
||||
if guild:
|
||||
result = discord.utils.get(guild.emojis, id=emoji_id)
|
||||
|
||||
if result is None:
|
||||
result = discord.utils.get(bot.emojis, id=emoji_id)
|
||||
|
||||
if result is None:
|
||||
raise BadArgument('Emoji "{}" not found.'.format(argument))
|
||||
|
||||
return result
|
||||
|
||||
class clean_content(Converter):
|
||||
"""Converts the argument to mention scrubbed version of
|
||||
said content.
|
||||
|
||||
This behaves similarly to :attr:`.Message.clean_content`.
|
||||
|
||||
Attributes
|
||||
------------
|
||||
fix_channel_mentions: bool
|
||||
Whether to clean channel mentions.
|
||||
use_nicknames: bool
|
||||
Whether to use nicknames when transforming mentions.
|
||||
escape_markdown: bool
|
||||
Whether to also escape special markdown characters.
|
||||
"""
|
||||
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False):
|
||||
self.fix_channel_mentions = fix_channel_mentions
|
||||
self.use_nicknames = use_nicknames
|
||||
self.escape_markdown = escape_markdown
|
||||
|
||||
@asyncio.coroutine
|
||||
def convert(self, ctx, argument):
|
||||
message = ctx.message
|
||||
transformations = {}
|
||||
|
||||
if self.fix_channel_mentions and ctx.guild:
|
||||
def resolve_channel(id, *, _get=ctx.guild.get_channel):
|
||||
ch = _get(id)
|
||||
return ('<#%s>' % id), ('#' + ch.name if ch else '#deleted-channel')
|
||||
|
||||
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
|
||||
|
||||
if self.use_nicknames and ctx.guild:
|
||||
def resolve_member(id, *, _get=ctx.guild.get_member):
|
||||
m = _get(id)
|
||||
return '@' + m.display_name if m else '@deleted-user'
|
||||
else:
|
||||
def resolve_member(id, *, _get=ctx.bot.get_user):
|
||||
m = _get(id)
|
||||
return '@' + m.name if m else '@deleted-user'
|
||||
|
||||
|
||||
transformations.update(
|
||||
('<@%s>' % member_id, resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
transformations.update(
|
||||
('<@!%s>' % member_id, resolve_member(member_id))
|
||||
for member_id in message.raw_mentions
|
||||
)
|
||||
|
||||
if ctx.guild:
|
||||
def resolve_role(id, *, _find=discord.utils.find, _roles=ctx.guild.roles):
|
||||
r = _find(lambda x: x.id == id, _roles)
|
||||
return '@' + r.name if r else '@deleted-role'
|
||||
|
||||
transformations.update(
|
||||
('<@&%s>' % role_id, resolve_role(role_id))
|
||||
for role_id in message.raw_role_mentions
|
||||
)
|
||||
|
||||
def repl(obj):
|
||||
return transformations.get(obj.group(0), '')
|
||||
|
||||
pattern = re.compile('|'.join(transformations.keys()))
|
||||
result = pattern.sub(repl, argument)
|
||||
|
||||
if self.escape_markdown:
|
||||
transformations = {
|
||||
re.escape(c): '\\' + c
|
||||
for c in ('*', '`', '_', '~', '\\')
|
||||
}
|
||||
|
||||
def replace(obj):
|
||||
return transformations.get(re.escape(obj.group(0)), '')
|
||||
|
||||
pattern = re.compile('|'.join(transformations.keys()))
|
||||
result = pattern.sub(replace, result)
|
||||
|
||||
# Completely ensure no mentions escape:
|
||||
return re.sub(r'@(everyone|here|[!&]?[0-9]{17,21})', '@\u200b\\1', result)
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 enum
|
||||
import time
|
||||
|
||||
__all__ = ['BucketType', 'Cooldown', 'CooldownMapping']
|
||||
|
||||
class BucketType(enum.Enum):
|
||||
default = 0
|
||||
user = 1
|
||||
guild = 2
|
||||
channel = 3
|
||||
|
||||
class Cooldown:
|
||||
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
|
||||
|
||||
def __init__(self, rate, per, type):
|
||||
self.rate = int(rate)
|
||||
self.per = float(per)
|
||||
self.type = type
|
||||
self._window = 0.0
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
if not isinstance(self.type, BucketType):
|
||||
raise TypeError('Cooldown type must be a BucketType')
|
||||
|
||||
def is_rate_limited(self):
|
||||
current = time.time()
|
||||
self._last = current
|
||||
|
||||
# first token used means that we start a new rate limit window
|
||||
if self._tokens == self.rate:
|
||||
self._window = current
|
||||
|
||||
# check if our window has passed and we can refresh our tokens
|
||||
if current > self._window + self.per:
|
||||
self._tokens = self.rate
|
||||
self._window = current
|
||||
|
||||
# check if we're rate limited
|
||||
if self._tokens == 0:
|
||||
return self.per - (current - self._window)
|
||||
|
||||
# we're not so decrement our tokens
|
||||
self._tokens -= 1
|
||||
|
||||
# see if we got rate limited due to this token change, and if
|
||||
# so update the window to point to our current time frame
|
||||
if self._tokens == 0:
|
||||
self._window = current
|
||||
|
||||
def reset(self):
|
||||
self._tokens = self.rate
|
||||
self._last = 0.0
|
||||
|
||||
def copy(self):
|
||||
return Cooldown(self.rate, self.per, self.type)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Cooldown rate: {0.rate} per: {0.per} window: {0._window} tokens: {0._tokens}>'.format(self)
|
||||
|
||||
class CooldownMapping:
|
||||
def __init__(self, original):
|
||||
self._cache = {}
|
||||
self._cooldown = original
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
return self._cooldown is not None
|
||||
|
||||
@classmethod
|
||||
def from_cooldown(cls, rate, per, type):
|
||||
return cls(Cooldown(rate, per, type))
|
||||
|
||||
def _bucket_key(self, ctx):
|
||||
msg = ctx.message
|
||||
bucket_type = self._cooldown.type
|
||||
if bucket_type is BucketType.user:
|
||||
return msg.author.id
|
||||
elif bucket_type is BucketType.guild:
|
||||
return getattr(msg.guild, 'id', msg.author.id)
|
||||
elif bucket_type is BucketType.channel:
|
||||
return msg.channel.id
|
||||
|
||||
def _verify_cache_integrity(self):
|
||||
# we want to delete all cache objects that haven't been used
|
||||
# in a cooldown window. e.g. if we have a command that has a
|
||||
# cooldown of 60s and it has not been used in 60s then that key should be deleted
|
||||
current = time.time()
|
||||
dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per]
|
||||
for k in dead_keys:
|
||||
del self._cache[k]
|
||||
|
||||
def get_bucket(self, ctx):
|
||||
if self._cooldown.type is BucketType.default:
|
||||
return self._cooldown
|
||||
|
||||
self._verify_cache_integrity()
|
||||
key = self._bucket_key(ctx)
|
||||
if key not in self._cache:
|
||||
bucket = self._cooldown.copy()
|
||||
self._cache[key] = bucket
|
||||
else:
|
||||
bucket = self._cache[key]
|
||||
|
||||
return bucket
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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', 'CommandNotFound',
|
||||
'DisabledCommand', 'CommandInvokeError', 'TooManyArguments',
|
||||
'UserInputError', 'CommandOnCooldown', 'NotOwner',
|
||||
'MissingPermissions', 'BotMissingPermissions']
|
||||
|
||||
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`.
|
||||
"""
|
||||
def __init__(self, message=None, *args):
|
||||
if message is not None:
|
||||
# clean-up @everyone and @here mentions
|
||||
m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere')
|
||||
super().__init__(m, *args)
|
||||
else:
|
||||
super().__init__(*args)
|
||||
|
||||
class UserInputError(CommandError):
|
||||
"""The base exception type for errors that involve errors
|
||||
regarding user input.
|
||||
|
||||
This inherits from :exc:`.CommandError`.
|
||||
"""
|
||||
pass
|
||||
|
||||
class CommandNotFound(CommandError):
|
||||
"""Exception raised when a command is attempted to be invoked
|
||||
but no command under that name is found.
|
||||
|
||||
This is not raised for invalid subcommands, rather just the
|
||||
initial main command that is attempted to be invoked.
|
||||
"""
|
||||
pass
|
||||
|
||||
class MissingRequiredArgument(UserInputError):
|
||||
"""Exception raised when parsing a command and a parameter
|
||||
that is required is not encountered.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
param: str
|
||||
The argument that is missing.
|
||||
"""
|
||||
def __init__(self, param):
|
||||
self.param = param.name
|
||||
super().__init__('{0.name} is a required argument that is missing.'.format(param))
|
||||
|
||||
class TooManyArguments(UserInputError):
|
||||
"""Exception raised when the command was passed too many arguments and its
|
||||
:attr:`.Command.ignore_extra` attribute was not set to ``True``.
|
||||
"""
|
||||
pass
|
||||
|
||||
class BadArgument(UserInputError):
|
||||
"""Exception raised when a parsing or conversion failure is encountered
|
||||
on an argument to pass into a command.
|
||||
"""
|
||||
pass
|
||||
|
||||
class CheckFailure(CommandError):
|
||||
"""Exception raised when the predicates in :attr:`.Command.checks` have failed."""
|
||||
pass
|
||||
|
||||
class NoPrivateMessage(CheckFailure):
|
||||
"""Exception raised when an operation does not work in private message
|
||||
contexts.
|
||||
"""
|
||||
pass
|
||||
|
||||
class NotOwner(CheckFailure):
|
||||
"""Exception raised when the message author is not the owner of the bot."""
|
||||
pass
|
||||
|
||||
class DisabledCommand(CommandError):
|
||||
"""Exception raised when the command being invoked is disabled."""
|
||||
pass
|
||||
|
||||
class CommandInvokeError(CommandError):
|
||||
"""Exception raised when the command being invoked raised an exception.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
original
|
||||
The original exception that was raised. You can also get this via
|
||||
the ``__cause__`` attribute.
|
||||
"""
|
||||
def __init__(self, e):
|
||||
self.original = e
|
||||
super().__init__('Command raised an exception: {0.__class__.__name__}: {0}'.format(e))
|
||||
|
||||
class CommandOnCooldown(CommandError):
|
||||
"""Exception raised when the command being invoked is on cooldown.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
cooldown: Cooldown
|
||||
A class with attributes ``rate``, ``per``, and ``type`` similar to
|
||||
the :func:`.cooldown` decorator.
|
||||
retry_after: float
|
||||
The amount of seconds to wait before you can retry again.
|
||||
"""
|
||||
def __init__(self, cooldown, retry_after):
|
||||
self.cooldown = cooldown
|
||||
self.retry_after = retry_after
|
||||
super().__init__('You are on cooldown. Try again in {:.2f}s'.format(retry_after))
|
||||
|
||||
class MissingPermissions(CheckFailure):
|
||||
"""Exception raised when the command invoker lacks permissions to run
|
||||
command.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_perms: list
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
def __init__(self, missing_perms, *args):
|
||||
self.missing_perms = missing_perms
|
||||
|
||||
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_perms]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' and '.join(missing)
|
||||
message = 'You are missing {} permission(s) to run command.'.format(fmt)
|
||||
super().__init__(message, *args)
|
||||
|
||||
class BotMissingPermissions(CheckFailure):
|
||||
"""Exception raised when the bot lacks permissions to run command.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
missing_perms: list
|
||||
The required permissions that are missing.
|
||||
"""
|
||||
def __init__(self, missing_perms, *args):
|
||||
self.missing_perms = missing_perms
|
||||
|
||||
missing = [perm.replace('_', ' ').replace('guild', 'server').title() for perm in missing_perms]
|
||||
|
||||
if len(missing) > 2:
|
||||
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
|
||||
else:
|
||||
fmt = ' and '.join(missing)
|
||||
message = 'Bot requires {} permission(s) to run command.'.format(fmt)
|
||||
super().__init__(message, *args)
|
||||
@@ -0,0 +1,346 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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 itertools
|
||||
import inspect
|
||||
import asyncio
|
||||
|
||||
from .core import GroupMixin, Command
|
||||
from .errors import CommandError
|
||||
# from discord.iterators import _FilteredAsyncIterator
|
||||
|
||||
# help -> shows info of bot on top/bottom and lists subcommands
|
||||
# help command -> shows detailed info of command
|
||||
# help command <subcommand chain> -> same as above
|
||||
|
||||
# <description>
|
||||
|
||||
# <command signature with aliases>
|
||||
|
||||
# <long doc>
|
||||
|
||||
# Cog:
|
||||
# <command> <shortdoc>
|
||||
# <command> <shortdoc>
|
||||
# Other Cog:
|
||||
# <command> <shortdoc>
|
||||
# No Category:
|
||||
# <command> <shortdoc>
|
||||
|
||||
# Type <prefix>help command for more info on a command.
|
||||
# You can also type <prefix>help category for more info on a category.
|
||||
|
||||
class Paginator:
|
||||
"""A class that aids in paginating code blocks for Discord messages.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
prefix: str
|
||||
The prefix inserted to every page. e.g. three backticks.
|
||||
suffix: str
|
||||
The suffix appended at the end of every page. e.g. three backticks.
|
||||
max_size: int
|
||||
The maximum amount of codepoints allowed in a page.
|
||||
"""
|
||||
def __init__(self, prefix='```', suffix='```', max_size=2000):
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
self.max_size = max_size - len(suffix)
|
||||
self._current_page = [prefix]
|
||||
self._count = len(prefix) + 1 # prefix + newline
|
||||
self._pages = []
|
||||
|
||||
def add_line(self, line='', *, empty=False):
|
||||
"""Adds a line to the current page.
|
||||
|
||||
If the line exceeds the :attr:`max_size` then an exception
|
||||
is raised.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
line: str
|
||||
The line to add.
|
||||
empty: bool
|
||||
Indicates if another empty line should be added.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
The line was too big for the current :attr:`max_size`.
|
||||
"""
|
||||
if len(line) > self.max_size - len(self.prefix) - 2:
|
||||
raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
|
||||
|
||||
if self._count + len(line) + 1 > self.max_size:
|
||||
self.close_page()
|
||||
|
||||
self._count += len(line) + 1
|
||||
self._current_page.append(line)
|
||||
|
||||
if empty:
|
||||
self._current_page.append('')
|
||||
self._count += 1
|
||||
|
||||
def close_page(self):
|
||||
"""Prematurely terminate a page."""
|
||||
self._current_page.append(self.suffix)
|
||||
self._pages.append('\n'.join(self._current_page))
|
||||
self._current_page = [self.prefix]
|
||||
self._count = len(self.prefix) + 1 # prefix + newline
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
"""Returns the rendered list of pages."""
|
||||
# we have more than just the prefix in our current page
|
||||
if len(self._current_page) > 1:
|
||||
self.close_page()
|
||||
return self._pages
|
||||
|
||||
def __repr__(self):
|
||||
fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
|
||||
return fmt.format(self)
|
||||
|
||||
class HelpFormatter:
|
||||
"""The default base implementation that handles formatting of the help
|
||||
command.
|
||||
|
||||
To override the behaviour of the formatter, :meth:`~.HelpFormatter.format`
|
||||
should be overridden. A number of utility functions are provided for use
|
||||
inside that method.
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
show_hidden: bool
|
||||
Dictates if hidden commands should be shown in the output.
|
||||
Defaults to ``False``.
|
||||
show_check_failure: bool
|
||||
Dictates if commands that have their :attr:`.Command.checks` failed
|
||||
shown. Defaults to ``False``.
|
||||
width: int
|
||||
The maximum number of characters that fit in a line.
|
||||
Defaults to 80.
|
||||
"""
|
||||
def __init__(self, show_hidden=False, show_check_failure=False, width=80):
|
||||
self.width = width
|
||||
self.show_hidden = show_hidden
|
||||
self.show_check_failure = show_check_failure
|
||||
|
||||
def has_subcommands(self):
|
||||
"""bool: Specifies if the command has subcommands."""
|
||||
return isinstance(self.command, GroupMixin)
|
||||
|
||||
def is_bot(self):
|
||||
"""bool: Specifies if the command being formatted is the bot itself."""
|
||||
return self.command is self.context.bot
|
||||
|
||||
def is_cog(self):
|
||||
"""bool: Specifies if the command being formatted is actually a cog."""
|
||||
return not self.is_bot() and not isinstance(self.command, Command)
|
||||
|
||||
def shorten(self, text):
|
||||
"""Shortens text to fit into the :attr:`width`."""
|
||||
if len(text) > self.width:
|
||||
return text[:self.width - 3] + '...'
|
||||
return text
|
||||
|
||||
@property
|
||||
def max_name_size(self):
|
||||
"""int: Returns the largest name length of a command or if it has subcommands
|
||||
the largest subcommand name."""
|
||||
try:
|
||||
commands = self.command.all_commands if not self.is_cog() else self.context.bot.all_commands
|
||||
if commands:
|
||||
return max(map(lambda c: len(c.name) if self.show_hidden or not c.hidden else 0, commands.values()))
|
||||
return 0
|
||||
except AttributeError:
|
||||
return len(self.command.name)
|
||||
|
||||
@property
|
||||
def clean_prefix(self):
|
||||
"""The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``."""
|
||||
user = self.context.bot.user
|
||||
# this breaks if the prefix mention is not the bot itself but I
|
||||
# consider this to be an *incredibly* strange use case. I'd rather go
|
||||
# for this common use case rather than waste performance for the
|
||||
# odd one.
|
||||
return self.context.prefix.replace(user.mention, '@' + user.name)
|
||||
|
||||
def get_command_signature(self):
|
||||
"""Retrieves the signature portion of the help page."""
|
||||
prefix = self.clean_prefix
|
||||
cmd = self.command
|
||||
return prefix + cmd.signature
|
||||
|
||||
def get_ending_note(self):
|
||||
command_name = self.context.invoked_with
|
||||
return "Type {0}{1} command for more info on a command.\n" \
|
||||
"You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def filter_command_list(self):
|
||||
"""Returns a filtered list of commands based on the two attributes
|
||||
provided, :attr:`show_check_failure` and :attr:`show_hidden`.
|
||||
Also filters based on if :meth:`~.HelpFormatter.is_cog` is valid.
|
||||
|
||||
Returns
|
||||
--------
|
||||
iterable
|
||||
An iterable with the filter being applied. The resulting value is
|
||||
a (key, value) tuple of the command name and the command itself.
|
||||
"""
|
||||
|
||||
def sane_no_suspension_point_predicate(tup):
|
||||
cmd = tup[1]
|
||||
if self.is_cog():
|
||||
# filter commands that don't exist to this cog.
|
||||
if cmd.instance is not self.command:
|
||||
return False
|
||||
|
||||
if cmd.hidden and not self.show_hidden:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@asyncio.coroutine
|
||||
def predicate(tup):
|
||||
if sane_no_suspension_point_predicate(tup) is False:
|
||||
return False
|
||||
|
||||
cmd = tup[1]
|
||||
try:
|
||||
return (yield from cmd.can_run(self.context))
|
||||
except CommandError:
|
||||
return False
|
||||
|
||||
iterator = self.command.all_commands.items() if not self.is_cog() else self.context.bot.all_commands.items()
|
||||
if self.show_check_failure:
|
||||
return filter(sane_no_suspension_point_predicate, iterator)
|
||||
|
||||
# Gotta run every check and verify it
|
||||
ret = []
|
||||
for elem in iterator:
|
||||
valid = yield from predicate(elem)
|
||||
if valid:
|
||||
ret.append(elem)
|
||||
|
||||
return ret
|
||||
|
||||
def _add_subcommands_to_page(self, max_width, commands):
|
||||
for name, command in commands:
|
||||
if name in command.aliases:
|
||||
# skip aliases
|
||||
continue
|
||||
|
||||
entry = ' {0:<{width}} {1}'.format(name, command.short_doc, width=max_width)
|
||||
shortened = self.shorten(entry)
|
||||
self._paginator.add_line(shortened)
|
||||
|
||||
@asyncio.coroutine
|
||||
def format_help_for(self, context, command_or_bot):
|
||||
"""Formats the help page and handles the actual heavy lifting of how
|
||||
the help command looks like. To change the behaviour, override the
|
||||
:meth:`~.HelpFormatter.format` method.
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
context: :class:`.Context`
|
||||
The context of the invoked help command.
|
||||
command_or_bot: :class:`.Command` or :class:`.Bot`
|
||||
The bot or command that we are getting the help of.
|
||||
|
||||
Returns
|
||||
--------
|
||||
list
|
||||
A paginated output of the help command.
|
||||
"""
|
||||
self.context = context
|
||||
self.command = command_or_bot
|
||||
return (yield from self.format())
|
||||
|
||||
@asyncio.coroutine
|
||||
def format(self):
|
||||
"""Handles the actual behaviour involved with formatting.
|
||||
|
||||
To change the behaviour, this method should be overridden.
|
||||
|
||||
Returns
|
||||
--------
|
||||
list
|
||||
A paginated output of the help command.
|
||||
"""
|
||||
self._paginator = Paginator()
|
||||
|
||||
# we need a padding of ~80 or so
|
||||
|
||||
description = self.command.description if not self.is_cog() else inspect.getdoc(self.command)
|
||||
|
||||
if description:
|
||||
# <description> portion
|
||||
self._paginator.add_line(description, empty=True)
|
||||
|
||||
if isinstance(self.command, Command):
|
||||
# <signature portion>
|
||||
signature = self.get_command_signature()
|
||||
self._paginator.add_line(signature, empty=True)
|
||||
|
||||
# <long doc> section
|
||||
if self.command.help:
|
||||
self._paginator.add_line(self.command.help, empty=True)
|
||||
|
||||
# end it here if it's just a regular command
|
||||
if not self.has_subcommands():
|
||||
self._paginator.close_page()
|
||||
return self._paginator.pages
|
||||
|
||||
max_width = self.max_name_size
|
||||
|
||||
def category(tup):
|
||||
cog = tup[1].cog_name
|
||||
# we insert the zero width space there to give it approximate
|
||||
# last place sorting position.
|
||||
return cog + ':' if cog is not None else '\u200bNo Category:'
|
||||
|
||||
filtered = yield from self.filter_command_list()
|
||||
if self.is_bot():
|
||||
data = sorted(filtered, key=category)
|
||||
for category, commands in itertools.groupby(data, key=category):
|
||||
# there simply is no prettier way of doing this.
|
||||
commands = sorted(commands)
|
||||
if len(commands) > 0:
|
||||
self._paginator.add_line(category)
|
||||
|
||||
self._add_subcommands_to_page(max_width, commands)
|
||||
else:
|
||||
filtered = sorted(filtered)
|
||||
if filtered:
|
||||
self._paginator.add_line('Commands:')
|
||||
self._add_subcommands_to_page(max_width, filtered)
|
||||
|
||||
# add the ending note
|
||||
self._paginator.add_line()
|
||||
ending_note = self.get_ending_note()
|
||||
self._paginator.add_line(ending_note)
|
||||
return self._paginator.pages
|
||||
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 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