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