aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/exts/filtering/_filter_context.py1
-rw-r--r--bot/exts/filtering/_filter_lists/domain.py54
-rw-r--r--bot/exts/filtering/_filters/domain.py23
-rw-r--r--bot/exts/filtering/_settings.py22
-rw-r--r--bot/exts/filtering/_settings_types/delete_messages.py7
-rw-r--r--bot/exts/filtering/_settings_types/infraction_and_notification.py8
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)