diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 64dd80621..de5821e5e 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -140,6 +140,7 @@ class BotBase(GroupMixin): 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.') @@ -275,6 +276,26 @@ class BotBase(GroupMixin): return (yield from discord.utils.async_all(f(ctx) for f in self._checks)) + @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:`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. @@ -815,6 +836,10 @@ class Bot(BotBase, discord.Client): 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:`application_info`. """ pass diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 635294f7b..f64e7482c 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -37,7 +37,7 @@ from . import converter as converters __all__ = [ 'Command', 'Group', 'GroupMixin', 'command', 'group', 'has_role', 'has_permissions', 'has_any_role', 'check', 'bot_has_role', 'bot_has_permissions', 'bot_has_any_role', - 'cooldown', 'guild_only', ] + 'cooldown', 'guild_only', 'is_owner'] def wrap_callback(coro): @functools.wraps(coro) @@ -1104,6 +1104,24 @@ def guild_only(): return check(predicate) +def is_owner(): + """A :func:`check` that checks if the person invoking this command is the + owner of the bot. + + This is powered by :meth:`Bot.is_owner`. + + This check raises a special exception, :exc:`NotOwner` that is derived + from :exc:`CheckFailure`. + """ + + @asyncio.coroutine + def predicate(ctx): + if not (yield from ctx.bot.is_owner(ctx.author)): + raise NotOwner('You do not own this bot.') + return True + + return check(predicate) + def cooldown(rate, per, type=BucketType.default): """A decorator that adds a cooldown to a :class:`Command` or its subclasses. diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index a6ee8cdc5..829fb011a 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -29,7 +29,7 @@ from discord.errors import DiscordException __all__ = [ 'CommandError', 'MissingRequiredArgument', 'BadArgument', 'NoPrivateMessage', 'CheckFailure', 'CommandNotFound', 'DisabledCommand', 'CommandInvokeError', 'TooManyArguments', - 'UserInputError', 'CommandOnCooldown' ] + 'UserInputError', 'CommandOnCooldown', 'NotOwner' ] class CommandError(DiscordException): """The base exception type for all command related errors. @@ -100,6 +100,10 @@ class NoPrivateMessage(CheckFailure): """ 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