aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/exts/filtering/_filter_context.py4
-rw-r--r--bot/exts/filtering/_filter_lists/domain.py18
-rw-r--r--bot/exts/filtering/_filter_lists/extension.py19
-rw-r--r--bot/exts/filtering/_filter_lists/filter_list.py10
-rw-r--r--bot/exts/filtering/_filter_lists/invite.py17
-rw-r--r--bot/exts/filtering/_filter_lists/token.py18
-rw-r--r--bot/exts/filtering/_settings_types/actions/infraction_and_notification.py5
-rw-r--r--bot/exts/filtering/_ui/filter.py4
-rw-r--r--bot/exts/filtering/filtering.py39
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)