diff options
| -rw-r--r-- | bot/exts/filtering/_filter_context.py | 4 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/domain.py | 18 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/extension.py | 19 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/filter_list.py | 10 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/invite.py | 17 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/token.py | 18 | ||||
| -rw-r--r-- | bot/exts/filtering/_settings_types/actions/infraction_and_notification.py | 5 | ||||
| -rw-r--r-- | bot/exts/filtering/_ui/filter.py | 4 | ||||
| -rw-r--r-- | bot/exts/filtering/filtering.py | 39 | 
9 files changed, 83 insertions, 51 deletions
| diff --git a/bot/exts/filtering/_filter_context.py b/bot/exts/filtering/_filter_context.py index 02738d452..5e2f5b45b 100644 --- a/bot/exts/filtering/_filter_context.py +++ b/bot/exts/filtering/_filter_context.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field, replace  from enum import Enum, auto  from typing import Optional, Union -from discord import DMChannel, Message, TextChannel, Thread, User +from discord import DMChannel, Member, Message, TextChannel, Thread, User  class Event(Enum): @@ -20,7 +20,7 @@ class FilterContext:      # Input context      event: Event  # The type of event -    author: User  # Who triggered the event +    author: User | Member | None  # Who triggered the event      channel: Union[TextChannel, Thread, DMChannel]  # The channel involved      content: Union[str, set]  # What actually needs filtering      message: Optional[Message]  # The message involved diff --git a/bot/exts/filtering/_filter_lists/domain.py b/bot/exts/filtering/_filter_lists/domain.py index ec43e92df..34ab5670c 100644 --- a/bot/exts/filtering/_filter_lists/domain.py +++ b/bot/exts/filtering/_filter_lists/domain.py @@ -4,7 +4,6 @@ import re  import typing  from functools import reduce  from operator import or_ -from typing import Optional, Type  from bot.exts.filtering._filter_context import Event, FilterContext  from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType @@ -36,20 +35,20 @@ class DomainsList(FilterList):          super().__init__()          filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT) -    def get_filter_type(self, content: str) -> Type[Filter]: +    def get_filter_type(self, content: str) -> type[Filter]:          """Get a subclass of filter matching the filter list and the filter's content."""          return DomainFilter      @property -    def filter_types(self) -> set[Type[Filter]]: +    def filter_types(self) -> set[type[Filter]]:          """Return the types of filters used by this list."""          return {DomainFilter} -    async def actions_for(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], Optional[str]]: -        """Dispatch the given event to the list's filters, and return actions to take and a message to relay to mods.""" +    async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: +        """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods."""          text = ctx.content          if not text: -            return None, "" +            return None, []          text = clean_input(text)          urls = {match.group(1).lower().rstrip("/") for match in URL_RE.finditer(text)} @@ -60,7 +59,7 @@ class DomainsList(FilterList):          )          ctx.notification_domain = new_ctx.notification_domain          actions = None -        message = "" +        messages = []          if triggers:              action_defaults = self[ListType.DENY].defaults.actions              actions = reduce( @@ -73,6 +72,7 @@ class DomainsList(FilterList):                  message = f"#{triggers[0].id} (`{triggers[0].content}`)"                  if triggers[0].description:                      message += f" - {triggers[0].description}" +                messages = [message]              else: -                message = ", ".join(f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers) -        return actions, message +                messages = [f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers] +        return actions, messages diff --git a/bot/exts/filtering/_filter_lists/extension.py b/bot/exts/filtering/_filter_lists/extension.py index ce1a46e4a..a58c6c45e 100644 --- a/bot/exts/filtering/_filter_lists/extension.py +++ b/bot/exts/filtering/_filter_lists/extension.py @@ -2,7 +2,6 @@ from __future__ import annotations  import typing  from os.path import splitext -from typing import Optional, Type  import bot  from bot.constants import Channels, URLs @@ -53,24 +52,24 @@ class ExtensionsList(FilterList):          filtering_cog.subscribe(self, Event.MESSAGE)          self._whitelisted_description = None -    def get_filter_type(self, content: str) -> Type[Filter]: +    def get_filter_type(self, content: str) -> type[Filter]:          """Get a subclass of filter matching the filter list and the filter's content."""          return ExtensionFilter      @property -    def filter_types(self) -> set[Type[Filter]]: +    def filter_types(self) -> set[type[Filter]]:          """Return the types of filters used by this list."""          return {ExtensionFilter} -    async def actions_for(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], Optional[str]]: -        """Dispatch the given event to the list's filters, and return actions to take and a message to relay to mods.""" +    async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: +        """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods."""          # Return early if the message doesn't have attachments. -        if not ctx.message.attachments: -            return None, "" +        if not ctx.message or not ctx.message.attachments: +            return None, []          _, failed = self[ListType.ALLOW].defaults.validations.evaluate(ctx)          if failed:  # There's no extension filtering in this context. -            return None, "" +            return None, []          # Find all extensions in the message.          all_ext = { @@ -84,7 +83,7 @@ class ExtensionsList(FilterList):          not_allowed = {ext: filename for ext, filename in all_ext if ext not in allowed_ext}          if not not_allowed:  # Yes, it's a double negative. Meaning all attachments are allowed :) -            return None, "" +            return None, []          # Something is disallowed.          if ".py" in not_allowed: @@ -110,4 +109,4 @@ class ExtensionsList(FilterList):              )          ctx.matches += not_allowed.values() -        return self[ListType.ALLOW].defaults.actions, ", ".join(f"`{ext}`" for ext in not_allowed) +        return self[ListType.ALLOW].defaults.actions, [f"`{ext}`" for ext in not_allowed] diff --git a/bot/exts/filtering/_filter_lists/filter_list.py b/bot/exts/filtering/_filter_lists/filter_list.py index ecbcb8f09..daab45b81 100644 --- a/bot/exts/filtering/_filter_lists/filter_list.py +++ b/bot/exts/filtering/_filter_lists/filter_list.py @@ -1,7 +1,7 @@  from abc import abstractmethod  from collections.abc import Iterator  from enum import Enum -from typing import Any, ItemsView, NamedTuple, Optional, Type +from typing import Any, ItemsView, NamedTuple  from discord.ext.commands import BadArgument @@ -128,17 +128,17 @@ class FilterList(FieldRequiring):          return new_filter      @abstractmethod -    def get_filter_type(self, content: str) -> Type[Filter]: +    def get_filter_type(self, content: str) -> type[Filter]:          """Get a subclass of filter matching the filter list and the filter's content."""      @property      @abstractmethod -    def filter_types(self) -> set[Type[Filter]]: +    def filter_types(self) -> set[type[Filter]]:          """Return the types of filters used by this list."""      @abstractmethod -    async def actions_for(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], Optional[str]]: -        """Dispatch the given event to the list's filters, and return actions to take and a message to relay to mods.""" +    async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: +        """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods."""      @staticmethod      def filter_list_result( diff --git a/bot/exts/filtering/_filter_lists/invite.py b/bot/exts/filtering/_filter_lists/invite.py index 30884a2ab..5bb4549ae 100644 --- a/bot/exts/filtering/_filter_lists/invite.py +++ b/bot/exts/filtering/_filter_lists/invite.py @@ -3,7 +3,6 @@ from __future__ import annotations  import typing  from functools import reduce  from operator import or_ -from typing import Optional, Type  from botcore.utils.regex import DISCORD_INVITE  from discord import Embed, Invite @@ -42,20 +41,20 @@ class InviteList(FilterList):          super().__init__()          filtering_cog.subscribe(self, Event.MESSAGE) -    def get_filter_type(self, content: str) -> Type[Filter]: +    def get_filter_type(self, content: str) -> type[Filter]:          """Get a subclass of filter matching the filter list and the filter's content."""          return InviteFilter      @property -    def filter_types(self) -> set[Type[Filter]]: +    def filter_types(self) -> set[type[Filter]]:          """Return the types of filters used by this list."""          return {InviteFilter} -    async def actions_for(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], Optional[str]]: -        """Dispatch the given event to the list's filters, and return actions to take and a message to relay to mods.""" +    async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: +        """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods."""          _, failed = self[ListType.ALLOW].defaults.validations.evaluate(ctx)          if failed:  # There's no invite filtering in this context. -            return None, "" +            return None, []          text = clean_input(ctx.content) @@ -65,7 +64,7 @@ class InviteList(FilterList):          matches = list(DISCORD_INVITE.finditer(text))          invite_codes = {m.group("invite") for m in matches}          if not invite_codes: -            return None, "" +            return None, []          # Sort the invites into three categories:          denied_by_default = dict()  # Denied unless whitelisted. @@ -107,7 +106,7 @@ class InviteList(FilterList):          })          if not disallowed_invites: -            return None, "" +            return None, []          actions = None          if len(disallowed_invites) > len(triggered):  # There are invites which weren't allowed but aren't blacklisted. @@ -124,7 +123,7 @@ class InviteList(FilterList):              actions = reduce(or_, (filter_.actions for filter_ in triggered))          ctx.matches += {match[0] for match in matches if match.group("invite") in disallowed_invites}          ctx.alert_embeds += (self._guild_embed(invite) for invite in disallowed_invites.values() if invite) -        return actions, ", ".join(f"`{invite}`" for invite in disallowed_invites) +        return actions, [f"`{invite}`" for invite in disallowed_invites]      @staticmethod      def _guild_embed(invite: Invite) -> Embed: diff --git a/bot/exts/filtering/_filter_lists/token.py b/bot/exts/filtering/_filter_lists/token.py index 2abf94553..c80ccfd68 100644 --- a/bot/exts/filtering/_filter_lists/token.py +++ b/bot/exts/filtering/_filter_lists/token.py @@ -4,7 +4,6 @@ import re  import typing  from functools import reduce  from operator import or_ -from typing import Optional, Type  from bot.exts.filtering._filter_context import Event, FilterContext  from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType @@ -37,20 +36,20 @@ class TokensList(FilterList):          super().__init__()          filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT) -    def get_filter_type(self, content: str) -> Type[Filter]: +    def get_filter_type(self, content: str) -> type[Filter]:          """Get a subclass of filter matching the filter list and the filter's content."""          return TokenFilter      @property -    def filter_types(self) -> set[Type[Filter]]: +    def filter_types(self) -> set[type[Filter]]:          """Return the types of filters used by this list."""          return {TokenFilter} -    async def actions_for(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], Optional[str]]: -        """Dispatch the given event to the list's filters, and return actions to take and a message to relay to mods.""" +    async def actions_for(self, ctx: FilterContext) -> tuple[ActionSettings | None, list[str]]: +        """Dispatch the given event to the list's filters, and return actions to take and messages to relay to mods."""          text = ctx.content          if not text: -            return None, "" +            return None, []          if SPOILER_RE.search(text):              text = self._expand_spoilers(text)          text = clean_input(text) @@ -60,7 +59,7 @@ class TokensList(FilterList):              ctx, self[ListType.DENY].filters, self[ListType.DENY].defaults.validations          )          actions = None -        message = "" +        messages = []          if triggers:              action_defaults = self[ListType.DENY].defaults.actions              actions = reduce( @@ -73,9 +72,10 @@ class TokensList(FilterList):                  message = f"#{triggers[0].id} (`{triggers[0].content}`)"                  if triggers[0].description:                      message += f" - {triggers[0].description}" +                messages = [message]              else: -                message = ", ".join(f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers) -        return actions, message +                messages = [f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers] +        return actions, messages      @staticmethod      def _expand_spoilers(text: str) -> str: diff --git a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py index 4ec06ef4c..7835a7d0b 100644 --- a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py +++ b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py @@ -106,7 +106,10 @@ class InfractionAndNotification(ActionEntry):          ) + ", ".join(infraction.name for infraction in Infraction),          "infraction_duration": "How long the infraction should last for in seconds, or 'None' for permanent.",          "infraction_reason": "The reason delivered with the infraction.", -        "infraction_channel": "The channel ID in which to invoke the infraction (and send the confirmation message).", +        "infraction_channel": ( +            "The channel ID in which to invoke the infraction (and send the confirmation message). " +            "If blank, the infraction will be sent in the context channel." +        ),          "dm_content": "The contents of a message to be DMed to the offending user.",          "dm_embed": "The contents of the embed to be DMed to the offending user."      } diff --git a/bot/exts/filtering/_ui/filter.py b/bot/exts/filtering/_ui/filter.py index e6a568ad0..e6330329d 100644 --- a/bot/exts/filtering/_ui/filter.py +++ b/bot/exts/filtering/_ui/filter.py @@ -452,9 +452,9 @@ def template_settings(filter_id: str, filter_list: FilterList, list_type: ListTy      except ValueError:          raise ValueError("Template value must be a non-negative integer.") -    if filter_id not in filter_list.filter_lists[list_type]: +    if filter_id not in filter_list[list_type].filters:          raise ValueError(              f"Could not find filter with ID `{filter_id}` in the {list_type.name} {filter_list.name} list."          ) -    filter_ = filter_list.filter_lists[list_type][filter_id] +    filter_ = filter_list[list_type].filters[filter_id]      return filter_overrides(filter_, filter_list, list_type) diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py index eb1615b28..c47ba653f 100644 --- a/bot/exts/filtering/filtering.py +++ b/bot/exts/filtering/filtering.py @@ -16,7 +16,7 @@ from discord.utils import escape_markdown  import bot  import bot.exts.filtering._ui.filter as filters_ui  from bot.bot import Bot -from bot.constants import Colours, MODERATION_ROLES, Roles, Webhooks +from bot.constants import Channels, Colours, MODERATION_ROLES, Roles, Webhooks  from bot.exts.filtering._filter_context import Event, FilterContext  from bot.exts.filtering._filter_lists import FilterList, ListType, filter_list_types, list_type_converter  from bot.exts.filtering._filter_lists.filter_list import AtomicList @@ -492,6 +492,37 @@ class Filtering(Cog):          embed.colour = Colour.blue()          await ctx.send(embed=embed) +    @filter.command(name="match") +    async def f_match(self, ctx: Context, message: Message | None, *, string: str | None) -> None: +        """ +        Post any responses from the filter lists for the given message or string. + +        If there's a message the string will be ignored. Note that if a message is provided, it will go through all +        validations appropriate to where it was sent and who sent it. + +        If a string is provided, it will be validated in the context of a user with no roles in python-general. +        """ +        if not message and not string: +            raise BadArgument(":x: Please provide input.") +        if message: +            filter_ctx = FilterContext( +                Event.MESSAGE, message.author, message.channel, message.content, message, message.embeds +            ) +        else: +            filter_ctx = FilterContext( +                Event.MESSAGE, None, ctx.guild.get_channel(Channels.python_general), string, None +            ) + +        _, list_messages = await self._resolve_action(filter_ctx) +        lines = [] +        for filter_list, list_message_list in list_messages.items(): +            if list_message_list: +                lines.extend([f"**{filter_list.name.title()}s**", *list_message_list, "\n"]) +        lines = lines[:-1]  # Remove last newline. + +        embed = Embed(colour=Colour.blue(), title="Match results") +        await LinePaginator.paginate(lines, ctx, embed, max_lines=10, empty=False) +      # endregion      # region: filterlist group @@ -651,7 +682,7 @@ class Filtering(Cog):              self.filter_lists[list_name] = filter_list_types[list_name](self)          return self.filter_lists[list_name].add_list(list_data) -    async def _resolve_action(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], dict[FilterList, str]]: +    async def _resolve_action(self, ctx: FilterContext) -> tuple[Optional[ActionSettings], dict[FilterList, list[str]]]:          """          Return the actions that should be taken for all filter lists in the given context. @@ -673,7 +704,7 @@ class Filtering(Cog):          return result_actions, messages -    async def _send_alert(self, ctx: FilterContext, triggered_filters: dict[FilterList, str]) -> None: +    async def _send_alert(self, ctx: FilterContext, triggered_filters: dict[FilterList, list[str]]) -> None:          """Build an alert message from the filter context, and send it via the alert webhook."""          if not self.webhook:              return @@ -691,7 +722,7 @@ class Filtering(Cog):          filters = []          for filter_list, list_message in triggered_filters.items():              if list_message: -                filters.append(f"**{filter_list.name.title()} Filters:** {list_message}") +                filters.append(f"**{filter_list.name.title()} Filters:** {', '.join(list_message)}")          filters = "\n".join(filters)          matches = "**Matches:** " + ", ".join(repr(match) for match in ctx.matches) | 
