[Mod] Context-based voice checks (#2351)

- Removed `redbot.cogs.mod.checks` module
- Moved logic for formatting a user-friendly list of permissions to `redbot.core.utils.chat_formatting`
- `[p]voice(un)ban` and `[p](un)mute voice` now check permissions in the user's voice channel

Resolves #2296.

Signed-off-by: Toby Harradine <tobyharradine@gmail.com>
This commit is contained in:
Toby Harradine 2019-01-10 11:35:37 +11:00 committed by GitHub
parent aac1460240
commit 8eb8848898
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 158 deletions

View File

@ -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)

View File

@ -2,19 +2,18 @@ import asyncio
import contextlib import contextlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple from collections import deque, defaultdict, namedtuple
from typing import cast from typing import cast, Optional
import discord import discord
from redbot.core import checks, Config, modlog, commands from redbot.core import checks, Config, modlog, commands
from redbot.core.bot import Red from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box, escape from redbot.core.utils.chat_formatting import box, escape, format_perms_list
from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions 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 redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, get_audit_reason
from .log import log from .log import log
from redbot.core.utils.common_filters import filter_invites, filter_various_mentions
_ = T_ = Translator("Mod", __file__) _ = T_ = Translator("Mod", __file__)
@ -781,15 +780,60 @@ class Mod(commands.Cog):
except discord.HTTPException: except discord.HTTPException:
return 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.command()
@commands.guild_only() @commands.guild_only()
@admin_or_voice_permissions(mute_members=True, deafen_members=True) @checks.admin_or_permissions(mute_members=True, deafen_members=True)
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): 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.""" """Ban a user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice user_voice_state: discord.VoiceState = user.voice
if user_voice_state is None: if (
await ctx.send(_("No voice state for that user!")) await self._voice_perm_check(
ctx, user_voice_state, deafen_members=True, mute_members=True
)
is False
):
return return
needs_mute = True if user_voice_state.mute is False else False needs_mute = True if user_voice_state.mute is False else False
needs_deafen = True if user_voice_state.deaf 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.command()
@commands.guild_only() @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): 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.""" """Unban a user from speaking and listening in the server's voice channels."""
user_voice_state = user.voice user_voice_state = user.voice
if user_voice_state is None: if (
await ctx.send(_("No voice state for that user!")) await self._voice_perm_check(
ctx, user_voice_state, deafen_members=True, mute_members=True
)
is False
):
return return
needs_unmute = True if user_voice_state.mute else False needs_unmute = True if user_voice_state.mute else False
needs_undeafen = True if user_voice_state.deaf else False needs_undeafen = True if user_voice_state.deaf else False
@ -912,25 +958,26 @@ class Mod(commands.Cog):
@mute.command(name="voice") @mute.command(name="voice")
@commands.guild_only() @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): async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
"""Mute a user in their current voice channel.""" """Mute a user in their current voice channel."""
user_voice_state = user.voice 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 guild = ctx.guild
author = ctx.author author = ctx.author
if user_voice_state:
channel = user_voice_state.channel channel = user_voice_state.channel
if channel:
audit_reason = get_audit_reason(author, reason) 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: if success:
await ctx.send( await ctx.send(
_("Muted {user} in channel {channel.name}").format( _("Muted {user} in channel {channel.name}").format(user=user, channel=channel)
user=user, channel=channel
)
) )
try: try:
await modlog.create_case( await modlog.create_case(
@ -947,12 +994,7 @@ class Mod(commands.Cog):
except RuntimeError as e: except RuntimeError as e:
await ctx.send(e) await ctx.send(e)
else: else:
await channel.send(issue) await ctx.send(issue)
else:
await ctx.send(_("That user is not in a voice channel right now!"))
else:
await ctx.send(_("No voice state for the target!"))
return
@mute.command(name="channel") @mute.command(name="channel")
@commands.guild_only() @commands.guild_only()
@ -1068,29 +1110,28 @@ class Mod(commands.Cog):
@unmute.command(name="voice") @unmute.command(name="voice")
@commands.guild_only() @commands.guild_only()
@mod_or_voice_permissions(mute_members=True)
@bot_has_voice_permissions(mute_members=True)
async def unmute_voice( async def unmute_voice(
self, ctx: commands.Context, user: discord.Member, *, reason: str = None self, ctx: commands.Context, user: discord.Member, *, reason: str = None
): ):
"""Unmute a user in their current voice channel.""" """Unmute a user in their current voice channel."""
user_voice_state = user.voice 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 guild = ctx.guild
author = ctx.author author = ctx.author
if user_voice_state:
channel = user_voice_state.channel channel = user_voice_state.channel
if channel:
audit_reason = get_audit_reason(author, reason) audit_reason = get_audit_reason(author, reason)
success, message = await self.unmute_user( success, message = await self.unmute_user(guild, channel, author, user, audit_reason)
guild, channel, author, user, audit_reason
)
if success: if success:
await ctx.send( await ctx.send(
_("Unmuted {user} in channel {channel.name}").format( _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel)
user=user, channel=channel
)
) )
try: try:
await modlog.create_case( await modlog.create_case(
@ -1108,11 +1149,6 @@ class Mod(commands.Cog):
await ctx.send(e) await ctx.send(e)
else: else:
await ctx.send(_("Unmute failed. Reason: {}").format(message)) await ctx.send(_("Unmute failed. Reason: {}").format(message))
else:
await ctx.send(_("That user is not in a voice channel right now!"))
else:
await ctx.send(_("No voice state for the target!"))
return
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@unmute.command(name="channel") @unmute.command(name="channel")
@ -1334,8 +1370,8 @@ class Mod(commands.Cog):
user = author user = author
# A special case for a special someone :^) # A special case for a special someone :^)
special_date = datetime(2016, 1, 10, 6, 8, 4, 443_000) special_date = datetime(2016, 1, 10, 6, 8, 4, 443000)
is_special = user.id == 96_130_341_705_637_888 and guild.id == 133_049_272_517_001_216 is_special = user.id == 96130341705637888 and guild.id == 133049272517001216
roles = sorted(user.roles)[1:] roles = sorted(user.roles)[1:]
names, nicks = await self.get_names_and_nicks(user) names, nicks = await self.get_names_and_nicks(user)

View File

@ -15,7 +15,7 @@ from pkg_resources import DistributionNotFound
from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands
from .data_manager import storage_type 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 from .utils import fuzzy_command_search, format_fuzzy_results
log = logging.getLogger("red") log = logging.getLogger("red")
@ -234,18 +234,13 @@ def init_events(bot, cli_flags):
else: else:
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False)) await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
elif isinstance(error, commands.BotMissingPermissions): elif isinstance(error, commands.BotMissingPermissions):
missing_perms: List[str] = [] if bin(error.missing.value).count("1") == 1: # Only one perm missing
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:
plural = "" plural = ""
else: else:
plural = "s" plural = "s"
await ctx.send( await ctx.send(
"I require the {perms} permission{plural} to execute that command.".format( "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): elif isinstance(error, commands.CheckFailure):

View File

@ -1,5 +1,8 @@
import itertools import itertools
from typing import Sequence, Iterator, List from typing import Sequence, Iterator, List
import discord
from redbot.core.i18n import Translator from redbot.core.i18n import Translator
_ = Translator("UtilsChatFormatting", __file__) _ = Translator("UtilsChatFormatting", __file__)
@ -329,7 +332,7 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
return text 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*. """Get comma-separted list, with the last element joined with *and*.
This uses an Oxford comma, because without one, items containing This uses an Oxford comma, because without one, items containing
@ -357,3 +360,29 @@ def humanize_list(items: Sequence[str]):
if len(items) == 1: if len(items) == 1:
return items[0] return items[0]
return ", ".join(items[:-1]) + _(", and ") + items[-1] 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")