From 3131b5368e212a3fe40ab7dce960ec1ed9ce7eb7 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 9 Mar 2020 23:02:52 -0600 Subject: [PATCH 001/103] revert the revert the revert git is hard... --- redbot/cogs/mutes/__init__.py | 7 + redbot/cogs/mutes/abc.py | 27 ++ redbot/cogs/mutes/converters.py | 48 +++ redbot/cogs/mutes/mutes.py | 698 ++++++++++++++++++++++++++++++++ redbot/cogs/mutes/voicemutes.py | 227 +++++++++++ 5 files changed, 1007 insertions(+) create mode 100644 redbot/cogs/mutes/__init__.py create mode 100644 redbot/cogs/mutes/abc.py create mode 100644 redbot/cogs/mutes/converters.py create mode 100644 redbot/cogs/mutes/mutes.py create mode 100644 redbot/cogs/mutes/voicemutes.py diff --git a/redbot/cogs/mutes/__init__.py b/redbot/cogs/mutes/__init__.py new file mode 100644 index 00000000000..1a5f91fed08 --- /dev/null +++ b/redbot/cogs/mutes/__init__.py @@ -0,0 +1,7 @@ +from redbot.core.bot import Red +from .mutes import Mutes + + +async def setup(bot: Red): + cog = Mutes(bot) + bot.add_cog(cog) diff --git a/redbot/cogs/mutes/abc.py b/redbot/cogs/mutes/abc.py new file mode 100644 index 00000000000..5403d9dc940 --- /dev/null +++ b/redbot/cogs/mutes/abc.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple, Optional, Dict +from datetime import datetime + +import discord +from redbot.core import Config, commands +from redbot.core.bot import Red + + +class MixinMeta(ABC): + """ + Base class for well behaved type hint detection with composite class. + + Basically, to keep developers sane when not all attributes are defined in each mixin. + """ + + def __init__(self, *_args): + self.config: Config + self.bot: Red + self._mutes_cache: Dict[int, Dict[int, Optional[datetime]]] + + @staticmethod + @abstractmethod + async def _voice_perm_check( + ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool + ) -> bool: + raise NotImplementedError() diff --git a/redbot/cogs/mutes/converters.py b/redbot/cogs/mutes/converters.py new file mode 100644 index 00000000000..78d0c4a4f5a --- /dev/null +++ b/redbot/cogs/mutes/converters.py @@ -0,0 +1,48 @@ +import logging +import re +from typing import Union, Dict + +from discord.ext.commands.converter import Converter +from redbot.core import commands + +log = logging.getLogger("red.cogs.mutes") + +# the following regex is slightly modified from Red +# https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/core/commands/converter.py#L55 +TIME_RE_STRING = r"|".join( + [ + r"((?P\d+?)\s?(weeks?|w))", + r"((?P\d+?)\s?(days?|d))", + r"((?P\d+?)\s?(hours?|hrs|hr?))", + r"((?P\d+?)\s?(minutes?|mins?|m(?!o)))", # prevent matching "months" + r"((?P\d+?)\s?(seconds?|secs?|s))", + ] +) +TIME_RE = re.compile(TIME_RE_STRING, re.I) +TIME_SPLIT = re.compile(r"t(?:ime)?=") + + +class MuteTime(Converter): + """ + This will parse my defined multi response pattern and provide usable formats + to be used in multiple reponses + """ + + async def convert(self, ctx: commands.Context, argument: str) -> Dict[str, Union[dict, str]]: + time_split = TIME_SPLIT.split(argument) + result: Dict[str, Union[dict, str]] = {} + if time_split: + maybe_time = time_split[-1] + else: + maybe_time = argument + + time_data = {} + for time in TIME_RE.finditer(maybe_time): + argument = argument.replace(time[0], "") + for k, v in time.groupdict().items(): + if v: + time_data[k] = int(v) + if time_data: + result["duration"] = time_data + result["reason"] = argument + return result diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py new file mode 100644 index 00000000000..a0162c342c1 --- /dev/null +++ b/redbot/cogs/mutes/mutes.py @@ -0,0 +1,698 @@ +import discord +import asyncio +import logging + +from abc import ABC +from typing import cast, Optional, Dict, List, Tuple +from datetime import datetime, timedelta + +from .converters import MuteTime +from .voicemutes import VoiceMutes + +from redbot.core.bot import Red +from redbot.core import commands, checks, i18n, modlog, Config +from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list +from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy + +T_ = i18n.Translator("Mutes", __file__) + +_ = lambda s: s +mute_unmute_issues = { + "already_muted": _("That user can't send messages in this channel."), + "already_unmuted": _("That user isn't muted in this channel."), + "hierarchy_problem": _( + "I cannot let you do that. You are not higher than the user in the role hierarchy." + ), + "is_admin": _("That user cannot be muted, as they have the Administrator permission."), + "permissions_issue": _( + "Failed to mute user. I need the manage roles " + "permission and the user I'm muting must be " + "lower than myself in the role hierarchy." + ), + "left_guild": _("The user has left the server while applying an overwrite."), + "unknown_channel": _("The channel I tried to mute the user in isn't found."), + "role_missing": _("The mute role no longer exists."), +} +_ = T_ + +log = logging.getLogger("red.cogs.mutes") + +__version__ = "1.0.0" + + +class CompositeMetaClass(type(commands.Cog), type(ABC)): + """ + This allows the metaclass used for proper type detection to + coexist with discord.py's metaclass + """ + + pass + + +class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass): + """ + Stuff for mutes goes here + """ + + def __init__(self, bot: Red): + self.bot = bot + self.config = Config.get_conf(self, 49615220001, force_registration=True) + default_guild = { + "mute_role": None, + "respect_hierarchy": True, + "muted_users": {}, + "default_time": {}, + "removed_users": [], + } + self.config.register_guild(**default_guild) + self.config.register_member(perms_cache={}) + self.config.register_channel(muted_users={}) + self._server_mutes: Dict[int, Dict[int, dict]] = {} + self._channel_mutes: Dict[int, Dict[int, dict]] = {} + self._ready = asyncio.Event() + self.bot.loop.create_task(self.initialize()) + self._unmute_tasks = {} + self._unmute_task = asyncio.create_task(self._handle_automatic_unmute()) + # dict of guild id, member id and time to be unmuted + + async def initialize(self): + guild_data = await self.config.all_guilds() + for g_id, mutes in guild_data.items(): + self._server_mutes[g_id] = mutes["muted_users"] + channel_data = await self.config.all_channels() + for c_id, mutes in channel_data.items(): + self._channel_mutes[c_id] = mutes["muted_users"] + self._ready.set() + + async def cog_before_invoke(self, ctx: commands.Context): + await self._ready.wait() + + def cog_unload(self): + self._unmute_task.cancel() + for task in self._unmute_tasks.values(): + task.cancel() + + async def _handle_automatic_unmute(self): + await self.bot.wait_until_red_ready() + await self._ready.wait() + while True: + # await self._clean_tasks() + try: + await self._handle_server_unmutes() + except Exception: + log.error("error checking server unmutes", exc_info=True) + await asyncio.sleep(0.1) + try: + await self._handle_channel_unmutes() + except Exception: + log.error("error checking channel unmutes", exc_info=True) + await asyncio.sleep(120) + + async def _clean_tasks(self): + log.debug("Cleaning unmute tasks") + is_debug = log.getEffectiveLevel() <= logging.DEBUG + for task_id in list(self._unmute_tasks.keys()): + task = self._unmute_tasks[task_id] + + if task.canceled(): + self._unmute_tasks.pop(task_id, None) + continue + + if task.done(): + try: + r = task.result() + except Exception: + if is_debug: + log.exception("Dead task when trying to unmute") + self._unmute_tasks.pop(task_id, None) + + async def _handle_server_unmutes(self): + log.debug("Checking server unmutes") + for g_id, mutes in self._server_mutes.items(): + to_remove = [] + guild = self.bot.get_guild(g_id) + if guild is None: + continue + for u_id, data in mutes.items(): + time_to_unmute = data["until"] - datetime.utcnow().timestamp() + if time_to_unmute < 120.0: + self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( + self._auto_unmute_user(guild, data) + ) + to_remove.append(u_id) + for u_id in to_remove: + del self._server_mutes[g_id][u_id] + await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) + + async def _auto_unmute_user(self, guild: discord.Guild, data: dict): + delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + if delay < 1: + delay = 0 + await asyncio.sleep(delay) + try: + member = guild.get_member(data["member"]) + author = guild.get_member(data["author"]) + if not member or not author: + return + success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) + if success: + try: + await modlog.create_case( + self.bot, + guild, + datetime.utcnow(), + "sunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + except discord.errors.Forbidden: + return + + async def _handle_channel_unmutes(self): + log.debug("Checking channel unmutes") + for c_id, mutes in self._channel_mutes.items(): + to_remove = [] + channel = self.bot.get_channel(c_id) + if channel is None: + continue + for u_id, data in mutes.items(): + time_to_unmute = data["until"] - datetime.utcnow().timestamp() + if time_to_unmute < 120.0: + self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( + self._auto_channel_unmute_user(channel, data) + ) + for u_id in to_remove: + del self._channel_mutes[c_id][u_id] + await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) + + async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): + delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + if delay < 1: + delay = 0 + await asyncio.sleep(delay) + try: + member = channel.guild.get_member(data["member"]) + author = channel.guild.get_member(data["author"]) + if not member or not author: + return + success, message = await self.channel_unmute_user( + channel.guild, channel, author, member, _("Automatic unmute") + ) + if success: + try: + await modlog.create_case( + self.bot, + channel.guild, + datetime.utcnow(), + "cunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + except discord.errors.Forbidden: + return + + @commands.Cog.listener() + async def on_member_remove(self, member: discord.Member): + guild = member.guild + if guild.id in self._server_mutes: + if member.id in self._server_mutes[guild.id]: + del self._server_mutes[guild.id][member.id] + for channel in guild.channels: + if channel.id in self._channel_mutes: + if member.id in self._channel_mutes[channel.id]: + del self._channel_mutes[channel.id][member.id] + mute_role = await self.config.guild(guild).mute_role() + if not mute_role: + return + if mute_role in [r.id for r in member.roles]: + async with self.config.guild(guild).removed_users() as removed_users: + removed_users.append(member.id) + + @commands.Cog.listener() + async def on_member_join(self, member: discord.Member): + guild = member.guild + mute_role = await self.config.guild(guild).mute_role() + if not mute_role: + return + async with self.config.guild(guild).removed_users() as removed_users: + if member.id in removed_users: + removed_users.remove(member.id) + role = guild.get_role(mute_role) + if not role: + return + try: + await member.add_roles(role, reason=_("Previously muted in this server.")) + except discord.errors.Forbidden: + return + + @commands.group() + @commands.guild_only() + @checks.mod_or_permissions(manage_roles=True) + async def muteset(self, ctx: commands.Context): + """Mute settings.""" + pass + + @muteset.command(name="role") + @checks.bot_has_permissions(manage_roles=True) + async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): + """ + Sets the role to be applied when muting a user. + + If no role is setup the bot will attempt to mute a user by setting + channel overwrites in all channels to prevent the user from sending messages. + """ + if not role: + await self.config.guild(ctx.guild).mute_role.set(None) + await ctx.send(_("Channel overwrites will be used for mutes instead.")) + else: + await self.config.guild(ctx.guild).mute_role.set(role.id) + await ctx.send(_("Mute role set to {role}").format(role=role.name)) + + @muteset.command(name="makerole") + @checks.bot_has_permissions(manage_roles=True) + async def make_mute_role(self, ctx: commands.Context, *, name: str): + """ + Create a Muted role. + + This will create a role and apply overwrites to all available channels + to more easily setup muting a user. + + If you already have a muted role created on the server use + `[p]muteset role ROLE_NAME_HERE` + """ + perms = discord.Permissions() + perms.update(send_messages=False, speak=False, add_reactions=False) + try: + role = await ctx.guild.create_role( + name=name, permissions=perms, reason=_("Mute role setup") + ) + except discord.errors.Forbidden: + return + for channel in ctx.guild.channels: + overs = discord.PermissionOverwrite() + if isinstance(channel, discord.TextChannel): + overs.send_messages = False + overs.add_reactions = False + if isinstance(channel, discord.VoiceChannel): + overs.speak = False + try: + await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) + except discord.errors.Forbidden: + continue + await self.config.guild(ctx.guild).mute_role.set(role.id) + await ctx.send(_("Mute role set to {role}").format(role=role.name)) + + @muteset.command(name="time") + async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): + """ + Set the default mute time for the mute command. + """ + data = time.get("duration", {}) + if not data: + await self.config.guild(ctx.guild).default_time.set(data) + await ctx.send(_("Default mute time removed.")) + else: + await self.config.guild(ctx.guild).default_time.set(data) + await ctx.send( + _("Default mute time set to {time}").format( + time=humanize_timedelta(timedelta=timedelta(**data)) + ) + ) + + @commands.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_roles=True) + async def mute( + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + time_and_reason: MuteTime = {}, + ): + """Mute users.""" + + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + time = "" + until = None + if duration: + until = datetime.utcnow() + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.utcnow() + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.mute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "smute", + user, + author, + reason, + until=until, + channel=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if not success_list: + return await ctx.send(issue) + if until: + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp(), + } + self._server_mutes[ctx.guild.id][user.id] = mute + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await ctx.send( + _("{users} {verb} been muted in this server{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time + ) + ) + + @commands.command(name="mutechannel") + @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + @checks.mod_or_permissions(manage_roles=True) + async def channel_mute( + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + time_and_reason: MuteTime = {}, + ): + """Mute a user in the current text channel.""" + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + until = None + time = "" + if duration: + until = datetime.utcnow() + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.utcnow() + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + channel = ctx.message.channel + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.channel_mute_user( + guild, channel, author, user, audit_reason + ) + if success: + success_list.append(user) + + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "cmute", + user, + author, + reason, + until=until, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + if until: + if channel.id not in self._channel_mutes: + self._channel_mutes[channel.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp(), + } + self._channel_mutes[channel.id][user.id] = mute + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await channel.send( + _("{users} {verb} been muted in this channel{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time + ) + ) + + @commands.command() + @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + @checks.mod_or_permissions(manage_roles=True) + async def unmute( + self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + ): + """Unmute users.""" + guild = ctx.guild + author = ctx.author + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, message = await self.unmute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "sunmute", + user, + author, + reason, + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if ctx.guild.id in self._server_mutes: + for user in success_list: + del self._server_mutes[ctx.guild.id][user.id] + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + await ctx.send( + _("{users} unmuted in this server.").format( + users=humanize_list([f"{u}" for u in success_list]) + ) + ) + + @checks.mod_or_permissions(manage_roles=True) + @commands.command(name="channelunmute") + @commands.bot_has_permissions(manage_roles=True) + @commands.guild_only() + async def unmute_channel( + self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + ): + """Unmute a user in this channel.""" + channel = ctx.channel + author = ctx.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, message = await self.channel_unmute_user( + guild, channel, author, user, audit_reason + ) + + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "cunmute", + user, + author, + reason, + until=None, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + for user in success_list: + try: + del self._channel_mutes[channel.id][user.id] + except KeyError: + pass + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + await ctx.send( + _("{users} unmuted in this channel.").format( + users=humanize_list([f"{u}" for u in success_list]) + ) + ) + + async def mute_user( + self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + ) -> Tuple[bool, Optional[str]]: + """ + Handles muting users + """ + mute_role = await self.config.guild(guild).mute_role() + if mute_role: + try: + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + role = guild.get_role(mute_role) + if not role: + return False, mute_unmute_issues["role_missing"] + await user.add_roles(role, reason=reason) + except discord.errors.Forbidden: + return False, mute_unmute_issues["permissions_issue"] + return True, None + else: + mute_success = [] + for channel in guild.channels: + success, issue = await self.channel_mute_user(guild, channel, author, user, reason) + if not success: + mute_success.append(f"{channel.mention} - {issue}") + await asyncio.sleep(0.1) + if mute_success: + return False, "\n".join(s for s in mute_success) + else: + return True, None + + async def unmute_user( + self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + ) -> Tuple[bool, Optional[str]]: + """ + Handles muting users + """ + mute_role = await self.config.guild(guild).mute_role() + if mute_role: + try: + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + role = guild.get_role(mute_role) + if not role: + return False, mute_unmute_issues["role_missing"] + await user.remove_roles(role, reason=reason) + except discord.errors.Forbidden: + return False, mute_unmute_issues["permissions_issue"] + return True, None + else: + mute_success = [] + for channel in guild.channels: + success, issue = await self.channel_unmute_user( + guild, channel, author, user, reason + ) + if not success: + mute_success.append(f"{channel.mention} - {issue}") + await asyncio.sleep(0.1) + if mute_success: + return False, "\n".join(s for s in mute_success) + else: + return True, None + + async def channel_mute_user( + self, + guild: discord.Guild, + channel: discord.abc.GuildChannel, + author: discord.Member, + user: discord.Member, + reason: str, + ) -> Tuple[bool, Optional[str]]: + """Mutes the specified user in the specified channel""" + overwrites = channel.overwrites_for(user) + permissions = channel.permissions_for(user) + + if permissions.administrator: + return False, _(mute_unmute_issues["is_admin"]) + + new_overs = {} + if not isinstance(channel, discord.TextChannel): + new_overs.update(speak=False) + if not isinstance(channel, discord.VoiceChannel): + new_overs.update(send_messages=False, add_reactions=False) + + if all(getattr(permissions, p) is False for p in new_overs.keys()): + return False, _(mute_unmute_issues["already_muted"]) + + elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + + old_overs = {k: getattr(overwrites, k) for k in new_overs} + overwrites.update(**new_overs) + try: + await channel.set_permissions(user, overwrite=overwrites, reason=reason) + except discord.Forbidden: + return False, _(mute_unmute_issues["permissions_issue"]) + except discord.NotFound as e: + if e.code == 10003: + return False, _(mute_unmute_issues["unknown_channel"]) + elif e.code == 10009: + return False, _(mute_unmute_issues["left_guild"]) + else: + await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) + return True, None + + async def channel_unmute_user( + self, + guild: discord.Guild, + channel: discord.abc.GuildChannel, + author: discord.Member, + user: discord.Member, + reason: str, + ) -> Tuple[bool, Optional[str]]: + overwrites = channel.overwrites_for(user) + perms_cache = await self.config.member(user).perms_cache() + + if channel.id in perms_cache: + old_values = perms_cache[channel.id] + else: + old_values = {"send_messages": None, "add_reactions": None, "speak": None} + + if all(getattr(overwrites, k) == v for k, v in old_values.items()): + return False, _(mute_unmute_issues["already_unmuted"]) + + elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + + overwrites.update(**old_values) + try: + if overwrites.is_empty(): + await channel.set_permissions( + user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason + ) + else: + await channel.set_permissions(user, overwrite=overwrites, reason=reason) + except discord.Forbidden: + return False, _(mute_unmute_issues["permissions_issue"]) + except discord.NotFound as e: + if e.code == 10003: + return False, _(mute_unmute_issues["unknown_channel"]) + elif e.code == 10009: + return False, _(mute_unmute_issues["left_guild"]) + else: + await self.config.member(user).clear_raw("perms_cache", str(channel.id)) + return True, None diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py new file mode 100644 index 00000000000..31140f4bb5a --- /dev/null +++ b/redbot/cogs/mutes/voicemutes.py @@ -0,0 +1,227 @@ +from typing import Optional +from .abc import MixinMeta + +import discord +from redbot.core import commands, checks, i18n, modlog +from redbot.core.utils.chat_formatting import format_perms_list +from redbot.core.utils.mod import get_audit_reason + +T_ = i18n.Translator("Mutes", __file__) + +_ = lambda s: s +_ = T_ + + +class VoiceMutes(MixinMeta): + """ + This handles all voice channel related muting + """ + + @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() + @checks.admin_or_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 ( + 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 + audit_reason = get_audit_reason(ctx.author, reason) + if needs_unmute and needs_undeafen: + await user.edit(mute=False, deafen=False, reason=audit_reason) + elif needs_unmute: + await user.edit(mute=False, reason=audit_reason) + elif needs_undeafen: + await user.edit(deafen=False, reason=audit_reason) + else: + await ctx.send(_("That user isn't muted or deafened by the server!")) + return + + guild = ctx.guild + author = ctx.author + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceunban", + user, + author, + reason, + until=None, + channel=None, + ) + except RuntimeError as e: + await ctx.send(e) + await ctx.send(_("User is now allowed to speak and listen in voice channels")) + + @commands.command() + @commands.guild_only() + @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: 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 + audit_reason = get_audit_reason(ctx.author, reason) + author = ctx.author + guild = ctx.guild + if needs_mute and needs_deafen: + await user.edit(mute=True, deafen=True, reason=audit_reason) + elif needs_mute: + await user.edit(mute=True, reason=audit_reason) + elif needs_deafen: + await user.edit(deafen=True, reason=audit_reason) + else: + await ctx.send(_("That user is already muted and deafened server-wide!")) + return + + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceban", + user, + author, + reason, + until=None, + channel=None, + ) + except RuntimeError as e: + await ctx.send(e) + await ctx.send(_("User has been banned from speaking or listening in voice channels")) + + @commands.command(name="voicemute") + @commands.guild_only() + 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 + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) + + success, issue = await self.mute_user(guild, channel, author, user, audit_reason) + + if success: + 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) + await ctx.send( + _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) + ) + else: + await ctx.send(issue) + + @commands.command(name="voiceunmute") + @commands.guild_only() + 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 + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) + + success, message = await self.unmute_user(guild, channel, author, user, audit_reason) + + 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) + await ctx.send( + _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) + ) From 11be71a48d3bf4d16bd8949a6c520e3781beffa7 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 9 Mar 2020 23:04:07 -0600 Subject: [PATCH 002/103] and remove old mutes --- redbot/cogs/mod/mod.py | 2 - redbot/cogs/mod/mutes.py | 477 --------------------------------------- 2 files changed, 479 deletions(-) diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 854e6944d3f..c70c98cd4aa 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -14,7 +14,6 @@ from redbot.core.utils._internal_utils import send_to_owners_with_prefix_replaced from .events import Events from .kickban import KickBanMixin -from .mutes import MuteMixin from .names import ModInfo from .slowmode import Slowmode from .settings import ModSettings @@ -38,7 +37,6 @@ class Mod( ModSettings, Events, KickBanMixin, - MuteMixin, ModInfo, Slowmode, commands.Cog, diff --git a/redbot/cogs/mod/mutes.py b/redbot/cogs/mod/mutes.py index 80c3df81d3d..e69de29bb2d 100644 --- a/redbot/cogs/mod/mutes.py +++ b/redbot/cogs/mod/mutes.py @@ -1,477 +0,0 @@ -import asyncio -from datetime import timezone -from typing import cast, Optional - -import discord -from redbot.core import commands, checks, i18n, modlog -from redbot.core.utils import AsyncIter -from redbot.core.utils.chat_formatting import format_perms_list -from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy -from .abc import MixinMeta - -T_ = i18n.Translator("Mod", __file__) - -_ = lambda s: s -mute_unmute_issues = { - "already_muted": _("That user can't send messages in this channel."), - "already_unmuted": _("That user isn't muted in this channel."), - "hierarchy_problem": _( - "I cannot let you do that. You are not higher than the user in the role hierarchy." - ), - "is_admin": _("That user cannot be muted, as they have the Administrator permission."), - "permissions_issue": _( - "Failed to mute user. I need the manage roles " - "permission and the user I'm muting must be " - "lower than myself in the role hierarchy." - ), - "left_guild": _("The user has left the server while applying an overwrite."), - "unknown_channel": _("The channel I tried to mute the user in isn't found."), -} -_ = T_ - - -class MuteMixin(MixinMeta): - """ - Stuff for mutes goes here - """ - - @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() - @checks.admin_or_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 ( - 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 - audit_reason = get_audit_reason(ctx.author, reason) - if needs_unmute and needs_undeafen: - await user.edit(mute=False, deafen=False, reason=audit_reason) - elif needs_unmute: - await user.edit(mute=False, reason=audit_reason) - elif needs_undeafen: - await user.edit(deafen=False, reason=audit_reason) - else: - await ctx.send(_("That user isn't muted or deafened by the server!")) - return - - guild = ctx.guild - author = ctx.author - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "voiceunban", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User is now allowed to speak and listen in voice channels")) - - @commands.command() - @commands.guild_only() - @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: 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 - audit_reason = get_audit_reason(ctx.author, reason) - author = ctx.author - guild = ctx.guild - if needs_mute and needs_deafen: - await user.edit(mute=True, deafen=True, reason=audit_reason) - elif needs_mute: - await user.edit(mute=True, reason=audit_reason) - elif needs_deafen: - await user.edit(deafen=True, reason=audit_reason) - else: - await ctx.send(_("That user is already muted and deafened server-wide!")) - return - - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "voiceban", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User has been banned from speaking or listening in voice channels")) - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_channels=True) - async def mute(self, ctx: commands.Context): - """Mute users.""" - pass - - @mute.command(name="voice") - @commands.guild_only() - 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 - channel = user_voice_state.channel - audit_reason = get_audit_reason(author, reason) - - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "vmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send( - _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) - ) - try: - if channel.permissions_for(ctx.me).move_members: - await user.move_to(channel) - else: - raise RuntimeError - except (discord.Forbidden, RuntimeError): - await ctx.send( - _( - "Because I don't have the Move Members permission, this will take into effect when the user rejoins." - ) - ) - else: - await ctx.send(issue) - - @mute.command(name="channel") - @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) - @checks.mod_or_permissions(administrator=True) - async def channel_mute( - self, ctx: commands.Context, user: discord.Member, *, reason: str = None - ): - """Mute a user in the current text channel.""" - author = ctx.message.author - channel = ctx.message.channel - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await channel.send(_("User has been muted in this channel.")) - else: - await channel.send(issue) - - @mute.command(name="server", aliases=["guild"]) - @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) - @checks.mod_or_permissions(administrator=True) - async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Mutes user in the server.""" - author = ctx.message.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - - mute_success = [] - async with ctx.typing(): - for channel in guild.channels: - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - mute_success.append((success, issue)) - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "smute", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User has been muted in this server.")) - - @commands.group() - @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) - @checks.mod_or_permissions(manage_channels=True) - async def unmute(self, ctx: commands.Context): - """Unmute users.""" - pass - - @unmute.command(name="voice") - @commands.guild_only() - 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 - channel = user_voice_state.channel - audit_reason = get_audit_reason(author, reason) - - success, message = await self.unmute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "vunmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send( - _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) - ) - try: - if channel.permissions_for(ctx.me).move_members: - await user.move_to(channel) - else: - raise RuntimeError - except (discord.Forbidden, RuntimeError): - await ctx.send( - _( - "Because I don't have the Move Members permission, this will take into effect when the user rejoins." - ) - ) - else: - await ctx.send(_("Unmute failed. Reason: {}").format(message)) - - @checks.mod_or_permissions(administrator=True) - @unmute.command(name="channel") - @commands.bot_has_permissions(manage_roles=True) - @commands.guild_only() - async def unmute_channel( - self, ctx: commands.Context, user: discord.Member, *, reason: str = None - ): - """Unmute a user in this channel.""" - channel = ctx.channel - author = ctx.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - - success, message = await self.unmute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cunmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send(_("User unmuted in this channel.")) - else: - await ctx.send(_("Unmute failed. Reason: {}").format(message)) - - @checks.mod_or_permissions(administrator=True) - @unmute.command(name="server", aliases=["guild"]) - @commands.bot_has_permissions(manage_roles=True) - @commands.guild_only() - async def unmute_guild( - self, ctx: commands.Context, user: discord.Member, *, reason: str = None - ): - """Unmute a user in this server.""" - guild = ctx.guild - author = ctx.author - audit_reason = get_audit_reason(author, reason) - - unmute_success = [] - async with ctx.typing(): - for channel in guild.channels: - success, message = await self.unmute_user( - guild, channel, author, user, audit_reason - ) - unmute_success.append((success, message)) - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "sunmute", - user, - author, - reason, - until=None, - ) - await ctx.send(_("User has been unmuted in this server.")) - - async def mute_user( - self, - guild: discord.Guild, - channel: discord.abc.GuildChannel, - author: discord.Member, - user: discord.Member, - reason: str, - ) -> (bool, str): - """Mutes the specified user in the specified channel""" - overwrites = channel.overwrites_for(user) - permissions = channel.permissions_for(user) - - if permissions.administrator: - return False, _(mute_unmute_issues["is_admin"]) - - new_overs = {} - if not isinstance(channel, discord.TextChannel): - new_overs.update(speak=False) - if not isinstance(channel, discord.VoiceChannel): - new_overs.update(send_messages=False, add_reactions=False) - - if all(getattr(permissions, p) is False for p in new_overs.keys()): - return False, _(mute_unmute_issues["already_muted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) - - old_overs = {k: getattr(overwrites, k) for k in new_overs} - overwrites.update(**new_overs) - try: - await channel.set_permissions(user, overwrite=overwrites, reason=reason) - except discord.Forbidden: - return False, _(mute_unmute_issues["permissions_issue"]) - except discord.NotFound as e: - if e.code == 10003: - return False, _(mute_unmute_issues["unknown_channel"]) - elif e.code == 10009: - return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) - return True, None - - async def unmute_user( - self, - guild: discord.Guild, - channel: discord.abc.GuildChannel, - author: discord.Member, - user: discord.Member, - reason: str, - ) -> (bool, str): - overwrites = channel.overwrites_for(user) - perms_cache = await self.config.member(user).perms_cache() - - if channel.id in perms_cache: - old_values = perms_cache[channel.id] - else: - old_values = {"send_messages": None, "add_reactions": None, "speak": None} - - if all(getattr(overwrites, k) == v for k, v in old_values.items()): - return False, _(mute_unmute_issues["already_unmuted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) - - overwrites.update(**old_values) - try: - if overwrites.is_empty(): - await channel.set_permissions( - user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason - ) - else: - await channel.set_permissions(user, overwrite=overwrites, reason=reason) - except discord.Forbidden: - return False, _(mute_unmute_issues["permissions_issue"]) - except discord.NotFound as e: - if e.code == 10003: - return False, _(mute_unmute_issues["unknown_channel"]) - elif e.code == 10009: - return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).clear_raw("perms_cache", str(channel.id)) - return True, None From 35af119a71f06251b394d90b4b286065899dcb74 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 8 Apr 2020 17:47:04 -0600 Subject: [PATCH 003/103] make voicemutes less yelly --- redbot/cogs/mutes/voicemutes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 31140f4bb5a..2da2b55818d 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -82,7 +82,7 @@ async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reaso elif needs_undeafen: await user.edit(deafen=False, reason=audit_reason) else: - await ctx.send(_("That user isn't muted or deafened by the server!")) + await ctx.send(_("That user isn't muted or deafened by the server.")) return guild = ctx.guild @@ -101,7 +101,7 @@ async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reaso ) except RuntimeError as e: await ctx.send(e) - await ctx.send(_("User is now allowed to speak and listen in voice channels")) + await ctx.send(_("User is now allowed to speak and listen in voice channels.")) @commands.command() @commands.guild_only() @@ -128,7 +128,7 @@ async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: elif needs_deafen: await user.edit(deafen=True, reason=audit_reason) else: - await ctx.send(_("That user is already muted and deafened server-wide!")) + await ctx.send(_("That user is already muted and deafened server-wide.")) return try: @@ -145,7 +145,7 @@ async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: ) except RuntimeError as e: await ctx.send(e) - await ctx.send(_("User has been banned from speaking or listening in voice channels")) + await ctx.send(_("User has been banned from speaking or listening in voice channels.")) @commands.command(name="voicemute") @commands.guild_only() @@ -182,7 +182,7 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso except RuntimeError as e: await ctx.send(e) await ctx.send( - _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) + _("Muted {user} in channel {channel.name}.").format(user=user, channel=channel) ) else: await ctx.send(issue) @@ -223,5 +223,5 @@ async def unmute_voice( except RuntimeError as e: await ctx.send(e) await ctx.send( - _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) + _("Unmuted {user} in channel {channel.name}.").format(user=user, channel=channel) ) From 997e1b2fdb96626cd81e967e67ad66b5a608bb1e Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 13:28:44 -0600 Subject: [PATCH 004/103] fix error when no args present in mute commands --- redbot/cogs/mutes/mutes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a0162c342c1..f529f4e8878 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -338,7 +338,8 @@ async def mute( time_and_reason: MuteTime = {}, ): """Mute users.""" - + if not users: + return await ctx.send_help() duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) time = "" @@ -395,7 +396,7 @@ async def mute( ) ) - @commands.command(name="mutechannel") + @commands.command(name="mutechannel", aliases=["channelmute"]) @commands.guild_only() @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) @@ -407,6 +408,8 @@ async def channel_mute( time_and_reason: MuteTime = {}, ): """Mute a user in the current text channel.""" + if not users: + return await ctx.send_help() duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) until = None @@ -474,6 +477,8 @@ async def unmute( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): """Unmute users.""" + if not users: + return await ctx.send_help() guild = ctx.guild author = ctx.author audit_reason = get_audit_reason(author, reason) @@ -506,13 +511,15 @@ async def unmute( ) @checks.mod_or_permissions(manage_roles=True) - @commands.command(name="channelunmute") + @commands.command(name="channelunmute", aliases=["unmutechannel"]) @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() async def unmute_channel( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): """Unmute a user in this channel.""" + if not users: + return await ctx.send_help() channel = ctx.channel author = ctx.author guild = ctx.guild From 80e038f6de47c54d6bc2bcba8cf0749b09daf287 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 13:54:25 -0600 Subject: [PATCH 005/103] update docstrings --- redbot/cogs/mutes/mutes.py | 43 +++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f529f4e8878..f6e15a3c712 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -263,8 +263,7 @@ async def muteset(self, ctx: commands.Context): @muteset.command(name="role") @checks.bot_has_permissions(manage_roles=True) async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): - """ - Sets the role to be applied when muting a user. + """Sets the role to be applied when muting a user. If no role is setup the bot will attempt to mute a user by setting channel overwrites in all channels to prevent the user from sending messages. @@ -279,8 +278,7 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): @muteset.command(name="makerole") @checks.bot_has_permissions(manage_roles=True) async def make_mute_role(self, ctx: commands.Context, *, name: str): - """ - Create a Muted role. + """Create a Muted role. This will create a role and apply overwrites to all available channels to more easily setup muting a user. @@ -337,7 +335,18 @@ async def mute( *, time_and_reason: MuteTime = {}, ): - """Mute users.""" + """Mute users. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason={}]` is the time to mute for and reason. Time is + any valid time length such as `30 minutes` or `2 days`. If nothing + is provided the mute will be indefinite. + + Examples: + `[p]mute @member1 @member2 spam 5 hours` + `[p]mute @member1 3 days` + + """ if not users: return await ctx.send_help() duration = time_and_reason.get("duration", {}) @@ -407,7 +416,17 @@ async def channel_mute( *, time_and_reason: MuteTime = {}, ): - """Mute a user in the current text channel.""" + """Mute a user in the current text channel. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason={}]` is the time to mute for and reason. Time is + any valid time length such as `30 minutes` or `2 days`. If nothing + is provided the mute will be indefinite. + + Examples: + `[p]mutechannel @member1 @member2 spam 5 hours` + `[p]mutechannel @member1 3 days` + """ if not users: return await ctx.send_help() duration = time_and_reason.get("duration", {}) @@ -476,7 +495,11 @@ async def channel_mute( async def unmute( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): - """Unmute users.""" + """Unmute users. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[reason]` is the reason for the unmute. + """ if not users: return await ctx.send_help() guild = ctx.guild @@ -517,7 +540,11 @@ async def unmute( async def unmute_channel( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): - """Unmute a user in this channel.""" + """Unmute a user in this channel. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[reason]` is the reason for the unmute. + """ if not users: return await ctx.send_help() channel = ctx.channel From 49fda232d4243934a5b9a8f84e5f65affbde86de Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 15:29:55 -0600 Subject: [PATCH 006/103] address review --- redbot/cogs/mutes/mutes.py | 90 +++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f6e15a3c712..a172fa07369 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1,5 +1,6 @@ -import discord import asyncio +import contextlib +import discord import logging from abc import ABC @@ -13,13 +14,15 @@ from redbot.core import commands, checks, i18n, modlog, Config from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy +from redbot.core.utils.menus import start_adding_reactions +from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate T_ = i18n.Translator("Mutes", __file__) _ = lambda s: s mute_unmute_issues = { - "already_muted": _("That user can't send messages in this channel."), - "already_unmuted": _("That user isn't muted in this channel."), + "already_muted": _("That user is already muted in this channel."), + "already_unmuted": _("That user is not muted in this channel."), "hierarchy_problem": _( "I cannot let you do that. You are not higher than the user in the role hierarchy." ), @@ -404,6 +407,43 @@ async def mute( users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) ) + if issue: + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message) + + async def handle_issues(self, ctx: commands.Context, message: str) -> None: + can_react = ctx.channel.permissions_for(ctx.me).add_reactions + if not can_react: + message += " (y/n)" + query: discord.Message = await ctx.send(message) + if can_react: + # noinspection PyAsyncCall + start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS) + pred = ReactionPredicate.yes_or_no(query, ctx.author) + event = "reaction_add" + else: + pred = MessagePredicate.yes_or_no(ctx) + event = "message" + try: + await ctx.bot.wait_for(event, check=pred, timeout=30) + except asyncio.TimeoutError: + await query.delete() + return + + if not pred.result: + if can_react: + await query.delete() + else: + await ctx.send(_("OK then.")) + return + else: + if can_react: + with contextlib.suppress(discord.Forbidden): + await query.clear_reactions() + await ctx.send(issue) @commands.command(name="mutechannel", aliases=["channelmute"]) @commands.guild_only() @@ -507,7 +547,7 @@ async def unmute( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, message = await self.unmute_user(guild, author, user, audit_reason) + success, issue = await self.unmute_user(guild, author, user, audit_reason) if success: success_list.append(user) try: @@ -523,15 +563,26 @@ async def unmute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) + if not success_list: + return await ctx.send(issue) if ctx.guild.id in self._server_mutes: - for user in success_list: - del self._server_mutes[ctx.guild.id][user.id] - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + if user.id in self._server_mutes[ctx.guild.id]: + for user in success_list: + del self._server_mutes[ctx.guild.id][user.id] + await self.config.guild(ctx.guild).muted_users.set( + self._server_mutes[ctx.guild.id] + ) await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) ) ) + if issue: + message = _( + "{users} could not be unmuted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="channelunmute", aliases=["unmutechannel"]) @@ -575,11 +626,14 @@ async def unmute_channel( log.error(_("Error creating modlog case"), exc_info=e) if success_list: for user in success_list: - try: + if ( + channel.id in self._channel_mutes + and user.id in self._channel_mutes[channel.id] + ): del self._channel_mutes[channel.id][user.id] - except KeyError: - pass - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + await self.config.channel(channel).muted_users.set( + self._channel_mutes[channel.id] + ) await ctx.send( _("{users} unmuted in this channel.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -611,8 +665,10 @@ async def mute_user( if not success: mute_success.append(f"{channel.mention} - {issue}") await asyncio.sleep(0.1) - if mute_success: + if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) + elif mute_success and len(mute_success) != len(guild.channels): + return True, "\n".join(s for s in mute_success) else: return True, None @@ -669,10 +725,7 @@ async def channel_mute_user( if not isinstance(channel, discord.VoiceChannel): new_overs.update(send_messages=False, add_reactions=False) - if all(getattr(permissions, p) is False for p in new_overs.keys()): - return False, _(mute_unmute_issues["already_muted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): return False, _(mute_unmute_issues["hierarchy_problem"]) old_overs = {k: getattr(overwrites, k) for k in new_overs} @@ -706,10 +759,7 @@ async def channel_unmute_user( else: old_values = {"send_messages": None, "add_reactions": None, "speak": None} - if all(getattr(overwrites, k) == v for k, v in old_values.items()): - return False, _(mute_unmute_issues["already_unmuted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): return False, _(mute_unmute_issues["hierarchy_problem"]) overwrites.update(**old_values) From bb3fa22754103999335f7bf5e03d08d44da6e9c4 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 15:30:31 -0600 Subject: [PATCH 007/103] black --- redbot/cogs/mutes/mutes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a172fa07369..47766f7e2f0 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -409,9 +409,9 @@ async def mute( ) if issue: message = _( - "{users} could not be muted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) await self.handle_issues(ctx, message) async def handle_issues(self, ctx: commands.Context, message: str) -> None: @@ -579,9 +579,9 @@ async def unmute( ) if issue: message = _( - "{users} could not be unmuted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) + "{users} could not be unmuted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) await self.handle_issues(ctx, message) @checks.mod_or_permissions(manage_roles=True) From d15e940d70914288f2b59f9b23d0ee9955646d02 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 15:33:01 -0600 Subject: [PATCH 008/103] oops --- redbot/cogs/mutes/mutes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 47766f7e2f0..b61b7059a8b 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -412,9 +412,9 @@ async def mute( "{users} could not be muted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message) + await self.handle_issues(ctx, message, issue) - async def handle_issues(self, ctx: commands.Context, message: str) -> None: + async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions if not can_react: message += " (y/n)" @@ -582,7 +582,7 @@ async def unmute( "{users} could not be unmuted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message) + await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="channelunmute", aliases=["unmutechannel"]) From 9c8582c55d2cced504a0717fb87554a2fa50d545 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 16:21:29 -0600 Subject: [PATCH 009/103] fix voicemutes --- redbot/cogs/mutes/voicemutes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 2da2b55818d..e6aeec3b980 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -164,7 +164,7 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso 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.channel_mute_user(guild, channel, author, user, audit_reason) if success: try: @@ -206,7 +206,9 @@ async def unmute_voice( 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.channel_unmute_user( + guild, channel, author, user, audit_reason + ) try: await modlog.create_case( From 7216a28618898d2c5baa3a5ceed1533ffba0f121 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 24 May 2020 14:24:01 -0600 Subject: [PATCH 010/103] remove mutes.py file --- redbot/cogs/mod/mutes.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 redbot/cogs/mod/mutes.py diff --git a/redbot/cogs/mod/mutes.py b/redbot/cogs/mod/mutes.py deleted file mode 100644 index e69de29bb2d..00000000000 From 97427c9239413e373557dc8a4a20a8b10b73a388 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 21 Aug 2020 15:42:08 -0600 Subject: [PATCH 011/103] Remove _voice_perm_check from mod since it's now in mutes cog --- redbot/cogs/mod/abc.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/redbot/cogs/mod/abc.py b/redbot/cogs/mod/abc.py index cfdd5a6596b..59837c4016b 100644 --- a/redbot/cogs/mod/abc.py +++ b/redbot/cogs/mod/abc.py @@ -17,10 +17,3 @@ def __init__(self, *_args): self.config: Config self.bot: Red self.cache: dict - - @staticmethod - @abstractmethod - async def _voice_perm_check( - ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool - ) -> bool: - raise NotImplementedError() From 56da8710b5ad4a9d8f4160e75d649334d8c6a1ed Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 21 Aug 2020 18:06:20 -0600 Subject: [PATCH 012/103] remove naive datetimes prevent muting the bot prevent muting yourself fix error message when lots of channels are present --- redbot/cogs/mutes/mutes.py | 67 ++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index b61b7059a8b..5e6386df4b7 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -4,15 +4,15 @@ import logging from abc import ABC -from typing import cast, Optional, Dict, List, Tuple -from datetime import datetime, timedelta +from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine +from datetime import datetime, timedelta, timezone from .converters import MuteTime from .voicemutes import VoiceMutes from redbot.core.bot import Red from redbot.core import commands, checks, i18n, modlog, Config -from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list +from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list, pagify from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate @@ -74,10 +74,25 @@ def __init__(self, bot: Red): self._channel_mutes: Dict[int, Dict[int, dict]] = {} self._ready = asyncio.Event() self.bot.loop.create_task(self.initialize()) - self._unmute_tasks = {} + self._unmute_tasks: Dict[str, Coroutine] = {} self._unmute_task = asyncio.create_task(self._handle_automatic_unmute()) # dict of guild id, member id and time to be unmuted + async def red_delete_data_for_user( + self, + *, + requester: Literal["discord_deleted_user", "owner", "user", "user_strict"], + user_id: int, + ): + if requester != "discord_deleted_user": + return + + await self._ready.wait() + all_members = await self.config.all_members() + for g_id, m_id in all_members.items(): + if m_id == user_id: + await self.config.member_from_ids(g_id, m_id).clear() + async def initialize(self): guild_data = await self.config.all_guilds() for g_id, mutes in guild_data.items(): @@ -137,7 +152,7 @@ async def _handle_server_unmutes(self): if guild is None: continue for u_id, data in mutes.items(): - time_to_unmute = data["until"] - datetime.utcnow().timestamp() + time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( self._auto_unmute_user(guild, data) @@ -148,7 +163,7 @@ async def _handle_server_unmutes(self): await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) async def _auto_unmute_user(self, guild: discord.Guild, data: dict): - delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) if delay < 1: delay = 0 await asyncio.sleep(delay) @@ -163,7 +178,7 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): await modlog.create_case( self.bot, guild, - datetime.utcnow(), + datetime.now(timezone.utc), "sunmute", member, author, @@ -183,7 +198,7 @@ async def _handle_channel_unmutes(self): if channel is None: continue for u_id, data in mutes.items(): - time_to_unmute = data["until"] - datetime.utcnow().timestamp() + time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( self._auto_channel_unmute_user(channel, data) @@ -193,7 +208,7 @@ async def _handle_channel_unmutes(self): await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): - delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) if delay < 1: delay = 0 await asyncio.sleep(delay) @@ -210,7 +225,7 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di await modlog.create_case( self.bot, channel.guild, - datetime.utcnow(), + datetime.now(timezone.utc), "cunmute", member, author, @@ -352,17 +367,21 @@ async def mute( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot mute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot mute yourself.")) duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) time = "" until = None if duration: - until = datetime.utcnow() + timedelta(**duration) + until = datetime.now(timezone.utc) + timedelta(**duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: - until = datetime.utcnow() + timedelta(**default_duration) + until = datetime.now(timezone.utc) + timedelta(**default_duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) author = ctx.message.author guild = ctx.guild @@ -386,8 +405,9 @@ async def mute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) - if not success_list: - return await ctx.send(issue) + if not success_list and issue: + resp = pagify(issue) + return await ctx.send_interactive(resp) if until: if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} @@ -469,17 +489,21 @@ async def channel_mute( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot mute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot mute yourself.")) duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) until = None time = "" if duration: - until = datetime.utcnow() + timedelta(**duration) + until = datetime.now(timezone.utc) + timedelta(**duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: - until = datetime.utcnow() + timedelta(**default_duration) + until = datetime.now(timezone.utc) + timedelta(**default_duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) author = ctx.message.author channel = ctx.message.channel @@ -542,6 +566,10 @@ async def unmute( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot unmute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot unmute yourself.")) guild = ctx.guild author = ctx.author audit_reason = get_audit_reason(author, reason) @@ -564,7 +592,8 @@ async def unmute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if not success_list: - return await ctx.send(issue) + resp = pagify(issue) + return await ctx.send_interactive(resp) if ctx.guild.id in self._server_mutes: if user.id in self._server_mutes[ctx.guild.id]: for user in success_list: @@ -598,6 +627,10 @@ async def unmute_channel( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot unmute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot unmute yourself.")) channel = ctx.channel author = ctx.author guild = ctx.guild From 773ada21414dc6ed6ffdcde35f4349aa8c220aa0 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 21 Aug 2020 18:18:02 -0600 Subject: [PATCH 013/103] change alias for channelunmute Be more verbose for creating default mute role --- redbot/cogs/mutes/mutes.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 5e6386df4b7..a6c054a6e27 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -311,7 +311,8 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): name=name, permissions=perms, reason=_("Mute role setup") ) except discord.errors.Forbidden: - return + return await ctx.send(_("I could not create a muted role in this server.")) + errors = [] for channel in ctx.guild.channels: overs = discord.PermissionOverwrite() if isinstance(channel, discord.TextChannel): @@ -322,7 +323,14 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): try: await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) except discord.errors.Forbidden: + errors.append(f"{channel.mention}") continue + if errors: + msg = _("I could not set overwrites for the following channels: {channels}").format( + channels=humanize_list(errors) + ) + for page in pagify(msg): + await ctx.send(page) await self.config.guild(ctx.guild).mute_role.set(role.id) await ctx.send(_("Mute role set to {role}").format(role=role.name)) @@ -614,7 +622,7 @@ async def unmute( await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) - @commands.command(name="channelunmute", aliases=["unmutechannel"]) + @commands.command(name="unmutechannel", aliases=["channelunmute"]) @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() async def unmute_channel( From 79e3cb0bf8adaed86c3df93a81fb24785777269e Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sat, 22 Aug 2020 15:58:55 -0600 Subject: [PATCH 014/103] add `[p]activemutes` to show current mutes in the server and time remaining on the mutes --- redbot/cogs/mutes/mutes.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a6c054a6e27..561b4635749 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -351,6 +351,54 @@ async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): ) ) + @commands.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_roles=True) + async def activemutes(self, ctx: commands.Context): + """ + Displays active mutes on this server. + """ + + msg = "" + for guild_id, mutes_data in self._server_mutes.items(): + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + msg += _("Server Mute: {member}").format(member=user_str) + if time_str: + msg += _("Remaining: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + for channel_id, mutes_data in self._channel_mutes.items(): + msg += f"<#{channel_id}>\n" + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + msg += _("Channel Mute: {member} ").format(member=user_str) + if time_str: + msg += _("Remaining: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + if msg: + await ctx.maybe_send_embed(msg) + else: + await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) + @commands.command() @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) From 6991ff4e59462e227e9ac0e8bcb45151bea6482f Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 31 Aug 2020 10:47:07 -0600 Subject: [PATCH 015/103] improve resolution of unmute time --- redbot/cogs/mutes/mutes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 561b4635749..ec9cfe25a4f 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -163,7 +163,7 @@ async def _handle_server_unmutes(self): await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) async def _auto_unmute_user(self, guild: discord.Guild, data: dict): - delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) + delay = data["until"] - datetime.now(timezone.utc).timestamp() if delay < 1: delay = 0 await asyncio.sleep(delay) @@ -208,7 +208,7 @@ async def _handle_channel_unmutes(self): await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): - delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) + delay = data["until"] - datetime.now(timezone.utc).timestamp() if delay < 1: delay = 0 await asyncio.sleep(delay) From 7438cf94a4b31968a38c13d4753495b45163f07c Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 31 Aug 2020 10:52:24 -0600 Subject: [PATCH 016/103] black --- redbot/cogs/mutes/mutes.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index ec9cfe25a4f..a3bc5add14e 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -283,8 +283,8 @@ async def muteset(self, ctx: commands.Context): async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): """Sets the role to be applied when muting a user. - If no role is setup the bot will attempt to mute a user by setting - channel overwrites in all channels to prevent the user from sending messages. + If no role is setup the bot will attempt to mute a user by setting + channel overwrites in all channels to prevent the user from sending messages. """ if not role: await self.config.guild(ctx.guild).mute_role.set(None) @@ -298,11 +298,11 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): async def make_mute_role(self, ctx: commands.Context, *, name: str): """Create a Muted role. - This will create a role and apply overwrites to all available channels - to more easily setup muting a user. + This will create a role and apply overwrites to all available channels + to more easily setup muting a user. - If you already have a muted role created on the server use - `[p]muteset role ROLE_NAME_HERE` + If you already have a muted role created on the server use + `[p]muteset role ROLE_NAME_HERE` """ perms = discord.Permissions() perms.update(send_messages=False, speak=False, add_reactions=False) @@ -337,7 +337,7 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): @muteset.command(name="time") async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): """ - Set the default mute time for the mute command. + Set the default mute time for the mute command. """ data = time.get("duration", {}) if not data: @@ -356,7 +356,7 @@ async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): @checks.mod_or_permissions(manage_roles=True) async def activemutes(self, ctx: commands.Context): """ - Displays active mutes on this server. + Displays active mutes on this server. """ msg = "" @@ -730,10 +730,14 @@ async def unmute_channel( ) async def mute_user( - self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + self, + guild: discord.Guild, + author: discord.Member, + user: discord.Member, + reason: str, ) -> Tuple[bool, Optional[str]]: """ - Handles muting users + Handles muting users """ mute_role = await self.config.guild(guild).mute_role() if mute_role: @@ -762,10 +766,14 @@ async def mute_user( return True, None async def unmute_user( - self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + self, + guild: discord.Guild, + author: discord.Member, + user: discord.Member, + reason: str, ) -> Tuple[bool, Optional[str]]: """ - Handles muting users + Handles muting users """ mute_role = await self.config.guild(guild).mute_role() if mute_role: From ef96ea8bf0feb09b5ca7b6bdfb95ab449e0db261 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 1 Sep 2020 14:18:21 -0600 Subject: [PATCH 017/103] Show indefinite mutes in activemutes and only show the current servers mutes in activemutes --- redbot/cogs/mutes/mutes.py | 105 ++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a3bc5add14e..46ba6966a19 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -152,6 +152,8 @@ async def _handle_server_unmutes(self): if guild is None: continue for u_id, data in mutes.items(): + if data["until"] is None: + continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( @@ -360,44 +362,51 @@ async def activemutes(self, ctx: commands.Context): """ msg = "" - for guild_id, mutes_data in self._server_mutes.items(): + if ctx.guild.id in self._server_mutes: + mutes_data = self._server_mutes[ctx.guild.id] for user_id, mutes in mutes_data.items(): user = ctx.guild.get_member(user_id) if not user: user_str = f"<@!{user_id}>" else: user_str = user.mention - time_left = timedelta( - seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() - ) - time_str = humanize_timedelta(timedelta=time_left) - msg += _("Server Mute: {member}").format(member=user_str) - if time_str: - msg += _("Remaining: {time_left}\n").format(time_left=time_str) - else: - msg += "\n" - for channel_id, mutes_data in self._channel_mutes.items(): - msg += f"<#{channel_id}>\n" - for user_id, mutes in mutes_data.items(): - user = ctx.guild.get_member(user_id) - if not user: - user_str = f"<@!{user_id}>" + if mutes["until"]: + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) else: - user_str = user.mention - - time_left = timedelta( - seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() - ) - time_str = humanize_timedelta(timedelta=time_left) - msg += _("Channel Mute: {member} ").format(member=user_str) + time_str = "" + msg += _("Server Mute: {member}").format(member=user_str) if time_str: msg += _("Remaining: {time_left}\n").format(time_left=time_str) else: msg += "\n" - if msg: - await ctx.maybe_send_embed(msg) - else: - await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) + for channel_id, mutes_data in self._channel_mutes.items(): + msg += f"<#{channel_id}>\n" + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + if mutes["until"]: + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + else: + time_str = "" + msg += _("Channel Mute: {member} ").format(member=user_str) + if time_str: + msg += _("Remaining: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + if msg: + for page in pagify(msg): + await ctx.maybe_send_embed(page) + return + await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) @commands.command() @commands.guild_only() @@ -464,17 +473,16 @@ async def mute( if not success_list and issue: resp = pagify(issue) return await ctx.send_interactive(resp) - if until: - if ctx.guild.id not in self._server_mutes: - self._server_mutes[ctx.guild.id] = {} - for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp(), - } - self._server_mutes[ctx.guild.id][user.id] = mute - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp() if until else None, + } + self._server_mutes[ctx.guild.id][user.id] = mute + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: verb = _("have") @@ -588,17 +596,16 @@ async def channel_mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - if until: - if channel.id not in self._channel_mutes: - self._channel_mutes[channel.id] = {} - for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp(), - } - self._channel_mutes[channel.id][user.id] = mute - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + if channel.id not in self._channel_mutes: + self._channel_mutes[channel.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp() if until else None, + } + self._channel_mutes[channel.id][user.id] = mute + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: verb = _("have") From ffdd31d2596692f27079437cfa6856af80100008 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 1 Sep 2020 16:18:04 -0600 Subject: [PATCH 018/103] replace message.created_at with timezone aware timezone --- redbot/cogs/mutes/mutes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 46ba6966a19..3a4b558ba00 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -460,7 +460,7 @@ async def mute( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "smute", user, author, @@ -585,7 +585,7 @@ async def channel_mute( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "cmute", user, author, @@ -645,7 +645,7 @@ async def unmute( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "sunmute", user, author, @@ -710,7 +710,7 @@ async def unmute_channel( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "cunmute", user, author, From 0cab70fa694048f99883a04a3976135c460ce2a9 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Thu, 3 Sep 2020 18:33:29 -0600 Subject: [PATCH 019/103] remove "server" from activemutes to clean up look since channelmutes will show channel --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 3a4b558ba00..e3c22eb9b54 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -377,7 +377,7 @@ async def activemutes(self, ctx: commands.Context): time_str = humanize_timedelta(timedelta=time_left) else: time_str = "" - msg += _("Server Mute: {member}").format(member=user_str) + msg += _("Mute: {member}").format(member=user_str) if time_str: msg += _("Remaining: {time_left}\n").format(time_left=time_str) else: From 238b5cccb5fc5cc01299138d30a4adae77c1fa58 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 7 Sep 2020 01:52:45 -0600 Subject: [PATCH 020/103] better cache management, add tracking for manual muted role removal in the cache and modlog cases --- redbot/cogs/mutes/mutes.py | 249 ++++++++++++++++++++++++++++--------- 1 file changed, 191 insertions(+), 58 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index e3c22eb9b54..155de9d8d9d 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -4,6 +4,7 @@ import logging from abc import ABC +from copy import copy from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine from datetime import datetime, timedelta, timezone @@ -96,10 +97,14 @@ async def red_delete_data_for_user( async def initialize(self): guild_data = await self.config.all_guilds() for g_id, mutes in guild_data.items(): - self._server_mutes[g_id] = mutes["muted_users"] + self._server_mutes[g_id] = {} + for user_id, mute in mutes["muted_users"].items(): + self._server_mutes[g_id][int(user_id)] = mute channel_data = await self.config.all_channels() for c_id, mutes in channel_data.items(): - self._channel_mutes[c_id] = mutes["muted_users"] + self._channel_mutes[c_id] = {} + for user_id, mute in mutes["muted_users"].items(): + self._channel_mutes[c_id][int(user_id)] = mute self._ready.set() async def cog_before_invoke(self, ctx: commands.Context): @@ -200,6 +205,8 @@ async def _handle_channel_unmutes(self): if channel is None: continue for u_id, data in mutes.items(): + if not data["until"]: + continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( @@ -239,6 +246,102 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di except discord.errors.Forbidden: return + @commands.Cog.listener() + async def on_member_update(self, before: discord.Member, after: discord.Member): + """ + Used to handle the cache if a member manually has the muted role removed + """ + guild = before.guild + mute_role_id = await self.config.guild(before.guild).mute_role() + mute_role = guild.get_role(mute_role_id) + if not mute_role: + return + b = set(before.roles) + a = set(after.roles) + roles_removed = list(b - a) + roles_added = list(a - b) + if mute_role in roles_removed: + # send modlog case for unmute and remove from cache + if after.id in self._server_mutes[guild.id]: + try: + await modlog.create_case( + self.bot, + guild, + datetime.utcnow(), + "sunmute", + after, + None, + _("Manually removed mute role"), + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + del self._server_mutes[guild.id][after.id] + if mute_role in roles_added: + # send modlog case for mute and add to cache + if after.id not in self._server_mutes[guild.id]: + try: + await modlog.create_case( + self.bot, + guild, + datetime.utcnow(), + "smute", + after, + None, + _("Manually applied mute role"), + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + self._server_mutes[guild.id][after.id] = { + "author": None, + "member": after.id, + "until": None, + } + await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) + + @commands.Cog.listener() + async def on_guild_channel_update( + self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel + ): + """ + This handles manually removing + """ + if after.id in self._channel_mutes: + before_perms: Dict[int, Dict[str, Optional[bool]]] = { + o.id: {name: attr for name, attr in p} for o, p in before.overwrites.items() + } + after_perms: Dict[int, Dict[str, Optional[bool]]] = { + o.id: {name: attr for name, attr in p} for o, p in after.overwrites.items() + } + to_del: int = [] + for user_id in self._channel_mutes[after.id].keys(): + if user_id in before_perms and ( + user_id not in after_perms or after_perms[user_id]["send_messages"] + ): + user = after.guild.get_member(user_id) + if not user: + user = discord.Object(id=user_id) + log.debug(f"{user} - {type(user)}") + to_del.append(user_id) + try: + log.debug("creating case") + await modlog.create_case( + self.bot, + after.guild, + datetime.utcnow(), + "cunmute", + user, + None, + _("Manually removed channel overwrites"), + until=None, + channel=after, + ) + log.debug("created case") + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + for u_id in to_del: + del self._channel_mutes[after.id][u_id] + await self.config.channel(after).muted_users.set(self._channel_mutes[after.id]) + @commands.Cog.listener() async def on_member_remove(self, member: discord.Member): guild = member.guild @@ -362,28 +465,11 @@ async def activemutes(self, ctx: commands.Context): """ msg = "" + to_del = [] if ctx.guild.id in self._server_mutes: mutes_data = self._server_mutes[ctx.guild.id] - for user_id, mutes in mutes_data.items(): - user = ctx.guild.get_member(user_id) - if not user: - user_str = f"<@!{user_id}>" - else: - user_str = user.mention - if mutes["until"]: - time_left = timedelta( - seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() - ) - time_str = humanize_timedelta(timedelta=time_left) - else: - time_str = "" - msg += _("Mute: {member}").format(member=user_str) - if time_str: - msg += _("Remaining: {time_left}\n").format(time_left=time_str) - else: - msg += "\n" - for channel_id, mutes_data in self._channel_mutes.items(): - msg += f"<#{channel_id}>\n" + if mutes_data: + msg += _("__Server Mutes__\n") for user_id, mutes in mutes_data.items(): user = ctx.guild.get_member(user_id) if not user: @@ -397,15 +483,41 @@ async def activemutes(self, ctx: commands.Context): time_str = humanize_timedelta(timedelta=time_left) else: time_str = "" - msg += _("Channel Mute: {member} ").format(member=user_str) + msg += _("{member}").format(member=user_str) if time_str: - msg += _("Remaining: {time_left}\n").format(time_left=time_str) + msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) else: msg += "\n" - if msg: - for page in pagify(msg): - await ctx.maybe_send_embed(page) - return + for channel_id, mutes_data in self._channel_mutes.items(): + if not mutes_data: + to_del.append(channel_id) + continue + msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) + if channel_id in [c.id for c in ctx.guild.channels]: + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + if mutes["until"]: + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + else: + time_str = "" + msg += _("{member} ").format(member=user_str) + if time_str: + msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + for c in to_del: + del self._channel_mutes[c] + if msg: + for page in pagify(msg): + await ctx.maybe_send_embed(page) + return await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) @commands.command() @@ -476,12 +588,9 @@ async def mute( if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp() if until else None, - } - self._server_mutes[ctx.guild.id][user.id] = mute + self._server_mutes[ctx.guild.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: @@ -596,15 +705,10 @@ async def channel_mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - if channel.id not in self._channel_mutes: - self._channel_mutes[channel.id] = {} for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp() if until else None, - } - self._channel_mutes[channel.id][user.id] = mute + self._channel_mutes[channel.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: @@ -657,13 +761,7 @@ async def unmute( if not success_list: resp = pagify(issue) return await ctx.send_interactive(resp) - if ctx.guild.id in self._server_mutes: - if user.id in self._server_mutes[ctx.guild.id]: - for user in success_list: - del self._server_mutes[ctx.guild.id][user.id] - await self.config.guild(ctx.guild).muted_users.set( - self._server_mutes[ctx.guild.id] - ) + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -721,15 +819,10 @@ async def unmute_channel( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - for user in success_list: - if ( - channel.id in self._channel_mutes - and user.id in self._channel_mutes[channel.id] - ): - del self._channel_mutes[channel.id][user.id] - await self.config.channel(channel).muted_users.set( - self._channel_mutes[channel.id] - ) + if self._channel_mutes[channel.id]: + await self.config.channel(channel).set(self._channel_mutes[channel.id]) + else: + await self.config.channel(channel).clear() await ctx.send( _("{users} unmuted in this channel.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -747,6 +840,7 @@ async def mute_user( Handles muting users """ mute_role = await self.config.guild(guild).mute_role() + if mute_role: try: if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): @@ -754,8 +848,22 @@ async def mute_user( role = guild.get_role(mute_role) if not role: return False, mute_unmute_issues["role_missing"] + + # This is here to prevent the modlog case from happening on role updates + # we need to update the cache early so it's there before we receive the member_update event + if guild.id not in self._server_mutes: + self._server_mutes[guild.id] = {} + + self._server_mutes[guild.id][user.id] = { + "author": author.id, + "member": user.id, + "until": None, + } await user.add_roles(role, reason=reason) except discord.errors.Forbidden: + del self._server_mutes[guild.id][ + user.id + ] # this is here so we don't have a bad cache return False, mute_unmute_issues["permissions_issue"] return True, None else: @@ -782,7 +890,9 @@ async def unmute_user( """ Handles muting users """ + mute_role = await self.config.guild(guild).mute_role() + _temp = None # used to keep the cache incase of permissions errors if mute_role: try: if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): @@ -790,8 +900,14 @@ async def unmute_user( role = guild.get_role(mute_role) if not role: return False, mute_unmute_issues["role_missing"] + if guild.id in self._server_mutes: + if user.id in self._server_mutes[guild.id]: + _temp = copy(self._server_mutes[guild.id][user.id]) + del self._server_mutes[guild.id][user.id] await user.remove_roles(role, reason=reason) except discord.errors.Forbidden: + if temp: + self._server_mutes[guild.id][user.id] = _temp return False, mute_unmute_issues["permissions_issue"] return True, None else: @@ -835,13 +951,23 @@ async def channel_mute_user( old_overs = {k: getattr(overwrites, k) for k in new_overs} overwrites.update(**new_overs) try: + if channel.id not in self._channel_mutes: + self._channel_mutes[channel.id] = {} + self._channel_mutes[channel.id][user.id] = { + "author": author.id, + "member": user.id, + "until": None, + } await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: + del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: + del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: + del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["left_guild"]) else: await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) @@ -858,6 +984,7 @@ async def channel_unmute_user( overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() + _temp = None # used to keep the cache incase we have permissions issues if channel.id in perms_cache: old_values = perms_cache[channel.id] else: @@ -874,12 +1001,18 @@ async def channel_unmute_user( ) else: await channel.set_permissions(user, overwrite=overwrites, reason=reason) + if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: + _temp = copy(self._channel_mutes[channel.id][user.id]) + del self._channel_mutes[channel.id][user.id] except discord.Forbidden: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["left_guild"]) else: await self.config.member(user).clear_raw("perms_cache", str(channel.id)) From b786c4b0ad54d1b0abe218dbd87616dbbfe804c2 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 8 Sep 2020 16:59:33 -0600 Subject: [PATCH 021/103] Fix keyerror in mutes command when unsuccessful mutes --- redbot/cogs/mutes/mutes.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 155de9d8d9d..68108bedc65 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -588,9 +588,10 @@ async def mute( if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} for user in success_list: - self._server_mutes[ctx.guild.id][user.id]["until"] = ( - until.timestamp() if until else None - ) + if user.id in self._server_mutes[ctx.guild.id]: + self._server_mutes[ctx.guild.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: @@ -706,9 +707,10 @@ async def channel_mute( log.error(_("Error creating modlog case"), exc_info=e) if success_list: for user in success_list: - self._channel_mutes[channel.id][user.id]["until"] = ( - until.timestamp() if until else None - ) + if user.id in self._channel_mutes[channel.id]: + self._channel_mutes[channel.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: @@ -761,7 +763,10 @@ async def unmute( if not success_list: resp = pagify(issue) return await ctx.send_interactive(resp) - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + if self._server_mutes[ctx.guild.id]: + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + else: + await self.config.guild(ctx.guild).clear() await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) From 33949e2d2ea0be660ac1cc6d7b5b21bfbab32707 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 8 Sep 2020 20:35:45 -0600 Subject: [PATCH 022/103] add typing indicator and improve config settings --- redbot/cogs/mutes/mutes.py | 429 +++++++++++++++++++------------------ 1 file changed, 224 insertions(+), 205 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 68108bedc65..a46fb932787 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -151,6 +151,7 @@ async def _clean_tasks(self): async def _handle_server_unmutes(self): log.debug("Checking server unmutes") + to_clear = [] for g_id, mutes in self._server_mutes.items(): to_remove = [] guild = self.bot.get_guild(g_id) @@ -167,7 +168,11 @@ async def _handle_server_unmutes(self): to_remove.append(u_id) for u_id in to_remove: del self._server_mutes[g_id][u_id] + if self._server_mutes[g_id] == {}: + to_clear.append(g_id) await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) + for g_id in to_clear: + await self.config.guild_from_id(g_id).clear() async def _auto_unmute_user(self, guild: discord.Guild, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -199,13 +204,14 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): async def _handle_channel_unmutes(self): log.debug("Checking channel unmutes") + to_clear = [] for c_id, mutes in self._channel_mutes.items(): to_remove = [] channel = self.bot.get_channel(c_id) if channel is None: continue for u_id, data in mutes.items(): - if not data["until"]: + if not data or not data["until"]: continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: @@ -214,7 +220,11 @@ async def _handle_channel_unmutes(self): ) for u_id in to_remove: del self._channel_mutes[c_id][u_id] + if self._channel_mutes[c_id] == {}: + to_clear.append(c_id) await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) + for c_id in to_clear: + await self.config.channel_from_id(c_id).clear() async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -409,35 +419,38 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): If you already have a muted role created on the server use `[p]muteset role ROLE_NAME_HERE` """ - perms = discord.Permissions() - perms.update(send_messages=False, speak=False, add_reactions=False) - try: - role = await ctx.guild.create_role( - name=name, permissions=perms, reason=_("Mute role setup") - ) - except discord.errors.Forbidden: - return await ctx.send(_("I could not create a muted role in this server.")) - errors = [] - for channel in ctx.guild.channels: - overs = discord.PermissionOverwrite() - if isinstance(channel, discord.TextChannel): - overs.send_messages = False - overs.add_reactions = False - if isinstance(channel, discord.VoiceChannel): - overs.speak = False + async with ctx.typing(): + perms = discord.Permissions() + perms.update(send_messages=False, speak=False, add_reactions=False) try: - await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) + role = await ctx.guild.create_role( + name=name, permissions=perms, reason=_("Mute role setup") + ) except discord.errors.Forbidden: - errors.append(f"{channel.mention}") - continue - if errors: - msg = _("I could not set overwrites for the following channels: {channels}").format( - channels=humanize_list(errors) - ) - for page in pagify(msg): - await ctx.send(page) - await self.config.guild(ctx.guild).mute_role.set(role.id) - await ctx.send(_("Mute role set to {role}").format(role=role.name)) + return await ctx.send(_("I could not create a muted role in this server.")) + errors = [] + for channel in ctx.guild.channels: + overs = discord.PermissionOverwrite() + if isinstance(channel, discord.TextChannel): + overs.send_messages = False + overs.add_reactions = False + if isinstance(channel, discord.VoiceChannel): + overs.speak = False + try: + await channel.set_permissions( + role, overwrite=overs, reason=_("Mute role setup") + ) + except discord.errors.Forbidden: + errors.append(f"{channel.mention}") + continue + if errors: + msg = _( + "I could not set overwrites for the following channels: {channels}" + ).format(channels=humanize_list(errors)) + for page in pagify(msg): + await ctx.send(page) + await self.config.guild(ctx.guild).mute_role.set(role.id) + await ctx.send(_("Mute role set to {role}").format(role=role.name)) @muteset.command(name="time") async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): @@ -548,65 +561,66 @@ async def mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) - duration = time_and_reason.get("duration", {}) - reason = time_and_reason.get("reason", None) - time = "" - until = None - if duration: - until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) - else: - default_duration = await self.config.guild(ctx.guild).default_time() - if default_duration: - until = datetime.now(timezone.utc) + timedelta(**default_duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) - author = ctx.message.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, issue = await self.mute_user(guild, author, user, audit_reason) - if success: - success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "smute", - user, - author, - reason, - until=until, - channel=None, + async with ctx.typing(): + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + time = "" + until = None + if duration: + until = datetime.now(timezone.utc) + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.now(timezone.utc) + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.mute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "smute", + user, + author, + reason, + until=until, + channel=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if not success_list and issue: + resp = pagify(issue) + return await ctx.send_interactive(resp) + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + for user in success_list: + if user.id in self._server_mutes[ctx.guild.id]: + self._server_mutes[ctx.guild.id][user.id]["until"] = ( + until.timestamp() if until else None ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if not success_list and issue: - resp = pagify(issue) - return await ctx.send_interactive(resp) - if ctx.guild.id not in self._server_mutes: - self._server_mutes[ctx.guild.id] = {} - for user in success_list: - if user.id in self._server_mutes[ctx.guild.id]: - self._server_mutes[ctx.guild.id][user.id]["until"] = ( - until.timestamp() if until else None + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await ctx.send( + _("{users} {verb} been muted in this server{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) - verb = _("has") - if len(success_list) > 1: - verb = _("have") - await ctx.send( - _("{users} {verb} been muted in this server{time}.").format( - users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - ) - if issue: - message = _( - "{users} could not be muted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message, issue) + if issue: + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message, issue) async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions @@ -667,59 +681,60 @@ async def channel_mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) - duration = time_and_reason.get("duration", {}) - reason = time_and_reason.get("reason", None) - until = None - time = "" - if duration: - until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) - else: - default_duration = await self.config.guild(ctx.guild).default_time() - if default_duration: - until = datetime.now(timezone.utc) + timedelta(**default_duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) - author = ctx.message.author - channel = ctx.message.channel - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, issue = await self.channel_mute_user( - guild, channel, author, user, audit_reason - ) - if success: - success_list.append(user) + async with ctx.typing(): + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + until = None + time = "" + if duration: + until = datetime.now(timezone.utc) + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.now(timezone.utc) + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + channel = ctx.message.channel + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.channel_mute_user( + guild, channel, author, user, audit_reason + ) + if success: + success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cmute", - user, - author, - reason, - until=until, - channel=channel, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if success_list: - for user in success_list: - if user.id in self._channel_mutes[channel.id]: - self._channel_mutes[channel.id][user.id]["until"] = ( - until.timestamp() if until else None + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "cmute", + user, + author, + reason, + until=until, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + for user in success_list: + if user.id in self._channel_mutes[channel.id]: + self._channel_mutes[channel.id][user.id]["until"] = ( + until.timestamp() if until else None + ) + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await channel.send( + _("{users} {verb} been muted in this channel{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) - verb = _("has") - if len(success_list) > 1: - verb = _("have") - await channel.send( - _("{users} {verb} been muted in this channel{time}.").format( - users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - ) @commands.command() @commands.guild_only() @@ -739,45 +754,48 @@ async def unmute( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) - guild = ctx.guild - author = ctx.author - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, issue = await self.unmute_user(guild, author, user, audit_reason) - if success: - success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "sunmute", - user, - author, - reason, - until=None, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if not success_list: - resp = pagify(issue) - return await ctx.send_interactive(resp) - if self._server_mutes[ctx.guild.id]: - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) - else: - await self.config.guild(ctx.guild).clear() - await ctx.send( - _("{users} unmuted in this server.").format( - users=humanize_list([f"{u}" for u in success_list]) + async with ctx.typing(): + guild = ctx.guild + author = ctx.author + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.unmute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "sunmute", + user, + author, + reason, + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if not success_list: + resp = pagify(issue) + return await ctx.send_interactive(resp) + if self._server_mutes[ctx.guild.id]: + await self.config.guild(ctx.guild).muted_users.set( + self._server_mutes[ctx.guild.id] + ) + else: + await self.config.guild(ctx.guild).clear() + await ctx.send( + _("{users} unmuted in this server.").format( + users=humanize_list([f"{u}" for u in success_list]) + ) ) - ) - if issue: - message = _( - "{users} could not be unmuted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message, issue) + if issue: + message = _( + "{users} could not be unmuted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="unmutechannel", aliases=["channelunmute"]) @@ -797,42 +815,43 @@ async def unmute_channel( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) - channel = ctx.channel - author = ctx.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, message = await self.channel_unmute_user( - guild, channel, author, user, audit_reason - ) + async with ctx.typing(): + channel = ctx.channel + author = ctx.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, message = await self.channel_unmute_user( + guild, channel, author, user, audit_reason + ) - if success: - success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cunmute", - user, - author, - reason, - until=None, - channel=channel, + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "cunmute", + user, + author, + reason, + until=None, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + if self._channel_mutes[channel.id]: + await self.config.channel(channel).set(self._channel_mutes[channel.id]) + else: + await self.config.channel(channel).clear() + await ctx.send( + _("{users} unmuted in this channel.").format( + users=humanize_list([f"{u}" for u in success_list]) ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if success_list: - if self._channel_mutes[channel.id]: - await self.config.channel(channel).set(self._channel_mutes[channel.id]) - else: - await self.config.channel(channel).clear() - await ctx.send( - _("{users} unmuted in this channel.").format( - users=humanize_list([f"{u}" for u in success_list]) ) - ) async def mute_user( self, From 9bfa5ec1cdca67f8db054e7e9d5dbb0c3dc15f1f Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 8 Sep 2020 20:38:42 -0600 Subject: [PATCH 023/103] flake8 issue --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a46fb932787..2ce920ab2ce 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -930,7 +930,7 @@ async def unmute_user( del self._server_mutes[guild.id][user.id] await user.remove_roles(role, reason=reason) except discord.errors.Forbidden: - if temp: + if _temp: self._server_mutes[guild.id][user.id] = _temp return False, mute_unmute_issues["permissions_issue"] return True, None From dea04ed44a6b5b5f282876a11f1bf63954947da6 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 9 Sep 2020 20:40:30 -0600 Subject: [PATCH 024/103] add one time message when attempting to mute without a role set, consume rate limits across channels for overwrite mutes --- redbot/cogs/mutes/mutes.py | 150 +++++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 31 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 2ce920ab2ce..72dbe8882be 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -29,7 +29,7 @@ ), "is_admin": _("That user cannot be muted, as they have the Administrator permission."), "permissions_issue": _( - "Failed to mute user. I need the manage roles " + "Failed to mute or unmute user. I need the manage roles " "permission and the user I'm muting must be " "lower than myself in the role hierarchy." ), @@ -62,6 +62,7 @@ def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, 49615220001, force_registration=True) default_guild = { + "sent_instructions": False, "mute_role": None, "respect_hierarchy": True, "muted_users": {}, @@ -403,6 +404,9 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): """ if not role: await self.config.guild(ctx.guild).mute_role.set(None) + await self.config.guild(ctx.guild).sent_instructions(False) + # reset this to warn users next time they may have accidentally + # removed the mute role await ctx.send(_("Channel overwrites will be used for mutes instead.")) else: await self.config.guild(ctx.guild).mute_role.set(role.id) @@ -429,29 +433,38 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): except discord.errors.Forbidden: return await ctx.send(_("I could not create a muted role in this server.")) errors = [] + tasks = [] for channel in ctx.guild.channels: - overs = discord.PermissionOverwrite() - if isinstance(channel, discord.TextChannel): - overs.send_messages = False - overs.add_reactions = False - if isinstance(channel, discord.VoiceChannel): - overs.speak = False - try: - await channel.set_permissions( - role, overwrite=overs, reason=_("Mute role setup") - ) - except discord.errors.Forbidden: - errors.append(f"{channel.mention}") - continue - if errors: + tasks.append(self._set_mute_role_overwrites(role, channel)) + errors = await asyncio.gather(*tasks) + if any(errors): msg = _( "I could not set overwrites for the following channels: {channels}" - ).format(channels=humanize_list(errors)) + ).format(channels=humanize_list([i for i in errors if i])) for page in pagify(msg): await ctx.send(page) await self.config.guild(ctx.guild).mute_role.set(role.id) await ctx.send(_("Mute role set to {role}").format(role=role.name)) + async def _set_mute_role_overwrites( + self, role: discord.Role, channel: discord.abc.GuildChannel + ) -> Optional[str]: + """ + This sets the supplied role and channel overwrites to what we want + by default for a mute role + """ + overs = discord.PermissionOverwrite() + if isinstance(channel, discord.TextChannel): + overs.send_messages = False + overs.add_reactions = False + if isinstance(channel, discord.VoiceChannel): + overs.speak = False + try: + await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) + return + except discord.errors.Forbidden: + return channel.mention + @muteset.command(name="time") async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): """ @@ -469,6 +482,66 @@ async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): ) ) + async def _check_for_mute_role(self, ctx: commands.Context) -> bool: + """ + This explains to the user whether or not mutes are setup correctly for + automatic unmutes. + """ + mute_role_id = await self.config.guild(ctx.guild).mute_role() + mute_role = ctx.guild.get_role(mute_role_id) + sent_instructions = await self.config.guild(ctx.guild).sent_instructions() + if mute_role or sent_instructions: + return True + else: + msg = _( + "This server does not have a mute role setup, " + "are you sure you want to continue with channel " + "overwrites? (Note: Overwrites will not be automatically unmuted." + " You can setup a mute role with `{prefix}muteset role` or " + "`{prefix}muteset makerole` if you just want a basic setup.)\n\n" + ).format(prefix=ctx.clean_prefix) + can_react = ctx.channel.permissions_for(ctx.me).add_reactions + if can_react: + msg += _( + "Reacting with \N{WHITE HEAVY CHECK MARK} will continue " + "the mute with overwrites and stop this message from appearing again, " + "Reacting with \N{CROSS MARK} will end the mute attempt." + ) + else: + msg += _( + "Saying `yes` will continue " + "the mute with overwrites and stop this message from appearing again, " + "saying `no` will end the mute attempt." + ) + query: discord.Message = await ctx.send(msg) + if can_react: + # noinspection PyAsyncCall + start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS) + pred = ReactionPredicate.yes_or_no(query, ctx.author) + event = "reaction_add" + else: + pred = MessagePredicate.yes_or_no(ctx) + event = "message" + try: + await ctx.bot.wait_for(event, check=pred, timeout=30) + except asyncio.TimeoutError: + await query.delete() + return False + + if not pred.result: + if can_react: + await query.delete() + else: + await ctx.send(_("OK then.")) + + return False + else: + if can_react: + with contextlib.suppress(discord.Forbidden): + await query.clear_reactions() + await self.config.guild(ctx.guild).sent_instructions.set(True) + return True + @commands.command() @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) @@ -505,8 +578,8 @@ async def activemutes(self, ctx: commands.Context): if not mutes_data: to_del.append(channel_id) continue - msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) if channel_id in [c.id for c in ctx.guild.channels]: + msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) for user_id, mutes in mutes_data.items(): user = ctx.guild.get_member(user_id) if not user: @@ -561,6 +634,8 @@ async def mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) @@ -681,6 +756,8 @@ async def channel_mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) @@ -754,6 +831,8 @@ async def unmute( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): guild = ctx.guild author = ctx.author @@ -815,6 +894,8 @@ async def unmute_channel( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): channel = ctx.channel author = ctx.author @@ -892,11 +973,18 @@ async def mute_user( return True, None else: mute_success = [] + perms_cache = {} + tasks = [] for channel in guild.channels: - success, issue = await self.channel_mute_user(guild, channel, author, user, reason) + tasks.append(self.channel_mute_user(guild, channel, author, user, reason)) + task_result = await asyncio.gather(*tasks) + for success, issue in task_result: if not success: mute_success.append(f"{channel.mention} - {issue}") - await asyncio.sleep(0.1) + else: + chan_id = next(iter(issue)) + perms_cache[str(chan_id)] = issue[chan_id] + await self.config.member(user).perms_cache.set(perms_cache) if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) elif mute_success and len(mute_success) != len(guild.channels): @@ -936,13 +1024,14 @@ async def unmute_user( return True, None else: mute_success = [] + tasks = [] for channel in guild.channels: - success, issue = await self.channel_unmute_user( - guild, channel, author, user, reason - ) + tasks.append(self.channel_unmute_user(guild, channel, author, user, reason)) + results = await asyncio.gather(*tasks) + for success, issue in results: if not success: mute_success.append(f"{channel.mention} - {issue}") - await asyncio.sleep(0.1) + await self.config.member(user).clear() if mute_success: return False, "\n".join(s for s in mute_success) else: @@ -993,9 +1082,7 @@ async def channel_mute_user( elif e.code == 10009: del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) - return True, None + return True, {str(channel.id): old_overs} async def channel_unmute_user( self, @@ -1029,15 +1116,16 @@ async def channel_unmute_user( _temp = copy(self._channel_mutes[channel.id][user.id]) del self._channel_mutes[channel.id][user.id] except discord.Forbidden: - self._channel_mutes[channel.id][user.id] = _temp + if channel.id in self._channel_mutes: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: - self._channel_mutes[channel.id][user.id] = _temp + if channel.id in self._channel_mutes: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: - self._channel_mutes[channel.id][user.id] = _temp + if channel.id in self._channel_mutes: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).clear_raw("perms_cache", str(channel.id)) return True, None From fefd13473aef61d3751b93863d8ef369ac2a9760 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 11 Sep 2020 23:07:04 -0600 Subject: [PATCH 025/103] Don't clear the whole guilds settings when a mute is finished. Optimize server mutes to better handle migration to API method later. Fix typehints. --- redbot/cogs/mutes/mutes.py | 82 ++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 72dbe8882be..e920f327fb1 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -5,7 +5,7 @@ from abc import ABC from copy import copy -from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine +from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine, Union from datetime import datetime, timedelta, timezone from .converters import MuteTime @@ -173,7 +173,7 @@ async def _handle_server_unmutes(self): to_clear.append(g_id) await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) for g_id in to_clear: - await self.config.guild_from_id(g_id).clear() + await self.config.guild_from_id(g_id).muted_users.clear() async def _auto_unmute_user(self, guild: discord.Guild, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -225,7 +225,7 @@ async def _handle_channel_unmutes(self): to_clear.append(c_id) await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) for c_id in to_clear: - await self.config.channel_from_id(c_id).clear() + await self.config.channel_from_id(c_id).muted_users.clear() async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -505,7 +505,7 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: msg += _( "Reacting with \N{WHITE HEAVY CHECK MARK} will continue " "the mute with overwrites and stop this message from appearing again, " - "Reacting with \N{CROSS MARK} will end the mute attempt." + "Reacting with \N{NEGATIVE SQUARED CROSS MARK} will end the mute attempt." ) else: msg += _( @@ -554,9 +554,14 @@ async def activemutes(self, ctx: commands.Context): to_del = [] if ctx.guild.id in self._server_mutes: mutes_data = self._server_mutes[ctx.guild.id] + to_rem = [] if mutes_data: + msg += _("__Server Mutes__\n") for user_id, mutes in mutes_data.items(): + if not mutes: + to_rem.append(user_id) + continue user = ctx.guild.get_member(user_id) if not user: user_str = f"<@!{user_id}>" @@ -574,6 +579,11 @@ async def activemutes(self, ctx: commands.Context): msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) else: msg += "\n" + for _id in to_rem: + try: + del self._server_mutes[ctx.guild.id][_id] + except KeyError: + pass for channel_id, mutes_data in self._channel_mutes.items(): if not mutes_data: to_del.append(channel_id) @@ -581,6 +591,8 @@ async def activemutes(self, ctx: commands.Context): if channel_id in [c.id for c in ctx.guild.channels]: msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) for user_id, mutes in mutes_data.items(): + if not mutes: + continue user = ctx.guild.get_member(user_id) if not user: user_str = f"<@!{user_id}>" @@ -654,7 +666,7 @@ async def mute( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, issue = await self.mute_user(guild, author, user, audit_reason) + success, issue = await self.mute_user(guild, author, user, until, audit_reason) if success: success_list.append(user) try: @@ -676,12 +688,6 @@ async def mute( return await ctx.send_interactive(resp) if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} - for user in success_list: - if user.id in self._server_mutes[ctx.guild.id]: - self._server_mutes[ctx.guild.id][user.id]["until"] = ( - until.timestamp() if until else None - ) - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: verb = _("have") @@ -778,7 +784,7 @@ async def channel_mute( success_list = [] for user in users: success, issue = await self.channel_mute_user( - guild, channel, author, user, audit_reason + guild, channel, author, user, until, audit_reason ) if success: success_list.append(user) @@ -798,12 +804,6 @@ async def channel_mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - for user in success_list: - if user.id in self._channel_mutes[channel.id]: - self._channel_mutes[channel.id][user.id]["until"] = ( - until.timestamp() if until else None - ) - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: verb = _("have") @@ -818,7 +818,11 @@ async def channel_mute( @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) async def unmute( - self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + reason: Optional[str] = None, ): """Unmute users. @@ -858,12 +862,13 @@ async def unmute( if not success_list: resp = pagify(issue) return await ctx.send_interactive(resp) - if self._server_mutes[ctx.guild.id]: + + if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: await self.config.guild(ctx.guild).muted_users.set( self._server_mutes[ctx.guild.id] ) else: - await self.config.guild(ctx.guild).clear() + await self.config.guild(ctx.guild).muted_users.clear() await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -881,7 +886,11 @@ async def unmute( @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() async def unmute_channel( - self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + reason: Optional[str] = None, ): """Unmute a user in this channel. @@ -927,7 +936,7 @@ async def unmute_channel( if self._channel_mutes[channel.id]: await self.config.channel(channel).set(self._channel_mutes[channel.id]) else: - await self.config.channel(channel).clear() + await self.config.channel(channel).muted_users.clear() await ctx.send( _("{users} unmuted in this channel.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -939,7 +948,8 @@ async def mute_user( guild: discord.Guild, author: discord.Member, user: discord.Member, - reason: str, + until: Optional[datetime] = None, + reason: Optional[str] = None, ) -> Tuple[bool, Optional[str]]: """ Handles muting users @@ -962,9 +972,10 @@ async def mute_user( self._server_mutes[guild.id][user.id] = { "author": author.id, "member": user.id, - "until": None, + "until": until.timestamp() if until else None, } await user.add_roles(role, reason=reason) + await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) except discord.errors.Forbidden: del self._server_mutes[guild.id][ user.id @@ -976,7 +987,7 @@ async def mute_user( perms_cache = {} tasks = [] for channel in guild.channels: - tasks.append(self.channel_mute_user(guild, channel, author, user, reason)) + tasks.append(self.channel_mute_user(guild, channel, author, user, until, reason)) task_result = await asyncio.gather(*tasks) for success, issue in task_result: if not success: @@ -997,7 +1008,7 @@ async def unmute_user( guild: discord.Guild, author: discord.Member, user: discord.Member, - reason: str, + reason: Optional[str] = None, ) -> Tuple[bool, Optional[str]]: """ Handles muting users @@ -1043,8 +1054,9 @@ async def channel_mute_user( channel: discord.abc.GuildChannel, author: discord.Member, user: discord.Member, - reason: str, - ) -> Tuple[bool, Optional[str]]: + until: Optional[datetime] = None, + reason: Optional[str] = None, + ) -> Tuple[bool, Union[str, Dict[str, Optional[bool]]]]: """Mutes the specified user in the specified channel""" overwrites = channel.overwrites_for(user) permissions = channel.permissions_for(user) @@ -1052,7 +1064,7 @@ async def channel_mute_user( if permissions.administrator: return False, _(mute_unmute_issues["is_admin"]) - new_overs = {} + new_overs: dict = {} if not isinstance(channel, discord.TextChannel): new_overs.update(speak=False) if not isinstance(channel, discord.VoiceChannel): @@ -1069,7 +1081,7 @@ async def channel_mute_user( self._channel_mutes[channel.id][user.id] = { "author": author.id, "member": user.id, - "until": None, + "until": until.timestamp() if until else None, } await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: @@ -1090,7 +1102,7 @@ async def channel_unmute_user( channel: discord.abc.GuildChannel, author: discord.Member, user: discord.Member, - reason: str, + reason: Optional[str] = None, ) -> Tuple[bool, Optional[str]]: overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() @@ -1116,16 +1128,16 @@ async def channel_unmute_user( _temp = copy(self._channel_mutes[channel.id][user.id]) del self._channel_mutes[channel.id][user.id] except discord.Forbidden: - if channel.id in self._channel_mutes: + if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: - if channel.id in self._channel_mutes: + if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: - if channel.id in self._channel_mutes: + if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["left_guild"]) return True, None From e4d36b4525ea21b977e84da3fa85b04cbfae36d3 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 13 Sep 2020 04:54:40 -0600 Subject: [PATCH 026/103] Utilize usage to make converter make more sense --- redbot/cogs/mutes/mutes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index e920f327fb1..0bb18347c3c 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -618,7 +618,7 @@ async def activemutes(self, ctx: commands.Context): return await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) - @commands.command() + @commands.command(usage="[users...] [time_and_reason]") @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) async def mute( @@ -734,7 +734,9 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - await query.clear_reactions() await ctx.send(issue) - @commands.command(name="mutechannel", aliases=["channelmute"]) + @commands.command( + name="mutechannel", aliases=["channelmute"], usage="[users...] [time_and_reason]" + ) @commands.guild_only() @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) From b09632a36ea5358e052729e0c00aad9b80270da4 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 13 Sep 2020 05:00:17 -0600 Subject: [PATCH 027/103] remove decorator permission checks and fix doc strings --- redbot/cogs/mutes/mutes.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 0bb18347c3c..098ebf19fa5 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -630,8 +630,8 @@ async def mute( ): """Mute users. - `[users]...` is a space separated list of usernames, ID's, or mentions. - `[time_and_reason={}]` is the time to mute for and reason. Time is + `[users...]` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason]` is the time to mute for and reason. Time is any valid time length such as `30 minutes` or `2 days`. If nothing is provided the mute will be indefinite. @@ -738,7 +738,6 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - name="mutechannel", aliases=["channelmute"], usage="[users...] [time_and_reason]" ) @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) async def channel_mute( self, @@ -749,8 +748,8 @@ async def channel_mute( ): """Mute a user in the current text channel. - `[users]...` is a space separated list of usernames, ID's, or mentions. - `[time_and_reason={}]` is the time to mute for and reason. Time is + `[users...]` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason]` is the time to mute for and reason. Time is any valid time length such as `30 minutes` or `2 days`. If nothing is provided the mute will be indefinite. @@ -815,9 +814,8 @@ async def channel_mute( ) ) - @commands.command() + @commands.command(usage="[users...] [reason]") @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) async def unmute( self, @@ -828,7 +826,7 @@ async def unmute( ): """Unmute users. - `[users]...` is a space separated list of usernames, ID's, or mentions. + `[users...]` is a space separated list of usernames, ID's, or mentions. `[reason]` is the reason for the unmute. """ if not users: @@ -884,8 +882,7 @@ async def unmute( await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) - @commands.command(name="unmutechannel", aliases=["channelunmute"]) - @commands.bot_has_permissions(manage_roles=True) + @commands.command(name="unmutechannel", aliases=["channelunmute"], usage="[users...] [reason]") @commands.guild_only() async def unmute_channel( self, @@ -896,7 +893,7 @@ async def unmute_channel( ): """Unmute a user in this channel. - `[users]...` is a space separated list of usernames, ID's, or mentions. + `[users...]` is a space separated list of usernames, ID's, or mentions. `[reason]` is the reason for the unmute. """ if not users: From a25947513d9bc225f22b5787eee32d208a9dfe06 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 13 Sep 2020 16:30:55 -0600 Subject: [PATCH 028/103] handle role changes better --- redbot/cogs/mutes/mutes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 098ebf19fa5..91d0510781e 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -273,6 +273,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): roles_added = list(a - b) if mute_role in roles_removed: # send modlog case for unmute and remove from cache + if guild.id not in self._server_mutes: + # they weren't a tracked mute so we can return early + return if after.id in self._server_mutes[guild.id]: try: await modlog.create_case( @@ -289,6 +292,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): del self._server_mutes[guild.id][after.id] if mute_role in roles_added: # send modlog case for mute and add to cache + if guild.id not in self._server_mutes: + # initialize the guild in the cache to prevent keyerrors + self._server_mutes[guild.id] = {} if after.id not in self._server_mutes[guild.id]: try: await modlog.create_case( @@ -307,7 +313,8 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): "member": after.id, "until": None, } - await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) + if guild.id in self._server_mutes: + await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) @commands.Cog.listener() async def on_guild_channel_update( From faa6f3e019305e2e4950dd0d43f78a3b0cad29bc Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 14 Sep 2020 23:29:50 -0600 Subject: [PATCH 029/103] More sanely handle channel mutes return and improve failed mutes dialogue. Re-enable task cleaner. Reduce wait time to improve resolution of mute time. --- redbot/cogs/mutes/mutes.py | 129 ++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 39 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 91d0510781e..309f1f2eddf 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -120,7 +120,7 @@ async def _handle_automatic_unmute(self): await self.bot.wait_until_red_ready() await self._ready.wait() while True: - # await self._clean_tasks() + await self._clean_tasks() try: await self._handle_server_unmutes() except Exception: @@ -130,7 +130,7 @@ async def _handle_automatic_unmute(self): await self._handle_channel_unmutes() except Exception: log.error("error checking channel unmutes", exc_info=True) - await asyncio.sleep(120) + await asyncio.sleep(30) async def _clean_tasks(self): log.debug("Cleaning unmute tasks") @@ -162,7 +162,7 @@ async def _handle_server_unmutes(self): if data["until"] is None: continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() - if time_to_unmute < 120.0: + if time_to_unmute < 60: self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( self._auto_unmute_user(guild, data) ) @@ -183,7 +183,7 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): try: member = guild.get_member(data["member"]) author = guild.get_member(data["author"]) - if not member or not author: + if not member: return success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) if success: @@ -237,10 +237,10 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di author = channel.guild.get_member(data["author"]) if not member or not author: return - success, message = await self.channel_unmute_user( + success = await self.channel_unmute_user( channel.guild, channel, author, member, _("Automatic unmute") ) - if success: + if success["success"]: try: await modlog.create_case( self.bot, @@ -321,7 +321,7 @@ async def on_guild_channel_update( self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel ): """ - This handles manually removing + This handles manually removing overwrites for a user that has been muted """ if after.id in self._channel_mutes: before_perms: Dict[int, Dict[str, Optional[bool]]] = { @@ -330,7 +330,7 @@ async def on_guild_channel_update( after_perms: Dict[int, Dict[str, Optional[bool]]] = { o.id: {name: attr for name, attr in p} for o, p in after.overwrites.items() } - to_del: int = [] + to_del: List[int] = [] for user_id in self._channel_mutes[after.id].keys(): if user_id in before_perms and ( user_id not in after_perms or after_perms[user_id]["send_messages"] @@ -691,8 +691,11 @@ async def mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if not success_list and issue: - resp = pagify(issue) - return await ctx.send_interactive(resp) + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + return await self.handle_issues(ctx, message, issue) if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} verb = _("has") @@ -739,7 +742,8 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - if can_react: with contextlib.suppress(discord.Forbidden): await query.clear_reactions() - await ctx.send(issue) + resp = pagify(issue) + await ctx.send_interactive(resp) @commands.command( name="mutechannel", aliases=["channelmute"], usage="[users...] [time_and_reason]" @@ -791,10 +795,10 @@ async def channel_mute( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, issue = await self.channel_mute_user( + success = await self.channel_mute_user( guild, channel, author, user, until, audit_reason ) - if success: + if success["success"]: success_list.append(user) try: @@ -811,6 +815,8 @@ async def channel_mute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) + else: + return await ctx.send(success["reason"]) if success_list: verb = _("has") if len(success_list) > 1: @@ -866,9 +872,12 @@ async def unmute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) - if not success_list: - resp = pagify(issue) - return await ctx.send_interactive(resp) + if not success_list and issue: + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + return await self.handle_issues(ctx, message, issue) if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: await self.config.guild(ctx.guild).muted_users.set( @@ -918,11 +927,11 @@ async def unmute_channel( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, message = await self.channel_unmute_user( + success = await self.channel_unmute_user( guild, channel, author, user, audit_reason ) - if success: + if success["success"]: success_list.append(user) try: await modlog.create_case( @@ -938,6 +947,8 @@ async def unmute_channel( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) + else: + return await ctx.send(success["reason"]) if success_list: if self._channel_mutes[channel.id]: await self.config.channel(channel).set(self._channel_mutes[channel.id]) @@ -995,12 +1006,14 @@ async def mute_user( for channel in guild.channels: tasks.append(self.channel_mute_user(guild, channel, author, user, until, reason)) task_result = await asyncio.gather(*tasks) - for success, issue in task_result: - if not success: - mute_success.append(f"{channel.mention} - {issue}") + for task in task_result: + if not task["success"]: + chan = task["channel"].mention + issue = task["issue"] + mute_success.append(f"{chan} - {issue}") else: - chan_id = next(iter(issue)) - perms_cache[str(chan_id)] = issue[chan_id] + chan_id = task["channel"].id + perms_cache[str(chan_id)] = issue.get("old_overs") await self.config.member(user).perms_cache.set(perms_cache) if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) @@ -1045,9 +1058,11 @@ async def unmute_user( for channel in guild.channels: tasks.append(self.channel_unmute_user(guild, channel, author, user, reason)) results = await asyncio.gather(*tasks) - for success, issue in results: - if not success: - mute_success.append(f"{channel.mention} - {issue}") + for task in results: + if not task["success"]: + chan = task["channel"].mention + issue = task["issue"] + mute_success.append(f"{chan} - {issue}") await self.config.member(user).clear() if mute_success: return False, "\n".join(s for s in mute_success) @@ -1062,13 +1077,17 @@ async def channel_mute_user( user: discord.Member, until: Optional[datetime] = None, reason: Optional[str] = None, - ) -> Tuple[bool, Union[str, Dict[str, Optional[bool]]]]: + ) -> Dict[str, Optional[Union[discord.abc.GuildChannel, str, bool]]]: """Mutes the specified user in the specified channel""" overwrites = channel.overwrites_for(user) permissions = channel.permissions_for(user) if permissions.administrator: - return False, _(mute_unmute_issues["is_admin"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["is_admin"]), + } new_overs: dict = {} if not isinstance(channel, discord.TextChannel): @@ -1077,7 +1096,11 @@ async def channel_mute_user( new_overs.update(send_messages=False, add_reactions=False) if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["hierarchy_problem"]), + } old_overs = {k: getattr(overwrites, k) for k in new_overs} overwrites.update(**new_overs) @@ -1092,15 +1115,27 @@ async def channel_mute_user( await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: del self._channel_mutes[channel.id][user.id] - return False, _(mute_unmute_issues["permissions_issue"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["permissions_issue"]), + } except discord.NotFound as e: if e.code == 10003: del self._channel_mutes[channel.id][user.id] - return False, _(mute_unmute_issues["unknown_channel"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["unknown_channel"]), + } elif e.code == 10009: del self._channel_mutes[channel.id][user.id] - return False, _(mute_unmute_issues["left_guild"]) - return True, {str(channel.id): old_overs} + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["left_guild"]), + } + return {"success": True, "channel": channel, "old_overs": old_overs} async def channel_unmute_user( self, @@ -1109,7 +1144,7 @@ async def channel_unmute_user( author: discord.Member, user: discord.Member, reason: Optional[str] = None, - ) -> Tuple[bool, Optional[str]]: + ) -> Dict[str, Optional[Union[discord.abc.GuildChannel, str, bool]]]: overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() @@ -1120,7 +1155,11 @@ async def channel_unmute_user( old_values = {"send_messages": None, "add_reactions": None, "speak": None} if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["hierarchy_problem"]), + } overwrites.update(**old_values) try: @@ -1136,14 +1175,26 @@ async def channel_unmute_user( except discord.Forbidden: if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp - return False, _(mute_unmute_issues["permissions_issue"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["permissions_issue"]), + } except discord.NotFound as e: if e.code == 10003: if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp - return False, _(mute_unmute_issues["unknown_channel"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["unknown_channel"]), + } elif e.code == 10009: if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp - return False, _(mute_unmute_issues["left_guild"]) - return True, None + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["left_guild"]), + } + return {"success": True, "channel": channel, "reason": None} From 21127761360e41d8e476c88a293b64d69df30030 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 15 Sep 2020 00:44:20 -0600 Subject: [PATCH 030/103] Handle re-mute on leave properly --- redbot/cogs/mutes/mutes.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 309f1f2eddf..af8c804456d 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -67,7 +67,7 @@ def __init__(self, bot: Red): "respect_hierarchy": True, "muted_users": {}, "default_time": {}, - "removed_users": [], + "removed_users": {}, } self.config.register_guild(**default_guild) self.config.register_member(perms_cache={}) @@ -363,8 +363,10 @@ async def on_guild_channel_update( @commands.Cog.listener() async def on_member_remove(self, member: discord.Member): guild = member.guild + until = None if guild.id in self._server_mutes: if member.id in self._server_mutes[guild.id]: + until = self._server_mutes[guild.id][member.id]["until"] del self._server_mutes[guild.id][member.id] for channel in guild.channels: if channel.id in self._channel_mutes: @@ -375,7 +377,7 @@ async def on_member_remove(self, member: discord.Member): return if mute_role in [r.id for r in member.roles]: async with self.config.guild(guild).removed_users() as removed_users: - removed_users.append(member.id) + removed_users[str(member.id)] = int(until) if until else None @commands.Cog.listener() async def on_member_join(self, member: discord.Member): @@ -384,15 +386,19 @@ async def on_member_join(self, member: discord.Member): if not mute_role: return async with self.config.guild(guild).removed_users() as removed_users: - if member.id in removed_users: - removed_users.remove(member.id) + if str(member.id) in removed_users: + until_ts = removed_users[str(member.id)] + until = datetime.fromtimestamp(until_ts, tz=timezone.utc) if until_ts else None + # datetime is required to utilize the mutes method + if until and until < datetime.now(tz=timezone.utc): + return + removed_users.pop(str(member.id)) role = guild.get_role(mute_role) if not role: return - try: - await member.add_roles(role, reason=_("Previously muted in this server.")) - except discord.errors.Forbidden: - return + await self.mute_user( + guild, guild.me, member, until, _("Previously muted in this server.") + ) @commands.group() @commands.guild_only() From 59fa1ab65393cca11187bbe00bbc0a9c6ec9a692 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 16 Sep 2020 12:49:51 -0600 Subject: [PATCH 031/103] fix unbound error in overwrites mute --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index af8c804456d..eb60760ca48 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1019,7 +1019,7 @@ async def mute_user( mute_success.append(f"{chan} - {issue}") else: chan_id = task["channel"].id - perms_cache[str(chan_id)] = issue.get("old_overs") + perms_cache[str(chan_id)] = task.get("old_overs") await self.config.member(user).perms_cache.set(perms_cache) if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) From 320030337c9ecd04c7380f4a2cf9362a51e740f9 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 9 Mar 2020 23:02:52 -0600 Subject: [PATCH 032/103] revert the revert the revert git is hard... --- redbot/cogs/mutes/__init__.py | 7 + redbot/cogs/mutes/abc.py | 27 ++ redbot/cogs/mutes/converters.py | 48 +++ redbot/cogs/mutes/mutes.py | 698 ++++++++++++++++++++++++++++++++ redbot/cogs/mutes/voicemutes.py | 227 +++++++++++ 5 files changed, 1007 insertions(+) create mode 100644 redbot/cogs/mutes/__init__.py create mode 100644 redbot/cogs/mutes/abc.py create mode 100644 redbot/cogs/mutes/converters.py create mode 100644 redbot/cogs/mutes/mutes.py create mode 100644 redbot/cogs/mutes/voicemutes.py diff --git a/redbot/cogs/mutes/__init__.py b/redbot/cogs/mutes/__init__.py new file mode 100644 index 00000000000..1a5f91fed08 --- /dev/null +++ b/redbot/cogs/mutes/__init__.py @@ -0,0 +1,7 @@ +from redbot.core.bot import Red +from .mutes import Mutes + + +async def setup(bot: Red): + cog = Mutes(bot) + bot.add_cog(cog) diff --git a/redbot/cogs/mutes/abc.py b/redbot/cogs/mutes/abc.py new file mode 100644 index 00000000000..5403d9dc940 --- /dev/null +++ b/redbot/cogs/mutes/abc.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple, Optional, Dict +from datetime import datetime + +import discord +from redbot.core import Config, commands +from redbot.core.bot import Red + + +class MixinMeta(ABC): + """ + Base class for well behaved type hint detection with composite class. + + Basically, to keep developers sane when not all attributes are defined in each mixin. + """ + + def __init__(self, *_args): + self.config: Config + self.bot: Red + self._mutes_cache: Dict[int, Dict[int, Optional[datetime]]] + + @staticmethod + @abstractmethod + async def _voice_perm_check( + ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool + ) -> bool: + raise NotImplementedError() diff --git a/redbot/cogs/mutes/converters.py b/redbot/cogs/mutes/converters.py new file mode 100644 index 00000000000..78d0c4a4f5a --- /dev/null +++ b/redbot/cogs/mutes/converters.py @@ -0,0 +1,48 @@ +import logging +import re +from typing import Union, Dict + +from discord.ext.commands.converter import Converter +from redbot.core import commands + +log = logging.getLogger("red.cogs.mutes") + +# the following regex is slightly modified from Red +# https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/core/commands/converter.py#L55 +TIME_RE_STRING = r"|".join( + [ + r"((?P\d+?)\s?(weeks?|w))", + r"((?P\d+?)\s?(days?|d))", + r"((?P\d+?)\s?(hours?|hrs|hr?))", + r"((?P\d+?)\s?(minutes?|mins?|m(?!o)))", # prevent matching "months" + r"((?P\d+?)\s?(seconds?|secs?|s))", + ] +) +TIME_RE = re.compile(TIME_RE_STRING, re.I) +TIME_SPLIT = re.compile(r"t(?:ime)?=") + + +class MuteTime(Converter): + """ + This will parse my defined multi response pattern and provide usable formats + to be used in multiple reponses + """ + + async def convert(self, ctx: commands.Context, argument: str) -> Dict[str, Union[dict, str]]: + time_split = TIME_SPLIT.split(argument) + result: Dict[str, Union[dict, str]] = {} + if time_split: + maybe_time = time_split[-1] + else: + maybe_time = argument + + time_data = {} + for time in TIME_RE.finditer(maybe_time): + argument = argument.replace(time[0], "") + for k, v in time.groupdict().items(): + if v: + time_data[k] = int(v) + if time_data: + result["duration"] = time_data + result["reason"] = argument + return result diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py new file mode 100644 index 00000000000..a0162c342c1 --- /dev/null +++ b/redbot/cogs/mutes/mutes.py @@ -0,0 +1,698 @@ +import discord +import asyncio +import logging + +from abc import ABC +from typing import cast, Optional, Dict, List, Tuple +from datetime import datetime, timedelta + +from .converters import MuteTime +from .voicemutes import VoiceMutes + +from redbot.core.bot import Red +from redbot.core import commands, checks, i18n, modlog, Config +from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list +from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy + +T_ = i18n.Translator("Mutes", __file__) + +_ = lambda s: s +mute_unmute_issues = { + "already_muted": _("That user can't send messages in this channel."), + "already_unmuted": _("That user isn't muted in this channel."), + "hierarchy_problem": _( + "I cannot let you do that. You are not higher than the user in the role hierarchy." + ), + "is_admin": _("That user cannot be muted, as they have the Administrator permission."), + "permissions_issue": _( + "Failed to mute user. I need the manage roles " + "permission and the user I'm muting must be " + "lower than myself in the role hierarchy." + ), + "left_guild": _("The user has left the server while applying an overwrite."), + "unknown_channel": _("The channel I tried to mute the user in isn't found."), + "role_missing": _("The mute role no longer exists."), +} +_ = T_ + +log = logging.getLogger("red.cogs.mutes") + +__version__ = "1.0.0" + + +class CompositeMetaClass(type(commands.Cog), type(ABC)): + """ + This allows the metaclass used for proper type detection to + coexist with discord.py's metaclass + """ + + pass + + +class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass): + """ + Stuff for mutes goes here + """ + + def __init__(self, bot: Red): + self.bot = bot + self.config = Config.get_conf(self, 49615220001, force_registration=True) + default_guild = { + "mute_role": None, + "respect_hierarchy": True, + "muted_users": {}, + "default_time": {}, + "removed_users": [], + } + self.config.register_guild(**default_guild) + self.config.register_member(perms_cache={}) + self.config.register_channel(muted_users={}) + self._server_mutes: Dict[int, Dict[int, dict]] = {} + self._channel_mutes: Dict[int, Dict[int, dict]] = {} + self._ready = asyncio.Event() + self.bot.loop.create_task(self.initialize()) + self._unmute_tasks = {} + self._unmute_task = asyncio.create_task(self._handle_automatic_unmute()) + # dict of guild id, member id and time to be unmuted + + async def initialize(self): + guild_data = await self.config.all_guilds() + for g_id, mutes in guild_data.items(): + self._server_mutes[g_id] = mutes["muted_users"] + channel_data = await self.config.all_channels() + for c_id, mutes in channel_data.items(): + self._channel_mutes[c_id] = mutes["muted_users"] + self._ready.set() + + async def cog_before_invoke(self, ctx: commands.Context): + await self._ready.wait() + + def cog_unload(self): + self._unmute_task.cancel() + for task in self._unmute_tasks.values(): + task.cancel() + + async def _handle_automatic_unmute(self): + await self.bot.wait_until_red_ready() + await self._ready.wait() + while True: + # await self._clean_tasks() + try: + await self._handle_server_unmutes() + except Exception: + log.error("error checking server unmutes", exc_info=True) + await asyncio.sleep(0.1) + try: + await self._handle_channel_unmutes() + except Exception: + log.error("error checking channel unmutes", exc_info=True) + await asyncio.sleep(120) + + async def _clean_tasks(self): + log.debug("Cleaning unmute tasks") + is_debug = log.getEffectiveLevel() <= logging.DEBUG + for task_id in list(self._unmute_tasks.keys()): + task = self._unmute_tasks[task_id] + + if task.canceled(): + self._unmute_tasks.pop(task_id, None) + continue + + if task.done(): + try: + r = task.result() + except Exception: + if is_debug: + log.exception("Dead task when trying to unmute") + self._unmute_tasks.pop(task_id, None) + + async def _handle_server_unmutes(self): + log.debug("Checking server unmutes") + for g_id, mutes in self._server_mutes.items(): + to_remove = [] + guild = self.bot.get_guild(g_id) + if guild is None: + continue + for u_id, data in mutes.items(): + time_to_unmute = data["until"] - datetime.utcnow().timestamp() + if time_to_unmute < 120.0: + self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( + self._auto_unmute_user(guild, data) + ) + to_remove.append(u_id) + for u_id in to_remove: + del self._server_mutes[g_id][u_id] + await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) + + async def _auto_unmute_user(self, guild: discord.Guild, data: dict): + delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + if delay < 1: + delay = 0 + await asyncio.sleep(delay) + try: + member = guild.get_member(data["member"]) + author = guild.get_member(data["author"]) + if not member or not author: + return + success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) + if success: + try: + await modlog.create_case( + self.bot, + guild, + datetime.utcnow(), + "sunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + except discord.errors.Forbidden: + return + + async def _handle_channel_unmutes(self): + log.debug("Checking channel unmutes") + for c_id, mutes in self._channel_mutes.items(): + to_remove = [] + channel = self.bot.get_channel(c_id) + if channel is None: + continue + for u_id, data in mutes.items(): + time_to_unmute = data["until"] - datetime.utcnow().timestamp() + if time_to_unmute < 120.0: + self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( + self._auto_channel_unmute_user(channel, data) + ) + for u_id in to_remove: + del self._channel_mutes[c_id][u_id] + await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) + + async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): + delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + if delay < 1: + delay = 0 + await asyncio.sleep(delay) + try: + member = channel.guild.get_member(data["member"]) + author = channel.guild.get_member(data["author"]) + if not member or not author: + return + success, message = await self.channel_unmute_user( + channel.guild, channel, author, member, _("Automatic unmute") + ) + if success: + try: + await modlog.create_case( + self.bot, + channel.guild, + datetime.utcnow(), + "cunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + except discord.errors.Forbidden: + return + + @commands.Cog.listener() + async def on_member_remove(self, member: discord.Member): + guild = member.guild + if guild.id in self._server_mutes: + if member.id in self._server_mutes[guild.id]: + del self._server_mutes[guild.id][member.id] + for channel in guild.channels: + if channel.id in self._channel_mutes: + if member.id in self._channel_mutes[channel.id]: + del self._channel_mutes[channel.id][member.id] + mute_role = await self.config.guild(guild).mute_role() + if not mute_role: + return + if mute_role in [r.id for r in member.roles]: + async with self.config.guild(guild).removed_users() as removed_users: + removed_users.append(member.id) + + @commands.Cog.listener() + async def on_member_join(self, member: discord.Member): + guild = member.guild + mute_role = await self.config.guild(guild).mute_role() + if not mute_role: + return + async with self.config.guild(guild).removed_users() as removed_users: + if member.id in removed_users: + removed_users.remove(member.id) + role = guild.get_role(mute_role) + if not role: + return + try: + await member.add_roles(role, reason=_("Previously muted in this server.")) + except discord.errors.Forbidden: + return + + @commands.group() + @commands.guild_only() + @checks.mod_or_permissions(manage_roles=True) + async def muteset(self, ctx: commands.Context): + """Mute settings.""" + pass + + @muteset.command(name="role") + @checks.bot_has_permissions(manage_roles=True) + async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): + """ + Sets the role to be applied when muting a user. + + If no role is setup the bot will attempt to mute a user by setting + channel overwrites in all channels to prevent the user from sending messages. + """ + if not role: + await self.config.guild(ctx.guild).mute_role.set(None) + await ctx.send(_("Channel overwrites will be used for mutes instead.")) + else: + await self.config.guild(ctx.guild).mute_role.set(role.id) + await ctx.send(_("Mute role set to {role}").format(role=role.name)) + + @muteset.command(name="makerole") + @checks.bot_has_permissions(manage_roles=True) + async def make_mute_role(self, ctx: commands.Context, *, name: str): + """ + Create a Muted role. + + This will create a role and apply overwrites to all available channels + to more easily setup muting a user. + + If you already have a muted role created on the server use + `[p]muteset role ROLE_NAME_HERE` + """ + perms = discord.Permissions() + perms.update(send_messages=False, speak=False, add_reactions=False) + try: + role = await ctx.guild.create_role( + name=name, permissions=perms, reason=_("Mute role setup") + ) + except discord.errors.Forbidden: + return + for channel in ctx.guild.channels: + overs = discord.PermissionOverwrite() + if isinstance(channel, discord.TextChannel): + overs.send_messages = False + overs.add_reactions = False + if isinstance(channel, discord.VoiceChannel): + overs.speak = False + try: + await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) + except discord.errors.Forbidden: + continue + await self.config.guild(ctx.guild).mute_role.set(role.id) + await ctx.send(_("Mute role set to {role}").format(role=role.name)) + + @muteset.command(name="time") + async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): + """ + Set the default mute time for the mute command. + """ + data = time.get("duration", {}) + if not data: + await self.config.guild(ctx.guild).default_time.set(data) + await ctx.send(_("Default mute time removed.")) + else: + await self.config.guild(ctx.guild).default_time.set(data) + await ctx.send( + _("Default mute time set to {time}").format( + time=humanize_timedelta(timedelta=timedelta(**data)) + ) + ) + + @commands.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_roles=True) + async def mute( + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + time_and_reason: MuteTime = {}, + ): + """Mute users.""" + + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + time = "" + until = None + if duration: + until = datetime.utcnow() + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.utcnow() + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.mute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "smute", + user, + author, + reason, + until=until, + channel=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if not success_list: + return await ctx.send(issue) + if until: + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp(), + } + self._server_mutes[ctx.guild.id][user.id] = mute + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await ctx.send( + _("{users} {verb} been muted in this server{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time + ) + ) + + @commands.command(name="mutechannel") + @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + @checks.mod_or_permissions(manage_roles=True) + async def channel_mute( + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + time_and_reason: MuteTime = {}, + ): + """Mute a user in the current text channel.""" + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + until = None + time = "" + if duration: + until = datetime.utcnow() + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.utcnow() + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + channel = ctx.message.channel + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.channel_mute_user( + guild, channel, author, user, audit_reason + ) + if success: + success_list.append(user) + + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "cmute", + user, + author, + reason, + until=until, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + if until: + if channel.id not in self._channel_mutes: + self._channel_mutes[channel.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp(), + } + self._channel_mutes[channel.id][user.id] = mute + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await channel.send( + _("{users} {verb} been muted in this channel{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time + ) + ) + + @commands.command() + @commands.guild_only() + @commands.bot_has_permissions(manage_roles=True) + @checks.mod_or_permissions(manage_roles=True) + async def unmute( + self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + ): + """Unmute users.""" + guild = ctx.guild + author = ctx.author + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, message = await self.unmute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "sunmute", + user, + author, + reason, + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if ctx.guild.id in self._server_mutes: + for user in success_list: + del self._server_mutes[ctx.guild.id][user.id] + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + await ctx.send( + _("{users} unmuted in this server.").format( + users=humanize_list([f"{u}" for u in success_list]) + ) + ) + + @checks.mod_or_permissions(manage_roles=True) + @commands.command(name="channelunmute") + @commands.bot_has_permissions(manage_roles=True) + @commands.guild_only() + async def unmute_channel( + self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + ): + """Unmute a user in this channel.""" + channel = ctx.channel + author = ctx.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, message = await self.channel_unmute_user( + guild, channel, author, user, audit_reason + ) + + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "cunmute", + user, + author, + reason, + until=None, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + for user in success_list: + try: + del self._channel_mutes[channel.id][user.id] + except KeyError: + pass + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + await ctx.send( + _("{users} unmuted in this channel.").format( + users=humanize_list([f"{u}" for u in success_list]) + ) + ) + + async def mute_user( + self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + ) -> Tuple[bool, Optional[str]]: + """ + Handles muting users + """ + mute_role = await self.config.guild(guild).mute_role() + if mute_role: + try: + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + role = guild.get_role(mute_role) + if not role: + return False, mute_unmute_issues["role_missing"] + await user.add_roles(role, reason=reason) + except discord.errors.Forbidden: + return False, mute_unmute_issues["permissions_issue"] + return True, None + else: + mute_success = [] + for channel in guild.channels: + success, issue = await self.channel_mute_user(guild, channel, author, user, reason) + if not success: + mute_success.append(f"{channel.mention} - {issue}") + await asyncio.sleep(0.1) + if mute_success: + return False, "\n".join(s for s in mute_success) + else: + return True, None + + async def unmute_user( + self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + ) -> Tuple[bool, Optional[str]]: + """ + Handles muting users + """ + mute_role = await self.config.guild(guild).mute_role() + if mute_role: + try: + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + role = guild.get_role(mute_role) + if not role: + return False, mute_unmute_issues["role_missing"] + await user.remove_roles(role, reason=reason) + except discord.errors.Forbidden: + return False, mute_unmute_issues["permissions_issue"] + return True, None + else: + mute_success = [] + for channel in guild.channels: + success, issue = await self.channel_unmute_user( + guild, channel, author, user, reason + ) + if not success: + mute_success.append(f"{channel.mention} - {issue}") + await asyncio.sleep(0.1) + if mute_success: + return False, "\n".join(s for s in mute_success) + else: + return True, None + + async def channel_mute_user( + self, + guild: discord.Guild, + channel: discord.abc.GuildChannel, + author: discord.Member, + user: discord.Member, + reason: str, + ) -> Tuple[bool, Optional[str]]: + """Mutes the specified user in the specified channel""" + overwrites = channel.overwrites_for(user) + permissions = channel.permissions_for(user) + + if permissions.administrator: + return False, _(mute_unmute_issues["is_admin"]) + + new_overs = {} + if not isinstance(channel, discord.TextChannel): + new_overs.update(speak=False) + if not isinstance(channel, discord.VoiceChannel): + new_overs.update(send_messages=False, add_reactions=False) + + if all(getattr(permissions, p) is False for p in new_overs.keys()): + return False, _(mute_unmute_issues["already_muted"]) + + elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + + old_overs = {k: getattr(overwrites, k) for k in new_overs} + overwrites.update(**new_overs) + try: + await channel.set_permissions(user, overwrite=overwrites, reason=reason) + except discord.Forbidden: + return False, _(mute_unmute_issues["permissions_issue"]) + except discord.NotFound as e: + if e.code == 10003: + return False, _(mute_unmute_issues["unknown_channel"]) + elif e.code == 10009: + return False, _(mute_unmute_issues["left_guild"]) + else: + await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) + return True, None + + async def channel_unmute_user( + self, + guild: discord.Guild, + channel: discord.abc.GuildChannel, + author: discord.Member, + user: discord.Member, + reason: str, + ) -> Tuple[bool, Optional[str]]: + overwrites = channel.overwrites_for(user) + perms_cache = await self.config.member(user).perms_cache() + + if channel.id in perms_cache: + old_values = perms_cache[channel.id] + else: + old_values = {"send_messages": None, "add_reactions": None, "speak": None} + + if all(getattr(overwrites, k) == v for k, v in old_values.items()): + return False, _(mute_unmute_issues["already_unmuted"]) + + elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + return False, _(mute_unmute_issues["hierarchy_problem"]) + + overwrites.update(**old_values) + try: + if overwrites.is_empty(): + await channel.set_permissions( + user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason + ) + else: + await channel.set_permissions(user, overwrite=overwrites, reason=reason) + except discord.Forbidden: + return False, _(mute_unmute_issues["permissions_issue"]) + except discord.NotFound as e: + if e.code == 10003: + return False, _(mute_unmute_issues["unknown_channel"]) + elif e.code == 10009: + return False, _(mute_unmute_issues["left_guild"]) + else: + await self.config.member(user).clear_raw("perms_cache", str(channel.id)) + return True, None diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py new file mode 100644 index 00000000000..31140f4bb5a --- /dev/null +++ b/redbot/cogs/mutes/voicemutes.py @@ -0,0 +1,227 @@ +from typing import Optional +from .abc import MixinMeta + +import discord +from redbot.core import commands, checks, i18n, modlog +from redbot.core.utils.chat_formatting import format_perms_list +from redbot.core.utils.mod import get_audit_reason + +T_ = i18n.Translator("Mutes", __file__) + +_ = lambda s: s +_ = T_ + + +class VoiceMutes(MixinMeta): + """ + This handles all voice channel related muting + """ + + @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() + @checks.admin_or_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 ( + 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 + audit_reason = get_audit_reason(ctx.author, reason) + if needs_unmute and needs_undeafen: + await user.edit(mute=False, deafen=False, reason=audit_reason) + elif needs_unmute: + await user.edit(mute=False, reason=audit_reason) + elif needs_undeafen: + await user.edit(deafen=False, reason=audit_reason) + else: + await ctx.send(_("That user isn't muted or deafened by the server!")) + return + + guild = ctx.guild + author = ctx.author + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceunban", + user, + author, + reason, + until=None, + channel=None, + ) + except RuntimeError as e: + await ctx.send(e) + await ctx.send(_("User is now allowed to speak and listen in voice channels")) + + @commands.command() + @commands.guild_only() + @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: 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 + audit_reason = get_audit_reason(ctx.author, reason) + author = ctx.author + guild = ctx.guild + if needs_mute and needs_deafen: + await user.edit(mute=True, deafen=True, reason=audit_reason) + elif needs_mute: + await user.edit(mute=True, reason=audit_reason) + elif needs_deafen: + await user.edit(deafen=True, reason=audit_reason) + else: + await ctx.send(_("That user is already muted and deafened server-wide!")) + return + + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceban", + user, + author, + reason, + until=None, + channel=None, + ) + except RuntimeError as e: + await ctx.send(e) + await ctx.send(_("User has been banned from speaking or listening in voice channels")) + + @commands.command(name="voicemute") + @commands.guild_only() + 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 + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) + + success, issue = await self.mute_user(guild, channel, author, user, audit_reason) + + if success: + 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) + await ctx.send( + _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) + ) + else: + await ctx.send(issue) + + @commands.command(name="voiceunmute") + @commands.guild_only() + 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 + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) + + success, message = await self.unmute_user(guild, channel, author, user, audit_reason) + + 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) + await ctx.send( + _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) + ) From 4cdcdcb05bf515571f3e1c7e3d77ac2e8a24daf2 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 9 Mar 2020 23:04:07 -0600 Subject: [PATCH 033/103] and remove old mutes --- redbot/cogs/mod/mod.py | 2 - redbot/cogs/mod/mutes.py | 477 --------------------------------------- 2 files changed, 479 deletions(-) diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index fd207b4524f..6cdea3b24e3 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -14,7 +14,6 @@ from redbot.core.utils._internal_utils import send_to_owners_with_prefix_replaced from .events import Events from .kickban import KickBanMixin -from .mutes import MuteMixin from .names import ModInfo from .slowmode import Slowmode from .settings import ModSettings @@ -38,7 +37,6 @@ class Mod( ModSettings, Events, KickBanMixin, - MuteMixin, ModInfo, Slowmode, commands.Cog, diff --git a/redbot/cogs/mod/mutes.py b/redbot/cogs/mod/mutes.py index b0dbc63a9b1..8b137891791 100644 --- a/redbot/cogs/mod/mutes.py +++ b/redbot/cogs/mod/mutes.py @@ -1,478 +1 @@ -import asyncio -from datetime import timezone -from typing import cast, Optional -import discord -from redbot.core import commands, checks, i18n, modlog -from redbot.core.utils import AsyncIter -from redbot.core.utils.chat_formatting import format_perms_list -from redbot.core.utils.mod import get_audit_reason -from .abc import MixinMeta -from .utils import is_allowed_by_hierarchy - -T_ = i18n.Translator("Mod", __file__) - -_ = lambda s: s -mute_unmute_issues = { - "already_muted": _("That user can't send messages in this channel."), - "already_unmuted": _("That user isn't muted in this channel."), - "hierarchy_problem": _( - "I cannot let you do that. You are not higher than the user in the role hierarchy." - ), - "is_admin": _("That user cannot be muted, as they have the Administrator permission."), - "permissions_issue": _( - "Failed to mute user. I need the manage roles " - "permission and the user I'm muting must be " - "lower than myself in the role hierarchy." - ), - "left_guild": _("The user has left the server while applying an overwrite."), - "unknown_channel": _("The channel I tried to mute the user in isn't found."), -} -_ = T_ - - -class MuteMixin(MixinMeta): - """ - Stuff for mutes goes here - """ - - @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() - @checks.admin_or_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 ( - 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 - audit_reason = get_audit_reason(ctx.author, reason) - if needs_unmute and needs_undeafen: - await user.edit(mute=False, deafen=False, reason=audit_reason) - elif needs_unmute: - await user.edit(mute=False, reason=audit_reason) - elif needs_undeafen: - await user.edit(deafen=False, reason=audit_reason) - else: - await ctx.send(_("That user isn't muted or deafened by the server!")) - return - - guild = ctx.guild - author = ctx.author - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "voiceunban", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User is now allowed to speak and listen in voice channels")) - - @commands.command() - @commands.guild_only() - @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: 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 - audit_reason = get_audit_reason(ctx.author, reason) - author = ctx.author - guild = ctx.guild - if needs_mute and needs_deafen: - await user.edit(mute=True, deafen=True, reason=audit_reason) - elif needs_mute: - await user.edit(mute=True, reason=audit_reason) - elif needs_deafen: - await user.edit(deafen=True, reason=audit_reason) - else: - await ctx.send(_("That user is already muted and deafened server-wide!")) - return - - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "voiceban", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User has been banned from speaking or listening in voice channels")) - - @commands.group() - @commands.guild_only() - @checks.mod_or_permissions(manage_channels=True) - async def mute(self, ctx: commands.Context): - """Mute users.""" - pass - - @mute.command(name="voice") - @commands.guild_only() - 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 - channel = user_voice_state.channel - audit_reason = get_audit_reason(author, reason) - - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "vmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send( - _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) - ) - try: - if channel.permissions_for(ctx.me).move_members: - await user.move_to(channel) - else: - raise RuntimeError - except (discord.Forbidden, RuntimeError): - await ctx.send( - _( - "Because I don't have the Move Members permission, this will take into effect when the user rejoins." - ) - ) - else: - await ctx.send(issue) - - @mute.command(name="channel") - @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) - @checks.mod_or_permissions(administrator=True) - async def channel_mute( - self, ctx: commands.Context, user: discord.Member, *, reason: str = None - ): - """Mute a user in the current text channel.""" - author = ctx.message.author - channel = ctx.message.channel - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await channel.send(_("User has been muted in this channel.")) - else: - await channel.send(issue) - - @mute.command(name="server", aliases=["guild"]) - @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) - @checks.mod_or_permissions(administrator=True) - async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): - """Mutes user in the server.""" - author = ctx.message.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - - mute_success = [] - async with ctx.typing(): - for channel in guild.channels: - success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - mute_success.append((success, issue)) - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "smute", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User has been muted in this server.")) - - @commands.group() - @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) - @checks.mod_or_permissions(manage_channels=True) - async def unmute(self, ctx: commands.Context): - """Unmute users.""" - pass - - @unmute.command(name="voice") - @commands.guild_only() - 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 - channel = user_voice_state.channel - audit_reason = get_audit_reason(author, reason) - - success, message = await self.unmute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "vunmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send( - _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) - ) - try: - if channel.permissions_for(ctx.me).move_members: - await user.move_to(channel) - else: - raise RuntimeError - except (discord.Forbidden, RuntimeError): - await ctx.send( - _( - "Because I don't have the Move Members permission, this will take into effect when the user rejoins." - ) - ) - else: - await ctx.send(_("Unmute failed. Reason: {}").format(message)) - - @checks.mod_or_permissions(administrator=True) - @unmute.command(name="channel") - @commands.bot_has_permissions(manage_roles=True) - @commands.guild_only() - async def unmute_channel( - self, ctx: commands.Context, user: discord.Member, *, reason: str = None - ): - """Unmute a user in this channel.""" - channel = ctx.channel - author = ctx.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - - success, message = await self.unmute_user(guild, channel, author, user, audit_reason) - - if success: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cunmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send(_("User unmuted in this channel.")) - else: - await ctx.send(_("Unmute failed. Reason: {}").format(message)) - - @checks.mod_or_permissions(administrator=True) - @unmute.command(name="server", aliases=["guild"]) - @commands.bot_has_permissions(manage_roles=True) - @commands.guild_only() - async def unmute_guild( - self, ctx: commands.Context, user: discord.Member, *, reason: str = None - ): - """Unmute a user in this server.""" - guild = ctx.guild - author = ctx.author - audit_reason = get_audit_reason(author, reason) - - unmute_success = [] - async with ctx.typing(): - for channel in guild.channels: - success, message = await self.unmute_user( - guild, channel, author, user, audit_reason - ) - unmute_success.append((success, message)) - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "sunmute", - user, - author, - reason, - until=None, - ) - await ctx.send(_("User has been unmuted in this server.")) - - async def mute_user( - self, - guild: discord.Guild, - channel: discord.abc.GuildChannel, - author: discord.Member, - user: discord.Member, - reason: str, - ) -> (bool, str): - """Mutes the specified user in the specified channel""" - overwrites = channel.overwrites_for(user) - permissions = channel.permissions_for(user) - - if permissions.administrator: - return False, _(mute_unmute_issues["is_admin"]) - - new_overs = {} - if not isinstance(channel, discord.TextChannel): - new_overs.update(speak=False) - if not isinstance(channel, discord.VoiceChannel): - new_overs.update(send_messages=False, add_reactions=False) - - if all(getattr(permissions, p) is False for p in new_overs.keys()): - return False, _(mute_unmute_issues["already_muted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) - - old_overs = {k: getattr(overwrites, k) for k in new_overs} - overwrites.update(**new_overs) - try: - await channel.set_permissions(user, overwrite=overwrites, reason=reason) - except discord.Forbidden: - return False, _(mute_unmute_issues["permissions_issue"]) - except discord.NotFound as e: - if e.code == 10003: - return False, _(mute_unmute_issues["unknown_channel"]) - elif e.code == 10009: - return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) - return True, None - - async def unmute_user( - self, - guild: discord.Guild, - channel: discord.abc.GuildChannel, - author: discord.Member, - user: discord.Member, - reason: str, - ) -> (bool, str): - overwrites = channel.overwrites_for(user) - perms_cache = await self.config.member(user).perms_cache() - - if channel.id in perms_cache: - old_values = perms_cache[channel.id] - else: - old_values = {"send_messages": None, "add_reactions": None, "speak": None} - - if all(getattr(overwrites, k) == v for k, v in old_values.items()): - return False, _(mute_unmute_issues["already_unmuted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) - - overwrites.update(**old_values) - try: - if overwrites.is_empty(): - await channel.set_permissions( - user, overwrite=cast(discord.PermissionOverwrite, None), reason=reason - ) - else: - await channel.set_permissions(user, overwrite=overwrites, reason=reason) - except discord.Forbidden: - return False, _(mute_unmute_issues["permissions_issue"]) - except discord.NotFound as e: - if e.code == 10003: - return False, _(mute_unmute_issues["unknown_channel"]) - elif e.code == 10009: - return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).clear_raw("perms_cache", str(channel.id)) - return True, None From e1dbd54e240cf36aee713e8b8587955d5d768da1 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 8 Apr 2020 17:47:04 -0600 Subject: [PATCH 034/103] make voicemutes less yelly --- redbot/cogs/mutes/voicemutes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 31140f4bb5a..2da2b55818d 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -82,7 +82,7 @@ async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reaso elif needs_undeafen: await user.edit(deafen=False, reason=audit_reason) else: - await ctx.send(_("That user isn't muted or deafened by the server!")) + await ctx.send(_("That user isn't muted or deafened by the server.")) return guild = ctx.guild @@ -101,7 +101,7 @@ async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reaso ) except RuntimeError as e: await ctx.send(e) - await ctx.send(_("User is now allowed to speak and listen in voice channels")) + await ctx.send(_("User is now allowed to speak and listen in voice channels.")) @commands.command() @commands.guild_only() @@ -128,7 +128,7 @@ async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: elif needs_deafen: await user.edit(deafen=True, reason=audit_reason) else: - await ctx.send(_("That user is already muted and deafened server-wide!")) + await ctx.send(_("That user is already muted and deafened server-wide.")) return try: @@ -145,7 +145,7 @@ async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: ) except RuntimeError as e: await ctx.send(e) - await ctx.send(_("User has been banned from speaking or listening in voice channels")) + await ctx.send(_("User has been banned from speaking or listening in voice channels.")) @commands.command(name="voicemute") @commands.guild_only() @@ -182,7 +182,7 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso except RuntimeError as e: await ctx.send(e) await ctx.send( - _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) + _("Muted {user} in channel {channel.name}.").format(user=user, channel=channel) ) else: await ctx.send(issue) @@ -223,5 +223,5 @@ async def unmute_voice( except RuntimeError as e: await ctx.send(e) await ctx.send( - _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) + _("Unmuted {user} in channel {channel.name}.").format(user=user, channel=channel) ) From f0aba922429238505dd00be51f297a79b7837a91 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 13:28:44 -0600 Subject: [PATCH 035/103] fix error when no args present in mute commands --- redbot/cogs/mutes/mutes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a0162c342c1..f529f4e8878 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -338,7 +338,8 @@ async def mute( time_and_reason: MuteTime = {}, ): """Mute users.""" - + if not users: + return await ctx.send_help() duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) time = "" @@ -395,7 +396,7 @@ async def mute( ) ) - @commands.command(name="mutechannel") + @commands.command(name="mutechannel", aliases=["channelmute"]) @commands.guild_only() @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) @@ -407,6 +408,8 @@ async def channel_mute( time_and_reason: MuteTime = {}, ): """Mute a user in the current text channel.""" + if not users: + return await ctx.send_help() duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) until = None @@ -474,6 +477,8 @@ async def unmute( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): """Unmute users.""" + if not users: + return await ctx.send_help() guild = ctx.guild author = ctx.author audit_reason = get_audit_reason(author, reason) @@ -506,13 +511,15 @@ async def unmute( ) @checks.mod_or_permissions(manage_roles=True) - @commands.command(name="channelunmute") + @commands.command(name="channelunmute", aliases=["unmutechannel"]) @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() async def unmute_channel( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): """Unmute a user in this channel.""" + if not users: + return await ctx.send_help() channel = ctx.channel author = ctx.author guild = ctx.guild From 48dae6062a193cbdf0cf7eaffa0c84d4220aaa1e Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 13:54:25 -0600 Subject: [PATCH 036/103] update docstrings --- redbot/cogs/mutes/mutes.py | 43 +++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f529f4e8878..f6e15a3c712 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -263,8 +263,7 @@ async def muteset(self, ctx: commands.Context): @muteset.command(name="role") @checks.bot_has_permissions(manage_roles=True) async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): - """ - Sets the role to be applied when muting a user. + """Sets the role to be applied when muting a user. If no role is setup the bot will attempt to mute a user by setting channel overwrites in all channels to prevent the user from sending messages. @@ -279,8 +278,7 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): @muteset.command(name="makerole") @checks.bot_has_permissions(manage_roles=True) async def make_mute_role(self, ctx: commands.Context, *, name: str): - """ - Create a Muted role. + """Create a Muted role. This will create a role and apply overwrites to all available channels to more easily setup muting a user. @@ -337,7 +335,18 @@ async def mute( *, time_and_reason: MuteTime = {}, ): - """Mute users.""" + """Mute users. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason={}]` is the time to mute for and reason. Time is + any valid time length such as `30 minutes` or `2 days`. If nothing + is provided the mute will be indefinite. + + Examples: + `[p]mute @member1 @member2 spam 5 hours` + `[p]mute @member1 3 days` + + """ if not users: return await ctx.send_help() duration = time_and_reason.get("duration", {}) @@ -407,7 +416,17 @@ async def channel_mute( *, time_and_reason: MuteTime = {}, ): - """Mute a user in the current text channel.""" + """Mute a user in the current text channel. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason={}]` is the time to mute for and reason. Time is + any valid time length such as `30 minutes` or `2 days`. If nothing + is provided the mute will be indefinite. + + Examples: + `[p]mutechannel @member1 @member2 spam 5 hours` + `[p]mutechannel @member1 3 days` + """ if not users: return await ctx.send_help() duration = time_and_reason.get("duration", {}) @@ -476,7 +495,11 @@ async def channel_mute( async def unmute( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): - """Unmute users.""" + """Unmute users. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[reason]` is the reason for the unmute. + """ if not users: return await ctx.send_help() guild = ctx.guild @@ -517,7 +540,11 @@ async def unmute( async def unmute_channel( self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None ): - """Unmute a user in this channel.""" + """Unmute a user in this channel. + + `[users]...` is a space separated list of usernames, ID's, or mentions. + `[reason]` is the reason for the unmute. + """ if not users: return await ctx.send_help() channel = ctx.channel From bb01487d6e1918de8956e08f6e585bfa0411ce76 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 15:29:55 -0600 Subject: [PATCH 037/103] address review --- redbot/cogs/mutes/mutes.py | 90 +++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f6e15a3c712..a172fa07369 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1,5 +1,6 @@ -import discord import asyncio +import contextlib +import discord import logging from abc import ABC @@ -13,13 +14,15 @@ from redbot.core import commands, checks, i18n, modlog, Config from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy +from redbot.core.utils.menus import start_adding_reactions +from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate T_ = i18n.Translator("Mutes", __file__) _ = lambda s: s mute_unmute_issues = { - "already_muted": _("That user can't send messages in this channel."), - "already_unmuted": _("That user isn't muted in this channel."), + "already_muted": _("That user is already muted in this channel."), + "already_unmuted": _("That user is not muted in this channel."), "hierarchy_problem": _( "I cannot let you do that. You are not higher than the user in the role hierarchy." ), @@ -404,6 +407,43 @@ async def mute( users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) ) + if issue: + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message) + + async def handle_issues(self, ctx: commands.Context, message: str) -> None: + can_react = ctx.channel.permissions_for(ctx.me).add_reactions + if not can_react: + message += " (y/n)" + query: discord.Message = await ctx.send(message) + if can_react: + # noinspection PyAsyncCall + start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS) + pred = ReactionPredicate.yes_or_no(query, ctx.author) + event = "reaction_add" + else: + pred = MessagePredicate.yes_or_no(ctx) + event = "message" + try: + await ctx.bot.wait_for(event, check=pred, timeout=30) + except asyncio.TimeoutError: + await query.delete() + return + + if not pred.result: + if can_react: + await query.delete() + else: + await ctx.send(_("OK then.")) + return + else: + if can_react: + with contextlib.suppress(discord.Forbidden): + await query.clear_reactions() + await ctx.send(issue) @commands.command(name="mutechannel", aliases=["channelmute"]) @commands.guild_only() @@ -507,7 +547,7 @@ async def unmute( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, message = await self.unmute_user(guild, author, user, audit_reason) + success, issue = await self.unmute_user(guild, author, user, audit_reason) if success: success_list.append(user) try: @@ -523,15 +563,26 @@ async def unmute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) + if not success_list: + return await ctx.send(issue) if ctx.guild.id in self._server_mutes: - for user in success_list: - del self._server_mutes[ctx.guild.id][user.id] - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + if user.id in self._server_mutes[ctx.guild.id]: + for user in success_list: + del self._server_mutes[ctx.guild.id][user.id] + await self.config.guild(ctx.guild).muted_users.set( + self._server_mutes[ctx.guild.id] + ) await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) ) ) + if issue: + message = _( + "{users} could not be unmuted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="channelunmute", aliases=["unmutechannel"]) @@ -575,11 +626,14 @@ async def unmute_channel( log.error(_("Error creating modlog case"), exc_info=e) if success_list: for user in success_list: - try: + if ( + channel.id in self._channel_mutes + and user.id in self._channel_mutes[channel.id] + ): del self._channel_mutes[channel.id][user.id] - except KeyError: - pass - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + await self.config.channel(channel).muted_users.set( + self._channel_mutes[channel.id] + ) await ctx.send( _("{users} unmuted in this channel.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -611,8 +665,10 @@ async def mute_user( if not success: mute_success.append(f"{channel.mention} - {issue}") await asyncio.sleep(0.1) - if mute_success: + if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) + elif mute_success and len(mute_success) != len(guild.channels): + return True, "\n".join(s for s in mute_success) else: return True, None @@ -669,10 +725,7 @@ async def channel_mute_user( if not isinstance(channel, discord.VoiceChannel): new_overs.update(send_messages=False, add_reactions=False) - if all(getattr(permissions, p) is False for p in new_overs.keys()): - return False, _(mute_unmute_issues["already_muted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): return False, _(mute_unmute_issues["hierarchy_problem"]) old_overs = {k: getattr(overwrites, k) for k in new_overs} @@ -706,10 +759,7 @@ async def channel_unmute_user( else: old_values = {"send_messages": None, "add_reactions": None, "speak": None} - if all(getattr(overwrites, k) == v for k, v in old_values.items()): - return False, _(mute_unmute_issues["already_unmuted"]) - - elif not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): return False, _(mute_unmute_issues["hierarchy_problem"]) overwrites.update(**old_values) From 6bef73003620ad87f771b54e8b10a0a4899242bc Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 15:30:31 -0600 Subject: [PATCH 038/103] black --- redbot/cogs/mutes/mutes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a172fa07369..47766f7e2f0 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -409,9 +409,9 @@ async def mute( ) if issue: message = _( - "{users} could not be muted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) await self.handle_issues(ctx, message) async def handle_issues(self, ctx: commands.Context, message: str) -> None: @@ -579,9 +579,9 @@ async def unmute( ) if issue: message = _( - "{users} could not be unmuted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) + "{users} could not be unmuted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) await self.handle_issues(ctx, message) @checks.mod_or_permissions(manage_roles=True) From c5d89f8b2af65c12839939b042dd01882d8566c3 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 15:33:01 -0600 Subject: [PATCH 039/103] oops --- redbot/cogs/mutes/mutes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 47766f7e2f0..b61b7059a8b 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -412,9 +412,9 @@ async def mute( "{users} could not be muted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message) + await self.handle_issues(ctx, message, issue) - async def handle_issues(self, ctx: commands.Context, message: str) -> None: + async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions if not can_react: message += " (y/n)" @@ -582,7 +582,7 @@ async def unmute( "{users} could not be unmuted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message) + await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="channelunmute", aliases=["unmutechannel"]) From 6c78406ab26629998d38bb5edfcfabbeff6251a5 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 3 May 2020 16:21:29 -0600 Subject: [PATCH 040/103] fix voicemutes --- redbot/cogs/mutes/voicemutes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 2da2b55818d..e6aeec3b980 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -164,7 +164,7 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso 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.channel_mute_user(guild, channel, author, user, audit_reason) if success: try: @@ -206,7 +206,9 @@ async def unmute_voice( 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.channel_unmute_user( + guild, channel, author, user, audit_reason + ) try: await modlog.create_case( From f654e78c84d5b0680a8ee0f12b139da51a24d4b0 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 21 Aug 2020 15:42:08 -0600 Subject: [PATCH 041/103] Remove _voice_perm_check from mod since it's now in mutes cog --- redbot/cogs/mod/abc.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/redbot/cogs/mod/abc.py b/redbot/cogs/mod/abc.py index cfdd5a6596b..59837c4016b 100644 --- a/redbot/cogs/mod/abc.py +++ b/redbot/cogs/mod/abc.py @@ -17,10 +17,3 @@ def __init__(self, *_args): self.config: Config self.bot: Red self.cache: dict - - @staticmethod - @abstractmethod - async def _voice_perm_check( - ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool - ) -> bool: - raise NotImplementedError() From 807ddb5b15b5dc2f3d0c37b00d191484959b2750 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 21 Aug 2020 18:06:20 -0600 Subject: [PATCH 042/103] remove naive datetimes prevent muting the bot prevent muting yourself fix error message when lots of channels are present --- redbot/cogs/mutes/mutes.py | 67 ++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index b61b7059a8b..5e6386df4b7 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -4,15 +4,15 @@ import logging from abc import ABC -from typing import cast, Optional, Dict, List, Tuple -from datetime import datetime, timedelta +from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine +from datetime import datetime, timedelta, timezone from .converters import MuteTime from .voicemutes import VoiceMutes from redbot.core.bot import Red from redbot.core import commands, checks, i18n, modlog, Config -from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list +from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list, pagify from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate @@ -74,10 +74,25 @@ def __init__(self, bot: Red): self._channel_mutes: Dict[int, Dict[int, dict]] = {} self._ready = asyncio.Event() self.bot.loop.create_task(self.initialize()) - self._unmute_tasks = {} + self._unmute_tasks: Dict[str, Coroutine] = {} self._unmute_task = asyncio.create_task(self._handle_automatic_unmute()) # dict of guild id, member id and time to be unmuted + async def red_delete_data_for_user( + self, + *, + requester: Literal["discord_deleted_user", "owner", "user", "user_strict"], + user_id: int, + ): + if requester != "discord_deleted_user": + return + + await self._ready.wait() + all_members = await self.config.all_members() + for g_id, m_id in all_members.items(): + if m_id == user_id: + await self.config.member_from_ids(g_id, m_id).clear() + async def initialize(self): guild_data = await self.config.all_guilds() for g_id, mutes in guild_data.items(): @@ -137,7 +152,7 @@ async def _handle_server_unmutes(self): if guild is None: continue for u_id, data in mutes.items(): - time_to_unmute = data["until"] - datetime.utcnow().timestamp() + time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( self._auto_unmute_user(guild, data) @@ -148,7 +163,7 @@ async def _handle_server_unmutes(self): await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) async def _auto_unmute_user(self, guild: discord.Guild, data: dict): - delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) if delay < 1: delay = 0 await asyncio.sleep(delay) @@ -163,7 +178,7 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): await modlog.create_case( self.bot, guild, - datetime.utcnow(), + datetime.now(timezone.utc), "sunmute", member, author, @@ -183,7 +198,7 @@ async def _handle_channel_unmutes(self): if channel is None: continue for u_id, data in mutes.items(): - time_to_unmute = data["until"] - datetime.utcnow().timestamp() + time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( self._auto_channel_unmute_user(channel, data) @@ -193,7 +208,7 @@ async def _handle_channel_unmutes(self): await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): - delay = 120 - (data["until"] - datetime.utcnow().timestamp()) + delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) if delay < 1: delay = 0 await asyncio.sleep(delay) @@ -210,7 +225,7 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di await modlog.create_case( self.bot, channel.guild, - datetime.utcnow(), + datetime.now(timezone.utc), "cunmute", member, author, @@ -352,17 +367,21 @@ async def mute( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot mute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot mute yourself.")) duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) time = "" until = None if duration: - until = datetime.utcnow() + timedelta(**duration) + until = datetime.now(timezone.utc) + timedelta(**duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: - until = datetime.utcnow() + timedelta(**default_duration) + until = datetime.now(timezone.utc) + timedelta(**default_duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) author = ctx.message.author guild = ctx.guild @@ -386,8 +405,9 @@ async def mute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) - if not success_list: - return await ctx.send(issue) + if not success_list and issue: + resp = pagify(issue) + return await ctx.send_interactive(resp) if until: if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} @@ -469,17 +489,21 @@ async def channel_mute( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot mute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot mute yourself.")) duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) until = None time = "" if duration: - until = datetime.utcnow() + timedelta(**duration) + until = datetime.now(timezone.utc) + timedelta(**duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: - until = datetime.utcnow() + timedelta(**default_duration) + until = datetime.now(timezone.utc) + timedelta(**default_duration) time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) author = ctx.message.author channel = ctx.message.channel @@ -542,6 +566,10 @@ async def unmute( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot unmute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot unmute yourself.")) guild = ctx.guild author = ctx.author audit_reason = get_audit_reason(author, reason) @@ -564,7 +592,8 @@ async def unmute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if not success_list: - return await ctx.send(issue) + resp = pagify(issue) + return await ctx.send_interactive(resp) if ctx.guild.id in self._server_mutes: if user.id in self._server_mutes[ctx.guild.id]: for user in success_list: @@ -598,6 +627,10 @@ async def unmute_channel( """ if not users: return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot unmute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot unmute yourself.")) channel = ctx.channel author = ctx.author guild = ctx.guild From 50dfda2e07aeabf7e17e24f66b033e0b2e9f89ac Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 21 Aug 2020 18:18:02 -0600 Subject: [PATCH 043/103] change alias for channelunmute Be more verbose for creating default mute role --- redbot/cogs/mutes/mutes.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 5e6386df4b7..a6c054a6e27 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -311,7 +311,8 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): name=name, permissions=perms, reason=_("Mute role setup") ) except discord.errors.Forbidden: - return + return await ctx.send(_("I could not create a muted role in this server.")) + errors = [] for channel in ctx.guild.channels: overs = discord.PermissionOverwrite() if isinstance(channel, discord.TextChannel): @@ -322,7 +323,14 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): try: await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) except discord.errors.Forbidden: + errors.append(f"{channel.mention}") continue + if errors: + msg = _("I could not set overwrites for the following channels: {channels}").format( + channels=humanize_list(errors) + ) + for page in pagify(msg): + await ctx.send(page) await self.config.guild(ctx.guild).mute_role.set(role.id) await ctx.send(_("Mute role set to {role}").format(role=role.name)) @@ -614,7 +622,7 @@ async def unmute( await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) - @commands.command(name="channelunmute", aliases=["unmutechannel"]) + @commands.command(name="unmutechannel", aliases=["channelunmute"]) @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() async def unmute_channel( From 5e0e4b4431dfae9a5ca4bae53a0c003a35454beb Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sat, 22 Aug 2020 15:58:55 -0600 Subject: [PATCH 044/103] add `[p]activemutes` to show current mutes in the server and time remaining on the mutes --- redbot/cogs/mutes/mutes.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a6c054a6e27..561b4635749 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -351,6 +351,54 @@ async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): ) ) + @commands.command() + @commands.guild_only() + @checks.mod_or_permissions(manage_roles=True) + async def activemutes(self, ctx: commands.Context): + """ + Displays active mutes on this server. + """ + + msg = "" + for guild_id, mutes_data in self._server_mutes.items(): + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + msg += _("Server Mute: {member}").format(member=user_str) + if time_str: + msg += _("Remaining: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + for channel_id, mutes_data in self._channel_mutes.items(): + msg += f"<#{channel_id}>\n" + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + msg += _("Channel Mute: {member} ").format(member=user_str) + if time_str: + msg += _("Remaining: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + if msg: + await ctx.maybe_send_embed(msg) + else: + await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) + @commands.command() @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) From 3697150e736af443b02feb78d901e2363bdbd8d0 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 31 Aug 2020 10:47:07 -0600 Subject: [PATCH 045/103] improve resolution of unmute time --- redbot/cogs/mutes/mutes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 561b4635749..ec9cfe25a4f 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -163,7 +163,7 @@ async def _handle_server_unmutes(self): await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) async def _auto_unmute_user(self, guild: discord.Guild, data: dict): - delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) + delay = data["until"] - datetime.now(timezone.utc).timestamp() if delay < 1: delay = 0 await asyncio.sleep(delay) @@ -208,7 +208,7 @@ async def _handle_channel_unmutes(self): await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): - delay = 120 - (data["until"] - datetime.now(timezone.utc).timestamp()) + delay = data["until"] - datetime.now(timezone.utc).timestamp() if delay < 1: delay = 0 await asyncio.sleep(delay) From 94f56513dacefafb08d8a4c964cb0db9360e0af3 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 31 Aug 2020 10:52:24 -0600 Subject: [PATCH 046/103] black --- redbot/cogs/mutes/mutes.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index ec9cfe25a4f..a3bc5add14e 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -283,8 +283,8 @@ async def muteset(self, ctx: commands.Context): async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): """Sets the role to be applied when muting a user. - If no role is setup the bot will attempt to mute a user by setting - channel overwrites in all channels to prevent the user from sending messages. + If no role is setup the bot will attempt to mute a user by setting + channel overwrites in all channels to prevent the user from sending messages. """ if not role: await self.config.guild(ctx.guild).mute_role.set(None) @@ -298,11 +298,11 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): async def make_mute_role(self, ctx: commands.Context, *, name: str): """Create a Muted role. - This will create a role and apply overwrites to all available channels - to more easily setup muting a user. + This will create a role and apply overwrites to all available channels + to more easily setup muting a user. - If you already have a muted role created on the server use - `[p]muteset role ROLE_NAME_HERE` + If you already have a muted role created on the server use + `[p]muteset role ROLE_NAME_HERE` """ perms = discord.Permissions() perms.update(send_messages=False, speak=False, add_reactions=False) @@ -337,7 +337,7 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): @muteset.command(name="time") async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): """ - Set the default mute time for the mute command. + Set the default mute time for the mute command. """ data = time.get("duration", {}) if not data: @@ -356,7 +356,7 @@ async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): @checks.mod_or_permissions(manage_roles=True) async def activemutes(self, ctx: commands.Context): """ - Displays active mutes on this server. + Displays active mutes on this server. """ msg = "" @@ -730,10 +730,14 @@ async def unmute_channel( ) async def mute_user( - self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + self, + guild: discord.Guild, + author: discord.Member, + user: discord.Member, + reason: str, ) -> Tuple[bool, Optional[str]]: """ - Handles muting users + Handles muting users """ mute_role = await self.config.guild(guild).mute_role() if mute_role: @@ -762,10 +766,14 @@ async def mute_user( return True, None async def unmute_user( - self, guild: discord.Guild, author: discord.Member, user: discord.Member, reason: str, + self, + guild: discord.Guild, + author: discord.Member, + user: discord.Member, + reason: str, ) -> Tuple[bool, Optional[str]]: """ - Handles muting users + Handles muting users """ mute_role = await self.config.guild(guild).mute_role() if mute_role: From 86a32229b2e4511fef8d1d080f5de5a3d184d244 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 1 Sep 2020 14:18:21 -0600 Subject: [PATCH 047/103] Show indefinite mutes in activemutes and only show the current servers mutes in activemutes --- redbot/cogs/mutes/mutes.py | 105 ++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a3bc5add14e..46ba6966a19 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -152,6 +152,8 @@ async def _handle_server_unmutes(self): if guild is None: continue for u_id, data in mutes.items(): + if data["until"] is None: + continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( @@ -360,44 +362,51 @@ async def activemutes(self, ctx: commands.Context): """ msg = "" - for guild_id, mutes_data in self._server_mutes.items(): + if ctx.guild.id in self._server_mutes: + mutes_data = self._server_mutes[ctx.guild.id] for user_id, mutes in mutes_data.items(): user = ctx.guild.get_member(user_id) if not user: user_str = f"<@!{user_id}>" else: user_str = user.mention - time_left = timedelta( - seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() - ) - time_str = humanize_timedelta(timedelta=time_left) - msg += _("Server Mute: {member}").format(member=user_str) - if time_str: - msg += _("Remaining: {time_left}\n").format(time_left=time_str) - else: - msg += "\n" - for channel_id, mutes_data in self._channel_mutes.items(): - msg += f"<#{channel_id}>\n" - for user_id, mutes in mutes_data.items(): - user = ctx.guild.get_member(user_id) - if not user: - user_str = f"<@!{user_id}>" + if mutes["until"]: + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) else: - user_str = user.mention - - time_left = timedelta( - seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() - ) - time_str = humanize_timedelta(timedelta=time_left) - msg += _("Channel Mute: {member} ").format(member=user_str) + time_str = "" + msg += _("Server Mute: {member}").format(member=user_str) if time_str: msg += _("Remaining: {time_left}\n").format(time_left=time_str) else: msg += "\n" - if msg: - await ctx.maybe_send_embed(msg) - else: - await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) + for channel_id, mutes_data in self._channel_mutes.items(): + msg += f"<#{channel_id}>\n" + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + if mutes["until"]: + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + else: + time_str = "" + msg += _("Channel Mute: {member} ").format(member=user_str) + if time_str: + msg += _("Remaining: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + if msg: + for page in pagify(msg): + await ctx.maybe_send_embed(page) + return + await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) @commands.command() @commands.guild_only() @@ -464,17 +473,16 @@ async def mute( if not success_list and issue: resp = pagify(issue) return await ctx.send_interactive(resp) - if until: - if ctx.guild.id not in self._server_mutes: - self._server_mutes[ctx.guild.id] = {} - for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp(), - } - self._server_mutes[ctx.guild.id][user.id] = mute - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp() if until else None, + } + self._server_mutes[ctx.guild.id][user.id] = mute + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: verb = _("have") @@ -588,17 +596,16 @@ async def channel_mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - if until: - if channel.id not in self._channel_mutes: - self._channel_mutes[channel.id] = {} - for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp(), - } - self._channel_mutes[channel.id][user.id] = mute - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + if channel.id not in self._channel_mutes: + self._channel_mutes[channel.id] = {} + for user in success_list: + mute = { + "author": ctx.message.author.id, + "member": user.id, + "until": until.timestamp() if until else None, + } + self._channel_mutes[channel.id][user.id] = mute + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: verb = _("have") From ef1325e6c2f84e0d52cf421591bc5d1d11b89ca4 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 1 Sep 2020 16:18:04 -0600 Subject: [PATCH 048/103] replace message.created_at with timezone aware timezone --- redbot/cogs/mutes/mutes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 46ba6966a19..3a4b558ba00 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -460,7 +460,7 @@ async def mute( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "smute", user, author, @@ -585,7 +585,7 @@ async def channel_mute( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "cmute", user, author, @@ -645,7 +645,7 @@ async def unmute( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "sunmute", user, author, @@ -710,7 +710,7 @@ async def unmute_channel( await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "cunmute", user, author, From 7ab1e3c327e6e893f9d12ffff2ea0c44c25c54d6 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Thu, 3 Sep 2020 18:33:29 -0600 Subject: [PATCH 049/103] remove "server" from activemutes to clean up look since channelmutes will show channel --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 3a4b558ba00..e3c22eb9b54 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -377,7 +377,7 @@ async def activemutes(self, ctx: commands.Context): time_str = humanize_timedelta(timedelta=time_left) else: time_str = "" - msg += _("Server Mute: {member}").format(member=user_str) + msg += _("Mute: {member}").format(member=user_str) if time_str: msg += _("Remaining: {time_left}\n").format(time_left=time_str) else: From ecf92be00b80ed4b325afeaaa1dc2442db70990b Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 7 Sep 2020 01:52:45 -0600 Subject: [PATCH 050/103] better cache management, add tracking for manual muted role removal in the cache and modlog cases --- redbot/cogs/mutes/mutes.py | 249 ++++++++++++++++++++++++++++--------- 1 file changed, 191 insertions(+), 58 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index e3c22eb9b54..155de9d8d9d 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -4,6 +4,7 @@ import logging from abc import ABC +from copy import copy from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine from datetime import datetime, timedelta, timezone @@ -96,10 +97,14 @@ async def red_delete_data_for_user( async def initialize(self): guild_data = await self.config.all_guilds() for g_id, mutes in guild_data.items(): - self._server_mutes[g_id] = mutes["muted_users"] + self._server_mutes[g_id] = {} + for user_id, mute in mutes["muted_users"].items(): + self._server_mutes[g_id][int(user_id)] = mute channel_data = await self.config.all_channels() for c_id, mutes in channel_data.items(): - self._channel_mutes[c_id] = mutes["muted_users"] + self._channel_mutes[c_id] = {} + for user_id, mute in mutes["muted_users"].items(): + self._channel_mutes[c_id][int(user_id)] = mute self._ready.set() async def cog_before_invoke(self, ctx: commands.Context): @@ -200,6 +205,8 @@ async def _handle_channel_unmutes(self): if channel is None: continue for u_id, data in mutes.items(): + if not data["until"]: + continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( @@ -239,6 +246,102 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di except discord.errors.Forbidden: return + @commands.Cog.listener() + async def on_member_update(self, before: discord.Member, after: discord.Member): + """ + Used to handle the cache if a member manually has the muted role removed + """ + guild = before.guild + mute_role_id = await self.config.guild(before.guild).mute_role() + mute_role = guild.get_role(mute_role_id) + if not mute_role: + return + b = set(before.roles) + a = set(after.roles) + roles_removed = list(b - a) + roles_added = list(a - b) + if mute_role in roles_removed: + # send modlog case for unmute and remove from cache + if after.id in self._server_mutes[guild.id]: + try: + await modlog.create_case( + self.bot, + guild, + datetime.utcnow(), + "sunmute", + after, + None, + _("Manually removed mute role"), + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + del self._server_mutes[guild.id][after.id] + if mute_role in roles_added: + # send modlog case for mute and add to cache + if after.id not in self._server_mutes[guild.id]: + try: + await modlog.create_case( + self.bot, + guild, + datetime.utcnow(), + "smute", + after, + None, + _("Manually applied mute role"), + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + self._server_mutes[guild.id][after.id] = { + "author": None, + "member": after.id, + "until": None, + } + await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) + + @commands.Cog.listener() + async def on_guild_channel_update( + self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel + ): + """ + This handles manually removing + """ + if after.id in self._channel_mutes: + before_perms: Dict[int, Dict[str, Optional[bool]]] = { + o.id: {name: attr for name, attr in p} for o, p in before.overwrites.items() + } + after_perms: Dict[int, Dict[str, Optional[bool]]] = { + o.id: {name: attr for name, attr in p} for o, p in after.overwrites.items() + } + to_del: int = [] + for user_id in self._channel_mutes[after.id].keys(): + if user_id in before_perms and ( + user_id not in after_perms or after_perms[user_id]["send_messages"] + ): + user = after.guild.get_member(user_id) + if not user: + user = discord.Object(id=user_id) + log.debug(f"{user} - {type(user)}") + to_del.append(user_id) + try: + log.debug("creating case") + await modlog.create_case( + self.bot, + after.guild, + datetime.utcnow(), + "cunmute", + user, + None, + _("Manually removed channel overwrites"), + until=None, + channel=after, + ) + log.debug("created case") + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + for u_id in to_del: + del self._channel_mutes[after.id][u_id] + await self.config.channel(after).muted_users.set(self._channel_mutes[after.id]) + @commands.Cog.listener() async def on_member_remove(self, member: discord.Member): guild = member.guild @@ -362,28 +465,11 @@ async def activemutes(self, ctx: commands.Context): """ msg = "" + to_del = [] if ctx.guild.id in self._server_mutes: mutes_data = self._server_mutes[ctx.guild.id] - for user_id, mutes in mutes_data.items(): - user = ctx.guild.get_member(user_id) - if not user: - user_str = f"<@!{user_id}>" - else: - user_str = user.mention - if mutes["until"]: - time_left = timedelta( - seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() - ) - time_str = humanize_timedelta(timedelta=time_left) - else: - time_str = "" - msg += _("Mute: {member}").format(member=user_str) - if time_str: - msg += _("Remaining: {time_left}\n").format(time_left=time_str) - else: - msg += "\n" - for channel_id, mutes_data in self._channel_mutes.items(): - msg += f"<#{channel_id}>\n" + if mutes_data: + msg += _("__Server Mutes__\n") for user_id, mutes in mutes_data.items(): user = ctx.guild.get_member(user_id) if not user: @@ -397,15 +483,41 @@ async def activemutes(self, ctx: commands.Context): time_str = humanize_timedelta(timedelta=time_left) else: time_str = "" - msg += _("Channel Mute: {member} ").format(member=user_str) + msg += _("{member}").format(member=user_str) if time_str: - msg += _("Remaining: {time_left}\n").format(time_left=time_str) + msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) else: msg += "\n" - if msg: - for page in pagify(msg): - await ctx.maybe_send_embed(page) - return + for channel_id, mutes_data in self._channel_mutes.items(): + if not mutes_data: + to_del.append(channel_id) + continue + msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) + if channel_id in [c.id for c in ctx.guild.channels]: + for user_id, mutes in mutes_data.items(): + user = ctx.guild.get_member(user_id) + if not user: + user_str = f"<@!{user_id}>" + else: + user_str = user.mention + if mutes["until"]: + time_left = timedelta( + seconds=mutes["until"] - datetime.now(timezone.utc).timestamp() + ) + time_str = humanize_timedelta(timedelta=time_left) + else: + time_str = "" + msg += _("{member} ").format(member=user_str) + if time_str: + msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) + else: + msg += "\n" + for c in to_del: + del self._channel_mutes[c] + if msg: + for page in pagify(msg): + await ctx.maybe_send_embed(page) + return await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) @commands.command() @@ -476,12 +588,9 @@ async def mute( if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp() if until else None, - } - self._server_mutes[ctx.guild.id][user.id] = mute + self._server_mutes[ctx.guild.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: @@ -596,15 +705,10 @@ async def channel_mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - if channel.id not in self._channel_mutes: - self._channel_mutes[channel.id] = {} for user in success_list: - mute = { - "author": ctx.message.author.id, - "member": user.id, - "until": until.timestamp() if until else None, - } - self._channel_mutes[channel.id][user.id] = mute + self._channel_mutes[channel.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: @@ -657,13 +761,7 @@ async def unmute( if not success_list: resp = pagify(issue) return await ctx.send_interactive(resp) - if ctx.guild.id in self._server_mutes: - if user.id in self._server_mutes[ctx.guild.id]: - for user in success_list: - del self._server_mutes[ctx.guild.id][user.id] - await self.config.guild(ctx.guild).muted_users.set( - self._server_mutes[ctx.guild.id] - ) + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -721,15 +819,10 @@ async def unmute_channel( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - for user in success_list: - if ( - channel.id in self._channel_mutes - and user.id in self._channel_mutes[channel.id] - ): - del self._channel_mutes[channel.id][user.id] - await self.config.channel(channel).muted_users.set( - self._channel_mutes[channel.id] - ) + if self._channel_mutes[channel.id]: + await self.config.channel(channel).set(self._channel_mutes[channel.id]) + else: + await self.config.channel(channel).clear() await ctx.send( _("{users} unmuted in this channel.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -747,6 +840,7 @@ async def mute_user( Handles muting users """ mute_role = await self.config.guild(guild).mute_role() + if mute_role: try: if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): @@ -754,8 +848,22 @@ async def mute_user( role = guild.get_role(mute_role) if not role: return False, mute_unmute_issues["role_missing"] + + # This is here to prevent the modlog case from happening on role updates + # we need to update the cache early so it's there before we receive the member_update event + if guild.id not in self._server_mutes: + self._server_mutes[guild.id] = {} + + self._server_mutes[guild.id][user.id] = { + "author": author.id, + "member": user.id, + "until": None, + } await user.add_roles(role, reason=reason) except discord.errors.Forbidden: + del self._server_mutes[guild.id][ + user.id + ] # this is here so we don't have a bad cache return False, mute_unmute_issues["permissions_issue"] return True, None else: @@ -782,7 +890,9 @@ async def unmute_user( """ Handles muting users """ + mute_role = await self.config.guild(guild).mute_role() + _temp = None # used to keep the cache incase of permissions errors if mute_role: try: if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): @@ -790,8 +900,14 @@ async def unmute_user( role = guild.get_role(mute_role) if not role: return False, mute_unmute_issues["role_missing"] + if guild.id in self._server_mutes: + if user.id in self._server_mutes[guild.id]: + _temp = copy(self._server_mutes[guild.id][user.id]) + del self._server_mutes[guild.id][user.id] await user.remove_roles(role, reason=reason) except discord.errors.Forbidden: + if temp: + self._server_mutes[guild.id][user.id] = _temp return False, mute_unmute_issues["permissions_issue"] return True, None else: @@ -835,13 +951,23 @@ async def channel_mute_user( old_overs = {k: getattr(overwrites, k) for k in new_overs} overwrites.update(**new_overs) try: + if channel.id not in self._channel_mutes: + self._channel_mutes[channel.id] = {} + self._channel_mutes[channel.id][user.id] = { + "author": author.id, + "member": user.id, + "until": None, + } await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: + del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: + del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: + del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["left_guild"]) else: await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) @@ -858,6 +984,7 @@ async def channel_unmute_user( overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() + _temp = None # used to keep the cache incase we have permissions issues if channel.id in perms_cache: old_values = perms_cache[channel.id] else: @@ -874,12 +1001,18 @@ async def channel_unmute_user( ) else: await channel.set_permissions(user, overwrite=overwrites, reason=reason) + if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: + _temp = copy(self._channel_mutes[channel.id][user.id]) + del self._channel_mutes[channel.id][user.id] except discord.Forbidden: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["left_guild"]) else: await self.config.member(user).clear_raw("perms_cache", str(channel.id)) From c0d1a347543284029e51aac0fa51df9fcbd8f60b Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 8 Sep 2020 16:59:33 -0600 Subject: [PATCH 051/103] Fix keyerror in mutes command when unsuccessful mutes --- redbot/cogs/mutes/mutes.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 155de9d8d9d..68108bedc65 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -588,9 +588,10 @@ async def mute( if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} for user in success_list: - self._server_mutes[ctx.guild.id][user.id]["until"] = ( - until.timestamp() if until else None - ) + if user.id in self._server_mutes[ctx.guild.id]: + self._server_mutes[ctx.guild.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: @@ -706,9 +707,10 @@ async def channel_mute( log.error(_("Error creating modlog case"), exc_info=e) if success_list: for user in success_list: - self._channel_mutes[channel.id][user.id]["until"] = ( - until.timestamp() if until else None - ) + if user.id in self._channel_mutes[channel.id]: + self._channel_mutes[channel.id][user.id]["until"] = ( + until.timestamp() if until else None + ) await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: @@ -761,7 +763,10 @@ async def unmute( if not success_list: resp = pagify(issue) return await ctx.send_interactive(resp) - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + if self._server_mutes[ctx.guild.id]: + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + else: + await self.config.guild(ctx.guild).clear() await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) From 9edd0f6d9677194a80c1d8dc4a1fbbda9603bbde Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 8 Sep 2020 20:35:45 -0600 Subject: [PATCH 052/103] add typing indicator and improve config settings --- redbot/cogs/mutes/mutes.py | 429 +++++++++++++++++++------------------ 1 file changed, 224 insertions(+), 205 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 68108bedc65..a46fb932787 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -151,6 +151,7 @@ async def _clean_tasks(self): async def _handle_server_unmutes(self): log.debug("Checking server unmutes") + to_clear = [] for g_id, mutes in self._server_mutes.items(): to_remove = [] guild = self.bot.get_guild(g_id) @@ -167,7 +168,11 @@ async def _handle_server_unmutes(self): to_remove.append(u_id) for u_id in to_remove: del self._server_mutes[g_id][u_id] + if self._server_mutes[g_id] == {}: + to_clear.append(g_id) await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) + for g_id in to_clear: + await self.config.guild_from_id(g_id).clear() async def _auto_unmute_user(self, guild: discord.Guild, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -199,13 +204,14 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): async def _handle_channel_unmutes(self): log.debug("Checking channel unmutes") + to_clear = [] for c_id, mutes in self._channel_mutes.items(): to_remove = [] channel = self.bot.get_channel(c_id) if channel is None: continue for u_id, data in mutes.items(): - if not data["until"]: + if not data or not data["until"]: continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() if time_to_unmute < 120.0: @@ -214,7 +220,11 @@ async def _handle_channel_unmutes(self): ) for u_id in to_remove: del self._channel_mutes[c_id][u_id] + if self._channel_mutes[c_id] == {}: + to_clear.append(c_id) await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) + for c_id in to_clear: + await self.config.channel_from_id(c_id).clear() async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -409,35 +419,38 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): If you already have a muted role created on the server use `[p]muteset role ROLE_NAME_HERE` """ - perms = discord.Permissions() - perms.update(send_messages=False, speak=False, add_reactions=False) - try: - role = await ctx.guild.create_role( - name=name, permissions=perms, reason=_("Mute role setup") - ) - except discord.errors.Forbidden: - return await ctx.send(_("I could not create a muted role in this server.")) - errors = [] - for channel in ctx.guild.channels: - overs = discord.PermissionOverwrite() - if isinstance(channel, discord.TextChannel): - overs.send_messages = False - overs.add_reactions = False - if isinstance(channel, discord.VoiceChannel): - overs.speak = False + async with ctx.typing(): + perms = discord.Permissions() + perms.update(send_messages=False, speak=False, add_reactions=False) try: - await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) + role = await ctx.guild.create_role( + name=name, permissions=perms, reason=_("Mute role setup") + ) except discord.errors.Forbidden: - errors.append(f"{channel.mention}") - continue - if errors: - msg = _("I could not set overwrites for the following channels: {channels}").format( - channels=humanize_list(errors) - ) - for page in pagify(msg): - await ctx.send(page) - await self.config.guild(ctx.guild).mute_role.set(role.id) - await ctx.send(_("Mute role set to {role}").format(role=role.name)) + return await ctx.send(_("I could not create a muted role in this server.")) + errors = [] + for channel in ctx.guild.channels: + overs = discord.PermissionOverwrite() + if isinstance(channel, discord.TextChannel): + overs.send_messages = False + overs.add_reactions = False + if isinstance(channel, discord.VoiceChannel): + overs.speak = False + try: + await channel.set_permissions( + role, overwrite=overs, reason=_("Mute role setup") + ) + except discord.errors.Forbidden: + errors.append(f"{channel.mention}") + continue + if errors: + msg = _( + "I could not set overwrites for the following channels: {channels}" + ).format(channels=humanize_list(errors)) + for page in pagify(msg): + await ctx.send(page) + await self.config.guild(ctx.guild).mute_role.set(role.id) + await ctx.send(_("Mute role set to {role}").format(role=role.name)) @muteset.command(name="time") async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): @@ -548,65 +561,66 @@ async def mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) - duration = time_and_reason.get("duration", {}) - reason = time_and_reason.get("reason", None) - time = "" - until = None - if duration: - until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) - else: - default_duration = await self.config.guild(ctx.guild).default_time() - if default_duration: - until = datetime.now(timezone.utc) + timedelta(**default_duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) - author = ctx.message.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, issue = await self.mute_user(guild, author, user, audit_reason) - if success: - success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "smute", - user, - author, - reason, - until=until, - channel=None, + async with ctx.typing(): + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + time = "" + until = None + if duration: + until = datetime.now(timezone.utc) + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.now(timezone.utc) + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.mute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "smute", + user, + author, + reason, + until=until, + channel=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if not success_list and issue: + resp = pagify(issue) + return await ctx.send_interactive(resp) + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + for user in success_list: + if user.id in self._server_mutes[ctx.guild.id]: + self._server_mutes[ctx.guild.id][user.id]["until"] = ( + until.timestamp() if until else None ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if not success_list and issue: - resp = pagify(issue) - return await ctx.send_interactive(resp) - if ctx.guild.id not in self._server_mutes: - self._server_mutes[ctx.guild.id] = {} - for user in success_list: - if user.id in self._server_mutes[ctx.guild.id]: - self._server_mutes[ctx.guild.id][user.id]["until"] = ( - until.timestamp() if until else None + await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await ctx.send( + _("{users} {verb} been muted in this server{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) - verb = _("has") - if len(success_list) > 1: - verb = _("have") - await ctx.send( - _("{users} {verb} been muted in this server{time}.").format( - users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - ) - if issue: - message = _( - "{users} could not be muted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message, issue) + if issue: + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message, issue) async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions @@ -667,59 +681,60 @@ async def channel_mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) - duration = time_and_reason.get("duration", {}) - reason = time_and_reason.get("reason", None) - until = None - time = "" - if duration: - until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) - else: - default_duration = await self.config.guild(ctx.guild).default_time() - if default_duration: - until = datetime.now(timezone.utc) + timedelta(**default_duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) - author = ctx.message.author - channel = ctx.message.channel - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, issue = await self.channel_mute_user( - guild, channel, author, user, audit_reason - ) - if success: - success_list.append(user) + async with ctx.typing(): + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + until = None + time = "" + if duration: + until = datetime.now(timezone.utc) + timedelta(**duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + else: + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.now(timezone.utc) + timedelta(**default_duration) + time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + author = ctx.message.author + channel = ctx.message.channel + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.channel_mute_user( + guild, channel, author, user, audit_reason + ) + if success: + success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cmute", - user, - author, - reason, - until=until, - channel=channel, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if success_list: - for user in success_list: - if user.id in self._channel_mutes[channel.id]: - self._channel_mutes[channel.id][user.id]["until"] = ( - until.timestamp() if until else None + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "cmute", + user, + author, + reason, + until=until, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + for user in success_list: + if user.id in self._channel_mutes[channel.id]: + self._channel_mutes[channel.id][user.id]["until"] = ( + until.timestamp() if until else None + ) + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + verb = _("has") + if len(success_list) > 1: + verb = _("have") + await channel.send( + _("{users} {verb} been muted in this channel{time}.").format( + users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) - verb = _("has") - if len(success_list) > 1: - verb = _("have") - await channel.send( - _("{users} {verb} been muted in this channel{time}.").format( - users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time ) - ) @commands.command() @commands.guild_only() @@ -739,45 +754,48 @@ async def unmute( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) - guild = ctx.guild - author = ctx.author - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, issue = await self.unmute_user(guild, author, user, audit_reason) - if success: - success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "sunmute", - user, - author, - reason, - until=None, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if not success_list: - resp = pagify(issue) - return await ctx.send_interactive(resp) - if self._server_mutes[ctx.guild.id]: - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) - else: - await self.config.guild(ctx.guild).clear() - await ctx.send( - _("{users} unmuted in this server.").format( - users=humanize_list([f"{u}" for u in success_list]) + async with ctx.typing(): + guild = ctx.guild + author = ctx.author + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, issue = await self.unmute_user(guild, author, user, audit_reason) + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "sunmute", + user, + author, + reason, + until=None, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if not success_list: + resp = pagify(issue) + return await ctx.send_interactive(resp) + if self._server_mutes[ctx.guild.id]: + await self.config.guild(ctx.guild).muted_users.set( + self._server_mutes[ctx.guild.id] + ) + else: + await self.config.guild(ctx.guild).clear() + await ctx.send( + _("{users} unmuted in this server.").format( + users=humanize_list([f"{u}" for u in success_list]) + ) ) - ) - if issue: - message = _( - "{users} could not be unmuted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message, issue) + if issue: + message = _( + "{users} could not be unmuted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="unmutechannel", aliases=["channelunmute"]) @@ -797,42 +815,43 @@ async def unmute_channel( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) - channel = ctx.channel - author = ctx.author - guild = ctx.guild - audit_reason = get_audit_reason(author, reason) - success_list = [] - for user in users: - success, message = await self.channel_unmute_user( - guild, channel, author, user, audit_reason - ) + async with ctx.typing(): + channel = ctx.channel + author = ctx.author + guild = ctx.guild + audit_reason = get_audit_reason(author, reason) + success_list = [] + for user in users: + success, message = await self.channel_unmute_user( + guild, channel, author, user, audit_reason + ) - if success: - success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cunmute", - user, - author, - reason, - until=None, - channel=channel, + if success: + success_list.append(user) + try: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "cunmute", + user, + author, + reason, + until=None, + channel=channel, + ) + except RuntimeError as e: + log.error(_("Error creating modlog case"), exc_info=e) + if success_list: + if self._channel_mutes[channel.id]: + await self.config.channel(channel).set(self._channel_mutes[channel.id]) + else: + await self.config.channel(channel).clear() + await ctx.send( + _("{users} unmuted in this channel.").format( + users=humanize_list([f"{u}" for u in success_list]) ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if success_list: - if self._channel_mutes[channel.id]: - await self.config.channel(channel).set(self._channel_mutes[channel.id]) - else: - await self.config.channel(channel).clear() - await ctx.send( - _("{users} unmuted in this channel.").format( - users=humanize_list([f"{u}" for u in success_list]) ) - ) async def mute_user( self, From 8aed8e5472bec96918df91435261d9c3afcbf186 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 8 Sep 2020 20:38:42 -0600 Subject: [PATCH 053/103] flake8 issue --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index a46fb932787..2ce920ab2ce 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -930,7 +930,7 @@ async def unmute_user( del self._server_mutes[guild.id][user.id] await user.remove_roles(role, reason=reason) except discord.errors.Forbidden: - if temp: + if _temp: self._server_mutes[guild.id][user.id] = _temp return False, mute_unmute_issues["permissions_issue"] return True, None From ad7478a22052eea332b752670c8350d6a0a2b169 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 9 Sep 2020 20:40:30 -0600 Subject: [PATCH 054/103] add one time message when attempting to mute without a role set, consume rate limits across channels for overwrite mutes --- redbot/cogs/mutes/mutes.py | 150 +++++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 31 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 2ce920ab2ce..72dbe8882be 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -29,7 +29,7 @@ ), "is_admin": _("That user cannot be muted, as they have the Administrator permission."), "permissions_issue": _( - "Failed to mute user. I need the manage roles " + "Failed to mute or unmute user. I need the manage roles " "permission and the user I'm muting must be " "lower than myself in the role hierarchy." ), @@ -62,6 +62,7 @@ def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, 49615220001, force_registration=True) default_guild = { + "sent_instructions": False, "mute_role": None, "respect_hierarchy": True, "muted_users": {}, @@ -403,6 +404,9 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): """ if not role: await self.config.guild(ctx.guild).mute_role.set(None) + await self.config.guild(ctx.guild).sent_instructions(False) + # reset this to warn users next time they may have accidentally + # removed the mute role await ctx.send(_("Channel overwrites will be used for mutes instead.")) else: await self.config.guild(ctx.guild).mute_role.set(role.id) @@ -429,29 +433,38 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): except discord.errors.Forbidden: return await ctx.send(_("I could not create a muted role in this server.")) errors = [] + tasks = [] for channel in ctx.guild.channels: - overs = discord.PermissionOverwrite() - if isinstance(channel, discord.TextChannel): - overs.send_messages = False - overs.add_reactions = False - if isinstance(channel, discord.VoiceChannel): - overs.speak = False - try: - await channel.set_permissions( - role, overwrite=overs, reason=_("Mute role setup") - ) - except discord.errors.Forbidden: - errors.append(f"{channel.mention}") - continue - if errors: + tasks.append(self._set_mute_role_overwrites(role, channel)) + errors = await asyncio.gather(*tasks) + if any(errors): msg = _( "I could not set overwrites for the following channels: {channels}" - ).format(channels=humanize_list(errors)) + ).format(channels=humanize_list([i for i in errors if i])) for page in pagify(msg): await ctx.send(page) await self.config.guild(ctx.guild).mute_role.set(role.id) await ctx.send(_("Mute role set to {role}").format(role=role.name)) + async def _set_mute_role_overwrites( + self, role: discord.Role, channel: discord.abc.GuildChannel + ) -> Optional[str]: + """ + This sets the supplied role and channel overwrites to what we want + by default for a mute role + """ + overs = discord.PermissionOverwrite() + if isinstance(channel, discord.TextChannel): + overs.send_messages = False + overs.add_reactions = False + if isinstance(channel, discord.VoiceChannel): + overs.speak = False + try: + await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) + return + except discord.errors.Forbidden: + return channel.mention + @muteset.command(name="time") async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): """ @@ -469,6 +482,66 @@ async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): ) ) + async def _check_for_mute_role(self, ctx: commands.Context) -> bool: + """ + This explains to the user whether or not mutes are setup correctly for + automatic unmutes. + """ + mute_role_id = await self.config.guild(ctx.guild).mute_role() + mute_role = ctx.guild.get_role(mute_role_id) + sent_instructions = await self.config.guild(ctx.guild).sent_instructions() + if mute_role or sent_instructions: + return True + else: + msg = _( + "This server does not have a mute role setup, " + "are you sure you want to continue with channel " + "overwrites? (Note: Overwrites will not be automatically unmuted." + " You can setup a mute role with `{prefix}muteset role` or " + "`{prefix}muteset makerole` if you just want a basic setup.)\n\n" + ).format(prefix=ctx.clean_prefix) + can_react = ctx.channel.permissions_for(ctx.me).add_reactions + if can_react: + msg += _( + "Reacting with \N{WHITE HEAVY CHECK MARK} will continue " + "the mute with overwrites and stop this message from appearing again, " + "Reacting with \N{CROSS MARK} will end the mute attempt." + ) + else: + msg += _( + "Saying `yes` will continue " + "the mute with overwrites and stop this message from appearing again, " + "saying `no` will end the mute attempt." + ) + query: discord.Message = await ctx.send(msg) + if can_react: + # noinspection PyAsyncCall + start_adding_reactions(query, ReactionPredicate.YES_OR_NO_EMOJIS) + pred = ReactionPredicate.yes_or_no(query, ctx.author) + event = "reaction_add" + else: + pred = MessagePredicate.yes_or_no(ctx) + event = "message" + try: + await ctx.bot.wait_for(event, check=pred, timeout=30) + except asyncio.TimeoutError: + await query.delete() + return False + + if not pred.result: + if can_react: + await query.delete() + else: + await ctx.send(_("OK then.")) + + return False + else: + if can_react: + with contextlib.suppress(discord.Forbidden): + await query.clear_reactions() + await self.config.guild(ctx.guild).sent_instructions.set(True) + return True + @commands.command() @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) @@ -505,8 +578,8 @@ async def activemutes(self, ctx: commands.Context): if not mutes_data: to_del.append(channel_id) continue - msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) if channel_id in [c.id for c in ctx.guild.channels]: + msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) for user_id, mutes in mutes_data.items(): user = ctx.guild.get_member(user_id) if not user: @@ -561,6 +634,8 @@ async def mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) @@ -681,6 +756,8 @@ async def channel_mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) @@ -754,6 +831,8 @@ async def unmute( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): guild = ctx.guild author = ctx.author @@ -815,6 +894,8 @@ async def unmute_channel( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) + if not await self._check_for_mute_role(ctx): + return async with ctx.typing(): channel = ctx.channel author = ctx.author @@ -892,11 +973,18 @@ async def mute_user( return True, None else: mute_success = [] + perms_cache = {} + tasks = [] for channel in guild.channels: - success, issue = await self.channel_mute_user(guild, channel, author, user, reason) + tasks.append(self.channel_mute_user(guild, channel, author, user, reason)) + task_result = await asyncio.gather(*tasks) + for success, issue in task_result: if not success: mute_success.append(f"{channel.mention} - {issue}") - await asyncio.sleep(0.1) + else: + chan_id = next(iter(issue)) + perms_cache[str(chan_id)] = issue[chan_id] + await self.config.member(user).perms_cache.set(perms_cache) if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) elif mute_success and len(mute_success) != len(guild.channels): @@ -936,13 +1024,14 @@ async def unmute_user( return True, None else: mute_success = [] + tasks = [] for channel in guild.channels: - success, issue = await self.channel_unmute_user( - guild, channel, author, user, reason - ) + tasks.append(self.channel_unmute_user(guild, channel, author, user, reason)) + results = await asyncio.gather(*tasks) + for success, issue in results: if not success: mute_success.append(f"{channel.mention} - {issue}") - await asyncio.sleep(0.1) + await self.config.member(user).clear() if mute_success: return False, "\n".join(s for s in mute_success) else: @@ -993,9 +1082,7 @@ async def channel_mute_user( elif e.code == 10009: del self._channel_mutes[channel.id][user.id] return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).set_raw("perms_cache", str(channel.id), value=old_overs) - return True, None + return True, {str(channel.id): old_overs} async def channel_unmute_user( self, @@ -1029,15 +1116,16 @@ async def channel_unmute_user( _temp = copy(self._channel_mutes[channel.id][user.id]) del self._channel_mutes[channel.id][user.id] except discord.Forbidden: - self._channel_mutes[channel.id][user.id] = _temp + if channel.id in self._channel_mutes: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: - self._channel_mutes[channel.id][user.id] = _temp + if channel.id in self._channel_mutes: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: - self._channel_mutes[channel.id][user.id] = _temp + if channel.id in self._channel_mutes: + self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["left_guild"]) - else: - await self.config.member(user).clear_raw("perms_cache", str(channel.id)) return True, None From cd036acaa15327841dc61508ba236f41c9ef5f5c Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 11 Sep 2020 23:07:04 -0600 Subject: [PATCH 055/103] Don't clear the whole guilds settings when a mute is finished. Optimize server mutes to better handle migration to API method later. Fix typehints. --- redbot/cogs/mutes/mutes.py | 82 ++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 72dbe8882be..e920f327fb1 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -5,7 +5,7 @@ from abc import ABC from copy import copy -from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine +from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine, Union from datetime import datetime, timedelta, timezone from .converters import MuteTime @@ -173,7 +173,7 @@ async def _handle_server_unmutes(self): to_clear.append(g_id) await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) for g_id in to_clear: - await self.config.guild_from_id(g_id).clear() + await self.config.guild_from_id(g_id).muted_users.clear() async def _auto_unmute_user(self, guild: discord.Guild, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -225,7 +225,7 @@ async def _handle_channel_unmutes(self): to_clear.append(c_id) await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) for c_id in to_clear: - await self.config.channel_from_id(c_id).clear() + await self.config.channel_from_id(c_id).muted_users.clear() async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -505,7 +505,7 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: msg += _( "Reacting with \N{WHITE HEAVY CHECK MARK} will continue " "the mute with overwrites and stop this message from appearing again, " - "Reacting with \N{CROSS MARK} will end the mute attempt." + "Reacting with \N{NEGATIVE SQUARED CROSS MARK} will end the mute attempt." ) else: msg += _( @@ -554,9 +554,14 @@ async def activemutes(self, ctx: commands.Context): to_del = [] if ctx.guild.id in self._server_mutes: mutes_data = self._server_mutes[ctx.guild.id] + to_rem = [] if mutes_data: + msg += _("__Server Mutes__\n") for user_id, mutes in mutes_data.items(): + if not mutes: + to_rem.append(user_id) + continue user = ctx.guild.get_member(user_id) if not user: user_str = f"<@!{user_id}>" @@ -574,6 +579,11 @@ async def activemutes(self, ctx: commands.Context): msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) else: msg += "\n" + for _id in to_rem: + try: + del self._server_mutes[ctx.guild.id][_id] + except KeyError: + pass for channel_id, mutes_data in self._channel_mutes.items(): if not mutes_data: to_del.append(channel_id) @@ -581,6 +591,8 @@ async def activemutes(self, ctx: commands.Context): if channel_id in [c.id for c in ctx.guild.channels]: msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) for user_id, mutes in mutes_data.items(): + if not mutes: + continue user = ctx.guild.get_member(user_id) if not user: user_str = f"<@!{user_id}>" @@ -654,7 +666,7 @@ async def mute( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, issue = await self.mute_user(guild, author, user, audit_reason) + success, issue = await self.mute_user(guild, author, user, until, audit_reason) if success: success_list.append(user) try: @@ -676,12 +688,6 @@ async def mute( return await ctx.send_interactive(resp) if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} - for user in success_list: - if user.id in self._server_mutes[ctx.guild.id]: - self._server_mutes[ctx.guild.id][user.id]["until"] = ( - until.timestamp() if until else None - ) - await self.config.guild(ctx.guild).muted_users.set(self._server_mutes[ctx.guild.id]) verb = _("has") if len(success_list) > 1: verb = _("have") @@ -778,7 +784,7 @@ async def channel_mute( success_list = [] for user in users: success, issue = await self.channel_mute_user( - guild, channel, author, user, audit_reason + guild, channel, author, user, until, audit_reason ) if success: success_list.append(user) @@ -798,12 +804,6 @@ async def channel_mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if success_list: - for user in success_list: - if user.id in self._channel_mutes[channel.id]: - self._channel_mutes[channel.id][user.id]["until"] = ( - until.timestamp() if until else None - ) - await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) verb = _("has") if len(success_list) > 1: verb = _("have") @@ -818,7 +818,11 @@ async def channel_mute( @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) async def unmute( - self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + reason: Optional[str] = None, ): """Unmute users. @@ -858,12 +862,13 @@ async def unmute( if not success_list: resp = pagify(issue) return await ctx.send_interactive(resp) - if self._server_mutes[ctx.guild.id]: + + if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: await self.config.guild(ctx.guild).muted_users.set( self._server_mutes[ctx.guild.id] ) else: - await self.config.guild(ctx.guild).clear() + await self.config.guild(ctx.guild).muted_users.clear() await ctx.send( _("{users} unmuted in this server.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -881,7 +886,11 @@ async def unmute( @commands.bot_has_permissions(manage_roles=True) @commands.guild_only() async def unmute_channel( - self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: str = None + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + reason: Optional[str] = None, ): """Unmute a user in this channel. @@ -927,7 +936,7 @@ async def unmute_channel( if self._channel_mutes[channel.id]: await self.config.channel(channel).set(self._channel_mutes[channel.id]) else: - await self.config.channel(channel).clear() + await self.config.channel(channel).muted_users.clear() await ctx.send( _("{users} unmuted in this channel.").format( users=humanize_list([f"{u}" for u in success_list]) @@ -939,7 +948,8 @@ async def mute_user( guild: discord.Guild, author: discord.Member, user: discord.Member, - reason: str, + until: Optional[datetime] = None, + reason: Optional[str] = None, ) -> Tuple[bool, Optional[str]]: """ Handles muting users @@ -962,9 +972,10 @@ async def mute_user( self._server_mutes[guild.id][user.id] = { "author": author.id, "member": user.id, - "until": None, + "until": until.timestamp() if until else None, } await user.add_roles(role, reason=reason) + await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) except discord.errors.Forbidden: del self._server_mutes[guild.id][ user.id @@ -976,7 +987,7 @@ async def mute_user( perms_cache = {} tasks = [] for channel in guild.channels: - tasks.append(self.channel_mute_user(guild, channel, author, user, reason)) + tasks.append(self.channel_mute_user(guild, channel, author, user, until, reason)) task_result = await asyncio.gather(*tasks) for success, issue in task_result: if not success: @@ -997,7 +1008,7 @@ async def unmute_user( guild: discord.Guild, author: discord.Member, user: discord.Member, - reason: str, + reason: Optional[str] = None, ) -> Tuple[bool, Optional[str]]: """ Handles muting users @@ -1043,8 +1054,9 @@ async def channel_mute_user( channel: discord.abc.GuildChannel, author: discord.Member, user: discord.Member, - reason: str, - ) -> Tuple[bool, Optional[str]]: + until: Optional[datetime] = None, + reason: Optional[str] = None, + ) -> Tuple[bool, Union[str, Dict[str, Optional[bool]]]]: """Mutes the specified user in the specified channel""" overwrites = channel.overwrites_for(user) permissions = channel.permissions_for(user) @@ -1052,7 +1064,7 @@ async def channel_mute_user( if permissions.administrator: return False, _(mute_unmute_issues["is_admin"]) - new_overs = {} + new_overs: dict = {} if not isinstance(channel, discord.TextChannel): new_overs.update(speak=False) if not isinstance(channel, discord.VoiceChannel): @@ -1069,7 +1081,7 @@ async def channel_mute_user( self._channel_mutes[channel.id][user.id] = { "author": author.id, "member": user.id, - "until": None, + "until": until.timestamp() if until else None, } await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: @@ -1090,7 +1102,7 @@ async def channel_unmute_user( channel: discord.abc.GuildChannel, author: discord.Member, user: discord.Member, - reason: str, + reason: Optional[str] = None, ) -> Tuple[bool, Optional[str]]: overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() @@ -1116,16 +1128,16 @@ async def channel_unmute_user( _temp = copy(self._channel_mutes[channel.id][user.id]) del self._channel_mutes[channel.id][user.id] except discord.Forbidden: - if channel.id in self._channel_mutes: + if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["permissions_issue"]) except discord.NotFound as e: if e.code == 10003: - if channel.id in self._channel_mutes: + if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["unknown_channel"]) elif e.code == 10009: - if channel.id in self._channel_mutes: + if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp return False, _(mute_unmute_issues["left_guild"]) return True, None From 347035d05755c836972d16b57d88b3b2e21549fc Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 13 Sep 2020 04:54:40 -0600 Subject: [PATCH 056/103] Utilize usage to make converter make more sense --- redbot/cogs/mutes/mutes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index e920f327fb1..0bb18347c3c 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -618,7 +618,7 @@ async def activemutes(self, ctx: commands.Context): return await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) - @commands.command() + @commands.command(usage="[users...] [time_and_reason]") @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) async def mute( @@ -734,7 +734,9 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - await query.clear_reactions() await ctx.send(issue) - @commands.command(name="mutechannel", aliases=["channelmute"]) + @commands.command( + name="mutechannel", aliases=["channelmute"], usage="[users...] [time_and_reason]" + ) @commands.guild_only() @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) From a74d324145ae53aa58e0d330e9dcfdeba95bdd6f Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 13 Sep 2020 05:00:17 -0600 Subject: [PATCH 057/103] remove decorator permission checks and fix doc strings --- redbot/cogs/mutes/mutes.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 0bb18347c3c..098ebf19fa5 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -630,8 +630,8 @@ async def mute( ): """Mute users. - `[users]...` is a space separated list of usernames, ID's, or mentions. - `[time_and_reason={}]` is the time to mute for and reason. Time is + `[users...]` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason]` is the time to mute for and reason. Time is any valid time length such as `30 minutes` or `2 days`. If nothing is provided the mute will be indefinite. @@ -738,7 +738,6 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - name="mutechannel", aliases=["channelmute"], usage="[users...] [time_and_reason]" ) @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) async def channel_mute( self, @@ -749,8 +748,8 @@ async def channel_mute( ): """Mute a user in the current text channel. - `[users]...` is a space separated list of usernames, ID's, or mentions. - `[time_and_reason={}]` is the time to mute for and reason. Time is + `[users...]` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason]` is the time to mute for and reason. Time is any valid time length such as `30 minutes` or `2 days`. If nothing is provided the mute will be indefinite. @@ -815,9 +814,8 @@ async def channel_mute( ) ) - @commands.command() + @commands.command(usage="[users...] [reason]") @commands.guild_only() - @commands.bot_has_permissions(manage_roles=True) @checks.mod_or_permissions(manage_roles=True) async def unmute( self, @@ -828,7 +826,7 @@ async def unmute( ): """Unmute users. - `[users]...` is a space separated list of usernames, ID's, or mentions. + `[users...]` is a space separated list of usernames, ID's, or mentions. `[reason]` is the reason for the unmute. """ if not users: @@ -884,8 +882,7 @@ async def unmute( await self.handle_issues(ctx, message, issue) @checks.mod_or_permissions(manage_roles=True) - @commands.command(name="unmutechannel", aliases=["channelunmute"]) - @commands.bot_has_permissions(manage_roles=True) + @commands.command(name="unmutechannel", aliases=["channelunmute"], usage="[users...] [reason]") @commands.guild_only() async def unmute_channel( self, @@ -896,7 +893,7 @@ async def unmute_channel( ): """Unmute a user in this channel. - `[users]...` is a space separated list of usernames, ID's, or mentions. + `[users...]` is a space separated list of usernames, ID's, or mentions. `[reason]` is the reason for the unmute. """ if not users: From bb215b1cb2f68ca66b569739080cd4cbbea3a2d2 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 13 Sep 2020 16:30:55 -0600 Subject: [PATCH 058/103] handle role changes better --- redbot/cogs/mutes/mutes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 098ebf19fa5..91d0510781e 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -273,6 +273,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): roles_added = list(a - b) if mute_role in roles_removed: # send modlog case for unmute and remove from cache + if guild.id not in self._server_mutes: + # they weren't a tracked mute so we can return early + return if after.id in self._server_mutes[guild.id]: try: await modlog.create_case( @@ -289,6 +292,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): del self._server_mutes[guild.id][after.id] if mute_role in roles_added: # send modlog case for mute and add to cache + if guild.id not in self._server_mutes: + # initialize the guild in the cache to prevent keyerrors + self._server_mutes[guild.id] = {} if after.id not in self._server_mutes[guild.id]: try: await modlog.create_case( @@ -307,7 +313,8 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): "member": after.id, "until": None, } - await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) + if guild.id in self._server_mutes: + await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) @commands.Cog.listener() async def on_guild_channel_update( From 1dbf3c0fb991d75872f833205216c2b3417067d2 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 14 Sep 2020 23:29:50 -0600 Subject: [PATCH 059/103] More sanely handle channel mutes return and improve failed mutes dialogue. Re-enable task cleaner. Reduce wait time to improve resolution of mute time. --- redbot/cogs/mutes/mutes.py | 129 ++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 39 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 91d0510781e..309f1f2eddf 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -120,7 +120,7 @@ async def _handle_automatic_unmute(self): await self.bot.wait_until_red_ready() await self._ready.wait() while True: - # await self._clean_tasks() + await self._clean_tasks() try: await self._handle_server_unmutes() except Exception: @@ -130,7 +130,7 @@ async def _handle_automatic_unmute(self): await self._handle_channel_unmutes() except Exception: log.error("error checking channel unmutes", exc_info=True) - await asyncio.sleep(120) + await asyncio.sleep(30) async def _clean_tasks(self): log.debug("Cleaning unmute tasks") @@ -162,7 +162,7 @@ async def _handle_server_unmutes(self): if data["until"] is None: continue time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() - if time_to_unmute < 120.0: + if time_to_unmute < 60: self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( self._auto_unmute_user(guild, data) ) @@ -183,7 +183,7 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): try: member = guild.get_member(data["member"]) author = guild.get_member(data["author"]) - if not member or not author: + if not member: return success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) if success: @@ -237,10 +237,10 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di author = channel.guild.get_member(data["author"]) if not member or not author: return - success, message = await self.channel_unmute_user( + success = await self.channel_unmute_user( channel.guild, channel, author, member, _("Automatic unmute") ) - if success: + if success["success"]: try: await modlog.create_case( self.bot, @@ -321,7 +321,7 @@ async def on_guild_channel_update( self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel ): """ - This handles manually removing + This handles manually removing overwrites for a user that has been muted """ if after.id in self._channel_mutes: before_perms: Dict[int, Dict[str, Optional[bool]]] = { @@ -330,7 +330,7 @@ async def on_guild_channel_update( after_perms: Dict[int, Dict[str, Optional[bool]]] = { o.id: {name: attr for name, attr in p} for o, p in after.overwrites.items() } - to_del: int = [] + to_del: List[int] = [] for user_id in self._channel_mutes[after.id].keys(): if user_id in before_perms and ( user_id not in after_perms or after_perms[user_id]["send_messages"] @@ -691,8 +691,11 @@ async def mute( except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) if not success_list and issue: - resp = pagify(issue) - return await ctx.send_interactive(resp) + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + return await self.handle_issues(ctx, message, issue) if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} verb = _("has") @@ -739,7 +742,8 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - if can_react: with contextlib.suppress(discord.Forbidden): await query.clear_reactions() - await ctx.send(issue) + resp = pagify(issue) + await ctx.send_interactive(resp) @commands.command( name="mutechannel", aliases=["channelmute"], usage="[users...] [time_and_reason]" @@ -791,10 +795,10 @@ async def channel_mute( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, issue = await self.channel_mute_user( + success = await self.channel_mute_user( guild, channel, author, user, until, audit_reason ) - if success: + if success["success"]: success_list.append(user) try: @@ -811,6 +815,8 @@ async def channel_mute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) + else: + return await ctx.send(success["reason"]) if success_list: verb = _("has") if len(success_list) > 1: @@ -866,9 +872,12 @@ async def unmute( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) - if not success_list: - resp = pagify(issue) - return await ctx.send_interactive(resp) + if not success_list and issue: + message = _( + "{users} could not be muted in some channels. " + "Would you like to see which channels and why?" + ).format(users=humanize_list([f"{u}" for u in users])) + return await self.handle_issues(ctx, message, issue) if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: await self.config.guild(ctx.guild).muted_users.set( @@ -918,11 +927,11 @@ async def unmute_channel( audit_reason = get_audit_reason(author, reason) success_list = [] for user in users: - success, message = await self.channel_unmute_user( + success = await self.channel_unmute_user( guild, channel, author, user, audit_reason ) - if success: + if success["success"]: success_list.append(user) try: await modlog.create_case( @@ -938,6 +947,8 @@ async def unmute_channel( ) except RuntimeError as e: log.error(_("Error creating modlog case"), exc_info=e) + else: + return await ctx.send(success["reason"]) if success_list: if self._channel_mutes[channel.id]: await self.config.channel(channel).set(self._channel_mutes[channel.id]) @@ -995,12 +1006,14 @@ async def mute_user( for channel in guild.channels: tasks.append(self.channel_mute_user(guild, channel, author, user, until, reason)) task_result = await asyncio.gather(*tasks) - for success, issue in task_result: - if not success: - mute_success.append(f"{channel.mention} - {issue}") + for task in task_result: + if not task["success"]: + chan = task["channel"].mention + issue = task["issue"] + mute_success.append(f"{chan} - {issue}") else: - chan_id = next(iter(issue)) - perms_cache[str(chan_id)] = issue[chan_id] + chan_id = task["channel"].id + perms_cache[str(chan_id)] = issue.get("old_overs") await self.config.member(user).perms_cache.set(perms_cache) if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) @@ -1045,9 +1058,11 @@ async def unmute_user( for channel in guild.channels: tasks.append(self.channel_unmute_user(guild, channel, author, user, reason)) results = await asyncio.gather(*tasks) - for success, issue in results: - if not success: - mute_success.append(f"{channel.mention} - {issue}") + for task in results: + if not task["success"]: + chan = task["channel"].mention + issue = task["issue"] + mute_success.append(f"{chan} - {issue}") await self.config.member(user).clear() if mute_success: return False, "\n".join(s for s in mute_success) @@ -1062,13 +1077,17 @@ async def channel_mute_user( user: discord.Member, until: Optional[datetime] = None, reason: Optional[str] = None, - ) -> Tuple[bool, Union[str, Dict[str, Optional[bool]]]]: + ) -> Dict[str, Optional[Union[discord.abc.GuildChannel, str, bool]]]: """Mutes the specified user in the specified channel""" overwrites = channel.overwrites_for(user) permissions = channel.permissions_for(user) if permissions.administrator: - return False, _(mute_unmute_issues["is_admin"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["is_admin"]), + } new_overs: dict = {} if not isinstance(channel, discord.TextChannel): @@ -1077,7 +1096,11 @@ async def channel_mute_user( new_overs.update(send_messages=False, add_reactions=False) if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["hierarchy_problem"]), + } old_overs = {k: getattr(overwrites, k) for k in new_overs} overwrites.update(**new_overs) @@ -1092,15 +1115,27 @@ async def channel_mute_user( await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: del self._channel_mutes[channel.id][user.id] - return False, _(mute_unmute_issues["permissions_issue"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["permissions_issue"]), + } except discord.NotFound as e: if e.code == 10003: del self._channel_mutes[channel.id][user.id] - return False, _(mute_unmute_issues["unknown_channel"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["unknown_channel"]), + } elif e.code == 10009: del self._channel_mutes[channel.id][user.id] - return False, _(mute_unmute_issues["left_guild"]) - return True, {str(channel.id): old_overs} + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["left_guild"]), + } + return {"success": True, "channel": channel, "old_overs": old_overs} async def channel_unmute_user( self, @@ -1109,7 +1144,7 @@ async def channel_unmute_user( author: discord.Member, user: discord.Member, reason: Optional[str] = None, - ) -> Tuple[bool, Optional[str]]: + ) -> Dict[str, Optional[Union[discord.abc.GuildChannel, str, bool]]]: overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() @@ -1120,7 +1155,11 @@ async def channel_unmute_user( old_values = {"send_messages": None, "add_reactions": None, "speak": None} if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["hierarchy_problem"]), + } overwrites.update(**old_values) try: @@ -1136,14 +1175,26 @@ async def channel_unmute_user( except discord.Forbidden: if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp - return False, _(mute_unmute_issues["permissions_issue"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["permissions_issue"]), + } except discord.NotFound as e: if e.code == 10003: if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp - return False, _(mute_unmute_issues["unknown_channel"]) + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["unknown_channel"]), + } elif e.code == 10009: if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp - return False, _(mute_unmute_issues["left_guild"]) - return True, None + return { + "success": False, + "channel": channel, + "reason": _(mute_unmute_issues["left_guild"]), + } + return {"success": True, "channel": channel, "reason": None} From 89f8a5d6ad04df449684031fd7c2661321bf7349 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 15 Sep 2020 00:44:20 -0600 Subject: [PATCH 060/103] Handle re-mute on leave properly --- redbot/cogs/mutes/mutes.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 309f1f2eddf..af8c804456d 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -67,7 +67,7 @@ def __init__(self, bot: Red): "respect_hierarchy": True, "muted_users": {}, "default_time": {}, - "removed_users": [], + "removed_users": {}, } self.config.register_guild(**default_guild) self.config.register_member(perms_cache={}) @@ -363,8 +363,10 @@ async def on_guild_channel_update( @commands.Cog.listener() async def on_member_remove(self, member: discord.Member): guild = member.guild + until = None if guild.id in self._server_mutes: if member.id in self._server_mutes[guild.id]: + until = self._server_mutes[guild.id][member.id]["until"] del self._server_mutes[guild.id][member.id] for channel in guild.channels: if channel.id in self._channel_mutes: @@ -375,7 +377,7 @@ async def on_member_remove(self, member: discord.Member): return if mute_role in [r.id for r in member.roles]: async with self.config.guild(guild).removed_users() as removed_users: - removed_users.append(member.id) + removed_users[str(member.id)] = int(until) if until else None @commands.Cog.listener() async def on_member_join(self, member: discord.Member): @@ -384,15 +386,19 @@ async def on_member_join(self, member: discord.Member): if not mute_role: return async with self.config.guild(guild).removed_users() as removed_users: - if member.id in removed_users: - removed_users.remove(member.id) + if str(member.id) in removed_users: + until_ts = removed_users[str(member.id)] + until = datetime.fromtimestamp(until_ts, tz=timezone.utc) if until_ts else None + # datetime is required to utilize the mutes method + if until and until < datetime.now(tz=timezone.utc): + return + removed_users.pop(str(member.id)) role = guild.get_role(mute_role) if not role: return - try: - await member.add_roles(role, reason=_("Previously muted in this server.")) - except discord.errors.Forbidden: - return + await self.mute_user( + guild, guild.me, member, until, _("Previously muted in this server.") + ) @commands.group() @commands.guild_only() From bb8ae079f9aa39cd31534667ff56273b037beba4 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 16 Sep 2020 12:49:51 -0600 Subject: [PATCH 061/103] fix unbound error in overwrites mute --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index af8c804456d..eb60760ca48 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1019,7 +1019,7 @@ async def mute_user( mute_success.append(f"{chan} - {issue}") else: chan_id = task["channel"].id - perms_cache[str(chan_id)] = issue.get("old_overs") + perms_cache[str(chan_id)] = task.get("old_overs") await self.config.member(user).perms_cache.set(perms_cache) if mute_success and len(mute_success) == len(guild.channels): return False, "\n".join(s for s in mute_success) From 80d77ccc55f984f32ec7d8b986fee243d131c2dd Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 4 Oct 2020 00:30:30 -0600 Subject: [PATCH 062/103] remove mutes.pt --- redbot/cogs/mod/mutes.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 redbot/cogs/mod/mutes.py diff --git a/redbot/cogs/mod/mutes.py b/redbot/cogs/mod/mutes.py deleted file mode 100644 index 8b137891791..00000000000 --- a/redbot/cogs/mod/mutes.py +++ /dev/null @@ -1 +0,0 @@ - From e6d2a1b02b4961c64e32350e7e848a5ee6259e5d Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 4 Oct 2020 00:42:50 -0600 Subject: [PATCH 063/103] remove reliance on mods is_allowed_by_hierarchy since we don't have a setting to control that anyways inside this. --- redbot/cogs/mutes/mutes.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index eb60760ca48..54c17f7d3e6 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -14,7 +14,7 @@ from redbot.core.bot import Red from redbot.core import commands, checks, i18n, modlog, Config from redbot.core.utils.chat_formatting import humanize_timedelta, humanize_list, pagify -from redbot.core.utils.mod import get_audit_reason, is_allowed_by_hierarchy +from redbot.core.utils.mod import get_audit_reason from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate @@ -116,6 +116,10 @@ def cog_unload(self): for task in self._unmute_tasks.values(): task.cancel() + async def is_allowed_by_hierarchy(self, guild: discord.Guild, mod: discord.Member, user: discord.Member): + is_special = mod == guild.owner or await self.bot.is_owner(mod) + return mod.top_role.position > user.top_role or is_special + async def _handle_automatic_unmute(self): await self.bot.wait_until_red_ready() await self._ready.wait() @@ -981,7 +985,7 @@ async def mute_user( if mute_role: try: - if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await self.is_allowed_by_hierarchy(guild, author, user): return False, _(mute_unmute_issues["hierarchy_problem"]) role = guild.get_role(mute_role) if not role: @@ -1043,7 +1047,7 @@ async def unmute_user( _temp = None # used to keep the cache incase of permissions errors if mute_role: try: - if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await self.is_allowed_by_hierarchy(guild, author, user): return False, _(mute_unmute_issues["hierarchy_problem"]) role = guild.get_role(mute_role) if not role: @@ -1101,7 +1105,7 @@ async def channel_mute_user( if not isinstance(channel, discord.VoiceChannel): new_overs.update(send_messages=False, add_reactions=False) - if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await self.is_allowed_by_hierarchy(guild, author, user): return { "success": False, "channel": channel, @@ -1160,7 +1164,7 @@ async def channel_unmute_user( else: old_values = {"send_messages": None, "add_reactions": None, "speak": None} - if not await is_allowed_by_hierarchy(self.bot, self.config, guild, author, user): + if not await self.is_allowed_by_hierarchy(guild, author, user): return { "success": False, "channel": channel, From 044aeff3812beaab1485c996162536e8105bc340 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 4 Oct 2020 00:44:03 -0600 Subject: [PATCH 064/103] black --- redbot/cogs/mutes/mutes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 54c17f7d3e6..1384ddfecf5 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -116,7 +116,9 @@ def cog_unload(self): for task in self._unmute_tasks.values(): task.cancel() - async def is_allowed_by_hierarchy(self, guild: discord.Guild, mod: discord.Member, user: discord.Member): + async def is_allowed_by_hierarchy( + self, guild: discord.Guild, mod: discord.Member, user: discord.Member + ): is_special = mod == guild.owner or await self.bot.is_owner(mod) return mod.top_role.position > user.top_role or is_special From eda86593706ff3a183b3d353037299c8341243e2 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 4 Oct 2020 17:39:07 -0600 Subject: [PATCH 065/103] fix hierarchy check --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 1384ddfecf5..7f18ec5c333 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -120,7 +120,7 @@ async def is_allowed_by_hierarchy( self, guild: discord.Guild, mod: discord.Member, user: discord.Member ): is_special = mod == guild.owner or await self.bot.is_owner(mod) - return mod.top_role.position > user.top_role or is_special + return mod.top_role.position > user.top_role.position or is_special async def _handle_automatic_unmute(self): await self.bot.wait_until_red_ready() From cf776b172fbb979468712986f327b93f64438e34 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 9 Oct 2020 14:18:51 -0600 Subject: [PATCH 066/103] wtf --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 7f18ec5c333..cbdb5bc1294 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -144,7 +144,7 @@ async def _clean_tasks(self): for task_id in list(self._unmute_tasks.keys()): task = self._unmute_tasks[task_id] - if task.canceled(): + if task.cancelled(): self._unmute_tasks.pop(task_id, None) continue From 83dba8a397f719c6d0f17c2838e2bcdf28d441ea Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 14 Oct 2020 07:41:53 -0600 Subject: [PATCH 067/103] Cache mute roles for large bots --- redbot/cogs/mutes/mutes.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index cbdb5bc1294..f9a3011577c 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -79,6 +79,7 @@ def __init__(self, bot: Red): self._unmute_tasks: Dict[str, Coroutine] = {} self._unmute_task = asyncio.create_task(self._handle_automatic_unmute()) # dict of guild id, member id and time to be unmuted + self.mute_role_cache = {} async def red_delete_data_for_user( self, @@ -99,6 +100,8 @@ async def initialize(self): guild_data = await self.config.all_guilds() for g_id, mutes in guild_data.items(): self._server_mutes[g_id] = {} + if mutes["mute_role"]: + self.mute_role_cache[g_id] = mutes["mute_role"] for user_id, mute in mutes["muted_users"].items(): self._server_mutes[g_id][int(user_id)] = mute channel_data = await self.config.all_channels() @@ -269,7 +272,9 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): Used to handle the cache if a member manually has the muted role removed """ guild = before.guild - mute_role_id = await self.config.guild(before.guild).mute_role() + if guild.id not in self.mute_role_cache: + return + mute_role_id = self.mute_role_cache[guild.id] mute_role = guild.get_role(mute_role_id) if not mute_role: return @@ -423,12 +428,14 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): """ if not role: await self.config.guild(ctx.guild).mute_role.set(None) - await self.config.guild(ctx.guild).sent_instructions(False) + del self.mute_role_cache[ctx.guild.id] + await self.config.guild(ctx.guild).sent_instructions.set(False) # reset this to warn users next time they may have accidentally # removed the mute role await ctx.send(_("Channel overwrites will be used for mutes instead.")) else: await self.config.guild(ctx.guild).mute_role.set(role.id) + self.mute_role_cache[guild.id] = role.id await ctx.send(_("Mute role set to {role}").format(role=role.name)) @muteset.command(name="makerole") From 67bd678df68c565d011fc9268d87d0adf483bb9c Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 14 Oct 2020 09:20:07 -0600 Subject: [PATCH 068/103] fix lint --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f9a3011577c..e936aabee2a 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -435,7 +435,7 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): await ctx.send(_("Channel overwrites will be used for mutes instead.")) else: await self.config.guild(ctx.guild).mute_role.set(role.id) - self.mute_role_cache[guild.id] = role.id + self.mute_role_cache[ctx.guild.id] = role.id await ctx.send(_("Mute role set to {role}").format(role=role.name)) @muteset.command(name="makerole") From a8fce002b30fe4807abdd154eb45a079fe39d289 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Thu, 15 Oct 2020 20:20:05 -0600 Subject: [PATCH 069/103] fix this error --- redbot/cogs/mutes/mutes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index e936aabee2a..cfb15b74aed 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1028,7 +1028,7 @@ async def mute_user( for task in task_result: if not task["success"]: chan = task["channel"].mention - issue = task["issue"] + issue = task["reason"] mute_success.append(f"{chan} - {issue}") else: chan_id = task["channel"].id @@ -1080,7 +1080,7 @@ async def unmute_user( for task in results: if not task["success"]: chan = task["channel"].mention - issue = task["issue"] + issue = task["reason"] mute_success.append(f"{chan} - {issue}") await self.config.member(user).clear() if mute_success: From b0d48ab78ec1628a4097d74d960e5b5510ed1c1f Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 18 Oct 2020 23:24:45 -0600 Subject: [PATCH 070/103] Address review 1 --- .github/CODEOWNERS | 1 + redbot/cogs/mod/abc.py | 7 + redbot/cogs/mod/kickban.py | 50 ++- redbot/cogs/mutes/__init__.py | 1 + redbot/cogs/mutes/converters.py | 4 + redbot/cogs/mutes/mutes.py | 663 ++++++++++++++++---------------- redbot/cogs/mutes/voicemutes.py | 126 +++--- 7 files changed, 460 insertions(+), 392 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ca3e04e921e..3363f7758a2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,7 @@ /redbot/cogs/audio/** @aikaterna @Drapersniper /redbot/cogs/downloader/* @jack1142 /redbot/cogs/streams/* @palmtree5 +/redbot/cogs/mutes/* @TrustyJAID # Trivia Lists /redbot/cogs/trivia/data/lists/whosthatpokemon*.yaml @aikaterna diff --git a/redbot/cogs/mod/abc.py b/redbot/cogs/mod/abc.py index 59837c4016b..cfdd5a6596b 100644 --- a/redbot/cogs/mod/abc.py +++ b/redbot/cogs/mod/abc.py @@ -17,3 +17,10 @@ def __init__(self, *_args): self.config: Config self.bot: Red self.cache: dict + + @staticmethod + @abstractmethod + async def _voice_perm_check( + ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool + ) -> bool: + raise NotImplementedError() diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py index 76bf3f9166b..c35a2abc5a9 100644 --- a/redbot/cogs/mod/kickban.py +++ b/redbot/cogs/mod/kickban.py @@ -8,7 +8,13 @@ from redbot.core import commands, i18n, checks, modlog from redbot.core.commands import UserInputOptional from redbot.core.utils import AsyncIter -from redbot.core.utils.chat_formatting import pagify, humanize_number, bold, humanize_list +from redbot.core.utils.chat_formatting import ( + pagify, + humanize_number, + bold, + humanize_list, + format_perms_list, +) from redbot.core.utils.mod import get_audit_reason from .abc import MixinMeta from .converters import RawUserIds @@ -60,6 +66,48 @@ async def get_invite_for_reinvite(ctx: commands.Context, max_age: int = 86400): 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 + async def ban_user( self, user: discord.Member, diff --git a/redbot/cogs/mutes/__init__.py b/redbot/cogs/mutes/__init__.py index 1a5f91fed08..430b4a3f727 100644 --- a/redbot/cogs/mutes/__init__.py +++ b/redbot/cogs/mutes/__init__.py @@ -5,3 +5,4 @@ async def setup(bot: Red): cog = Mutes(bot) bot.add_cog(cog) + await cog.initialize() diff --git a/redbot/cogs/mutes/converters.py b/redbot/cogs/mutes/converters.py index 78d0c4a4f5a..5c8b47cb007 100644 --- a/redbot/cogs/mutes/converters.py +++ b/redbot/cogs/mutes/converters.py @@ -8,6 +8,10 @@ log = logging.getLogger("red.cogs.mutes") # the following regex is slightly modified from Red +# it's changed to be slightly more strict on matching with finditer +# this is to prevent "empty" matches when parsing the full reason +# This is also designed more to allow time interval at the beginning or the end of the mute +# to account for those times when you think of adding time *after* already typing out the reason # https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/core/commands/converter.py#L55 TIME_RE_STRING = r"|".join( [ diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index cfb15b74aed..3a5142d1038 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -18,10 +18,9 @@ from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate -T_ = i18n.Translator("Mutes", __file__) +_ = i18n.Translator("Mutes", __file__) -_ = lambda s: s -mute_unmute_issues = { +MUTE_UNMUTE_ISSUES = { "already_muted": _("That user is already muted in this channel."), "already_unmuted": _("That user is not muted in this channel."), "hierarchy_problem": _( @@ -37,7 +36,6 @@ "unknown_channel": _("The channel I tried to mute the user in isn't found."), "role_missing": _("The mute role no longer exists."), } -_ = T_ log = logging.getLogger("red.cogs.mutes") @@ -55,7 +53,7 @@ class CompositeMetaClass(type(commands.Cog), type(ABC)): class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass): """ - Stuff for mutes goes here + Mute users temporarily or indefinitely """ def __init__(self, bot: Red): @@ -64,10 +62,9 @@ def __init__(self, bot: Red): default_guild = { "sent_instructions": False, "mute_role": None, - "respect_hierarchy": True, + "notification_channel": None, "muted_users": {}, "default_time": {}, - "removed_users": {}, } self.config.register_guild(**default_guild) self.config.register_member(perms_cache={}) @@ -75,11 +72,9 @@ def __init__(self, bot: Red): self._server_mutes: Dict[int, Dict[int, dict]] = {} self._channel_mutes: Dict[int, Dict[int, dict]] = {} self._ready = asyncio.Event() - self.bot.loop.create_task(self.initialize()) - self._unmute_tasks: Dict[str, Coroutine] = {} - self._unmute_task = asyncio.create_task(self._handle_automatic_unmute()) - # dict of guild id, member id and time to be unmuted - self.mute_role_cache = {} + self._unmute_tasks: Dict[str, asyncio.Task] = {} + self._unmute_task = None + self.mute_role_cache: Dict[int, int] = {} async def red_delete_data_for_user( self, @@ -92,9 +87,10 @@ async def red_delete_data_for_user( await self._ready.wait() all_members = await self.config.all_members() - for g_id, m_id in all_members.items(): - if m_id == user_id: - await self.config.member_from_ids(g_id, m_id).clear() + for g_id, data in all_members.items(): + for m_id, mutes in data.items(): + if m_id == user_id: + await self.config.member_from_ids(g_id, m_id).clear() async def initialize(self): guild_data = await self.config.all_guilds() @@ -109,6 +105,7 @@ async def initialize(self): self._channel_mutes[c_id] = {} for user_id, mute in mutes["muted_users"].items(): self._channel_mutes[c_id][int(user_id)] = mute + self._unmute_task = asyncio.create_task(self._handle_automatic_unmute()) self._ready.set() async def cog_before_invoke(self, ctx: commands.Context): @@ -161,28 +158,21 @@ async def _clean_tasks(self): async def _handle_server_unmutes(self): log.debug("Checking server unmutes") - to_clear = [] - for g_id, mutes in self._server_mutes.items(): - to_remove = [] + for g_id in self._server_mutes: guild = self.bot.get_guild(g_id) - if guild is None: + if guild is None or await self.bot.cog_disabled_in_guild(self, guild): continue - for u_id, data in mutes.items(): - if data["until"] is None: + for u_id in self._server_mutes[guild.id]: + if self._server_mutes[guild.id][u_id]["until"] is None: continue - time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() + time_to_unmute = ( + self._server_mutes[guild.id][u_id]["until"] + - datetime.now(timezone.utc).timestamp() + ) if time_to_unmute < 60: self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( - self._auto_unmute_user(guild, data) + self._auto_unmute_user(guild, self._server_mutes[guild.id][u_id]) ) - to_remove.append(u_id) - for u_id in to_remove: - del self._server_mutes[g_id][u_id] - if self._server_mutes[g_id] == {}: - to_clear.append(g_id) - await self.config.guild(guild).muted_users.set(self._server_mutes[g_id]) - for g_id in to_clear: - await self.config.guild_from_id(g_id).muted_users.clear() async def _auto_unmute_user(self, guild: discord.Guild, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -196,45 +186,54 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): return success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) if success: - try: - await modlog.create_case( - self.bot, - guild, - datetime.now(timezone.utc), - "sunmute", - member, - author, - _("Automatic unmute"), - until=None, + await modlog.create_case( + self.bot, + guild, + datetime.now(timezone.utc), + "sunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + async with self.config.guild(guild).muted_users() as muted_users: + if str(member.id) in muted_users: + del muted_users[str(member.id)] + else: + chan_id = await self.config.guild(guild).notification_channel() + notification_channel = guild.get_channel(chan_id) + if not notification_channel: + return + await notification_channel.send( + _("I am unable to unmute {user} for the following reason:\n{reason}").format( + user=member, reason=message ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) + ) except discord.errors.Forbidden: return async def _handle_channel_unmutes(self): log.debug("Checking channel unmutes") - to_clear = [] - for c_id, mutes in self._channel_mutes.items(): - to_remove = [] + for c_id in self._channel_mutes: channel = self.bot.get_channel(c_id) - if channel is None: + if channel is None or await self.bot.cog_disabled_in_guild(self, channel.guild): continue - for u_id, data in mutes.items(): - if not data or not data["until"]: + for u_id in self._channel_mutes[channel.id]: + if ( + not self._channel_mutes[channel.id][u_id] + or not self._channel_mutes[channel.id][u_id]["until"] + ): continue - time_to_unmute = data["until"] - datetime.now(timezone.utc).timestamp() + time_to_unmute = ( + self._channel_mutes[channel.id][u_id]["until"] + - datetime.now(timezone.utc).timestamp() + ) if time_to_unmute < 120.0: self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( - self._auto_channel_unmute_user(channel, data) + self._auto_channel_unmute_user( + channel, self._channel_mutes[channel.id][u_id] + ) ) - for u_id in to_remove: - del self._channel_mutes[c_id][u_id] - if self._channel_mutes[c_id] == {}: - to_clear.append(c_id) - await self.config.channel(channel).muted_users.set(self._channel_mutes[c_id]) - for c_id in to_clear: - await self.config.channel_from_id(c_id).muted_users.clear() async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): delay = data["until"] - datetime.now(timezone.utc).timestamp() @@ -244,25 +243,35 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di try: member = channel.guild.get_member(data["member"]) author = channel.guild.get_member(data["author"]) - if not member or not author: + if not member: return success = await self.channel_unmute_user( channel.guild, channel, author, member, _("Automatic unmute") ) if success["success"]: - try: - await modlog.create_case( - self.bot, - channel.guild, - datetime.now(timezone.utc), - "cunmute", - member, - author, - _("Automatic unmute"), - until=None, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) + await modlog.create_case( + self.bot, + channel.guild, + datetime.now(timezone.utc), + "cunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + async with self.config.channel(channel).muted_users() as muted_users: + if str(member.id) in muted_users: + del muted_users[str(member.id)] + else: + chan_id = await self.config.guild(channel.guild).notification_channel() + notification_channel = channel.guild.get_channel(chan_id) + if not notification_channel: + return + await notification_channel.send( + _( + "I am unable to mute {user} in {channel} for the following reason:\n{reason}" + ).format(user=member, channel=channel.mention, reason=success["reason"]) + ) except discord.errors.Forbidden: return @@ -272,8 +281,11 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): Used to handle the cache if a member manually has the muted role removed """ guild = before.guild + if await self.bot.cog_disabled_in_guild(self, guild): + return if guild.id not in self.mute_role_cache: return + should_save = False mute_role_id = self.mute_role_cache[guild.id] mute_role = guild.get_role(mute_role_id) if not mute_role: @@ -288,43 +300,39 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): # they weren't a tracked mute so we can return early return if after.id in self._server_mutes[guild.id]: - try: - await modlog.create_case( - self.bot, - guild, - datetime.utcnow(), - "sunmute", - after, - None, - _("Manually removed mute role"), - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) + await modlog.create_case( + self.bot, + guild, + datetime.now(timezone.utc), + "sunmute", + after, + None, + _("Manually removed mute role"), + ) del self._server_mutes[guild.id][after.id] - if mute_role in roles_added: + should_save = True + elif mute_role in roles_added: # send modlog case for mute and add to cache if guild.id not in self._server_mutes: # initialize the guild in the cache to prevent keyerrors self._server_mutes[guild.id] = {} if after.id not in self._server_mutes[guild.id]: - try: - await modlog.create_case( - self.bot, - guild, - datetime.utcnow(), - "smute", - after, - None, - _("Manually applied mute role"), - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) + await modlog.create_case( + self.bot, + guild, + datetime.now(timezone.utc), + "smute", + after, + None, + _("Manually applied mute role"), + ) self._server_mutes[guild.id][after.id] = { "author": None, "member": after.id, "until": None, } - if guild.id in self._server_mutes: + should_save = True + if should_save: await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) @commands.Cog.listener() @@ -344,51 +352,31 @@ async def on_guild_channel_update( to_del: List[int] = [] for user_id in self._channel_mutes[after.id].keys(): if user_id in before_perms and ( - user_id not in after_perms or after_perms[user_id]["send_messages"] + user_id not in after_perms + or any((after_perms[user_id]["send_messages"], after_perms[user_id]["speak"])) ): user = after.guild.get_member(user_id) if not user: user = discord.Object(id=user_id) log.debug(f"{user} - {type(user)}") to_del.append(user_id) - try: - log.debug("creating case") - await modlog.create_case( - self.bot, - after.guild, - datetime.utcnow(), - "cunmute", - user, - None, - _("Manually removed channel overwrites"), - until=None, - channel=after, - ) - log.debug("created case") - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - for u_id in to_del: - del self._channel_mutes[after.id][u_id] - await self.config.channel(after).muted_users.set(self._channel_mutes[after.id]) - - @commands.Cog.listener() - async def on_member_remove(self, member: discord.Member): - guild = member.guild - until = None - if guild.id in self._server_mutes: - if member.id in self._server_mutes[guild.id]: - until = self._server_mutes[guild.id][member.id]["until"] - del self._server_mutes[guild.id][member.id] - for channel in guild.channels: - if channel.id in self._channel_mutes: - if member.id in self._channel_mutes[channel.id]: - del self._channel_mutes[channel.id][member.id] - mute_role = await self.config.guild(guild).mute_role() - if not mute_role: - return - if mute_role in [r.id for r in member.roles]: - async with self.config.guild(guild).removed_users() as removed_users: - removed_users[str(member.id)] = int(until) if until else None + log.debug("creating case") + await modlog.create_case( + self.bot, + after.guild, + datetime.now(timezone.utc), + "cunmute", + user, + None, + _("Manually removed channel overwrites"), + until=None, + channel=after, + ) + log.debug("created case") + if to_del: + for u_id in to_del: + del self._channel_mutes[after.id][u_id] + await self.config.channel(after).muted_users.set(self._channel_mutes[after.id]) @commands.Cog.listener() async def on_member_join(self, member: discord.Member): @@ -396,29 +384,33 @@ async def on_member_join(self, member: discord.Member): mute_role = await self.config.guild(guild).mute_role() if not mute_role: return - async with self.config.guild(guild).removed_users() as removed_users: - if str(member.id) in removed_users: - until_ts = removed_users[str(member.id)] - until = datetime.fromtimestamp(until_ts, tz=timezone.utc) if until_ts else None - # datetime is required to utilize the mutes method - if until and until < datetime.now(tz=timezone.utc): - return - removed_users.pop(str(member.id)) + if guild.id in self._server_mutes: + if member.id in self._server_mutes[guild.id]: role = guild.get_role(mute_role) if not role: return + until = self._server_mutes[guild.id][member.id]["until"] await self.mute_user( guild, guild.me, member, until, _("Previously muted in this server.") ) @commands.group() @commands.guild_only() - @checks.mod_or_permissions(manage_roles=True) async def muteset(self, ctx: commands.Context): """Mute settings.""" pass + @muteset.command(name="notification") + @checks.mod_or_permissions(manage_messages=True) + async def notification_channel_set(self, ctx: commands.Context, channel: discord.TextChannel): + """Set The notification channel for automatic unmute issues.""" + await self.config.guild(ctx.guild).notification_channel.set(channel.id) + await ctx.send( + _("I will post unmute issues in {channel}.").format(channel=channel.mention) + ) + @muteset.command(name="role") + @checks.admin_or_permissions(manage_roles=True) @checks.bot_has_permissions(manage_roles=True) async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): """Sets the role to be applied when muting a user. @@ -437,8 +429,17 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): await self.config.guild(ctx.guild).mute_role.set(role.id) self.mute_role_cache[ctx.guild.id] = role.id await ctx.send(_("Mute role set to {role}").format(role=role.name)) + if not await self.config.guild(ctx.guild).notification_channel(): + command_1 = f"`{ctx.clean_prefix}muteset notification`" + await ctx.send( + _( + "No notification channel has been setup, " + "use {command_1} to be updated when there's an issue in automatic unmutes." + ).format(command_1=command_1) + ) @muteset.command(name="makerole") + @checks.admin_or_permissions(manage_roles=True) @checks.bot_has_permissions(manage_roles=True) async def make_mute_role(self, ctx: commands.Context, *, name: str): """Create a Muted role. @@ -467,10 +468,18 @@ async def make_mute_role(self, ctx: commands.Context, *, name: str): msg = _( "I could not set overwrites for the following channels: {channels}" ).format(channels=humanize_list([i for i in errors if i])) - for page in pagify(msg): + for page in pagify(msg, delims=[" "]): await ctx.send(page) await self.config.guild(ctx.guild).mute_role.set(role.id) await ctx.send(_("Mute role set to {role}").format(role=role.name)) + if not await self.config.guild(ctx.guild).notification_channel(): + command_1 = f"`{ctx.clean_prefix}muteset notification`" + await ctx.send( + _( + "No notification channel has been setup, " + "use {command_1} to be updated when there's an issue in automatic unmutes." + ).format(command_1=command_1) + ) async def _set_mute_role_overwrites( self, role: discord.Role, channel: discord.abc.GuildChannel @@ -480,30 +489,34 @@ async def _set_mute_role_overwrites( by default for a mute role """ overs = discord.PermissionOverwrite() - if isinstance(channel, discord.TextChannel): - overs.send_messages = False - overs.add_reactions = False - if isinstance(channel, discord.VoiceChannel): - overs.speak = False + overs.send_messages = False + overs.add_reactions = False + overs.speak = False try: await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) return except discord.errors.Forbidden: return channel.mention - @muteset.command(name="time") - async def default_mute_time(self, ctx: commands.Context, *, time: MuteTime): + @muteset.command(name="defaulttime", aliases=["time"]) + @checks.mod_or_permissions(manage_messages=True) + async def default_mute_time(self, ctx: commands.Context, *, time: Optional[MuteTime] = None): """ Set the default mute time for the mute command. + + If no time interval is provided this will be cleared. """ - data = time.get("duration", {}) - if not data: - await self.config.guild(ctx.guild).default_time.set(data) + + if not time: + await self.config.guild(ctx.guild).default_time.clear() await ctx.send(_("Default mute time removed.")) else: + data = time.get("duration", {}) + if not data: + return await ctx.send(_("Please provide a valid time format.")) await self.config.guild(ctx.guild).default_time.set(data) await ctx.send( - _("Default mute time set to {time}").format( + _("Default mute time set to {time}.").format( time=humanize_timedelta(timedelta=timedelta(**data)) ) ) @@ -519,13 +532,14 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: if mute_role or sent_instructions: return True else: + command_1 = f"{ctx.clean_prefix}muteset role" + command_2 = f"{ctx.clean_prefix}muteset makerole" msg = _( "This server does not have a mute role setup, " "are you sure you want to continue with channel " - "overwrites? (Note: Overwrites will not be automatically unmuted." - " You can setup a mute role with `{prefix}muteset role` or " - "`{prefix}muteset makerole` if you just want a basic setup.)\n\n" - ).format(prefix=ctx.clean_prefix) + "overwrites? You can setup a mute role with `{command_1}` or" + "`{command_2}` if you just want a basic role created setup.)\n\n" + ).format(command_1=command_1, command_2=command_2) can_react = ctx.channel.permissions_for(ctx.me).add_reactions if can_react: msg += _( @@ -577,16 +591,13 @@ async def activemutes(self, ctx: commands.Context): """ msg = "" - to_del = [] if ctx.guild.id in self._server_mutes: mutes_data = self._server_mutes[ctx.guild.id] - to_rem = [] if mutes_data: msg += _("__Server Mutes__\n") for user_id, mutes in mutes_data.items(): if not mutes: - to_rem.append(user_id) continue user = ctx.guild.get_member(user_id) if not user: @@ -600,21 +611,15 @@ async def activemutes(self, ctx: commands.Context): time_str = humanize_timedelta(timedelta=time_left) else: time_str = "" - msg += _("{member}").format(member=user_str) + msg += f"{user_str} " if time_str: msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) else: msg += "\n" - for _id in to_rem: - try: - del self._server_mutes[ctx.guild.id][_id] - except KeyError: - pass for channel_id, mutes_data in self._channel_mutes.items(): if not mutes_data: - to_del.append(channel_id) continue - if channel_id in [c.id for c in ctx.guild.channels]: + if ctx.guild.get_channel(channel_id): msg += _("__<#{channel_id}> Mutes__\n").format(channel_id=channel_id) for user_id, mutes in mutes_data.items(): if not mutes: @@ -631,20 +636,18 @@ async def activemutes(self, ctx: commands.Context): time_str = humanize_timedelta(timedelta=time_left) else: time_str = "" - msg += _("{member} ").format(member=user_str) + msg += f"{user_str} " if time_str: msg += _("__Remaining__: {time_left}\n").format(time_left=time_str) else: msg += "\n" - for c in to_del: - del self._channel_mutes[c] if msg: for page in pagify(msg): await ctx.maybe_send_embed(page) return await ctx.maybe_send_embed(_("There are no mutes on this server right now.")) - @commands.command(usage="[users...] [time_and_reason]") + @commands.command(usage=" [time_and_reason]") @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) async def mute( @@ -656,10 +659,10 @@ async def mute( ): """Mute users. - `[users...]` is a space separated list of usernames, ID's, or mentions. + `` is a space separated list of usernames, ID's, or mentions. `[time_and_reason]` is the time to mute for and reason. Time is any valid time length such as `30 minutes` or `2 days`. If nothing - is provided the mute will be indefinite. + is provided the mute will use the set default time or indefinite if not set. Examples: `[p]mute @member1 @member2 spam 5 hours` @@ -681,56 +684,53 @@ async def mute( until = None if duration: until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + time = _(" for {duration}").format( + duration=humanize_timedelta(timedelta=timedelta(**duration)) + ) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: until = datetime.now(timezone.utc) + timedelta(**default_duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + time = _(" for {duration}").format( + duration=humanize_timedelta(timedelta=timedelta(**default_duration)) + ) author = ctx.message.author guild = ctx.guild audit_reason = get_audit_reason(author, reason) success_list = [] + issue_list = [] for user in users: success, issue = await self.mute_user(guild, author, user, until, audit_reason) + if issue: + issue_list.append((user, issue)) if success: success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "smute", - user, - author, - reason, - until=until, - channel=None, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if not success_list and issue: - message = _( - "{users} could not be muted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - return await self.handle_issues(ctx, message, issue) - if ctx.guild.id not in self._server_mutes: - self._server_mutes[ctx.guild.id] = {} - verb = _("has") - if len(success_list) > 1: - verb = _("have") - await ctx.send( - _("{users} {verb} been muted in this server{time}.").format( - users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "smute", + user, + author, + reason, + until=until, + channel=None, + ) + if success_list: + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + msg = _("{users} has been muted in this server{time}.") + if len(success_list) > 1: + msg = _("{users} have been muted in this server{time}.") + await ctx.send( + msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) ) - ) - if issue: + if issue_list: message = _( "{users} could not be muted in some channels. " "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message, issue) + ).format(users=humanize_list([f"{u}" for u, x in issue_list])) + await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions @@ -765,7 +765,7 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - await ctx.send_interactive(resp) @commands.command( - name="mutechannel", aliases=["channelmute"], usage="[users...] [time_and_reason]" + name="mutechannel", aliases=["channelmute"], usage=" [time_and_reason]" ) @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) @@ -778,7 +778,7 @@ async def channel_mute( ): """Mute a user in the current text channel. - `[users...]` is a space separated list of usernames, ID's, or mentions. + `` is a space separated list of usernames, ID's, or mentions. `[time_and_reason]` is the time to mute for and reason. Time is any valid time length such as `30 minutes` or `2 days`. If nothing is provided the mute will be indefinite. @@ -793,8 +793,6 @@ async def channel_mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) - if not await self._check_for_mute_role(ctx): - return async with ctx.typing(): duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) @@ -802,16 +800,21 @@ async def channel_mute( time = "" if duration: until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**duration)) + time = _(" for {duration}").format( + duration=humanize_timedelta(timedelta=timedelta(**duration)) + ) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: until = datetime.now(timezone.utc) + timedelta(**default_duration) - time = _(" for ") + humanize_timedelta(timedelta=timedelta(**default_duration)) + time = _(" for {duration}").format( + duration=humanize_timedelta(timedelta=timedelta(**default_duration)) + ) author = ctx.message.author channel = ctx.message.channel guild = ctx.guild audit_reason = get_audit_reason(author, reason) + issue_list = [] success_list = [] for user in users: success = await self.channel_mute_user( @@ -820,33 +823,36 @@ async def channel_mute( if success["success"]: success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cmute", - user, - author, - reason, - until=until, - channel=channel, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "cmute", + user, + author, + reason, + until=until, + channel=channel, + ) + async with self.config.member(user).perms_cache() as cache: + cache[channel.id] = success["old_overs"] else: - return await ctx.send(success["reason"]) + issue_list.append((user, success["reason"])) + if success_list: - verb = _("has") + msg = _("{users} has been muted in this channel{time}.") if len(success_list) > 1: - verb = _("have") + msg = _("{users} have been muted in this channel{time}.") await channel.send( - _("{users} {verb} been muted in this channel{time}.").format( - users=humanize_list([f"{u}" for u in success_list]), verb=verb, time=time - ) + msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) ) + if issue_list: + msg = _("The following users could not be muted\n") + for user, issue in issue_list: + msg += f"{user}: {issue}\n" + await ctx.send_interactive(pagify(msg)) - @commands.command(usage="[users...] [reason]") + @commands.command(usage=" [reason]") @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) async def unmute( @@ -858,7 +864,7 @@ async def unmute( ): """Unmute users. - `[users...]` is a space separated list of usernames, ID's, or mentions. + `` is a space separated list of usernames, ID's, or mentions. `[reason]` is the reason for the unmute. """ if not users: @@ -873,51 +879,46 @@ async def unmute( guild = ctx.guild author = ctx.author audit_reason = get_audit_reason(author, reason) + issue_list = [] success_list = [] for user in users: success, issue = await self.unmute_user(guild, author, user, audit_reason) + if success: success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "sunmute", - user, - author, - reason, - until=None, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) - if not success_list and issue: - message = _( - "{users} could not be muted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - return await self.handle_issues(ctx, message, issue) - - if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: - await self.config.guild(ctx.guild).muted_users.set( - self._server_mutes[ctx.guild.id] - ) - else: - await self.config.guild(ctx.guild).muted_users.clear() - await ctx.send( - _("{users} unmuted in this server.").format( - users=humanize_list([f"{u}" for u in success_list]) + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "sunmute", + user, + author, + reason, + until=None, + ) + else: + issue_list.append((user, issue)) + if success_list: + if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: + await self.config.guild(ctx.guild).muted_users.set( + self._server_mutes[ctx.guild.id] + ) + else: + await self.config.guild(ctx.guild).muted_users.clear() + await ctx.send( + _("{users} unmuted in this server.").format( + users=humanize_list([f"{u}" for u in success_list]) + ) ) - ) - if issue: + if issue_list: message = _( "{users} could not be unmuted in some channels. " "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u in users])) - await self.handle_issues(ctx, message, issue) + ).format(users=humanize_list([f"{u}" for u, x in issue_list])) + await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) @checks.mod_or_permissions(manage_roles=True) - @commands.command(name="unmutechannel", aliases=["channelunmute"], usage="[users...] [reason]") + @commands.command(name="unmutechannel", aliases=["channelunmute"], usage=" [reason]") @commands.guild_only() async def unmute_channel( self, @@ -928,7 +929,7 @@ async def unmute_channel( ): """Unmute a user in this channel. - `[users...]` is a space separated list of usernames, ID's, or mentions. + `` is a space separated list of usernames, ID's, or mentions. `[reason]` is the reason for the unmute. """ if not users: @@ -937,8 +938,6 @@ async def unmute_channel( return await ctx.send(_("You cannot unmute me.")) if ctx.author in users: return await ctx.send(_("You cannot unmute yourself.")) - if not await self._check_for_mute_role(ctx): - return async with ctx.typing(): channel = ctx.channel author = ctx.author @@ -952,25 +951,24 @@ async def unmute_channel( if success["success"]: success_list.append(user) - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "cunmute", - user, - author, - reason, - until=None, - channel=channel, - ) - except RuntimeError as e: - log.error(_("Error creating modlog case"), exc_info=e) + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "cunmute", + user, + author, + reason, + until=None, + channel=channel, + ) else: return await ctx.send(success["reason"]) if success_list: if self._channel_mutes[channel.id]: - await self.config.channel(channel).set(self._channel_mutes[channel.id]) + await self.config.channel(channel).muted_users.set( + self._channel_mutes[channel.id] + ) else: await self.config.channel(channel).muted_users.clear() await ctx.send( @@ -993,30 +991,30 @@ async def mute_user( mute_role = await self.config.guild(guild).mute_role() if mute_role: - try: - if not await self.is_allowed_by_hierarchy(guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) - role = guild.get_role(mute_role) - if not role: - return False, mute_unmute_issues["role_missing"] - - # This is here to prevent the modlog case from happening on role updates - # we need to update the cache early so it's there before we receive the member_update event - if guild.id not in self._server_mutes: - self._server_mutes[guild.id] = {} + if not await self.is_allowed_by_hierarchy(guild, author, user): + return False, _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + role = guild.get_role(mute_role) + if not role: + return False, MUTE_UNMUTE_ISSUES["role_missing"] + + # This is here to prevent the modlog case from happening on role updates + # we need to update the cache early so it's there before we receive the member_update event + if guild.id not in self._server_mutes: + self._server_mutes[guild.id] = {} - self._server_mutes[guild.id][user.id] = { - "author": author.id, - "member": user.id, - "until": until.timestamp() if until else None, - } + self._server_mutes[guild.id][user.id] = { + "author": author.id, + "member": user.id, + "until": until.timestamp() if until else None, + } + try: await user.add_roles(role, reason=reason) await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) except discord.errors.Forbidden: del self._server_mutes[guild.id][ user.id ] # this is here so we don't have a bad cache - return False, mute_unmute_issues["permissions_issue"] + return False, MUTE_UNMUTE_ISSUES["permissions_issue"] return True, None else: mute_success = [] @@ -1055,21 +1053,21 @@ async def unmute_user( mute_role = await self.config.guild(guild).mute_role() _temp = None # used to keep the cache incase of permissions errors if mute_role: + if not await self.is_allowed_by_hierarchy(guild, author, user): + return False, _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + role = guild.get_role(mute_role) + if not role: + return False, MUTE_UNMUTE_ISSUES["role_missing"] + if guild.id in self._server_mutes: + if user.id in self._server_mutes[guild.id]: + _temp = copy(self._server_mutes[guild.id][user.id]) + del self._server_mutes[guild.id][user.id] try: - if not await self.is_allowed_by_hierarchy(guild, author, user): - return False, _(mute_unmute_issues["hierarchy_problem"]) - role = guild.get_role(mute_role) - if not role: - return False, mute_unmute_issues["role_missing"] - if guild.id in self._server_mutes: - if user.id in self._server_mutes[guild.id]: - _temp = copy(self._server_mutes[guild.id][user.id]) - del self._server_mutes[guild.id][user.id] await user.remove_roles(role, reason=reason) except discord.errors.Forbidden: if _temp: self._server_mutes[guild.id][user.id] = _temp - return False, mute_unmute_issues["permissions_issue"] + return False, MUTE_UNMUTE_ISSUES["permissions_issue"] return True, None else: mute_success = [] @@ -1105,7 +1103,7 @@ async def channel_mute_user( return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["is_admin"]), + "reason": _(MUTE_UNMUTE_ISSUES["is_admin"]), } new_overs: dict = {} @@ -1118,7 +1116,7 @@ async def channel_mute_user( return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["hierarchy_problem"]), + "reason": _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]), } old_overs = {k: getattr(overwrites, k) for k in new_overs} @@ -1137,7 +1135,7 @@ async def channel_mute_user( return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["permissions_issue"]), + "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), } except discord.NotFound as e: if e.code == 10003: @@ -1145,14 +1143,14 @@ async def channel_mute_user( return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["unknown_channel"]), + "reason": _(MUTE_UNMUTE_ISSUES["unknown_channel"]), } elif e.code == 10009: del self._channel_mutes[channel.id][user.id] return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["left_guild"]), + "reason": _(MUTE_UNMUTE_ISSUES["left_guild"]), } return {"success": True, "channel": channel, "old_overs": old_overs} @@ -1164,6 +1162,7 @@ async def channel_unmute_user( user: discord.Member, reason: Optional[str] = None, ) -> Dict[str, Optional[Union[discord.abc.GuildChannel, str, bool]]]: + """Unmutes the specified user in a specified channel""" overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() @@ -1177,10 +1176,13 @@ async def channel_unmute_user( return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["hierarchy_problem"]), + "reason": _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]), } overwrites.update(**old_values) + if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: + _temp = copy(self._channel_mutes[channel.id][user.id]) + del self._channel_mutes[channel.id][user.id] try: if overwrites.is_empty(): await channel.set_permissions( @@ -1188,16 +1190,13 @@ async def channel_unmute_user( ) else: await channel.set_permissions(user, overwrite=overwrites, reason=reason) - if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: - _temp = copy(self._channel_mutes[channel.id][user.id]) - del self._channel_mutes[channel.id][user.id] except discord.Forbidden: if channel.id in self._channel_mutes and _temp: self._channel_mutes[channel.id][user.id] = _temp return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["permissions_issue"]), + "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), } except discord.NotFound as e: if e.code == 10003: @@ -1206,7 +1205,7 @@ async def channel_unmute_user( return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["unknown_channel"]), + "reason": _(MUTE_UNMUTE_ISSUES["unknown_channel"]), } elif e.code == 10009: if channel.id in self._channel_mutes and _temp: @@ -1214,6 +1213,6 @@ async def channel_unmute_user( return { "success": False, "channel": channel, - "reason": _(mute_unmute_issues["left_guild"]), + "reason": _(MUTE_UNMUTE_ISSUES["left_guild"]), } return {"success": True, "channel": channel, "reason": None} diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index e6aeec3b980..7423e203f70 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -6,10 +6,7 @@ from redbot.core.utils.chat_formatting import format_perms_list from redbot.core.utils.mod import get_audit_reason -T_ = i18n.Translator("Mutes", __file__) - -_ = lambda s: s -_ = T_ +_ = i18n.Translator("Mutes", __file__) class VoiceMutes(MixinMeta): @@ -87,20 +84,17 @@ async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reaso guild = ctx.guild author = ctx.author - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "voiceunban", - user, - author, - reason, - until=None, - channel=None, - ) - except RuntimeError as e: - await ctx.send(e) + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceunban", + user, + author, + reason, + until=None, + channel=None, + ) await ctx.send(_("User is now allowed to speak and listen in voice channels.")) @commands.command() @@ -131,20 +125,17 @@ async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: await ctx.send(_("That user is already muted and deafened server-wide.")) return - try: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "voiceban", - user, - author, - reason, - until=None, - channel=None, - ) - except RuntimeError as e: - await ctx.send(e) + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceban", + user, + author, + reason, + until=None, + channel=None, + ) await ctx.send(_("User has been banned from speaking or listening in voice channels.")) @commands.command(name="voicemute") @@ -164,28 +155,36 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso channel = user_voice_state.channel audit_reason = get_audit_reason(author, reason) - success, issue = await self.channel_mute_user(guild, channel, author, user, audit_reason) + success, issue = await self.mute_user(guild, channel, author, user, audit_reason) - if success: - 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) + if success["success"]: + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "vmute", + user, + author, + reason, + until=None, + channel=channel, + ) await ctx.send( - _("Muted {user} in channel {channel.name}.").format(user=user, channel=channel) + _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) ) + try: + if channel.permissions_for(ctx.me).move_members: + await user.move_to(channel) + else: + raise RuntimeError + except (discord.Forbidden, RuntimeError): + await ctx.send( + _( + "Because I don't have the Move Members permission, this will take into effect when the user rejoins." + ) + ) else: - await ctx.send(issue) + await ctx.send(issuee) @commands.command(name="voiceunmute") @commands.guild_only() @@ -206,11 +205,9 @@ async def unmute_voice( channel = user_voice_state.channel audit_reason = get_audit_reason(author, reason) - success, message = await self.channel_unmute_user( - guild, channel, author, user, audit_reason - ) + success, message = await self.unmute_user(guild, channel, author, user, audit_reason) - try: + if success: await modlog.create_case( self.bot, guild, @@ -222,8 +219,19 @@ async def unmute_voice( until=None, channel=channel, ) - except RuntimeError as e: - await ctx.send(e) - await ctx.send( - _("Unmuted {user} in channel {channel.name}.").format(user=user, channel=channel) - ) + await ctx.send( + _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) + ) + try: + if channel.permissions_for(ctx.me).move_members: + await user.move_to(channel) + else: + raise RuntimeError + except (discord.Forbidden, RuntimeError): + await ctx.send( + _( + "Because I don't have the Move Members permission, this will take into effect when the user rejoins." + ) + ) + else: + await ctx.send(_("Unmute failed. Reason: {}").format(message)) From 10d30db4e5477dee94d9daa4a24cb8acd8273c7e Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 18 Oct 2020 23:57:44 -0600 Subject: [PATCH 071/103] lint --- redbot/cogs/mutes/voicemutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 7423e203f70..8083722c4ac 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -184,7 +184,7 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso ) ) else: - await ctx.send(issuee) + await ctx.send(issue) @commands.command(name="voiceunmute") @commands.guild_only() From e56dab608cf8c6fc694f5b17dd1fac88e653b057 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 19 Oct 2020 10:20:21 -0600 Subject: [PATCH 072/103] fix string i18n issue --- redbot/cogs/mutes/mutes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 3a5142d1038..98d673bcfb6 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -20,6 +20,8 @@ _ = i18n.Translator("Mutes", __file__) +_ = lambda s: s + MUTE_UNMUTE_ISSUES = { "already_muted": _("That user is already muted in this channel."), "already_unmuted": _("That user is not muted in this channel."), @@ -995,7 +997,7 @@ async def mute_user( return False, _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) role = guild.get_role(mute_role) if not role: - return False, MUTE_UNMUTE_ISSUES["role_missing"] + return False, _(MUTE_UNMUTE_ISSUES["role_missing"]) # This is here to prevent the modlog case from happening on role updates # we need to update the cache early so it's there before we receive the member_update event @@ -1014,7 +1016,7 @@ async def mute_user( del self._server_mutes[guild.id][ user.id ] # this is here so we don't have a bad cache - return False, MUTE_UNMUTE_ISSUES["permissions_issue"] + return False, _(MUTE_UNMUTE_ISSUES["permissions_issue"]) return True, None else: mute_success = [] @@ -1057,7 +1059,7 @@ async def unmute_user( return False, _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) role = guild.get_role(mute_role) if not role: - return False, MUTE_UNMUTE_ISSUES["role_missing"] + return False, _(MUTE_UNMUTE_ISSUES["role_missing"]) if guild.id in self._server_mutes: if user.id in self._server_mutes[guild.id]: _temp = copy(self._server_mutes[guild.id][user.id]) @@ -1067,7 +1069,7 @@ async def unmute_user( except discord.errors.Forbidden: if _temp: self._server_mutes[guild.id][user.id] = _temp - return False, MUTE_UNMUTE_ISSUES["permissions_issue"] + return False, _(MUTE_UNMUTE_ISSUES["permissions_issue"]) return True, None else: mute_success = [] From 4ced3467b470446526f161b1463234f28d985f39 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 19 Oct 2020 11:20:57 -0600 Subject: [PATCH 073/103] remove unused typing.Coroutine import and fix i18n again --- redbot/cogs/mutes/mutes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 98d673bcfb6..90798b3282e 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -5,7 +5,7 @@ from abc import ABC from copy import copy -from typing import cast, Optional, Dict, List, Tuple, Literal, Coroutine, Union +from typing import cast, Optional, Dict, List, Tuple, Literal, Union from datetime import datetime, timedelta, timezone from .converters import MuteTime @@ -18,7 +18,7 @@ from redbot.core.utils.menus import start_adding_reactions from redbot.core.utils.predicates import MessagePredicate, ReactionPredicate -_ = i18n.Translator("Mutes", __file__) +T_ = i18n.Translator("Mutes", __file__) _ = lambda s: s @@ -38,6 +38,7 @@ "unknown_channel": _("The channel I tried to mute the user in isn't found."), "role_missing": _("The mute role no longer exists."), } +_ = T_ log = logging.getLogger("red.cogs.mutes") From e965d3ee41b407121e72ad4560136c250640e4aa Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Mon, 19 Oct 2020 11:25:14 -0600 Subject: [PATCH 074/103] missed this docstring --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 90798b3282e..17f2d87603d 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -784,7 +784,7 @@ async def channel_mute( `` is a space separated list of usernames, ID's, or mentions. `[time_and_reason]` is the time to mute for and reason. Time is any valid time length such as `30 minutes` or `2 days`. If nothing - is provided the mute will be indefinite. + is provided the mute will use the set default time or indefinite if not set. Examples: `[p]mutechannel @member1 @member2 spam 5 hours` From 28fd0d02706391680a773fb98630317d2ac8a354 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 20 Oct 2020 11:13:15 -0600 Subject: [PATCH 075/103] Put voiceban and voiceunban back in mod where it's more appropriate --- redbot/cogs/mod/kickban.py | 82 +++++++++++++++++++++++++++++++++ redbot/cogs/mutes/voicemutes.py | 82 --------------------------------- 2 files changed, 82 insertions(+), 82 deletions(-) diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py index c35a2abc5a9..bf2bb05e126 100644 --- a/redbot/cogs/mod/kickban.py +++ b/redbot/cogs/mod/kickban.py @@ -680,6 +680,88 @@ async def voicekick( channel=case_channel, ) + @commands.command() + @commands.guild_only() + @checks.admin_or_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 ( + 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 + audit_reason = get_audit_reason(ctx.author, reason) + if needs_unmute and needs_undeafen: + await user.edit(mute=False, deafen=False, reason=audit_reason) + elif needs_unmute: + await user.edit(mute=False, reason=audit_reason) + elif needs_undeafen: + await user.edit(deafen=False, reason=audit_reason) + else: + await ctx.send(_("That user isn't muted or deafened by the server.")) + return + + guild = ctx.guild + author = ctx.author + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceunban", + user, + author, + reason, + until=None, + channel=None, + ) + await ctx.send(_("User is now allowed to speak and listen in voice channels.")) + + @commands.command() + @commands.guild_only() + @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: 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 + audit_reason = get_audit_reason(ctx.author, reason) + author = ctx.author + guild = ctx.guild + if needs_mute and needs_deafen: + await user.edit(mute=True, deafen=True, reason=audit_reason) + elif needs_mute: + await user.edit(mute=True, reason=audit_reason) + elif needs_deafen: + await user.edit(deafen=True, reason=audit_reason) + else: + await ctx.send(_("That user is already muted and deafened server-wide.")) + return + + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at, + "voiceban", + user, + author, + reason, + until=None, + channel=None, + ) + await ctx.send(_("User has been banned from speaking or listening in voice channels.")) + @commands.command() @commands.guild_only() @commands.bot_has_permissions(ban_members=True) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 8083722c4ac..d597cf29d16 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -56,88 +56,6 @@ async def _voice_perm_check( return False return True - @commands.command() - @commands.guild_only() - @checks.admin_or_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 ( - 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 - audit_reason = get_audit_reason(ctx.author, reason) - if needs_unmute and needs_undeafen: - await user.edit(mute=False, deafen=False, reason=audit_reason) - elif needs_unmute: - await user.edit(mute=False, reason=audit_reason) - elif needs_undeafen: - await user.edit(deafen=False, reason=audit_reason) - else: - await ctx.send(_("That user isn't muted or deafened by the server.")) - return - - guild = ctx.guild - author = ctx.author - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "voiceunban", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User is now allowed to speak and listen in voice channels.")) - - @commands.command() - @commands.guild_only() - @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: 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 - audit_reason = get_audit_reason(ctx.author, reason) - author = ctx.author - guild = ctx.guild - if needs_mute and needs_deafen: - await user.edit(mute=True, deafen=True, reason=audit_reason) - elif needs_mute: - await user.edit(mute=True, reason=audit_reason) - elif needs_deafen: - await user.edit(deafen=True, reason=audit_reason) - else: - await ctx.send(_("That user is already muted and deafened server-wide.")) - return - - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at, - "voiceban", - user, - author, - reason, - until=None, - channel=None, - ) - await ctx.send(_("User has been banned from speaking or listening in voice channels.")) - @commands.command(name="voicemute") @commands.guild_only() async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): From 37cfec1632d9cc4e23c239d90909ec46099e30e9 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 20 Oct 2020 16:43:49 -0600 Subject: [PATCH 076/103] Address review 2 electric boogaloo --- redbot/cogs/mutes/mutes.py | 29 +++++++++++++++++++++-------- redbot/cogs/mutes/voicemutes.py | 19 ++++++++++--------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 17f2d87603d..7e4228cd933 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -56,7 +56,7 @@ class CompositeMetaClass(type(commands.Cog), type(ABC)): class Mutes(VoiceMutes, commands.Cog, metaclass=CompositeMetaClass): """ - Mute users temporarily or indefinitely + Mute users temporarily or indefinitely. """ def __init__(self, bot: Red): @@ -345,6 +345,8 @@ async def on_guild_channel_update( """ This handles manually removing overwrites for a user that has been muted """ + if await self.bot.cog_disabled_in_guild(self, after.guild): + return if after.id in self._channel_mutes: before_perms: Dict[int, Dict[str, Optional[bool]]] = { o.id: {name: attr for name, attr in p} for o, p in before.overwrites.items() @@ -404,13 +406,24 @@ async def muteset(self, ctx: commands.Context): pass @muteset.command(name="notification") - @checks.mod_or_permissions(manage_messages=True) - async def notification_channel_set(self, ctx: commands.Context, channel: discord.TextChannel): - """Set The notification channel for automatic unmute issues.""" - await self.config.guild(ctx.guild).notification_channel.set(channel.id) - await ctx.send( - _("I will post unmute issues in {channel}.").format(channel=channel.mention) - ) + @checks.admin_or_permissions(manage_channels=True) + async def notification_channel_set( + self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None + ): + """ + Set the notification channel for automatic unmute issues. + + If no channel is provided this will be cleared and notifications + about issues when unmuting users will not be sent anywhere. + """ + if channel is None: + await self.config.guild(ctx.guild).notification_channel.clear() + await ctx.send(_("Notification channel for unmute issues has been cleard.")) + else: + await self.config.guild(ctx.guild).notification_channel.set(channel.id) + await ctx.send( + _("I will post unmute issues in {channel}.").format(channel=channel.mention) + ) @muteset.command(name="role") @checks.admin_or_permissions(manage_roles=True) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index d597cf29d16..ed47397818b 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -1,4 +1,5 @@ from typing import Optional +from datetime import timezone from .abc import MixinMeta import discord @@ -73,13 +74,13 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso 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 = await self.channel_mute_user(guild, channel, author, user, audit_reason) if success["success"]: await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "vmute", user, author, @@ -88,7 +89,7 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso channel=channel, ) await ctx.send( - _("Muted {user} in channel {channel.name}").format(user=user, channel=channel) + _("Muted {user} in channel {channel.name}.").format(user=user, channel=channel) ) try: if channel.permissions_for(ctx.me).move_members: @@ -102,7 +103,7 @@ async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reaso ) ) else: - await ctx.send(issue) + await ctx.send(success["reason"]) @commands.command(name="voiceunmute") @commands.guild_only() @@ -123,13 +124,13 @@ async def unmute_voice( 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 = await self.channel_unmute_user(guild, channel, author, user, audit_reason) - if success: + if success["success"]: await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "vunmute", user, author, @@ -138,7 +139,7 @@ async def unmute_voice( channel=channel, ) await ctx.send( - _("Unmuted {user} in channel {channel.name}").format(user=user, channel=channel) + _("Unmuted {user} in channel {channel.name}.").format(user=user, channel=channel) ) try: if channel.permissions_for(ctx.me).move_members: @@ -152,4 +153,4 @@ async def unmute_voice( ) ) else: - await ctx.send(_("Unmute failed. Reason: {}").format(message)) + await ctx.send(_("Unmute failed. Reason: {}").format(success["reason"])) From ab94c4627dcc11de6a1f638edecdc92f24faaa87 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 20 Oct 2020 23:52:21 -0600 Subject: [PATCH 077/103] Make voicemutes use same methods as channel mute --- redbot/cogs/mod/kickban.py | 4 +- redbot/cogs/mutes/mutes.py | 84 +++++++++--- redbot/cogs/mutes/voicemutes.py | 233 ++++++++++++++++++++------------ 3 files changed, 220 insertions(+), 101 deletions(-) diff --git a/redbot/cogs/mod/kickban.py b/redbot/cogs/mod/kickban.py index bf2bb05e126..56737b0b303 100644 --- a/redbot/cogs/mod/kickban.py +++ b/redbot/cogs/mod/kickban.py @@ -711,7 +711,7 @@ async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reaso await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "voiceunban", user, author, @@ -752,7 +752,7 @@ async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: await modlog.create_case( self.bot, guild, - ctx.message.created_at, + ctx.message.created_at.replace(tzinfo=timezone.utc), "voiceban", user, author, diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 7e4228cd933..ea17e5c47de 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -37,6 +37,9 @@ "left_guild": _("The user has left the server while applying an overwrite."), "unknown_channel": _("The channel I tried to mute the user in isn't found."), "role_missing": _("The mute role no longer exists."), + "voice_mute_permission": _( + "Because I don't have the Move Members permission, this will take into effect when the user rejoins." + ), } _ = T_ @@ -386,6 +389,8 @@ async def on_guild_channel_update( @commands.Cog.listener() async def on_member_join(self, member: discord.Member): guild = member.guild + if await self.bot.cog_disabled_in_guild(self, guild): + return mute_role = await self.config.guild(guild).mute_role() if not mute_role: return @@ -960,6 +965,7 @@ async def unmute_channel( guild = ctx.guild audit_reason = get_audit_reason(author, reason) success_list = [] + issue_list = [] for user in users: success = await self.channel_unmute_user( guild, channel, author, user, audit_reason @@ -979,9 +985,9 @@ async def unmute_channel( channel=channel, ) else: - return await ctx.send(success["reason"]) + issue_list.append((user, success["reason"])) if success_list: - if self._channel_mutes[channel.id]: + if channel.id in self._channel_mutes and self._channel_mutes[channel.id]: await self.config.channel(channel).muted_users.set( self._channel_mutes[channel.id] ) @@ -992,6 +998,11 @@ async def unmute_channel( users=humanize_list([f"{u}" for u in success_list]) ) ) + if issue_list: + message = _( + "{users} could not be unmuted in this channels. " "Would you like to see why?" + ).format(users=humanize_list([f"{u}" for u, x in issue_list])) + await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) async def mute_user( self, @@ -1027,9 +1038,9 @@ async def mute_user( await user.add_roles(role, reason=reason) await self.config.guild(guild).muted_users.set(self._server_mutes[guild.id]) except discord.errors.Forbidden: - del self._server_mutes[guild.id][ - user.id - ] # this is here so we don't have a bad cache + if guild.id in self._server_mutes and user.id in self._server_mutes[guild.id]: + del self._server_mutes[guild.id][user.id] + # this is here so we don't have a bad cache return False, _(MUTE_UNMUTE_ISSUES["permissions_issue"]) return True, None else: @@ -1123,10 +1134,14 @@ async def channel_mute_user( } new_overs: dict = {} - if not isinstance(channel, discord.TextChannel): - new_overs.update(speak=False) - if not isinstance(channel, discord.VoiceChannel): - new_overs.update(send_messages=False, add_reactions=False) + move_channel = False + new_overs.update(send_messages=False, add_reactions=False, speak=False) + send_reason = None + if user.voice and user.voice.channel: + if channel.permissions_for(guild.me).move_members: + move_channel = True + else: + send_reason = _(MUTE_UNMUTE_ISSUES["voice_mute_permission"]) if not await self.is_allowed_by_hierarchy(guild, author, user): return { @@ -1137,17 +1152,18 @@ async def channel_mute_user( old_overs = {k: getattr(overwrites, k) for k in new_overs} overwrites.update(**new_overs) - try: - if channel.id not in self._channel_mutes: - self._channel_mutes[channel.id] = {} + if channel.id not in self._channel_mutes: + self._channel_mutes[channel.id] = {} self._channel_mutes[channel.id][user.id] = { "author": author.id, "member": user.id, "until": until.timestamp() if until else None, } + try: await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: - del self._channel_mutes[channel.id][user.id] + if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: + del self._channel_mutes[channel.id][user.id] return { "success": False, "channel": channel, @@ -1155,20 +1171,40 @@ async def channel_mute_user( } except discord.NotFound as e: if e.code == 10003: - del self._channel_mutes[channel.id][user.id] + if ( + channel.id in self._channel_mutes + and user.id in self._channel_mutes[channel.id] + ): + del self._channel_mutes[channel.id][user.id] return { "success": False, "channel": channel, "reason": _(MUTE_UNMUTE_ISSUES["unknown_channel"]), } elif e.code == 10009: - del self._channel_mutes[channel.id][user.id] + if ( + channel.id in self._channel_mutes + and user.id in self._channel_mutes[channel.id] + ): + del self._channel_mutes[channel.id][user.id] return { "success": False, "channel": channel, "reason": _(MUTE_UNMUTE_ISSUES["left_guild"]), } - return {"success": True, "channel": channel, "old_overs": old_overs} + if move_channel: + try: + await user.move_to(channel) + except discord.HTTPException: + # catch all discord errors because the result will be the same + # we successfully muted by this point but can't move the user + return { + "success": True, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["voice_mute_permission"]), + "old_overs": old_overs, + } + return {"success": True, "channel": channel, "old_overs": old_overs, "reason": send_reason} async def channel_unmute_user( self, @@ -1182,12 +1218,17 @@ async def channel_unmute_user( overwrites = channel.overwrites_for(user) perms_cache = await self.config.member(user).perms_cache() + move_channel = False _temp = None # used to keep the cache incase we have permissions issues if channel.id in perms_cache: old_values = perms_cache[channel.id] else: old_values = {"send_messages": None, "add_reactions": None, "speak": None} + if user.voice and user.voice.channel: + if channel.permissions_for(guild.me).move_members: + move_channel = True + if not await self.is_allowed_by_hierarchy(guild, author, user): return { "success": False, @@ -1231,4 +1272,15 @@ async def channel_unmute_user( "channel": channel, "reason": _(MUTE_UNMUTE_ISSUES["left_guild"]), } + if move_channel: + try: + await user.move_to(channel) + except discord.HTTPException: + # catch all discord errors because the result will be the same + # we successfully muted by this point but can't move the user + return { + "success": True, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["voice_mute_permission"]), + } return {"success": True, "channel": channel, "reason": None} diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index ed47397818b..6a5fa1ae716 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -1,12 +1,19 @@ from typing import Optional -from datetime import timezone +from datetime import timezone, timedelta, datetime from .abc import MixinMeta import discord from redbot.core import commands, checks, i18n, modlog -from redbot.core.utils.chat_formatting import format_perms_list +from redbot.core.utils.chat_formatting import ( + humanize_timedelta, + humanize_list, + pagify, + format_perms_list, +) from redbot.core.utils.mod import get_audit_reason +from .converters import MuteTime + _ = i18n.Translator("Mutes", __file__) @@ -59,98 +66,158 @@ async def _voice_perm_check( @commands.command(name="voicemute") @commands.guild_only() - async def voice_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None): + async def voice_mute( + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + time_and_reason: MuteTime = {}, + ): """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 - channel = user_voice_state.channel - audit_reason = get_audit_reason(author, reason) - - success = await self.channel_mute_user(guild, channel, author, user, audit_reason) - - if success["success"]: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "vmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send( - _("Muted {user} in channel {channel.name}.").format(user=user, channel=channel) - ) - try: - if channel.permissions_for(ctx.me).move_members: - await user.move_to(channel) + if not users: + return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot mute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot mute yourself.")) + async with ctx.typing(): + success_list = [] + issue_list = [] + for user in users: + user_voice_state = user.voice + + if ( + await self._voice_perm_check( + ctx, user_voice_state, mute_members=True, manage_channels=True + ) + is False + ): + continue + duration = time_and_reason.get("duration", {}) + reason = time_and_reason.get("reason", None) + until = None + time = "" + if duration: + until = datetime.now(timezone.utc) + timedelta(**duration) + time = _(" for {duration}").format( + duration=humanize_timedelta(timedelta=timedelta(**duration)) + ) else: - raise RuntimeError - except (discord.Forbidden, RuntimeError): - await ctx.send( - _( - "Because I don't have the Move Members permission, this will take into effect when the user rejoins." + default_duration = await self.config.guild(ctx.guild).default_time() + if default_duration: + until = datetime.now(timezone.utc) + timedelta(**default_duration) + time = _(" for {duration}").format( + duration=humanize_timedelta(timedelta=timedelta(**default_duration)) + ) + guild = ctx.guild + author = ctx.author + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) + + success = await self.channel_mute_user( + guild, channel, author, user, until, audit_reason + ) + + if success["success"]: + if "reason" in success and success["reason"]: + issue_list.append((user, success["reason"])) + else: + success_list.append(user) + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "vmute", + user, + author, + reason, + until=None, + channel=channel, ) + async with self.config.member(user).perms_cache() as cache: + cache[channel.id] = success["old_overs"] + else: + issue_list.append((user, success["reason"])) + + if success_list: + msg = _("{users} has been muted in this channel{time}.") + if len(success_list) > 1: + msg = _("{users} have been muted in this channel{time}.") + await ctx.send( + msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) ) - else: - await ctx.send(success["reason"]) + if issue_list: + msg = _("The following users could not be muted\n") + for user, issue in issue_list: + msg += f"{user}: {issue}\n" + await ctx.send_interactive(pagify(msg)) @commands.command(name="voiceunmute") @commands.guild_only() async def unmute_voice( - self, ctx: commands.Context, user: discord.Member, *, reason: str = None + self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: Optional[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 - channel = user_voice_state.channel - audit_reason = get_audit_reason(author, reason) - - success = await self.channel_unmute_user(guild, channel, author, user, audit_reason) - - if success["success"]: - await modlog.create_case( - self.bot, - guild, - ctx.message.created_at.replace(tzinfo=timezone.utc), - "vunmute", - user, - author, - reason, - until=None, - channel=channel, - ) - await ctx.send( - _("Unmuted {user} in channel {channel.name}.").format(user=user, channel=channel) - ) - try: - if channel.permissions_for(ctx.me).move_members: - await user.move_to(channel) + if not users: + return await ctx.send_help() + if ctx.me in users: + return await ctx.send(_("You cannot unmute me.")) + if ctx.author in users: + return await ctx.send(_("You cannot unmute yourself.")) + async with ctx.typing(): + issue_list = [] + success_list = [] + for user in users: + user_voice_state = user.voice + if ( + await self._voice_perm_check( + ctx, user_voice_state, mute_members=True, manage_channels=True + ) + is False + ): + continue + guild = ctx.guild + author = ctx.author + channel = user_voice_state.channel + audit_reason = get_audit_reason(author, reason) + + success = await self.channel_unmute_user( + guild, channel, author, user, audit_reason + ) + + if success["success"]: + if "reason" in success and success["reason"]: + issue_list.append((user, success["reason"])) + else: + success_list.append(user) + await modlog.create_case( + self.bot, + guild, + ctx.message.created_at.replace(tzinfo=timezone.utc), + "vunmute", + user, + author, + reason, + until=None, + channel=channel, + ) + else: + issue_list.append((user, success["reason"])) + if success_list: + if channel.id in self._channel_mutes and self._channel_mutes[channel.id]: + await self.config.channel(channel).muted_users.set( + self._channel_mutes[channel.id] + ) else: - raise RuntimeError - except (discord.Forbidden, RuntimeError): + await self.config.channel(channel).muted_users.clear() await ctx.send( - _( - "Because I don't have the Move Members permission, this will take into effect when the user rejoins." + _("{users} unmuted in this channel.").format( + users=humanize_list([f"{u}" for u in success_list]) ) ) - else: - await ctx.send(_("Unmute failed. Reason: {}").format(success["reason"])) + if issue_list: + message = _( + "{users} could not be unmuted in this channels. " + "Would you like to see why?" + ).format(users=humanize_list([f"{u}" for u, x in issue_list])) + await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) From d84100bc66642e0eb88a7201f7297e294fb8c85f Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Tue, 20 Oct 2020 23:53:09 -0600 Subject: [PATCH 078/103] black --- redbot/cogs/mutes/voicemutes.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 6a5fa1ae716..f66a6c5161a 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -155,7 +155,11 @@ async def voice_mute( @commands.command(name="voiceunmute") @commands.guild_only() async def unmute_voice( - self, ctx: commands.Context, users: commands.Greedy[discord.Member], *, reason: Optional[str] = None + self, + ctx: commands.Context, + users: commands.Greedy[discord.Member], + *, + reason: Optional[str] = None, ): """Unmute a user in their current voice channel.""" if not users: @@ -217,7 +221,6 @@ async def unmute_voice( ) if issue_list: message = _( - "{users} could not be unmuted in this channels. " - "Would you like to see why?" + "{users} could not be unmuted in this channels. " "Would you like to see why?" ).format(users=humanize_list([f"{u}" for u, x in issue_list])) await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) From 1d7d0412e23b45116fe95dc434ed7c1a80f25d5a Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 21 Oct 2020 14:13:47 -0600 Subject: [PATCH 079/103] handle humanize_list doesn't accept generators --- redbot/cogs/mutes/mutes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index ea17e5c47de..edb7fcb43e3 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -751,7 +751,7 @@ async def mute( "{users} could not be muted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) + await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions @@ -936,7 +936,7 @@ async def unmute( "{users} could not be unmuted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) + await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="unmutechannel", aliases=["channelunmute"], usage=" [reason]") @@ -1002,7 +1002,7 @@ async def unmute_channel( message = _( "{users} could not be unmuted in this channels. " "Would you like to see why?" ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) + await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) async def mute_user( self, From a2a7ecc4deb66dd75ab17826267e70df70b8441c Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 21 Oct 2020 14:17:37 -0600 Subject: [PATCH 080/103] update voicemutes docstrings --- redbot/cogs/mutes/voicemutes.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index f66a6c5161a..0e5d0e1924d 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -73,7 +73,16 @@ async def voice_mute( *, time_and_reason: MuteTime = {}, ): - """Mute a user in their current voice channel.""" + """Mute a user in their current voice channel. + + `` is a space separated list of usernames, ID's, or mentions. + `[time_and_reason]` is the time to mute for and reason. Time is + any valid time length such as `30 minutes` or `2 days`. If nothing + is provided the mute will use the set default time or indefinite if not set. + + Examples: + `[p]voicemute @member1 @member2 spam 5 hours` + `[p]voicemute @member1 3 days`""" if not users: return await ctx.send_help() if ctx.me in users: @@ -161,7 +170,10 @@ async def unmute_voice( *, reason: Optional[str] = None, ): - """Unmute a user in their current voice channel.""" + """Unmute a user in their current voice channel. + + `` is a space separated list of usernames, ID's, or mentions. + `[reason]` is the reason for the unmute.""" if not users: return await ctx.send_help() if ctx.me in users: From a34a66ca4a4469de7e9335aec54641f6ed6de985 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 21 Oct 2020 14:30:46 -0600 Subject: [PATCH 081/103] make voiceperm check consistent with rest of error handling --- redbot/cogs/mutes/voicemutes.py | 44 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 0e5d0e1924d..9c25f81f701 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Tuple from datetime import timezone, timedelta, datetime from .abc import MixinMeta @@ -25,7 +25,7 @@ class VoiceMutes(MixinMeta): @staticmethod async def _voice_perm_check( ctx: commands.Context, user_voice_state: Optional[discord.VoiceState], **perms: bool - ) -> bool: + ) -> Tuple[bool, Optional[str]]: """Check if the bot and user have sufficient permissions for voicebans. This also verifies that the user's voice state and connected @@ -40,29 +40,30 @@ async def _voice_perm_check( """ 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 + return False, None 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( + return ( + False, _("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( + + return ( + False, _( "You must have the {perms} permission(s) in that user's channel to use this " "command." - ).format(perms=format_perms_list(required_perms)) + ).format(perms=format_perms_list(required_perms)), ) - return False - return True + return True, None @commands.command(name="voicemute") @commands.guild_only() @@ -94,13 +95,11 @@ async def voice_mute( issue_list = [] for user in users: user_voice_state = user.voice - - if ( - await self._voice_perm_check( - ctx, user_voice_state, mute_members=True, manage_channels=True - ) - is False - ): + can_move, perm_reason = await self._voice_perm_check( + ctx, user_voice_state, mute_members=True, manage_channels=True + ) + if not can_move: + issue_list.append((user, perm_reason)) continue duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) @@ -185,12 +184,11 @@ async def unmute_voice( success_list = [] for user in users: user_voice_state = user.voice - if ( - await self._voice_perm_check( - ctx, user_voice_state, mute_members=True, manage_channels=True - ) - is False - ): + can_move, perm_reason = await self._voice_perm_check( + ctx, user_voice_state, mute_members=True, manage_channels=True + ) + if not can_move: + issue_list.append((user, perm_reason)) continue guild = ctx.guild author = ctx.author From 55ef2744a6c96b0611474881899195a77b5674c8 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Wed, 21 Oct 2020 15:40:43 -0600 Subject: [PATCH 082/103] bleh --- redbot/cogs/mutes/mutes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index edb7fcb43e3..60c35dc468a 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -35,7 +35,7 @@ "lower than myself in the role hierarchy." ), "left_guild": _("The user has left the server while applying an overwrite."), - "unknown_channel": _("The channel I tried to mute the user in isn't found."), + "unknown_channel": _("The channel I tried to mute or unmute the user in isn't found."), "role_missing": _("The mute role no longer exists."), "voice_mute_permission": _( "Because I don't have the Move Members permission, this will take into effect when the user rejoins." @@ -1154,11 +1154,11 @@ async def channel_mute_user( overwrites.update(**new_overs) if channel.id not in self._channel_mutes: self._channel_mutes[channel.id] = {} - self._channel_mutes[channel.id][user.id] = { - "author": author.id, - "member": user.id, - "until": until.timestamp() if until else None, - } + self._channel_mutes[channel.id][user.id] = { + "author": author.id, + "member": user.id, + "until": until.timestamp() if until else None, + } try: await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: From f269521c2c796ad2d3498278457fd3a79c7efe72 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Thu, 22 Oct 2020 16:21:50 -0600 Subject: [PATCH 083/103] fix modlog case spam when overrides are in place --- redbot/cogs/mutes/mutes.py | 102 +++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 60c35dc468a..f01c617428a 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -220,6 +220,7 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): async def _handle_channel_unmutes(self): log.debug("Checking channel unmutes") + multiple_mutes = {} for c_id in self._channel_mutes: channel = self.bot.get_channel(c_id) if channel is None or await self.bot.cog_disabled_in_guild(self, channel.guild): @@ -235,13 +236,81 @@ async def _handle_channel_unmutes(self): - datetime.now(timezone.utc).timestamp() ) if time_to_unmute < 120.0: - self._unmute_tasks[f"{c_id}{u_id}"] = asyncio.create_task( - self._auto_channel_unmute_user( - channel, self._channel_mutes[channel.id][u_id] + if channel.guild.id not in multiple_mutes: + multiple_mutes[channel.guild.id] = {} + if u_id not in multiple_mutes[channel.guild.id]: + multiple_mutes[channel.guild.id][u_id] = { + channel.id: self._channel_mutes[channel.id][u_id] + } + else: + multiple_mutes[channel.guild.id][u_id][channel.id] = self._channel_mutes[ + channel.id + ][u_id] + + for guild_id, users in multiple_mutes.items(): + guild = self.bot.get_guild(guild_id) + for user, channels in users.items(): + if len(channels) > 1: + member = guild.get_member(user) + + for channel, mute_data in channels.items(): + author = guild.get_member(mute_data["author"]) + self._unmute_tasks[f"{channel}{user}"] = asyncio.create_task( + self._auto_channel_unmute_user_silent( + guild.get_channel(channel), mute_data + ) ) + await modlog.create_case( + self.bot, + guild, + datetime.now(timezone.utc), + "sunmute", + member, + author, + _("Automatic unmute"), + until=None, ) + else: + for channel, mute_data in channels.items(): + self._unmute_tasks[f"{channel}{user}"] = asyncio.create_task( + self._auto_channel_unmute_user(guild.get_channel(channel), mute_data) + ) + + async def _auto_channel_unmute_user_silent( + self, channel: discord.abc.GuildChannel, data: dict + ): + """This is meant for mass channel unmute without sending the modlog case""" + delay = data["until"] - datetime.now(timezone.utc).timestamp() + if delay < 1: + delay = 0 + await asyncio.sleep(delay) + try: + member = channel.guild.get_member(data["member"]) + author = channel.guild.get_member(data["author"]) + if not member: + return + success = await self.channel_unmute_user( + channel.guild, channel, author, member, _("Automatic unmute") + ) + if success["success"]: + async with self.config.channel(channel).muted_users() as muted_users: + if str(member.id) in muted_users: + del muted_users[str(member.id)] + else: + chan_id = await self.config.guild(channel.guild).notification_channel() + notification_channel = channel.guild.get_channel(chan_id) + if not notification_channel: + return + await notification_channel.send( + _( + "I am unable to mute {user} in {channel} for the following reason:\n{reason}" + ).format(user=member, channel=channel.mention, reason=success["reason"]) + ) + except discord.errors.Forbidden: + return - async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: dict): + async def _auto_channel_unmute_user(self, channel: discord.abc.GuildChannel, data: dict): + """This is meant to unmute a user in individual channels""" delay = data["until"] - datetime.now(timezone.utc).timestamp() if delay < 1: delay = 0 @@ -261,7 +330,7 @@ async def _auto_channel_unmute_user(self, channel: discord.TextChannel, data: di datetime.now(timezone.utc), "cunmute", member, - author, + channel.guild.me, _("Automatic unmute"), until=None, ) @@ -410,6 +479,29 @@ async def muteset(self, ctx: commands.Context): """Mute settings.""" pass + @muteset.command(name="settings", aliases=["showsettings"]) + @checks.mod_or_permissions(manage_channels=True) + async def show_mutes_settings(self, ctx: commands.Context): + """ + Shows the current mute settings for this guild. + """ + data = await self.config.guild(ctx.guild).all() + + mute_role = ctx.guild.get_role(data["mute_role"]) + notification_channel = ctx.guild.get_channel(data["notification_channel"]) + sent_instructions = str(data["sent_instructions"]) + default_time = timedelta(**data["default_time"]) + msg = _( + "Mute Role: {role}\nNotificaiton Channel: {channel}\n" + "Sent Instructions: {instructions}\nDefault Time: {time}" + ).format( + role=mute_role.mention if mute_role else _("None"), + channel=notification_channel.mention if notification_channel else _("None"), + instructions=sent_instructions, + time=humanize_timedelta(default_time) if default_time else _("None"), + ) + await ctx.maybe_send_embed(msg) + @muteset.command(name="notification") @checks.admin_or_permissions(manage_channels=True) async def notification_channel_set( From b518270b870eec85faea6208f005654a950cf1d1 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 23 Oct 2020 18:52:29 -0600 Subject: [PATCH 084/103] --- redbot/cogs/mutes/mutes.py | 401 ++++++++++++++++++-------------- redbot/cogs/mutes/voicemutes.py | 4 +- 2 files changed, 231 insertions(+), 174 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f01c617428a..265dfedea3e 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -72,6 +72,7 @@ def __init__(self, bot: Red): "muted_users": {}, "default_time": {}, } + self.config.register_global(force_role_mutes=False) self.config.register_guild(**default_guild) self.config.register_member(perms_cache={}) self.config.register_channel(muted_users={}) @@ -176,7 +177,11 @@ async def _handle_server_unmutes(self): - datetime.now(timezone.utc).timestamp() ) if time_to_unmute < 60: - self._unmute_tasks[f"{g_id}{u_id}"] = asyncio.create_task( + task_name = f"server-unmute-{g_id}-{u_id}" + if task_name in self._unmute_tasks: + continue + log.debug(f"Creating task: {task_name}") + self._unmute_tasks[task_name] = asyncio.create_task( self._auto_unmute_user(guild, self._server_mutes[guild.id][u_id]) ) @@ -185,38 +190,39 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): if delay < 1: delay = 0 await asyncio.sleep(delay) - try: - member = guild.get_member(data["member"]) - author = guild.get_member(data["author"]) - if not member: - return - success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) - if success: - await modlog.create_case( - self.bot, - guild, - datetime.now(timezone.utc), - "sunmute", - member, - author, - _("Automatic unmute"), - until=None, - ) - async with self.config.guild(guild).muted_users() as muted_users: - if str(member.id) in muted_users: - del muted_users[str(member.id)] - else: - chan_id = await self.config.guild(guild).notification_channel() - notification_channel = guild.get_channel(chan_id) - if not notification_channel: - return - await notification_channel.send( - _("I am unable to unmute {user} for the following reason:\n{reason}").format( - user=member, reason=message - ) - ) - except discord.errors.Forbidden: + + member = guild.get_member(data["member"]) + author = guild.get_member(data["author"]) + if not member: return + success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) + if success: + await modlog.create_case( + self.bot, + guild, + datetime.now(timezone.utc), + "sunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + async with self.config.guild(guild).muted_users() as muted_users: + if str(member.id) in muted_users: + del muted_users[str(member.id)] + else: + chan_id = await self.config.guild(guild).notification_channel() + notification_channel = guild.get_channel(chan_id) + if not notification_channel: + return + try: + error_msg = _( + "I am unable to unmute {user} for the following reason:\n{reason}" + ).format(user=member, reason=message) + await notification_channel.send(error_msg) + except discord.errors.Forbidden: + log.info(error_msg) + return async def _handle_channel_unmutes(self): log.debug("Checking channel unmutes") @@ -251,104 +257,124 @@ async def _handle_channel_unmutes(self): guild = self.bot.get_guild(guild_id) for user, channels in users.items(): if len(channels) > 1: + task_name = f"server-unmute-channels-{user}" + if task_name in self._unmute_tasks: + continue + log.debug(f"Creating task: {task_name}") member = guild.get_member(user) - - for channel, mute_data in channels.items(): - author = guild.get_member(mute_data["author"]) - self._unmute_tasks[f"{channel}{user}"] = asyncio.create_task( - self._auto_channel_unmute_user_silent( - guild.get_channel(channel), mute_data - ) - ) - await modlog.create_case( - self.bot, - guild, - datetime.now(timezone.utc), - "sunmute", - member, - author, - _("Automatic unmute"), - until=None, + self._unmute_tasks[task_name] = asyncio.create_task( + self._auto_channel_unmute_user_2(member, guild, channels) ) + else: for channel, mute_data in channels.items(): - self._unmute_tasks[f"{channel}{user}"] = asyncio.create_task( + task_name = f"channel-unmute-{channel}-{user}" + log.debug(f"Creating task: {task_name}") + if task_name in self._unmute_tasks: + continue + self._unmute_tasks[task_name] = asyncio.create_task( self._auto_channel_unmute_user(guild.get_channel(channel), mute_data) ) - async def _auto_channel_unmute_user_silent( - self, channel: discord.abc.GuildChannel, data: dict + async def _auto_channel_unmute_user_2( + self, member: discord.Member, guild: discord.Guild, channels: Dict[int, dict] ): - """This is meant for mass channel unmute without sending the modlog case""" - delay = data["until"] - datetime.now(timezone.utc).timestamp() - if delay < 1: - delay = 0 - await asyncio.sleep(delay) - try: - member = channel.guild.get_member(data["member"]) - author = channel.guild.get_member(data["author"]) - if not member: - return - success = await self.channel_unmute_user( - channel.guild, channel, author, member, _("Automatic unmute") + + tasks = [] + for channel, mute_data in channels.items(): + author = guild.get_member(mute_data["author"]) + tasks.append( + self._auto_channel_unmute_user(guild.get_channel(channel), mute_data, False) ) - if success["success"]: - async with self.config.channel(channel).muted_users() as muted_users: - if str(member.id) in muted_users: - del muted_users[str(member.id)] - else: - chan_id = await self.config.guild(channel.guild).notification_channel() - notification_channel = channel.guild.get_channel(chan_id) - if not notification_channel: - return - await notification_channel.send( - _( - "I am unable to mute {user} in {channel} for the following reason:\n{reason}" - ).format(user=member, channel=channel.mention, reason=success["reason"]) + result = await asyncio.gather(*tasks) + await modlog.create_case( + self.bot, + guild, + datetime.now(timezone.utc), + "sunmute", + member, + author, + _("Automatic unmute"), + until=None, + ) + if any(result): + reasons = {} + for member, channel, reason in result: + if reason not in reasons: + reasons[reason] = [channel] + else: + reason[reason].append(channel) + error_msg = _("{member} could not be unmuted for the following reasons:\n").format( + member=member + ) + for reason, channel_list in reasons: + error_msg += _("{reason} In the following channels: {channels}\n").format( + reason=reason, + channels=humanize_list([c.mention for c in channel_list]), ) - except discord.errors.Forbidden: - return + chan_id = await self.config.guild(guild).notification_channel() + notification_channel = guild.get_channel(chan_id) + if notification_channel is None: + return None + try: + await notification_channel.send(error_msg) + except discord.errors.Forbidden: + log.info(error_msg) + return None - async def _auto_channel_unmute_user(self, channel: discord.abc.GuildChannel, data: dict): + async def _auto_channel_unmute_user( + self, channel: discord.abc.GuildChannel, data: dict, create_case: bool = True + ) -> Optional[Tuple[discord.Member, discord.abc.GuildChannel, str]]: """This is meant to unmute a user in individual channels""" delay = data["until"] - datetime.now(timezone.utc).timestamp() if delay < 1: delay = 0 await asyncio.sleep(delay) - try: - member = channel.guild.get_member(data["member"]) - author = channel.guild.get_member(data["author"]) - if not member: - return - success = await self.channel_unmute_user( - channel.guild, channel, author, member, _("Automatic unmute") - ) - if success["success"]: + member = channel.guild.get_member(data["member"]) + author = channel.guild.get_member(data["author"]) + if not member: + return None + success = await self.channel_unmute_user( + channel.guild, channel, author, member, _("Automatic unmute") + ) + if success["success"]: + if create_case: + if isinstance(channel, discord.VoiceChannel): + unmute_type = "vunmute" + else: + unmute_type = "cunmute" await modlog.create_case( self.bot, channel.guild, datetime.now(timezone.utc), - "cunmute", + unmute_type, member, channel.guild.me, _("Automatic unmute"), until=None, + channel=channel, ) - async with self.config.channel(channel).muted_users() as muted_users: - if str(member.id) in muted_users: - del muted_users[str(member.id)] - else: + async with self.config.channel(channel).muted_users() as muted_users: + if str(member.id) in muted_users: + del muted_users[str(member.id)] + return None + else: + error_msg = _( + "I am unable to unmute {user} in {channel} for the following reason:\n{reason}" + ).format(user=member, channel=channel.mention, reason=success["reason"]) + if create_case: chan_id = await self.config.guild(channel.guild).notification_channel() notification_channel = channel.guild.get_channel(chan_id) if not notification_channel: - return - await notification_channel.send( - _( - "I am unable to mute {user} in {channel} for the following reason:\n{reason}" - ).format(user=member, channel=channel.mention, reason=success["reason"]) - ) - except discord.errors.Forbidden: - return + return None + try: + + await notification_channel.send(error_msg) + except discord.errors.Forbidden: + log.info(error_msg) + return None + else: + return (member, channel, success["reason"]) @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): @@ -479,6 +505,18 @@ async def muteset(self, ctx: commands.Context): """Mute settings.""" pass + @muteset.command(name="forcerole") + @commands.is_owner() + async def force_role_mutes(self, ctx: commands.Context, force_role_mutes: bool): + """ + Whether or not to force role only mutes on the bot + """ + await self.config.force_role_mutes.set(force_role_mutes) + if force_role_mutes: + await ctx.send(_("Okay I will enforce role mutes before muting users.")) + else: + await ctx.send(_("Okay I will allow channel overwrites for muting users.")) + @muteset.command(name="settings", aliases=["showsettings"]) @checks.mod_or_permissions(manage_channels=True) async def show_mutes_settings(self, ctx: commands.Context): @@ -489,15 +527,12 @@ async def show_mutes_settings(self, ctx: commands.Context): mute_role = ctx.guild.get_role(data["mute_role"]) notification_channel = ctx.guild.get_channel(data["notification_channel"]) - sent_instructions = str(data["sent_instructions"]) default_time = timedelta(**data["default_time"]) msg = _( - "Mute Role: {role}\nNotificaiton Channel: {channel}\n" - "Sent Instructions: {instructions}\nDefault Time: {time}" + "Mute Role: {role}\nNotification Channel: {channel}\n" "Default Time: {time}" ).format( role=mute_role.mention if mute_role else _("None"), channel=notification_channel.mention if notification_channel else _("None"), - instructions=sent_instructions, time=humanize_timedelta(default_time) if default_time else _("None"), ) await ctx.maybe_send_embed(msg) @@ -639,20 +674,24 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: This explains to the user whether or not mutes are setup correctly for automatic unmutes. """ + command_1 = f"{ctx.clean_prefix}muteset role" + command_2 = f"{ctx.clean_prefix}muteset makerole" + msg = _( + "This server does not have a mute role setup, " + " You can setup a mute role with `{command_1}` or" + "`{command_2}` if you just want a basic role created setup.\n\n" + ).format(command_1=command_1, command_2=command_2) mute_role_id = await self.config.guild(ctx.guild).mute_role() mute_role = ctx.guild.get_role(mute_role_id) sent_instructions = await self.config.guild(ctx.guild).sent_instructions() + force_role_mutes = await self.config.force_role_mutes() + if force_role_mutes and not mute_role: + await ctx.send(msg) + return False if mute_role or sent_instructions: return True else: - command_1 = f"{ctx.clean_prefix}muteset role" - command_2 = f"{ctx.clean_prefix}muteset makerole" - msg = _( - "This server does not have a mute role setup, " - "are you sure you want to continue with channel " - "overwrites? You can setup a mute role with `{command_1}` or" - "`{command_2}` if you just want a basic role created setup.)\n\n" - ).format(command_1=command_1, command_2=command_2) + msg += _("are you sure you want to continue with channel overwrites?") can_react = ctx.channel.permissions_for(ctx.me).add_reactions if can_react: msg += _( @@ -790,6 +829,7 @@ async def mute( return await ctx.send(_("You cannot mute yourself.")) if not await self._check_for_mute_role(ctx): return + mute_role = await self.config.guild(ctx.guild).mute_role() async with ctx.typing(): duration = time_and_reason.get("duration", {}) reason = time_and_reason.get("reason", None) @@ -829,21 +869,28 @@ async def mute( until=until, channel=None, ) - if success_list: - if ctx.guild.id not in self._server_mutes: - self._server_mutes[ctx.guild.id] = {} - msg = _("{users} has been muted in this server{time}.") - if len(success_list) > 1: - msg = _("{users} have been muted in this server{time}.") - await ctx.send( - msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) - ) - if issue_list: + if success_list: + if ctx.guild.id not in self._server_mutes: + self._server_mutes[ctx.guild.id] = {} + msg = _("{users} has been muted in this server{time}.") + if len(success_list) > 1: + msg = _("{users} have been muted in this server{time}.") + await ctx.send( + msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) + ) + if issue_list: + if not mute_role: message = _( "{users} could not be muted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u, x in issue_list])) await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) + else: + message = _("{users} could not be muted. {reason}").format( + users=humanize_list([f"{u}" for u, x in issue_list]), + reason=humanize_list([x for u, x in issue_list]), + ) + await ctx.send_interactive(message) async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions @@ -952,18 +999,18 @@ async def channel_mute( else: issue_list.append((user, success["reason"])) - if success_list: - msg = _("{users} has been muted in this channel{time}.") - if len(success_list) > 1: - msg = _("{users} have been muted in this channel{time}.") - await channel.send( - msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) - ) - if issue_list: - msg = _("The following users could not be muted\n") - for user, issue in issue_list: - msg += f"{user}: {issue}\n" - await ctx.send_interactive(pagify(msg)) + if success_list: + msg = _("{users} has been muted in this channel{time}.") + if len(success_list) > 1: + msg = _("{users} have been muted in this channel{time}.") + await channel.send( + msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) + ) + if issue_list: + msg = _("The following users could not be muted\n") + for user, issue in issue_list: + msg += f"{user}: {issue}\n" + await ctx.send_interactive(pagify(msg)) @commands.command(usage=" [reason]") @commands.guild_only() @@ -988,6 +1035,7 @@ async def unmute( return await ctx.send(_("You cannot unmute yourself.")) if not await self._check_for_mute_role(ctx): return + mute_role = await self.config.guild(ctx.guild).mute_role() async with ctx.typing(): guild = ctx.guild author = ctx.author @@ -1011,24 +1059,31 @@ async def unmute( ) else: issue_list.append((user, issue)) - if success_list: - if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: - await self.config.guild(ctx.guild).muted_users.set( - self._server_mutes[ctx.guild.id] - ) - else: - await self.config.guild(ctx.guild).muted_users.clear() - await ctx.send( - _("{users} unmuted in this server.").format( - users=humanize_list([f"{u}" for u in success_list]) - ) + if success_list: + if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: + await self.config.guild(ctx.guild).muted_users.set( + self._server_mutes[ctx.guild.id] + ) + else: + await self.config.guild(ctx.guild).muted_users.clear() + await ctx.send( + _("{users} unmuted in this server.").format( + users=humanize_list([f"{u}" for u in success_list]) ) - if issue_list: + ) + if issue_list: + if not mute_role: message = _( "{users} could not be unmuted in some channels. " "Would you like to see which channels and why?" ).format(users=humanize_list([f"{u}" for u, x in issue_list])) await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) + else: + message = _("{users} could not be unmuted. {reason}").format( + users=humanize_list([f"{u}" for u, x in issue_list]), + reason=humanize_list([x for u, x in issue_list]), + ) + await ctx.send_interactive(message) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="unmutechannel", aliases=["channelunmute"], usage=" [reason]") @@ -1078,23 +1133,21 @@ async def unmute_channel( ) else: issue_list.append((user, success["reason"])) - if success_list: - if channel.id in self._channel_mutes and self._channel_mutes[channel.id]: - await self.config.channel(channel).muted_users.set( - self._channel_mutes[channel.id] - ) - else: - await self.config.channel(channel).muted_users.clear() - await ctx.send( - _("{users} unmuted in this channel.").format( - users=humanize_list([f"{u}" for u in success_list]) - ) + if success_list: + if channel.id in self._channel_mutes and self._channel_mutes[channel.id]: + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) + else: + await self.config.channel(channel).muted_users.clear() + await ctx.send( + _("{users} unmuted in this channel.").format( + users=humanize_list([f"{u}" for u in success_list]) ) - if issue_list: - message = _( - "{users} could not be unmuted in this channels. " "Would you like to see why?" - ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) + ) + if issue_list: + message = _( + "{users} could not be unmuted in this channels. " "Would you like to see why?" + ).format(users=humanize_list([f"{u}" for u, x in issue_list])) + await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) async def mute_user( self, @@ -1246,6 +1299,12 @@ async def channel_mute_user( overwrites.update(**new_overs) if channel.id not in self._channel_mutes: self._channel_mutes[channel.id] = {} + if user.id in self._channel_mutes[channel.id]: + return { + "success": False, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["already_muted"]), + } self._channel_mutes[channel.id][user.id] = { "author": author.id, "member": user.id, @@ -1311,7 +1370,6 @@ async def channel_unmute_user( perms_cache = await self.config.member(user).perms_cache() move_channel = False - _temp = None # used to keep the cache incase we have permissions issues if channel.id in perms_cache: old_values = perms_cache[channel.id] else: @@ -1330,8 +1388,13 @@ async def channel_unmute_user( overwrites.update(**old_values) if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: - _temp = copy(self._channel_mutes[channel.id][user.id]) del self._channel_mutes[channel.id][user.id] + elif user.id not in self._channel_mutes[channel.id]: + return { + "success": False, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["already_unmuted"]), + } try: if overwrites.is_empty(): await channel.set_permissions( @@ -1340,8 +1403,6 @@ async def channel_unmute_user( else: await channel.set_permissions(user, overwrite=overwrites, reason=reason) except discord.Forbidden: - if channel.id in self._channel_mutes and _temp: - self._channel_mutes[channel.id][user.id] = _temp return { "success": False, "channel": channel, @@ -1349,16 +1410,12 @@ async def channel_unmute_user( } except discord.NotFound as e: if e.code == 10003: - if channel.id in self._channel_mutes and _temp: - self._channel_mutes[channel.id][user.id] = _temp return { "success": False, "channel": channel, "reason": _(MUTE_UNMUTE_ISSUES["unknown_channel"]), } elif e.code == 10009: - if channel.id in self._channel_mutes and _temp: - self._channel_mutes[channel.id][user.id] = _temp return { "success": False, "channel": channel, diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 9c25f81f701..0e02e68204d 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -65,7 +65,7 @@ async def _voice_perm_check( ) return True, None - @commands.command(name="voicemute") + @commands.command(name="voicemute", usage=" [reason]") @commands.guild_only() async def voice_mute( self, @@ -160,7 +160,7 @@ async def voice_mute( msg += f"{user}: {issue}\n" await ctx.send_interactive(pagify(msg)) - @commands.command(name="voiceunmute") + @commands.command(name="voiceunmute", usage=" [reason]") @commands.guild_only() async def unmute_voice( self, From ad6e0b0f205478aae65a77b6a2bf2537e561d268 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 23 Oct 2020 18:53:12 -0600 Subject: [PATCH 085/103] bleck --- redbot/cogs/mutes/mutes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 265dfedea3e..c5264cb3d3f 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -677,10 +677,10 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: command_1 = f"{ctx.clean_prefix}muteset role" command_2 = f"{ctx.clean_prefix}muteset makerole" msg = _( - "This server does not have a mute role setup, " - " You can setup a mute role with `{command_1}` or" - "`{command_2}` if you just want a basic role created setup.\n\n" - ).format(command_1=command_1, command_2=command_2) + "This server does not have a mute role setup, " + " You can setup a mute role with `{command_1}` or" + "`{command_2}` if you just want a basic role created setup.\n\n" + ).format(command_1=command_1, command_2=command_2) mute_role_id = await self.config.guild(ctx.guild).mute_role() mute_role = ctx.guild.get_role(mute_role_id) sent_instructions = await self.config.guild(ctx.guild).sent_instructions() From c17c49fbf2d3a282b097ab307afe4d3359c2e75b Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Fri, 23 Oct 2020 19:26:19 -0600 Subject: [PATCH 086/103] use total_seconds() instead of a dict, sorry everyone already using this lmao --- redbot/cogs/mutes/converters.py | 9 ++++++--- redbot/cogs/mutes/mutes.py | 32 ++++++++++++++------------------ redbot/cogs/mutes/voicemutes.py | 14 ++++++++------ 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/redbot/cogs/mutes/converters.py b/redbot/cogs/mutes/converters.py index 5c8b47cb007..fae51088aa4 100644 --- a/redbot/cogs/mutes/converters.py +++ b/redbot/cogs/mutes/converters.py @@ -1,6 +1,7 @@ import logging import re from typing import Union, Dict +from datetime import timedelta from discord.ext.commands.converter import Converter from redbot.core import commands @@ -32,9 +33,11 @@ class MuteTime(Converter): to be used in multiple reponses """ - async def convert(self, ctx: commands.Context, argument: str) -> Dict[str, Union[dict, str]]: + async def convert( + self, ctx: commands.Context, argument: str + ) -> Dict[str, Union[timedelta, str, None]]: time_split = TIME_SPLIT.split(argument) - result: Dict[str, Union[dict, str]] = {} + result: Dict[str, Union[timedelta, str, None]] = {} if time_split: maybe_time = time_split[-1] else: @@ -47,6 +50,6 @@ async def convert(self, ctx: commands.Context, argument: str) -> Dict[str, Union if v: time_data[k] = int(v) if time_data: - result["duration"] = time_data + result["duration"] = timedelta(**time_data) result["reason"] = argument return result diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index c5264cb3d3f..4d439382cb6 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -527,7 +527,7 @@ async def show_mutes_settings(self, ctx: commands.Context): mute_role = ctx.guild.get_role(data["mute_role"]) notification_channel = ctx.guild.get_channel(data["notification_channel"]) - default_time = timedelta(**data["default_time"]) + default_time = timedelta(seconds=data["default_time"]) msg = _( "Mute Role: {role}\nNotification Channel: {channel}\n" "Default Time: {time}" ).format( @@ -662,10 +662,10 @@ async def default_mute_time(self, ctx: commands.Context, *, time: Optional[MuteT data = time.get("duration", {}) if not data: return await ctx.send(_("Please provide a valid time format.")) - await self.config.guild(ctx.guild).default_time.set(data) + await self.config.guild(ctx.guild).default_time.set(data.total_seconds()) await ctx.send( _("Default mute time set to {time}.").format( - time=humanize_timedelta(timedelta=timedelta(**data)) + time=humanize_timedelta(timedelta=data) ) ) @@ -831,21 +831,19 @@ async def mute( return mute_role = await self.config.guild(ctx.guild).mute_role() async with ctx.typing(): - duration = time_and_reason.get("duration", {}) + duration = time_and_reason.get("duration", None) reason = time_and_reason.get("reason", None) time = "" until = None if duration: - until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for {duration}").format( - duration=humanize_timedelta(timedelta=timedelta(**duration)) - ) + until = datetime.now(timezone.utc) + duration + time = _(" for {duration}").format(duration=humanize_timedelta(timedelta=duration)) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: - until = datetime.now(timezone.utc) + timedelta(**default_duration) + until = datetime.now(timezone.utc) + timedelta(seconds=default_duration) time = _(" for {duration}").format( - duration=humanize_timedelta(timedelta=timedelta(**default_duration)) + duration=humanize_timedelta(timedelta=timedelta(seconds=default_duration)) ) author = ctx.message.author guild = ctx.guild @@ -954,21 +952,19 @@ async def channel_mute( if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) async with ctx.typing(): - duration = time_and_reason.get("duration", {}) + duration = time_and_reason.get("duration", None) reason = time_and_reason.get("reason", None) - until = None time = "" + until = None if duration: - until = datetime.now(timezone.utc) + timedelta(**duration) - time = _(" for {duration}").format( - duration=humanize_timedelta(timedelta=timedelta(**duration)) - ) + until = datetime.now(timezone.utc) + duration + time = _(" for {duration}").format(duration=humanize_timedelta(timedelta=duration)) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: - until = datetime.now(timezone.utc) + timedelta(**default_duration) + until = datetime.now(timezone.utc) + timedelta(seconds=default_duration) time = _(" for {duration}").format( - duration=humanize_timedelta(timedelta=timedelta(**default_duration)) + duration=humanize_timedelta(timedelta=timedelta(seconds=default_duration)) ) author = ctx.message.author channel = ctx.message.channel diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 0e02e68204d..bfab5713fbc 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -101,21 +101,23 @@ async def voice_mute( if not can_move: issue_list.append((user, perm_reason)) continue - duration = time_and_reason.get("duration", {}) + duration = time_and_reason.get("duration", None) reason = time_and_reason.get("reason", None) - until = None time = "" + until = None if duration: - until = datetime.now(timezone.utc) + timedelta(**duration) + until = datetime.now(timezone.utc) + duration time = _(" for {duration}").format( - duration=humanize_timedelta(timedelta=timedelta(**duration)) + duration=humanize_timedelta(timedelta=duration) ) else: default_duration = await self.config.guild(ctx.guild).default_time() if default_duration: - until = datetime.now(timezone.utc) + timedelta(**default_duration) + until = datetime.now(timezone.utc) + timedelta(seconds=default_duration) time = _(" for {duration}").format( - duration=humanize_timedelta(timedelta=timedelta(**default_duration)) + duration=humanize_timedelta( + timedelta=timedelta(seconds=default_duration) + ) ) guild = ctx.guild author = ctx.author From b9310df97a4670c42a6657e30f6221aa5c6ff088 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sat, 24 Oct 2020 23:22:18 -0600 Subject: [PATCH 087/103] <:excited:474074780887285776> This should be everything --- redbot/cogs/mutes/mutes.py | 330 +++++++++++++++++++++----------- redbot/cogs/mutes/voicemutes.py | 56 +++--- 2 files changed, 242 insertions(+), 144 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 4d439382cb6..f0dc94d3051 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -70,9 +70,15 @@ def __init__(self, bot: Red): "mute_role": None, "notification_channel": None, "muted_users": {}, - "default_time": {}, + "default_time": 0, } - self.config.register_global(force_role_mutes=False) + self.config.register_global(force_role_mutes=True) + # Tbh I would rather force everyone to use role mutes. + # I also honestly think everyone would agree they're the + # way to go. If for whatever reason someone wants to + # enable channel overwrite mutes for their bot they can. + # Channel overwrite logic still needs to be in place + # for channel mutes methods. self.config.register_guild(**default_guild) self.config.register_member(perms_cache={}) self.config.register_channel(muted_users={}) @@ -89,6 +95,11 @@ async def red_delete_data_for_user( requester: Literal["discord_deleted_user", "owner", "user", "user_strict"], user_id: int, ): + """Mutes are considered somewhat critical + Therefore the only data that we should delete + is that which comes from discord requesting us to + remove data about a user + """ if requester != "discord_deleted_user": return @@ -130,6 +141,13 @@ async def is_allowed_by_hierarchy( return mod.top_role.position > user.top_role.position or is_special async def _handle_automatic_unmute(self): + """This is the core task creator and loop + for automatic unmutes + + A resolution of 30 seconds seems appropriate + to allow for decent resolution on low timed + unmutes and without being too busy on our event loop + """ await self.bot.wait_until_red_ready() await self._ready.wait() while True: @@ -146,6 +164,10 @@ async def _handle_automatic_unmute(self): await asyncio.sleep(30) async def _clean_tasks(self): + """This is here to cleanup our tasks + and log when we have something going wrong + inside our tasks. + """ log.debug("Cleaning unmute tasks") is_debug = log.getEffectiveLevel() <= logging.DEBUG for task_id in list(self._unmute_tasks.keys()): @@ -164,6 +186,7 @@ async def _clean_tasks(self): self._unmute_tasks.pop(task_id, None) async def _handle_server_unmutes(self): + """This is where the logic for role unmutes is taken care of""" log.debug("Checking server unmutes") for g_id in self._server_mutes: guild = self.bot.get_guild(g_id) @@ -176,7 +199,7 @@ async def _handle_server_unmutes(self): self._server_mutes[guild.id][u_id]["until"] - datetime.now(timezone.utc).timestamp() ) - if time_to_unmute < 60: + if time_to_unmute < 60.0: task_name = f"server-unmute-{g_id}-{u_id}" if task_name in self._unmute_tasks: continue @@ -186,6 +209,14 @@ async def _handle_server_unmutes(self): ) async def _auto_unmute_user(self, guild: discord.Guild, data: dict): + """ + This handles role unmutes automatically + + Since channel overwrite mutes are handled under the separate + _auto_channel_unmute_user methods here we don't + need to worry about the dict response for message + since only role based mutes get added here + """ delay = data["until"] - datetime.now(timezone.utc).timestamp() if delay < 1: delay = 0 @@ -195,8 +226,11 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): author = guild.get_member(data["author"]) if not member: return - success, message = await self.unmute_user(guild, author, member, _("Automatic unmute")) - if success: + success = await self.unmute_user(guild, author, member, _("Automatic unmute")) + async with self.config.guild(guild).muted_users() as muted_users: + if str(member.id) in muted_users: + del muted_users[str(member.id)] + if success["success"]: await modlog.create_case( self.bot, guild, @@ -207,24 +241,24 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): _("Automatic unmute"), until=None, ) - async with self.config.guild(guild).muted_users() as muted_users: - if str(member.id) in muted_users: - del muted_users[str(member.id)] else: chan_id = await self.config.guild(guild).notification_channel() notification_channel = guild.get_channel(chan_id) if not notification_channel: return + if not notification_channel.permissions_for(guild.me).send_messages: + return + error_msg = _( + "I am unable to unmute {user} for the following reason:\n{reason}" + ).format(user=member, reason=success["reason"]) try: - error_msg = _( - "I am unable to unmute {user} for the following reason:\n{reason}" - ).format(user=member, reason=message) await notification_channel.send(error_msg) except discord.errors.Forbidden: log.info(error_msg) return async def _handle_channel_unmutes(self): + """This is where the logic for handling channel unmutes is taken care of""" log.debug("Checking channel unmutes") multiple_mutes = {} for c_id in self._channel_mutes: @@ -241,7 +275,7 @@ async def _handle_channel_unmutes(self): self._channel_mutes[channel.id][u_id]["until"] - datetime.now(timezone.utc).timestamp() ) - if time_to_unmute < 120.0: + if time_to_unmute < 60.0: if channel.guild.id not in multiple_mutes: multiple_mutes[channel.guild.id] = {} if u_id not in multiple_mutes[channel.guild.id]: @@ -257,13 +291,13 @@ async def _handle_channel_unmutes(self): guild = self.bot.get_guild(guild_id) for user, channels in users.items(): if len(channels) > 1: - task_name = f"server-unmute-channels-{user}" + task_name = f"server-unmute-channels-{guild_id}-{user}" if task_name in self._unmute_tasks: continue log.debug(f"Creating task: {task_name}") member = guild.get_member(user) self._unmute_tasks[task_name] = asyncio.create_task( - self._auto_channel_unmute_user_2(member, guild, channels) + self._auto_channel_unmute_user_multi(member, guild, channels) ) else: @@ -276,17 +310,17 @@ async def _handle_channel_unmutes(self): self._auto_channel_unmute_user(guild.get_channel(channel), mute_data) ) - async def _auto_channel_unmute_user_2( + async def _auto_channel_unmute_user_multi( self, member: discord.Member, guild: discord.Guild, channels: Dict[int, dict] ): - + """This is meant to handle multiple channels all being unmuted at once""" tasks = [] for channel, mute_data in channels.items(): author = guild.get_member(mute_data["author"]) tasks.append( self._auto_channel_unmute_user(guild.get_channel(channel), mute_data, False) ) - result = await asyncio.gather(*tasks) + results = await asyncio.gather(*tasks) await modlog.create_case( self.bot, guild, @@ -297,9 +331,12 @@ async def _auto_channel_unmute_user_2( _("Automatic unmute"), until=None, ) - if any(result): + if any(results): reasons = {} - for member, channel, reason in result: + for result in results: + if not result: + continue + member, channel, reason = result if reason not in reasons: reasons[reason] = [channel] else: @@ -316,6 +353,8 @@ async def _auto_channel_unmute_user_2( notification_channel = guild.get_channel(chan_id) if notification_channel is None: return None + if not notification_channel.permissions_for(guild.me).send_messages: + return None try: await notification_channel.send(error_msg) except discord.errors.Forbidden: @@ -367,8 +406,9 @@ async def _auto_channel_unmute_user( notification_channel = channel.guild.get_channel(chan_id) if not notification_channel: return None + if not notification_channel.permissions_for(channel.guild.me).send_messages: + return None try: - await notification_channel.send(error_msg) except discord.errors.Forbidden: log.info(error_msg) @@ -454,9 +494,16 @@ async def on_guild_channel_update( } to_del: List[int] = [] for user_id in self._channel_mutes[after.id].keys(): + send_messages = ( + after_perms[user_id]["send_messages"] is None + or after_perms[user_id]["send_messages"] is True + ) + speak = ( + after_perms[user_id]["speak"] is None or after_perms[user_id]["speak"] is True + ) + # explicit is better than implicit :thinkies: if user_id in before_perms and ( - user_id not in after_perms - or any((after_perms[user_id]["send_messages"], after_perms[user_id]["speak"])) + user_id not in after_perms or any((send_messages, speak)) ): user = after.guild.get_member(user_id) if not user: @@ -488,13 +535,16 @@ async def on_member_join(self, member: discord.Member): return mute_role = await self.config.guild(guild).mute_role() if not mute_role: + # channel overwrite mutes would quickly allow a malicious + # user to globally rate limit the bot therefore we are not + # going to support re-muting users via channel overwrites return if guild.id in self._server_mutes: if member.id in self._server_mutes[guild.id]: role = guild.get_role(mute_role) if not role: return - until = self._server_mutes[guild.id][member.id]["until"] + until = datetime.fromtimestamp(self._server_mutes[guild.id][member.id]["until"]) await self.mute_user( guild, guild.me, member, until, _("Previously muted in this server.") ) @@ -533,7 +583,7 @@ async def show_mutes_settings(self, ctx: commands.Context): ).format( role=mute_role.mention if mute_role else _("None"), channel=notification_channel.mention if notification_channel else _("None"), - time=humanize_timedelta(default_time) if default_time else _("None"), + time=humanize_timedelta(timedelta=default_time) if default_time else _("None"), ) await ctx.maybe_send_embed(msg) @@ -559,12 +609,15 @@ async def notification_channel_set( @muteset.command(name="role") @checks.admin_or_permissions(manage_roles=True) - @checks.bot_has_permissions(manage_roles=True) + @commands.bot_has_guild_permissions(manage_roles=True) async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): """Sets the role to be applied when muting a user. If no role is setup the bot will attempt to mute a user by setting channel overwrites in all channels to prevent the user from sending messages. + + Note: If no role is setup a user may be able to leave the server + and rejoin no longer being muted. """ if not role: await self.config.guild(ctx.guild).mute_role.set(None) @@ -588,7 +641,7 @@ async def mute_role(self, ctx: commands.Context, *, role: discord.Role = None): @muteset.command(name="makerole") @checks.admin_or_permissions(manage_roles=True) - @checks.bot_has_permissions(manage_roles=True) + @commands.bot_has_guild_permissions(manage_roles=True) async def make_mute_role(self, ctx: commands.Context, *, name: str): """Create a Muted role. @@ -636,13 +689,15 @@ async def _set_mute_role_overwrites( This sets the supplied role and channel overwrites to what we want by default for a mute role """ + if not channel.permissions_for(channel.guild.me).manage_permissions: + return channel.mention overs = discord.PermissionOverwrite() overs.send_messages = False overs.add_reactions = False overs.speak = False try: await channel.set_permissions(role, overwrite=overs, reason=_("Mute role setup")) - return + return None except discord.errors.Forbidden: return channel.mention @@ -677,7 +732,7 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: command_1 = f"{ctx.clean_prefix}muteset role" command_2 = f"{ctx.clean_prefix}muteset makerole" msg = _( - "This server does not have a mute role setup, " + "This server does not have a mute role setup. " " You can setup a mute role with `{command_1}` or" "`{command_2}` if you just want a basic role created setup.\n\n" ).format(command_1=command_1, command_2=command_2) @@ -691,7 +746,14 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: if mute_role or sent_instructions: return True else: - msg += _("are you sure you want to continue with channel overwrites?") + msg += _( + "Channel overwrites for muting users can get expensive on Discord's API " + "as such we recommend that you have an admin setup a mute role instead. " + "Channel overwrites will also not re-apply on guild join, so a user " + "who has been muted may leave and re-join and no longer be muted." + "Role mutes do not have this issue.\n\n" + "Are you sure you want to continue with channel overwrites? " + ) can_react = ctx.channel.permissions_for(ctx.me).add_reactions if can_react: msg += _( @@ -741,7 +803,6 @@ async def activemutes(self, ctx: commands.Context): """ Displays active mutes on this server. """ - msg = "" if ctx.guild.id in self._server_mutes: mutes_data = self._server_mutes[ctx.guild.id] @@ -829,7 +890,6 @@ async def mute( return await ctx.send(_("You cannot mute yourself.")) if not await self._check_for_mute_role(ctx): return - mute_role = await self.config.guild(ctx.guild).mute_role() async with ctx.typing(): duration = time_and_reason.get("duration", None) reason = time_and_reason.get("reason", None) @@ -851,11 +911,12 @@ async def mute( success_list = [] issue_list = [] for user in users: - success, issue = await self.mute_user(guild, author, user, until, audit_reason) - if issue: - issue_list.append((user, issue)) - if success: + success = await self.mute_user(guild, author, user, until, audit_reason) + if success["success"]: success_list.append(user) + if success["channels"]: + # incase we only muted a user in 1 channel not all + issue_list.append(success) await modlog.create_case( self.bot, guild, @@ -867,6 +928,8 @@ async def mute( until=until, channel=None, ) + else: + issue_list.append(success) if success_list: if ctx.guild.id not in self._server_mutes: self._server_mutes[ctx.guild.id] = {} @@ -877,20 +940,38 @@ async def mute( msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) ) if issue_list: - if not mute_role: - message = _( - "{users} could not be muted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) - else: - message = _("{users} could not be muted. {reason}").format( - users=humanize_list([f"{u}" for u, x in issue_list]), - reason=humanize_list([x for u, x in issue_list]), + await self.handle_issues(ctx, issue_list) + + def parse_issues(self, issue_list: dict) -> str: + reasons = {} + reason_msg = issue_list["reason"] + channel_msg = "" + error_msg = _("{member} could not be unmuted for the following reasons:\n").format( + member=issue_list["user"] + ) + if issue_list["channels"]: + for channel, reason in issue_list["channels"]: + if reason not in reasons: + reasons[reason] = [channel] + else: + reasons[reason].append(channel) + + for reason, channel_list in reasons.items(): + channel_msg += _("{reason} In the following channels: {channels}\n").format( + reason=reason, + channels=humanize_list([c.mention for c in channel_list]), ) - await ctx.send_interactive(message) + error_msg += reason_msg or channel_msg + return error_msg + + async def handle_issues(self, ctx: commands.Context, issue_list: List[dict]) -> None: + """ + This is to handle the various issues that can return for each user/channel + """ + message = _( + "Some users could not be properly muted. Would you like to see who, where, and why?" + ) - async def handle_issues(self, ctx: commands.Context, message: str, issue: str) -> None: can_react = ctx.channel.permissions_for(ctx.me).add_reactions if not can_react: message += " (y/n)" @@ -919,6 +1000,7 @@ async def handle_issues(self, ctx: commands.Context, message: str, issue: str) - if can_react: with contextlib.suppress(discord.Forbidden): await query.clear_reactions() + issue = "\n".join(self.parse_issues(issue) for issue in issue_list) resp = pagify(issue) await ctx.send_interactive(resp) @@ -1031,7 +1113,6 @@ async def unmute( return await ctx.send(_("You cannot unmute yourself.")) if not await self._check_for_mute_role(ctx): return - mute_role = await self.config.guild(ctx.guild).mute_role() async with ctx.typing(): guild = ctx.guild author = ctx.author @@ -1039,9 +1120,9 @@ async def unmute( issue_list = [] success_list = [] for user in users: - success, issue = await self.unmute_user(guild, author, user, audit_reason) + success = await self.unmute_user(guild, author, user, audit_reason) - if success: + if success["success"]: success_list.append(user) await modlog.create_case( self.bot, @@ -1054,7 +1135,7 @@ async def unmute( until=None, ) else: - issue_list.append((user, issue)) + issue_list.append(success) if success_list: if ctx.guild.id in self._server_mutes and self._server_mutes[ctx.guild.id]: await self.config.guild(ctx.guild).muted_users.set( @@ -1068,18 +1149,7 @@ async def unmute( ) ) if issue_list: - if not mute_role: - message = _( - "{users} could not be unmuted in some channels. " - "Would you like to see which channels and why?" - ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) - else: - message = _("{users} could not be unmuted. {reason}").format( - users=humanize_list([f"{u}" for u, x in issue_list]), - reason=humanize_list([x for u, x in issue_list]), - ) - await ctx.send_interactive(message) + await self.handle_issues(ctx, issue_list) @checks.mod_or_permissions(manage_roles=True) @commands.command(name="unmutechannel", aliases=["channelunmute"], usage=" [reason]") @@ -1140,10 +1210,10 @@ async def unmute_channel( ) ) if issue_list: - message = _( - "{users} could not be unmuted in this channels. " "Would you like to see why?" - ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list([x for u, x in issue_list])) + msg = _("The following users could not be unmuted\n") + for user, issue in issue_list: + msg += f"{user}: {issue}\n" + await ctx.send_interactive(pagify(msg)) async def mute_user( self, @@ -1152,19 +1222,41 @@ async def mute_user( user: discord.Member, until: Optional[datetime] = None, reason: Optional[str] = None, - ) -> Tuple[bool, Optional[str]]: + ) -> Dict[ + str, Optional[Union[List[Tuple[discord.abc.GuildChannel, str]], discord.Member, bool, str]] + ]: """ Handles muting users """ + permissions = user.guild_permissions + ret: Dict[ + str, + Union[bool, Optional[str], List[Tuple[discord.abc.GuildChannel, str]], discord.Member], + ] = { + "success": False, + "reason": None, + "channels": [], + "user": user, + } + # TODO: This typing is ugly and should probably be an object on its own + # along with this entire method and some othe refactorization + # v1.0.0 is meant to look ugly right :') + if permissions.administrator: + ret["reason"] = _(MUTE_UNMUTE_ISSUES["is_admin"]) + return ret mute_role = await self.config.guild(guild).mute_role() if mute_role: if not await self.is_allowed_by_hierarchy(guild, author, user): - return False, _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + ret["reason"] = _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + return ret role = guild.get_role(mute_role) if not role: - return False, _(MUTE_UNMUTE_ISSUES["role_missing"]) - + ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"]) + return ret + if not guild.me.guild_permissions.manage_roles: + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + return ret # This is here to prevent the modlog case from happening on role updates # we need to update the cache early so it's there before we receive the member_update event if guild.id not in self._server_mutes: @@ -1181,11 +1273,11 @@ async def mute_user( except discord.errors.Forbidden: if guild.id in self._server_mutes and user.id in self._server_mutes[guild.id]: del self._server_mutes[guild.id][user.id] - # this is here so we don't have a bad cache - return False, _(MUTE_UNMUTE_ISSUES["permissions_issue"]) - return True, None + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + return ret + ret["success"] = True + return ret else: - mute_success = [] perms_cache = {} tasks = [] for channel in guild.channels: @@ -1193,19 +1285,13 @@ async def mute_user( task_result = await asyncio.gather(*tasks) for task in task_result: if not task["success"]: - chan = task["channel"].mention - issue = task["reason"] - mute_success.append(f"{chan} - {issue}") + ret["channels"].append((task["channel"], task["reason"])) else: chan_id = task["channel"].id perms_cache[str(chan_id)] = task.get("old_overs") + ret["success"] = True await self.config.member(user).perms_cache.set(perms_cache) - if mute_success and len(mute_success) == len(guild.channels): - return False, "\n".join(s for s in mute_success) - elif mute_success and len(mute_success) != len(guild.channels): - return True, "\n".join(s for s in mute_success) - else: - return True, None + return ret async def unmute_user( self, @@ -1213,46 +1299,58 @@ async def unmute_user( author: discord.Member, user: discord.Member, reason: Optional[str] = None, - ) -> Tuple[bool, Optional[str]]: + ) -> Dict[ + str, + Union[bool, Optional[str], List[Tuple[discord.abc.GuildChannel, str]], discord.Member], + ]: """ - Handles muting users + Handles unmuting users """ - + ret: Dict[ + str, + Union[bool, Optional[str], List[Tuple[discord.abc.GuildChannel, str]], discord.Member], + ] = { + "success": False, + "reason": None, + "channels": [], + "user": user, + } mute_role = await self.config.guild(guild).mute_role() - _temp = None # used to keep the cache incase of permissions errors if mute_role: if not await self.is_allowed_by_hierarchy(guild, author, user): - return False, _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + ret["reason"] = _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + return ret role = guild.get_role(mute_role) if not role: - return False, _(MUTE_UNMUTE_ISSUES["role_missing"]) + ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"]) + return ret + if guild.id in self._server_mutes: if user.id in self._server_mutes[guild.id]: - _temp = copy(self._server_mutes[guild.id][user.id]) del self._server_mutes[guild.id][user.id] + if not guild.me.guild_permissions.manage_roles: + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + return ret try: await user.remove_roles(role, reason=reason) except discord.errors.Forbidden: - if _temp: - self._server_mutes[guild.id][user.id] = _temp - return False, _(MUTE_UNMUTE_ISSUES["permissions_issue"]) - return True, None + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + return ret + ret["success"] = True + return ret else: - mute_success = [] + mute_success = {} tasks = [] for channel in guild.channels: tasks.append(self.channel_unmute_user(guild, channel, author, user, reason)) results = await asyncio.gather(*tasks) for task in results: if not task["success"]: - chan = task["channel"].mention - issue = task["reason"] - mute_success.append(f"{chan} - {issue}") + ret["channels"].append((task["channel"], task["reason"])) + else: + ret["success"] = True await self.config.member(user).clear() - if mute_success: - return False, "\n".join(s for s in mute_success) - else: - return True, None + return ret async def channel_mute_user( self, @@ -1301,6 +1399,12 @@ async def channel_mute_user( "channel": channel, "reason": _(MUTE_UNMUTE_ISSUES["already_muted"]), } + if not channel.permissions_for(guild.me).manage_permissions: + return { + "success": False, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), + } self._channel_mutes[channel.id][user.id] = { "author": author.id, "member": user.id, @@ -1308,15 +1412,9 @@ async def channel_mute_user( } try: await channel.set_permissions(user, overwrite=overwrites, reason=reason) - except discord.Forbidden: + except discord.NotFound as e: if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: del self._channel_mutes[channel.id][user.id] - return { - "success": False, - "channel": channel, - "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), - } - except discord.NotFound as e: if e.code == 10003: if ( channel.id in self._channel_mutes @@ -1391,6 +1489,12 @@ async def channel_unmute_user( "channel": channel, "reason": _(MUTE_UNMUTE_ISSUES["already_unmuted"]), } + if not channel.permissions_for(guild.me).manage_permissions: + return { + "success": False, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), + } try: if overwrites.is_empty(): await channel.set_permissions( @@ -1398,12 +1502,6 @@ async def channel_unmute_user( ) else: await channel.set_permissions(user, overwrite=overwrites, reason=reason) - except discord.Forbidden: - return { - "success": False, - "channel": channel, - "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), - } except discord.NotFound as e: if e.code == 10003: return { diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index bfab5713fbc..99f45efd6c4 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -149,18 +149,18 @@ async def voice_mute( else: issue_list.append((user, success["reason"])) - if success_list: - msg = _("{users} has been muted in this channel{time}.") - if len(success_list) > 1: - msg = _("{users} have been muted in this channel{time}.") - await ctx.send( - msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) - ) - if issue_list: - msg = _("The following users could not be muted\n") - for user, issue in issue_list: - msg += f"{user}: {issue}\n" - await ctx.send_interactive(pagify(msg)) + if success_list: + msg = _("{users} has been muted in this channel{time}.") + if len(success_list) > 1: + msg = _("{users} have been muted in this channel{time}.") + await ctx.send( + msg.format(users=humanize_list([f"{u}" for u in success_list]), time=time) + ) + if issue_list: + msg = _("The following users could not be muted\n") + for user, issue in issue_list: + msg += f"{user}: {issue}\n" + await ctx.send_interactive(pagify(msg)) @commands.command(name="voiceunmute", usage=" [reason]") @commands.guild_only() @@ -219,20 +219,20 @@ async def unmute_voice( ) else: issue_list.append((user, success["reason"])) - if success_list: - if channel.id in self._channel_mutes and self._channel_mutes[channel.id]: - await self.config.channel(channel).muted_users.set( - self._channel_mutes[channel.id] - ) - else: - await self.config.channel(channel).muted_users.clear() - await ctx.send( - _("{users} unmuted in this channel.").format( - users=humanize_list([f"{u}" for u in success_list]) - ) + if success_list: + if channel.id in self._channel_mutes and self._channel_mutes[channel.id]: + await self.config.channel(channel).muted_users.set( + self._channel_mutes[channel.id] + ) + else: + await self.config.channel(channel).muted_users.clear() + await ctx.send( + _("{users} unmuted in this channel.").format( + users=humanize_list([f"{u}" for u in success_list]) ) - if issue_list: - message = _( - "{users} could not be unmuted in this channels. " "Would you like to see why?" - ).format(users=humanize_list([f"{u}" for u, x in issue_list])) - await self.handle_issues(ctx, message, humanize_list(x for u, x in issue_list)) + ) + if issue_list: + msg = _("The following users could not be unmuted\n") + for user, issue in issue_list: + msg += f"{user}: {issue}\n" + await ctx.send_interactive(pagify(msg)) From 6e9f2a2a6fc15cdb5270137f2a13a19807909807 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sat, 24 Oct 2020 23:24:42 -0600 Subject: [PATCH 088/103] black --- redbot/cogs/mutes/voicemutes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 99f45efd6c4..ac8462175c7 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -221,9 +221,7 @@ async def unmute_voice( issue_list.append((user, success["reason"])) if success_list: if channel.id in self._channel_mutes and self._channel_mutes[channel.id]: - await self.config.channel(channel).muted_users.set( - self._channel_mutes[channel.id] - ) + await self.config.channel(channel).muted_users.set(self._channel_mutes[channel.id]) else: await self.config.channel(channel).muted_users.clear() await ctx.send( From a1a2906913f1eeeffdd6ad46ed482c1b9e097560 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 12:05:33 -0600 Subject: [PATCH 089/103] fix the things --- redbot/cogs/mutes/mutes.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f0dc94d3051..7f76f20604b 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -4,7 +4,6 @@ import logging from abc import ABC -from copy import copy from typing import cast, Optional, Dict, List, Tuple, Literal, Union from datetime import datetime, timedelta, timezone @@ -321,6 +320,16 @@ async def _auto_channel_unmute_user_multi( self._auto_channel_unmute_user(guild.get_channel(channel), mute_data, False) ) results = await asyncio.gather(*tasks) + unmuted_channels = [guild.get_channel(c) for c in channels.keys()] + for result in results: + if not result: + continue + _mmeber, channel, reason = result + unmuted_channels.pop(channel) + modlog_reason = _("Automatic unmute") + "\n".join( + [c.mention for c in unmuted_channels if c is not None] + ) + await modlog.create_case( self.bot, guild, @@ -328,7 +337,7 @@ async def _auto_channel_unmute_user_multi( "sunmute", member, author, - _("Automatic unmute"), + modlog_reason, until=None, ) if any(results): @@ -336,7 +345,7 @@ async def _auto_channel_unmute_user_multi( for result in results: if not result: continue - member, channel, reason = result + _member, channel, reason = result if reason not in reasons: reasons[reason] = [channel] else: @@ -750,7 +759,7 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: "Channel overwrites for muting users can get expensive on Discord's API " "as such we recommend that you have an admin setup a mute role instead. " "Channel overwrites will also not re-apply on guild join, so a user " - "who has been muted may leave and re-join and no longer be muted." + "who has been muted may leave and re-join and no longer be muted. " "Role mutes do not have this issue.\n\n" "Are you sure you want to continue with channel overwrites? " ) @@ -1254,7 +1263,7 @@ async def mute_user( if not role: ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"]) return ret - if not guild.me.guild_permissions.manage_roles: + if not guild.me.guild_permissions.manage_roles or role >= guild.me.top_role: ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) return ret # This is here to prevent the modlog case from happening on role updates @@ -1328,7 +1337,7 @@ async def unmute_user( if guild.id in self._server_mutes: if user.id in self._server_mutes[guild.id]: del self._server_mutes[guild.id][user.id] - if not guild.me.guild_permissions.manage_roles: + if not guild.me.guild_permissions.manage_roles or role >= guild.me.top_role: ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) return ret try: @@ -1339,7 +1348,6 @@ async def unmute_user( ret["success"] = True return ret else: - mute_success = {} tasks = [] for channel in guild.channels: tasks.append(self.channel_unmute_user(guild, channel, author, user, reason)) From bc7cb3fd9da8ee0600d597d3c1f4685965ef390b Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 12:21:59 -0600 Subject: [PATCH 090/103] bleh --- redbot/cogs/mutes/mutes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 7f76f20604b..646359ad6b3 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -326,9 +326,11 @@ async def _auto_channel_unmute_user_multi( continue _mmeber, channel, reason = result unmuted_channels.pop(channel) - modlog_reason = _("Automatic unmute") + "\n".join( - [c.mention for c in unmuted_channels if c is not None] - ) + modlog_reason = _("Automatic unmute") + + channel_list = humanize_list([c.mention for c in unmuted_channels if c is not None]) + if channel_list: + modlog_reason += _("\nUnmuted in channels: ") + channel_list await modlog.create_case( self.bot, From 0d22cdb241a4397f348f29516f2722218b7ede1d Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 17:19:28 -0600 Subject: [PATCH 091/103] more cleanup --- redbot/cogs/mutes/mutes.py | 66 +++++++++++++++++++++------------ redbot/cogs/mutes/voicemutes.py | 2 +- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 646359ad6b3..47b9c9fba96 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -28,11 +28,16 @@ "I cannot let you do that. You are not higher than the user in the role hierarchy." ), "is_admin": _("That user cannot be muted, as they have the Administrator permission."), - "permissions_issue": _( + "permissions_issue_role": _( "Failed to mute or unmute user. I need the manage roles " "permission and the user I'm muting must be " "lower than myself in the role hierarchy." ), + "permissions_issue_channel": _( + "Failed to mute or unmute user. I need the manage permissions " + "permission and the user I'm muting must be " + "lower than myself in the role hierarchy." + ), "left_guild": _("The user has left the server while applying an overwrite."), "unknown_channel": _("The channel I tried to mute or unmute the user in isn't found."), "role_missing": _("The mute role no longer exists."), @@ -505,13 +510,17 @@ async def on_guild_channel_update( } to_del: List[int] = [] for user_id in self._channel_mutes[after.id].keys(): - send_messages = ( - after_perms[user_id]["send_messages"] is None - or after_perms[user_id]["send_messages"] is True - ) - speak = ( - after_perms[user_id]["speak"] is None or after_perms[user_id]["speak"] is True - ) + send_messages = False + speak = False + if user_id in after_perms: + send_messages = ( + after_perms[user_id]["send_messages"] is None + or after_perms[user_id]["send_messages"] is True + ) + speak = ( + after_perms[user_id]["speak"] is None + or after_perms[user_id]["speak"] is True + ) # explicit is better than implicit :thinkies: if user_id in before_perms and ( user_id not in after_perms or any((send_messages, speak)) @@ -754,6 +763,7 @@ async def _check_for_mute_role(self, ctx: commands.Context) -> bool: if force_role_mutes and not mute_role: await ctx.send(msg) return False + if mute_role or sent_instructions: return True else: @@ -899,6 +909,7 @@ async def mute( return await ctx.send(_("You cannot mute me.")) if ctx.author in users: return await ctx.send(_("You cannot mute yourself.")) + if not await self._check_for_mute_role(ctx): return async with ctx.typing(): @@ -955,7 +966,7 @@ async def mute( def parse_issues(self, issue_list: dict) -> str: reasons = {} - reason_msg = issue_list["reason"] + reason_msg = issue_list["reason"] + "\n" channel_msg = "" error_msg = _("{member} could not be unmuted for the following reasons:\n").format( member=issue_list["user"] @@ -968,7 +979,7 @@ def parse_issues(self, issue_list: dict) -> str: reasons[reason].append(channel) for reason, channel_list in reasons.items(): - channel_msg += _("{reason} In the following channels: {channels}\n").format( + channel_msg += _("- {reason} In the following channels: {channels}\n").format( reason=reason, channels=humanize_list([c.mention for c in channel_list]), ) @@ -1018,8 +1029,8 @@ async def handle_issues(self, ctx: commands.Context, issue_list: List[dict]) -> @commands.command( name="mutechannel", aliases=["channelmute"], usage=" [time_and_reason]" ) - @commands.guild_only() @checks.mod_or_permissions(manage_roles=True) + @commands.bot_has_guild_permissions(manage_permissions=True) async def channel_mute( self, ctx: commands.Context, @@ -1164,7 +1175,7 @@ async def unmute( @checks.mod_or_permissions(manage_roles=True) @commands.command(name="unmutechannel", aliases=["channelunmute"], usage=" [reason]") - @commands.guild_only() + @commands.bot_has_guild_permissions(manage_permissions=True) async def unmute_channel( self, ctx: commands.Context, @@ -1255,18 +1266,19 @@ async def mute_user( if permissions.administrator: ret["reason"] = _(MUTE_UNMUTE_ISSUES["is_admin"]) return ret + if not await self.is_allowed_by_hierarchy(guild, author, user): + ret["reason"] = _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + return ret mute_role = await self.config.guild(guild).mute_role() if mute_role: - if not await self.is_allowed_by_hierarchy(guild, author, user): - ret["reason"] = _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) - return ret + role = guild.get_role(mute_role) if not role: ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"]) return ret if not guild.me.guild_permissions.manage_roles or role >= guild.me.top_role: - ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue_role"]) return ret # This is here to prevent the modlog case from happening on role updates # we need to update the cache early so it's there before we receive the member_update event @@ -1284,7 +1296,7 @@ async def mute_user( except discord.errors.Forbidden: if guild.id in self._server_mutes and user.id in self._server_mutes[guild.id]: del self._server_mutes[guild.id][user.id] - ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue_role"]) return ret ret["success"] = True return ret @@ -1327,10 +1339,11 @@ async def unmute_user( "user": user, } mute_role = await self.config.guild(guild).mute_role() + if not await self.is_allowed_by_hierarchy(guild, author, user): + ret["reason"] = _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) + return ret if mute_role: - if not await self.is_allowed_by_hierarchy(guild, author, user): - ret["reason"] = _(MUTE_UNMUTE_ISSUES["hierarchy_problem"]) - return ret + role = guild.get_role(mute_role) if not role: ret["reason"] = _(MUTE_UNMUTE_ISSUES["role_missing"]) @@ -1340,12 +1353,12 @@ async def unmute_user( if user.id in self._server_mutes[guild.id]: del self._server_mutes[guild.id][user.id] if not guild.me.guild_permissions.manage_roles or role >= guild.me.top_role: - ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue_role"]) return ret try: await user.remove_roles(role, reason=reason) except discord.errors.Forbidden: - ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue"]) + ret["reason"] = _(MUTE_UNMUTE_ISSUES["permissions_issue_role"]) return ret ret["success"] = True return ret @@ -1413,7 +1426,7 @@ async def channel_mute_user( return { "success": False, "channel": channel, - "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), + "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue_channel"]), } self._channel_mutes[channel.id][user.id] = { "author": author.id, @@ -1422,6 +1435,8 @@ async def channel_mute_user( } try: await channel.set_permissions(user, overwrite=overwrites, reason=reason) + async with self.config.channel(channel).muted_users() as muted_users: + muted_users[str(user.id)] = self._channel_mutes[channel.id][user.id] except discord.NotFound as e: if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: del self._channel_mutes[channel.id][user.id] @@ -1503,7 +1518,7 @@ async def channel_unmute_user( return { "success": False, "channel": channel, - "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue"]), + "reason": _(MUTE_UNMUTE_ISSUES["permissions_issue_channel"]), } try: if overwrites.is_empty(): @@ -1512,6 +1527,9 @@ async def channel_unmute_user( ) else: await channel.set_permissions(user, overwrite=overwrites, reason=reason) + async with self.config.channel(channel).muted_users() as muted_users: + if str(user.id) in muted_users: + del muted_users[str(user.id)] except discord.NotFound as e: if e.code == 10003: return { diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index ac8462175c7..730dc45b4d0 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -141,7 +141,7 @@ async def voice_mute( user, author, reason, - until=None, + until=until, channel=channel, ) async with self.config.member(user).perms_cache() as cache: From 20a59d28c0440c50b56a4d45a58ebe7a43eb9e5b Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 17:32:55 -0600 Subject: [PATCH 092/103] lmao hang on --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 47b9c9fba96..43eac0dfd21 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -966,7 +966,7 @@ async def mute( def parse_issues(self, issue_list: dict) -> str: reasons = {} - reason_msg = issue_list["reason"] + "\n" + reason_msg = issue_list["reason"] + "\n" if issue_list["reason"] else None channel_msg = "" error_msg = _("{member} could not be unmuted for the following reasons:\n").format( member=issue_list["user"] From b017478a474bbbd072049dc63f6568493c3440bf Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 17:42:54 -0600 Subject: [PATCH 093/103] fix voice mutes thingy --- redbot/cogs/mutes/voicemutes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 730dc45b4d0..13dfd9a24b6 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -39,8 +39,7 @@ async def _voice_perm_check( """ 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, None + return False, _("That user is not in a voice channel.") voice_channel: discord.VoiceChannel = user_voice_state.channel required_perms = discord.Permissions() required_perms.update(**perms) From f1b15815bc333dfb51ed65f01e4032c12c0d8169 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 17:56:49 -0600 Subject: [PATCH 094/103] Title Case Permissions --- redbot/cogs/mutes/mutes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 43eac0dfd21..81576e4911a 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -29,12 +29,12 @@ ), "is_admin": _("That user cannot be muted, as they have the Administrator permission."), "permissions_issue_role": _( - "Failed to mute or unmute user. I need the manage roles " + "Failed to mute or unmute user. I need the Manage Roles " "permission and the user I'm muting must be " "lower than myself in the role hierarchy." ), "permissions_issue_channel": _( - "Failed to mute or unmute user. I need the manage permissions " + "Failed to mute or unmute user. I need the Manage Permissions " "permission and the user I'm muting must be " "lower than myself in the role hierarchy." ), From 34fd1020604e6e6ee7a9bb7f561167167d678568 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 18:08:41 -0600 Subject: [PATCH 095/103] oh I see --- redbot/cogs/mutes/mutes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 81576e4911a..76bb3fc0618 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -34,9 +34,7 @@ "lower than myself in the role hierarchy." ), "permissions_issue_channel": _( - "Failed to mute or unmute user. I need the Manage Permissions " - "permission and the user I'm muting must be " - "lower than myself in the role hierarchy." + "Failed to mute or unmute user. I need the Manage Permissions permission." ), "left_guild": _("The user has left the server while applying an overwrite."), "unknown_channel": _("The channel I tried to mute or unmute the user in isn't found."), From 0e608e3d6486161a713cae6ddc7448408907a744 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 18:16:57 -0600 Subject: [PATCH 096/103] I'm running out of funny one-liners for commit messages --- redbot/cogs/mutes/mutes.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 76bb3fc0618..611f73be73a 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1504,13 +1504,14 @@ async def channel_unmute_user( } overwrites.update(**old_values) - if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: - del self._channel_mutes[channel.id][user.id] - elif user.id not in self._channel_mutes[channel.id]: - return { - "success": False, - "channel": channel, - "reason": _(MUTE_UNMUTE_ISSUES["already_unmuted"]), + if channel.id in self._channel_mutes: + if user.id in self._channel_mutes[channel.id] + del self._channel_mutes[channel.id][user.id] + else: + return { + "success": False, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["already_unmuted"]), } if not channel.permissions_for(guild.me).manage_permissions: return { From 31895952879aacab189edbf7c9104044b06b12f0 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 18:18:32 -0600 Subject: [PATCH 097/103] oof --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 611f73be73a..ea4b7b71b57 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1512,7 +1512,7 @@ async def channel_unmute_user( "success": False, "channel": channel, "reason": _(MUTE_UNMUTE_ISSUES["already_unmuted"]), - } + } if not channel.permissions_for(guild.me).manage_permissions: return { "success": False, From 8a69d122335c6f55abb15d550a7333a9df18522a Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 18:20:36 -0600 Subject: [PATCH 098/103] ugh --- redbot/cogs/mutes/mutes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index ea4b7b71b57..3d9f5b677c6 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1505,7 +1505,7 @@ async def channel_unmute_user( overwrites.update(**old_values) if channel.id in self._channel_mutes: - if user.id in self._channel_mutes[channel.id] + if user.id in self._channel_mutes[channel.id]: del self._channel_mutes[channel.id][user.id] else: return { From ae3b7d4a65be0a2b2e7ead11b249375e47ef6fd9 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 18:45:22 -0600 Subject: [PATCH 099/103] let's try this --- redbot/cogs/mutes/mutes.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 3d9f5b677c6..f2e6f1a9697 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -1504,15 +1504,14 @@ async def channel_unmute_user( } overwrites.update(**old_values) - if channel.id in self._channel_mutes: - if user.id in self._channel_mutes[channel.id]: - del self._channel_mutes[channel.id][user.id] - else: - return { - "success": False, - "channel": channel, - "reason": _(MUTE_UNMUTE_ISSUES["already_unmuted"]), - } + if channel.id in self._channel_mutes and user.id in self._channel_mutes[channel.id]: + del self._channel_mutes[channel.id][user.id] + else: + return { + "success": False, + "channel": channel, + "reason": _(MUTE_UNMUTE_ISSUES["already_unmuted"]), + } if not channel.permissions_for(guild.me).manage_permissions: return { "success": False, From f603f556f924837639a79f90e05241f53a5969ed Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 19:03:23 -0600 Subject: [PATCH 100/103] voicemutes manage_permissions --- redbot/cogs/mutes/voicemutes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/voicemutes.py b/redbot/cogs/mutes/voicemutes.py index 13dfd9a24b6..6965ff7f5fb 100644 --- a/redbot/cogs/mutes/voicemutes.py +++ b/redbot/cogs/mutes/voicemutes.py @@ -95,7 +95,7 @@ async def voice_mute( for user in users: user_voice_state = user.voice can_move, perm_reason = await self._voice_perm_check( - ctx, user_voice_state, mute_members=True, manage_channels=True + ctx, user_voice_state, mute_members=True, manage_permissions=True ) if not can_move: issue_list.append((user, perm_reason)) @@ -186,7 +186,7 @@ async def unmute_voice( for user in users: user_voice_state = user.voice can_move, perm_reason = await self._voice_perm_check( - ctx, user_voice_state, mute_members=True, manage_channels=True + ctx, user_voice_state, mute_members=True, manage_permissions=True ) if not can_move: issue_list.append((user, perm_reason)) From 3b0b6d240ba1f2f1388da988eff31edfb947efbb Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 19:28:11 -0600 Subject: [PATCH 101/103] Cleanup mutes if they expire when member is not present --- redbot/cogs/mutes/mutes.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index f2e6f1a9697..ef421e4fc3f 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -227,6 +227,10 @@ async def _auto_unmute_user(self, guild: discord.Guild, data: dict): member = guild.get_member(data["member"]) author = guild.get_member(data["author"]) if not member: + async with self.config.guild(guild).muted_users() as muted_users: + if str(data["member"]) in muted_users: + del muted_users[str(data["member"])] + del self._server_mutes[guild.id][data["member"]] return success = await self.unmute_user(guild, author, member, _("Automatic unmute")) async with self.config.guild(guild).muted_users() as muted_users: @@ -386,6 +390,11 @@ async def _auto_channel_unmute_user( member = channel.guild.get_member(data["member"]) author = channel.guild.get_member(data["author"]) if not member: + async with self.config.channel(channel).muted_users() as muted_users: + if str(data["member"]) in muted_users: + del muted_users[str(member.id)] + if channel.id in self._channel_mutes and data["member"] in self._channel_mutes[channel.id]: + del self._channel_mutes[channel.id][user.id] return None success = await self.channel_unmute_user( channel.guild, channel, author, member, _("Automatic unmute") From d42a34ff1f7cc5c2f833d41615083454650c3f11 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 19:28:36 -0600 Subject: [PATCH 102/103] black --- redbot/cogs/mutes/mutes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index ef421e4fc3f..6ef292ad4b3 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -393,7 +393,10 @@ async def _auto_channel_unmute_user( async with self.config.channel(channel).muted_users() as muted_users: if str(data["member"]) in muted_users: del muted_users[str(member.id)] - if channel.id in self._channel_mutes and data["member"] in self._channel_mutes[channel.id]: + if ( + channel.id in self._channel_mutes + and data["member"] in self._channel_mutes[channel.id] + ): del self._channel_mutes[channel.id][user.id] return None success = await self.channel_unmute_user( From 01fbb3dcd5a5f20a6bbeab5df81284c0865f6809 Mon Sep 17 00:00:00 2001 From: TrustyJAID Date: Sun, 25 Oct 2020 19:33:47 -0600 Subject: [PATCH 103/103] linters go brr --- redbot/cogs/mutes/mutes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redbot/cogs/mutes/mutes.py b/redbot/cogs/mutes/mutes.py index 6ef292ad4b3..fe6bff31f28 100644 --- a/redbot/cogs/mutes/mutes.py +++ b/redbot/cogs/mutes/mutes.py @@ -392,12 +392,12 @@ async def _auto_channel_unmute_user( if not member: async with self.config.channel(channel).muted_users() as muted_users: if str(data["member"]) in muted_users: - del muted_users[str(member.id)] + del muted_users[str(data["member"])] if ( channel.id in self._channel_mutes and data["member"] in self._channel_mutes[channel.id] ): - del self._channel_mutes[channel.id][user.id] + del self._channel_mutes[channel.id][data["member"]] return None success = await self.channel_unmute_user( channel.guild, channel, author, member, _("Automatic unmute")