diff --git a/redbot/cogs/mod/checks.py b/redbot/cogs/mod/checks.py deleted file mode 100644 index 8553695a8..000000000 --- a/redbot/cogs/mod/checks.py +++ /dev/null @@ -1,66 +0,0 @@ -from redbot.core import commands - - -def mod_or_voice_permissions(**perms): - async def pred(ctx: commands.Context): - author = ctx.author - guild = ctx.guild - if await ctx.bot.is_owner(author) or guild.owner == author: - # Author is bot owner or guild owner - return True - - admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role()) - mod_role = guild.get_role(await ctx.bot.db.guild(guild).mod_role()) - - if admin_role in author.roles or mod_role in author.roles: - return True - - for vc in guild.voice_channels: - resolved = vc.permissions_for(author) - good = resolved.administrator or all( - getattr(resolved, name, None) == value for name, value in perms.items() - ) - if not good: - return False - else: - return True - - return commands.permissions_check(pred) - - -def admin_or_voice_permissions(**perms): - async def pred(ctx: commands.Context): - author = ctx.author - guild = ctx.guild - if await ctx.bot.is_owner(author) or guild.owner == author: - return True - admin_role = guild.get_role(await ctx.bot.db.guild(guild).admin_role()) - if admin_role in author.roles: - return True - for vc in guild.voice_channels: - resolved = vc.permissions_for(author) - good = resolved.administrator or all( - getattr(resolved, name, None) == value for name, value in perms.items() - ) - if not good: - return False - else: - return True - - return commands.permissions_check(pred) - - -def bot_has_voice_permissions(**perms): - async def pred(ctx: commands.Context): - guild = ctx.guild - for vc in guild.voice_channels: - resolved = vc.permissions_for(guild.me) - good = resolved.administrator or all( - getattr(resolved, name, None) == value for name, value in perms.items() - ) - if not good: - return False - else: - return True - - return commands.check(pred) diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 86b61c288..8928e7514 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -2,19 +2,18 @@ import asyncio import contextlib from datetime import datetime, timedelta from collections import deque, defaultdict, namedtuple -from typing import cast +from typing import cast, Optional import discord from redbot.core import checks, Config, modlog, commands from redbot.core.bot import Red from redbot.core.i18n import Translator, cog_i18n -from redbot.core.utils.chat_formatting import box, escape -from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions +from redbot.core.utils.chat_formatting import box, escape, format_perms_list +from redbot.core.utils.common_filters import filter_invites, filter_various_mentions from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason from .log import log -from redbot.core.utils.common_filters import filter_invites, filter_various_mentions _ = T_ = Translator("Mod", __file__) @@ -781,15 +780,60 @@ class Mod(commands.Cog): except discord.HTTPException: return + @staticmethod + async def _voice_perm_check( + ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool + ) -> bool: + """Check if the bot and user have sufficient permissions for voicebans. + + This also verifies that the user's voice state and connected + channel are not ``None``. + + Returns + ------- + bool + ``True`` if the permissions are sufficient and the user has + a valid voice state. + + """ + if user_voice_state is None or user_voice_state.channel is None: + await ctx.send(_("That user is not in a voice channel.")) + return False + voice_channel: discord.VoiceChannel = user_voice_state.channel + required_perms = discord.Permissions() + required_perms.update(**perms) + if not voice_channel.permissions_for(ctx.me) >= required_perms: + await ctx.send( + _("I require the {perms} permission(s) in that user's channel to do that.").format( + perms=format_perms_list(required_perms) + ) + ) + return False + if ( + ctx.permission_state is commands.PermState.NORMAL + and not voice_channel.permissions_for(ctx.author) >= required_perms + ): + await ctx.send( + _( + "You must have the {perms} permission(s) in that user's channel to use this " + "command." + ).format(perms=format_perms_list(required_perms)) + ) + return False + return True + @commands.command() @commands.guild_only() - @admin_or_voice_permissions(mute_members=True, deafen_members=True) - @bot_has_voice_permissions(mute_members=True, deafen_members=True) + @checks.admin_or_permissions(mute_members=True, deafen_members=True) async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Ban a user from speaking and listening in the server's voice channels.""" - user_voice_state = user.voice - if user_voice_state is None: - await ctx.send(_("No voice state for that user!")) + user_voice_state: discord.VoiceState = user.voice + if ( + await self._voice_perm_check( + ctx, user_voice_state, deafen_members=True, mute_members=True + ) + is False + ): return needs_mute = True if user_voice_state.mute is False else False needs_deafen = True if user_voice_state.deaf is False else False @@ -824,13 +868,15 @@ class Mod(commands.Cog): @commands.command() @commands.guild_only() - @admin_or_voice_permissions(mute_members=True, deafen_members=True) - @bot_has_voice_permissions(mute_members=True, deafen_members=True) async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Unban a user from speaking and listening in the server's voice channels.""" user_voice_state = user.voice - if user_voice_state is None: - await ctx.send(_("No voice state for that user!")) + if ( + await self._voice_perm_check( + ctx, user_voice_state, deafen_members=True, mute_members=True + ) + is False + ): return needs_unmute = True if user_voice_state.mute else False needs_undeafen = True if user_voice_state.deaf else False @@ -912,47 +958,43 @@ class Mod(commands.Cog): @mute.command(name="voice") @commands.guild_only() - @mod_or_voice_permissions(mute_members=True) - @bot_has_voice_permissions(mute_members=True) async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): """Mute a user in their current voice channel.""" user_voice_state = user.voice + if ( + await self._voice_perm_check( + ctx, user_voice_state, mute_members=True, manage_channels=True + ) + is False + ): + return guild = ctx.guild author = ctx.author - if user_voice_state: - channel = user_voice_state.channel - if channel: - audit_reason = get_audit_reason(author, reason) + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) + success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - if success: - await ctx.send( - _("Muted {user} in channel {channel.name}").format( - user=user, channel=channel - ) - ) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "vmute", - user, - author, - reason, - until=None, - channel=channel, - ) - except RuntimeError as e: - await ctx.send(e) - else: - await channel.send(issue) - else: - await ctx.send(_("That user is not in a voice channel right now!")) + if success: + await ctx.send( + _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) + ) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "vmute", + user, + author, + reason, + until=None, + channel=channel, + ) + except RuntimeError as e: + await ctx.send(e) else: - await ctx.send(_("No voice state for the target!")) - return + await ctx.send(issue) @mute.command(name="channel") @commands.guild_only() @@ -1068,51 +1110,45 @@ class Mod(commands.Cog): @unmute.command(name="voice") @commands.guild_only() - @mod_or_voice_permissions(mute_members=True) - @bot_has_voice_permissions(mute_members=True) async def unmute_voice( self, ctx: commands.Context, user: discord.Member, *, reason: str = None ): """Unmute a user in their current voice channel.""" user_voice_state = user.voice + if ( + await self._voice_perm_check( + ctx, user_voice_state, mute_members=True, manage_channels=True + ) + is False + ): + return guild = ctx.guild author = ctx.author - if user_voice_state: - channel = user_voice_state.channel - if channel: - audit_reason = get_audit_reason(author, reason) + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) - success, message = await self.unmute_user( - guild, channel, author, user, audit_reason + success, message = await self.unmute_user(guild, channel, author, user, audit_reason) + + if success: + await ctx.send( + _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) + ) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "vunmute", + user, + author, + reason, + until=None, + channel=channel, ) - - if success: - await ctx.send( - _("Unmuted {user} in channel {channel.name}").format( - user=user, channel=channel - ) - ) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "vunmute", - user, - author, - reason, - until=None, - channel=channel, - ) - except RuntimeError as e: - await ctx.send(e) - else: - await ctx.send(_("Unmute failed. Reason: {}").format(message)) - else: - await ctx.send(_("That user is not in a voice channel right now!")) + except RuntimeError as e: + await ctx.send(e) else: - await ctx.send(_("No voice state for the target!")) - return + await ctx.send(_("Unmute failed. Reason: {}").format(message)) @checks.mod_or_permissions(administrator=True) @unmute.command(name="channel") @@ -1334,8 +1370,8 @@ class Mod(commands.Cog): user = author # A special case for a special someone :^) - special_date = datetime(2016, 1, 10, 6, 8, 4, 443_000) - is_special = user.id == 96_130_341_705_637_888 and guild.id == 133_049_272_517_001_216 + special_date = datetime(2016, 1, 10, 6, 8, 4, 443000) + is_special = user.id == 96130341705637888 and guild.id == 133049272517001216 roles = sorted(user.roles)[1:] names, nicks = await self.get_names_and_nicks(user) diff --git a/redbot/core/events.py b/redbot/core/events.py index 427eea041..9286fb2d5 100644 --- a/redbot/core/events.py +++ b/redbot/core/events.py @@ -15,7 +15,7 @@ from pkg_resources import DistributionNotFound from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands from .data_manager import storage_type -from .utils.chat_formatting import inline, bordered, humanize_list +from .utils.chat_formatting import inline, bordered, format_perms_list from .utils import fuzzy_command_search, format_fuzzy_results log = logging.getLogger("red") @@ -234,18 +234,13 @@ def init_events(bot, cli_flags): else: await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False)) elif isinstance(error, commands.BotMissingPermissions): - missing_perms: List[str] = [] - for perm, value in error.missing: - if value is True: - perm_name = '"' + perm.replace("_", " ").title() + '"' - missing_perms.append(perm_name) - if len(missing_perms) == 1: + if bin(error.missing.value).count("1") == 1: # Only one perm missing plural = "" else: plural = "s" await ctx.send( "I require the {perms} permission{plural} to execute that command.".format( - perms=humanize_list(missing_perms), plural=plural + perms=format_perms_list(error.missing), plural=plural ) ) elif isinstance(error, commands.CheckFailure): diff --git a/redbot/core/utils/chat_formatting.py b/redbot/core/utils/chat_formatting.py index 4fea93fe8..f0fb8aa21 100644 --- a/redbot/core/utils/chat_formatting.py +++ b/redbot/core/utils/chat_formatting.py @@ -1,5 +1,8 @@ import itertools from typing import Sequence, Iterator, List + +import discord + from redbot.core.i18n import Translator _ = Translator("UtilsChatFormatting", __file__) @@ -329,7 +332,7 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False) return text -def humanize_list(items: Sequence[str]): +def humanize_list(items: Sequence[str]) -> str: """Get comma-separted list, with the last element joined with *and*. This uses an Oxford comma, because without one, items containing @@ -357,3 +360,29 @@ def humanize_list(items: Sequence[str]): if len(items) == 1: return items[0] return ", ".join(items[:-1]) + _(", and ") + items[-1] + + +def format_perms_list(perms: discord.Permissions) -> str: + """Format a list of permission names. + + This will return a humanized list of the names of all enabled + permissions in the provided `discord.Permissions` object. + + Parameters + ---------- + perms : discord.Permissions + The permissions object with the requested permissions to list + enabled. + + Returns + ------- + str + The humanized list. + + """ + perm_names: List[str] = [] + for perm, value in perms: + if value is True: + perm_name = '"' + perm.replace("_", " ").title() + '"' + perm_names.append(perm_name) + return humanize_list(perm_names).replace("Guild", "Server")