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) |