diff options
| -rw-r--r-- | bot/exts/filtering/_filter_context.py | 1 | ||||
| -rw-r--r-- | bot/exts/filtering/_filter_lists/domain.py | 54 | ||||
| -rw-r--r-- | bot/exts/filtering/_filters/domain.py | 23 | ||||
| -rw-r--r-- | bot/exts/filtering/_settings.py | 22 | ||||
| -rw-r--r-- | bot/exts/filtering/_settings_types/delete_messages.py | 7 | ||||
| -rw-r--r-- | bot/exts/filtering/_settings_types/infraction_and_notification.py | 8 | 
6 files changed, 103 insertions, 12 deletions
| diff --git a/bot/exts/filtering/_filter_context.py b/bot/exts/filtering/_filter_context.py index 2fec9ce42..02738d452 100644 --- a/bot/exts/filtering/_filter_context.py +++ b/bot/exts/filtering/_filter_context.py @@ -33,6 +33,7 @@ class FilterContext:      alert_embeds: list = field(default_factory=list)  # Any embeds to add to the alert      action_descriptions: list = field(default_factory=list)  # What actions were taken      matches: list = field(default_factory=list)  # What exactly was found +    notification_domain: str = field(default_factory=str)  # A domain to send the user for context      def replace(self, **changes) -> FilterContext:          """Return a new context object assigning new values to the specified fields.""" diff --git a/bot/exts/filtering/_filter_lists/domain.py b/bot/exts/filtering/_filter_lists/domain.py new file mode 100644 index 000000000..a84328394 --- /dev/null +++ b/bot/exts/filtering/_filter_lists/domain.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import re +import typing +from functools import reduce +from operator import or_ +from typing import Optional + +from bot.exts.filtering._filter_context import Event, FilterContext +from bot.exts.filtering._filter_lists.filter_list import FilterList, ListType +from bot.exts.filtering._filters.domain import DomainFilter +from bot.exts.filtering._settings import ActionSettings +from bot.exts.filtering._utils import clean_input + +if typing.TYPE_CHECKING: +    from bot.exts.filtering.filtering import Filtering + +URL_RE = re.compile(r"(https?://[^\s]+)", flags=re.IGNORECASE) + + +class DomainsList(FilterList): +    """A list of filters, each looking for a specific domain given by URL.""" + +    name = "domain" + +    def __init__(self, filtering_cog: Filtering): +        super().__init__(DomainFilter) +        filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT) + +    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.""" +        text = ctx.content +        if not text: +            return None, "" + +        text = clean_input(text) +        urls = {match.group(1).lower() for match in URL_RE.finditer(text)} +        new_ctx = ctx.replace(content=urls) + +        triggers = self.filter_list_result( +            new_ctx, self.filter_lists[ListType.DENY], self.defaults[ListType.DENY]["validations"] +        ) +        ctx.notification_domain = new_ctx.notification_domain +        actions = None +        message = "" +        if triggers: +            actions = reduce(or_, (filter_.actions for filter_ in triggers)) +            if len(triggers) == 1: +                message = f"#{triggers[0].id} (`{triggers[0].content}`)" +                if triggers[0].description: +                    message += f" - {triggers[0].description}" +            else: +                message = ", ".join(f"#{filter_.id} (`{filter_.content}`)" for filter_ in triggers) +        return actions, message diff --git a/bot/exts/filtering/_filters/domain.py b/bot/exts/filtering/_filters/domain.py new file mode 100644 index 000000000..5d48c545f --- /dev/null +++ b/bot/exts/filtering/_filters/domain.py @@ -0,0 +1,23 @@ +import tldextract + +from bot.exts.filtering._filter_context import FilterContext +from bot.exts.filtering._filters.filter import Filter + + +class DomainFilter(Filter): +    """A filter which looks for a specific domain given by URL.""" + +    def triggered_on(self, ctx: FilterContext) -> bool: +        """Searches for a domain within a given context.""" +        domain = tldextract.extract(self.content).registered_domain + +        for found_url in ctx.content: +            if self.content in found_url and tldextract.extract(found_url).registered_domain == domain: +                ctx.matches.append(self.content) +                if ( +                        ("delete_messages" in self.actions and self.actions.get("delete_messages").delete_messages) +                        or not ctx.notification_domain +                ):  # Override this field only if this filter causes deletion. +                    ctx.notification_domain = self.content +                return True +        return False diff --git a/bot/exts/filtering/_settings.py b/bot/exts/filtering/_settings.py index 96e1c1f7f..b53400b78 100644 --- a/bot/exts/filtering/_settings.py +++ b/bot/exts/filtering/_settings.py @@ -1,6 +1,7 @@  from __future__ import annotations +  from abc import abstractmethod -from typing import Iterator, Mapping, Optional +from typing import Any, Iterator, Mapping, Optional, TypeVar  from bot.exts.filtering._filter_context import FilterContext  from bot.exts.filtering._settings_types import settings_types @@ -8,6 +9,8 @@ from bot.exts.filtering._settings_types.settings_entry import ActionEntry, Valid  from bot.exts.filtering._utils import FieldRequiring  from bot.log import get_logger +TSettings = TypeVar("TSettings", bound="Settings") +  log = get_logger(__name__)  _already_warned: set[str] = set() @@ -15,7 +18,7 @@ _already_warned: set[str] = set()  def create_settings(settings_data: dict) -> tuple[Optional[ActionSettings], Optional[ValidationSettings]]:      """ -    Create and return instances of the Settings subclasses from the given data +    Create and return instances of the Settings subclasses from the given data.      Additionally, warn for data entries with no matching class.      """ @@ -75,23 +78,30 @@ class Settings(FieldRequiring):                          f"Attempted to load a {entry_name} setting, but the response is malformed: {entry_data}"                      ) from e -    def __contains__(self, item) -> bool: +    def __contains__(self, item: str) -> bool:          return item in self._entries      def __setitem__(self, key: str, value: entry_type) -> None:          self._entries[key] = value -    def copy(self): +    def copy(self: TSettings) -> TSettings: +        """Create a shallow copy of the object."""          copy = self.__class__({}) -        copy._entries = self._entries +        copy._entries = self._entries.copy()          return copy      def items(self) -> Iterator[tuple[str, entry_type]]: +        """Return an iterator for the items in the entries dictionary."""          yield from self._entries.items()      def update(self, mapping: Mapping[str, entry_type], **kwargs: entry_type) -> None: +        """Update the entries with items from `mapping` and the kwargs."""          self._entries.update(mapping, **kwargs) +    def get(self, key: str, default: Optional[Any] = None) -> entry_type: +        """Get the entry matching the key, or fall back to the default value if the key is missing.""" +        return self._entries.get(key, default) +      @classmethod      def create(cls, settings_data: dict) -> Optional[Settings]:          """ @@ -152,7 +162,7 @@ class ActionSettings(Settings):          super().__init__(settings_data)      def __or__(self, other: ActionSettings) -> ActionSettings: -        """Combine the entries of two collections of settings into a new ActionsSettings""" +        """Combine the entries of two collections of settings into a new ActionsSettings."""          actions = {}          # A settings object doesn't necessarily have all types of entries (e.g in the case of filter overrides).          for entry in self._entries: diff --git a/bot/exts/filtering/_settings_types/delete_messages.py b/bot/exts/filtering/_settings_types/delete_messages.py index b0a018433..ad715f04c 100644 --- a/bot/exts/filtering/_settings_types/delete_messages.py +++ b/bot/exts/filtering/_settings_types/delete_messages.py @@ -14,11 +14,11 @@ class DeleteMessages(ActionEntry):      def __init__(self, entry_data: Any):          super().__init__(entry_data) -        self.delete: bool = entry_data +        self.delete_messages: bool = entry_data      async def action(self, ctx: FilterContext) -> None:          """Delete the context message(s).""" -        if not self.delete or ctx.event not in (Event.MESSAGE, Event.MESSAGE_EDIT): +        if not self.delete_messages or ctx.event not in (Event.MESSAGE, Event.MESSAGE_EDIT):              return          with suppress(NotFound): @@ -31,5 +31,4 @@ class DeleteMessages(ActionEntry):          if not isinstance(other, DeleteMessages):              return NotImplemented -        return DeleteMessages(self.delete or other.delete) - +        return DeleteMessages(self.delete_messages or other.delete_messages) diff --git a/bot/exts/filtering/_settings_types/infraction_and_notification.py b/bot/exts/filtering/_settings_types/infraction_and_notification.py index d308bf444..82e2ff6d6 100644 --- a/bot/exts/filtering/_settings_types/infraction_and_notification.py +++ b/bot/exts/filtering/_settings_types/infraction_and_notification.py @@ -80,8 +80,12 @@ class InfractionAndNotification(ActionEntry):              dm_embed = self.dm_embed          if dm_content or dm_embed: -            dm_content = f"Hey {ctx.author.mention}!\n{dm_content}" -            dm_embed = Embed(description=dm_embed, colour=Colour.og_blurple()) if dm_embed else None +            formatting = {"domain": ctx.notification_domain} +            dm_content = f"Hey {ctx.author.mention}!\n{dm_content.format(**formatting)}" +            if dm_embed: +                dm_embed = Embed(description=dm_embed.format(**formatting), colour=Colour.og_blurple()) +            else: +                dm_embed = None              try:                  await ctx.author.send(dm_content, embed=dm_embed) | 
