From f8558b98c14fe9947e3b70c627c10ead7bd89770 Mon Sep 17 00:00:00 2001 From: Michael H Date: Mon, 24 Sep 2018 21:30:28 -0400 Subject: [PATCH] Automated mod action immunity settings (#2129) Refactors, and fixes some logic in filter which was encountered while applying the settings to core --- redbot/cogs/filter/filter.py | 79 ++++++++++++------------------------ redbot/cogs/mod/mod.py | 3 ++ redbot/core/bot.py | 37 +++++++++++++++++ redbot/core/core_commands.py | 78 ++++++++++++++++++++++++++++++++++- 4 files changed, 142 insertions(+), 55 deletions(-) diff --git a/redbot/cogs/filter/filter.py b/redbot/cogs/filter/filter.py index de8c9f22c..e91dae5df 100644 --- a/redbot/cogs/filter/filter.py +++ b/redbot/cogs/filter/filter.py @@ -249,70 +249,41 @@ class Filter: mod_or_superior = await is_mod_or_superior(self.bot, obj=author) if mod_or_superior: return - - await self.check_filter(message) - - async def on_message_edit(self, _, message): - author = message.author - if message.guild is None or self.bot.user == author: - return - valid_user = isinstance(author, discord.Member) and not author.bot - if not valid_user: - return - - # Bots and mods or superior are ignored from the filter - mod_or_superior = await is_mod_or_superior(self.bot, obj=author) - if mod_or_superior: + # As is anyone configured to be + if await self.bot.is_automod_immune(message): return await self.check_filter(message) + async def on_message_edit(self, _prior, message): + # message content has to change for non-bot's currently. + # if this changes, we should compare before passing it. + await self.on_message(message) + async def on_member_update(self, before: discord.Member, after: discord.Member): - if not after.guild.me.guild_permissions.manage_nicknames: - return # No permissions to manage nicknames, so can't do anything - word_list = await self.settings.guild(after.guild).filter() - filter_names = await self.settings.guild(after.guild).filter_names() - name_to_use = await self.settings.guild(after.guild).filter_default_name() - if not filter_names: - return - - name_filtered = False - nick_filtered = False - - for w in word_list: - if w in after.name: - name_filtered = True - if after.nick and w in after.nick: # since Member.nick can be None - nick_filtered = True - if name_filtered and nick_filtered: # Both true, so break from loop - break - - if name_filtered and after.nick is None: - try: - await after.edit(nick=name_to_use, reason="Filtered name") - except: - pass - elif nick_filtered: - try: - await after.edit(nick=None, reason="Filtered nickname") - except: - pass + if before.display_name != after.display_name: + await self.maybe_filter_name(after) async def on_member_join(self, member: discord.Member): - guild = member.guild - if not guild.me.guild_permissions.manage_nicknames: - return - word_list = await self.settings.guild(guild).filter() - filter_names = await self.settings.guild(guild).filter_names() - name_to_use = await self.settings.guild(guild).filter_default_name() + await self.maybe_filter_name(member) - if not filter_names: + async def maybe_filter_name(self, member: discord.Member): + if not member.guild.me.guild_permissions.manage_nicknames: + return # No permissions to manage nicknames, so can't do anything + if member.top_role >= member.guild.me.top_role: + return # Discord Hierarchy applies to nicks + if await self.bot.is_automod_immune(member): + return + word_list = await self.settings.guild(member.guild).filter() + if not await self.settings.guild(member.guild).filter_names(): return for w in word_list: - if w in member.name: + if w in member.display_name.lower(): + name_to_use = await self.settings.guild(member.guild).filter_default_name() + reason = "Filtered nick" if member.nick else "Filtered name" try: - await member.edit(nick=name_to_use, reason="Filtered name") - except: + await member.edit(nick=name_to_use, reason=reason) + except discord.HTTPException: pass - break + return diff --git a/redbot/cogs/mod/mod.py b/redbot/cogs/mod/mod.py index 5ce445126..7a4e3b797 100644 --- a/redbot/cogs/mod/mod.py +++ b/redbot/cogs/mod/mod.py @@ -1501,6 +1501,9 @@ class Mod: mod_or_superior = await is_mod_or_superior(self.bot, obj=author) if mod_or_superior: return + # As are anyone configured to be + if await self.bot.is_automod_immune(message): + return deleted = await self.check_duplicates(message) if not deleted: deleted = await self.check_mention_spam(message) diff --git a/redbot/core/bot.py b/redbot/core/bot.py index 9d2a5e29e..d4befbc6d 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -5,6 +5,7 @@ from collections import Counter from enum import Enum from importlib.machinery import ModuleSpec from pathlib import Path +from typing import Union import discord import sys @@ -72,6 +73,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): use_bot_color=False, fuzzy=False, disabled_commands=[], + autoimmune_ids=[], ) self.db.register_user(embeds=None) @@ -294,6 +296,41 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin): if pkg_name.startswith("redbot.cogs."): del sys.modules["redbot.cogs"].__dict__[name] + async def is_automod_immune( + self, to_check: Union[discord.Message, commands.Context, discord.abc.User, discord.Role] + ) -> bool: + """ + Checks if the user, message, context, or role should be considered immune from automated + moderation actions. + + This will return ``False`` in direct messages. + + Parameters + ---------- + to_check : `discord.Message` or `commands.Context` or `discord.abc.User` or `discord.Role` + Something to check if it would be immune + + Returns + ------- + bool + ``True`` if immune + + """ + guild = to_check.guild + if not guild: + return False + + if isinstance(to_check, discord.Role): + ids_to_check = [to_check.id] + else: + author = getattr(to_check, "author", to_check) + ids_to_check = [r.id for r in author.roles] + ids_to_check.append(author.id) + + immune_ids = await self.db.guild(guild).autoimmune_ids() + + return any(i in immune_ids for i in ids_to_check) + @staticmethod async def send_filtered( destination: discord.abc.Messageable, diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index dbc47c86f..6009c508b 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -14,7 +14,7 @@ from pathlib import Path from random import SystemRandom from string import ascii_letters, digits from distutils.version import StrictVersion -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union import aiohttp import discord @@ -1694,6 +1694,82 @@ class Core(CoreLogic): await ctx.bot.db.disabled_command_msg.set(message) await ctx.tick() + @commands.guild_only() + @checks.guildowner_or_permissions(manage_server=True) + @commands.group(name="autoimmune") + async def autoimmune_group(self, ctx: commands.Context): + """ + Server settings for immunity from automated actions + """ + pass + + @autoimmune_group.command(name="list") + async def autoimmune_list(self, ctx: commands.Context): + """ + Get's the current members and roles + + configured for automatic moderation action immunity + """ + ai_ids = await ctx.bot.db.guild(ctx.guild).autoimmune_ids() + + roles = {r.name for r in ctx.guild.roles if r.id in ai_ids} + members = {str(m) for m in ctx.guild.members if m.id in ai_ids} + + output = "" + if roles: + output += _("Roles immune from automated moderation actions:\n") + output += ", ".join(roles) + if members: + if roles: + output += "\n" + output += _("Members immune from automated moderation actions:\n") + output += ", ".join(members) + + if not output: + output = _("No immunty settings here.") + + for page in pagify(output): + await ctx.send(page) + + @autoimmune_group.command(name="add") + async def autoimmune_add( + self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role] + ): + """ + Makes a user or roles immune from automated moderation actions + """ + async with ctx.bot.db.guild(ctx.guild).autoimmune_ids() as ai_ids: + if user_or_role.id in ai_ids: + return await ctx.send(_("Already added.")) + ai_ids.append(user_or_role.id) + await ctx.tick() + + @autoimmune_group.command(name="remove") + async def autoimmune_remove( + self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role] + ): + """ + Makes a user or roles immune from automated moderation actions + """ + async with ctx.bot.db.guild(ctx.guild).autoimmune_ids() as ai_ids: + if user_or_role.id not in ai_ids: + return await ctx.send(_("Not in list.")) + ai_ids.remove(user_or_role.id) + await ctx.tick() + + @autoimmune_group.command(name="isimmune") + async def autoimmune_checkimmune( + self, ctx: commands.Context, user_or_role: Union[discord.Member, discord.Role] + ): + """ + Checks if a user or role would be considered immune from automated actions + """ + + if await ctx.bot.is_automod_immune(user_or_role): + await ctx.send(_("They are immune")) + else: + await ctx.send(_("They are not Immune")) + # RPC handlers async def rpc_load(self, request): cog_name = request.params[0]