mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
[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:
parent
aac1460240
commit
8eb8848898
@ -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)
|
|
||||||
@ -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,47 +958,43 @@ 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
|
audit_reason = get_audit_reason(author, reason)
|
||||||
if 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:
|
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:
|
||||||
)
|
await modlog.create_case(
|
||||||
try:
|
self.bot,
|
||||||
await modlog.create_case(
|
guild,
|
||||||
self.bot,
|
ctx.message.created_at,
|
||||||
guild,
|
"vmute",
|
||||||
ctx.message.created_at,
|
user,
|
||||||
"vmute",
|
author,
|
||||||
user,
|
reason,
|
||||||
author,
|
until=None,
|
||||||
reason,
|
channel=channel,
|
||||||
until=None,
|
)
|
||||||
channel=channel,
|
except RuntimeError as e:
|
||||||
)
|
await ctx.send(e)
|
||||||
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!"))
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("No voice state for the target!"))
|
await ctx.send(issue)
|
||||||
return
|
|
||||||
|
|
||||||
@mute.command(name="channel")
|
@mute.command(name="channel")
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@ -1068,51 +1110,45 @@ 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
|
audit_reason = get_audit_reason(author, reason)
|
||||||
if channel:
|
|
||||||
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:
|
||||||
|
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:
|
||||||
if success:
|
await ctx.send(e)
|
||||||
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!"))
|
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("No voice state for the target!"))
|
await ctx.send(_("Unmute failed. Reason: {}").format(message))
|
||||||
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)
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user