diff options
author | 2022-03-02 00:08:34 +0200 | |
---|---|---|
committer | 2022-07-16 02:11:57 +0300 | |
commit | 1bc334457b36f79fd30f4b85b961c9c096a3c3bc (patch) | |
tree | 8b65a950645b0b9ed3f497360fa3cfd5f211af66 | |
parent | Fix argument completion for non-last args (diff) |
Add domain filtering
The domain filtering works very similarly to the token filtering, and the domain matching itself is based on the implementation in the old system.
The deletion setting is accessed explicitly in the domain filter in order to allow DMing the user the domain the message got deleted for.
This is fine, since practical uses are more important than the theory this system was designed with. Of course, breaking the design should still be avoided whenever possible.
-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) |