diff --git a/redbot/core/bot.py b/redbot/core/bot.py index c3d609e19..5eb2b027b 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -27,6 +27,7 @@ from types import MappingProxyType import discord from discord.ext.commands import when_mentioned_or +from discord.ext.commands.bot import BotBase from . import Config, i18n, commands, errors, drivers, modlog, bank from .cog_manager import CogManager, CogManagerUI @@ -59,7 +60,7 @@ def _is_submodule(parent, child): # barely spurious warning caused by our intentional shadowing -class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: disable=no-member +class RedBase(commands.GroupMixin, BotBase, RPCMixin): # pylint: disable=no-member """Mixin for the main bot class. This exists because `Red` inherits from `discord.AutoShardedClient`, which @@ -150,6 +151,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d self._main_dir = bot_dir self._cog_mgr = CogManager() + self._use_team_features = cli_flags.use_team_features super().__init__(*args, help_command=None, **kwargs) # Do not manually use the help formatter attribute here, see `send_help_for`, # for a documented API. The internals of this object are still subject to change. @@ -627,10 +629,42 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d global_setting = await self._config.embeds() return global_setting - async def is_owner(self, user) -> bool: + async def is_owner(self, user: Union[discord.User, discord.Member]) -> bool: + """ + Determines if the user should be considered a bot owner. + + This takes into account CLI flags and application ownership. + + By default, + application team members are not considered owners, + while individual application owners are. + + Parameters + ---------- + user: Union[discord.User, discord.Member] + + Returns + ------- + bool + """ if user.id in self._co_owners: return True - return await super().is_owner(user) + + if self.owner_id: + return self.owner_id == user.id + elif self.owner_ids: + return user.id in self.owner_ids + else: + app = await self.application_info() + if app.team: + if self._use_team_features: + self.owner_ids = ids = {m.id for m in app.team.members} + return user.id in ids + else: + self.owner_id = owner_id = app.owner.id + return user.id == owner_id + + return False async def is_admin(self, member: discord.Member) -> bool: """Checks if a member is an admin of their guild.""" @@ -1069,10 +1103,11 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): # pylint: d await self.wait_until_red_ready() destinations = [] opt_outs = await self._config.owner_opt_out_list() - for user_id in (self.owner_id, *self._co_owners): + team_ids = () if not self._use_team_features else self.owner_ids + for user_id in set((self.owner_id, *self._co_owners, *team_ids)): if user_id not in opt_outs: user = self.get_user(user_id) - if user: + if user and not user.bot: # user.bot is possible with flags and teams destinations.append(user) else: log.warning( diff --git a/redbot/core/cli.py b/redbot/core/cli.py index 9c2575795..f638530d1 100644 --- a/redbot/core/cli.py +++ b/redbot/core/cli.py @@ -200,6 +200,18 @@ def parse_cli_flags(args): parser.add_argument( "instance_name", nargs="?", help="Name of the bot instance created during `redbot-setup`." ) + parser.add_argument( + "--team-members-are-owners", + action="store_true", + dest="use_team_features", + default=False, + help=( + "Treat application team members as owners. " + "This is off by default. Owners can load and run arbitrary code. " + "Do not enable if you would not trust all of your team members with " + "all of the data on the host machine." + ), + ) args = parser.parse_args(args) diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py index 93c78b3e9..313f72f2f 100644 --- a/redbot/core/commands/commands.py +++ b/redbot/core/commands/commands.py @@ -330,15 +330,27 @@ class Command(CogCommandMixin, commands.Command): if not change_permission_state: ctx.permission_state = original_state - async def _verify_checks(self, ctx): + async def prepare(self, ctx): + ctx.command = self + if not self.enabled: raise commands.DisabledCommand(f"{self.name} command is disabled") - if not (await self.can_run(ctx, change_permission_state=True)): + if not await self.can_run(ctx, change_permission_state=True): raise commands.CheckFailure( f"The check functions for command {self.qualified_name} failed." ) + if self.cooldown_after_parsing: + await self._parse_arguments(ctx) + self._prepare_cooldowns(ctx) + else: + self._prepare_cooldowns(ctx) + await self._parse_arguments(ctx) + if self._max_concurrency is not None: + await self._max_concurrency.acquire(ctx) + await self.call_before_hooks(ctx) + async def do_conversion( self, ctx: "Context", converter, argument: str, param: inspect.Parameter ): @@ -625,14 +637,14 @@ class Group(GroupMixin, Command, CogGroupMixin, commands.Group): if ctx.invoked_subcommand is None or self == ctx.invoked_subcommand: if self.autohelp and not self.invoke_without_command: - await self._verify_checks(ctx) + await self.can_run(ctx, change_permission_state=True) await ctx.send_help() elif self.invoke_without_command: # So invoke_without_command when a subcommand of this group is invoked # will skip the the invokation of *this* command. However, because of # how our permissions system works, we don't want it to skip the checks # as well. - await self._verify_checks(ctx) + await self.can_run(ctx, change_permission_state=True) # this is actually why we don't prepare earlier. await super().invoke(ctx) @@ -778,6 +790,3 @@ class _AlwaysAvailableCommand(Command): async def can_run(self, ctx, *args, **kwargs) -> bool: return not ctx.author.bot - - async def _verify_checks(self, ctx) -> bool: - return not ctx.author.bot diff --git a/redbot/core/commands/requires.py b/redbot/core/commands/requires.py index f3614e0fa..88b348209 100644 --- a/redbot/core/commands/requires.py +++ b/redbot/core/commands/requires.py @@ -757,16 +757,10 @@ class _RulesDict(Dict[Union[int, str], PermState]): def _validate_perms_dict(perms: Dict[str, bool]) -> None: + invalid_keys = set(perms.keys()) - set(discord.Permissions.VALID_FLAGS) + if invalid_keys: + raise TypeError(f"Invalid perm name(s): {', '.join(invalid_keys)}") for perm, value in perms.items(): - try: - attr = getattr(discord.Permissions, perm) - except AttributeError: - attr = None - - if attr is None or not isinstance(attr, property): - # We reject invalid permissions - raise TypeError(f"Unknown permission name '{perm}'") - if value is not True: # We reject any permission not specified as 'True', since this is the only value which # makes practical sense. diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index 31872df37..0cee0ce2c 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -319,7 +319,10 @@ class Core(commands.Cog, CoreLogic): python_version = "[{}.{}.{}]({})".format(*sys.version_info[:3], python_url) red_version = "[{}]({})".format(__version__, red_pypi) app_info = await self.bot.application_info() - owner = app_info.owner + if app_info.team: + owner = app_info.team.name + else: + owner = app_info.owner custom_info = await self.bot._config.custom_info() async with aiohttp.ClientSession() as session: diff --git a/redbot/core/events.py b/redbot/core/events.py index e72c505a4..c36874a6c 100644 --- a/redbot/core/events.py +++ b/redbot/core/events.py @@ -49,8 +49,13 @@ def init_events(bot, cli_flags): users = len(set([m for m in bot.get_all_members()])) app_info = await bot.application_info() - if bot.owner_id is None: - bot.owner_id = app_info.owner.id + + if app_info.team: + if bot._use_team_features: + bot.owner_ids = {m.id for m in app_info.team.members} + else: + if bot.owner_id is None: + bot.owner_id = app_info.owner.id try: invite_url = discord.utils.oauth_url(app_info.id) @@ -213,6 +218,12 @@ def init_events(bot, cli_flags): ), delete_after=error.retry_after, ) + elif isinstance(error, commands.MaxConcurrencyReached): + await ctx.send( + "Too many people using this command. It can only be used {} time(s) per {} concurrently.".format( + error.number, error.per.name + ) + ) else: log.exception(type(error).__name__, exc_info=error) diff --git a/setup.cfg b/setup.cfg index 4fb7d934e..6e43c7d6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ packages = find_namespace: python_requires = >=3.8.1 install_requires = aiohttp==3.6.2 - aiohttp-json-rpc==0.12.1 + aiohttp-json-rpc==0.12.2 aiosqlite==0.11.0 appdirs==1.4.3 apsw-wheels==3.30.1.post3 @@ -38,7 +38,7 @@ install_requires = Click==7.0 colorama==0.4.3 contextlib2==0.5.5 - discord.py==1.2.5 + discord.py==1.3.0 distro==1.4.0; sys_platform == "linux" fuzzywuzzy==0.17.0 idna==2.8 @@ -46,7 +46,7 @@ install_requires = python-Levenshtein-wheels==0.13.1 pytz==2019.3 PyYAML==5.3 - Red-Lavalink==0.4.1 + Red-Lavalink==0.4.2 schema==0.7.1 tqdm==4.41.1 uvloop==0.14.0; sys_platform != "win32" and platform_python_implementation == "CPython"