aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar mbaruh <[email protected]>2022-11-25 21:00:13 +0200
committerGravatar mbaruh <[email protected]>2022-11-25 21:00:13 +0200
commitafd9d4ec993dba3c76ab8a8341b718c75d2a68e6 (patch)
tree93f0a230b3bd56fe9d9edc724624f0e33c9cef35
parentAdd message edit filtering (diff)
Add nickname filter
The nickname filter works in much the same way as the one in the old system, with the following changes: - The lock is per user, rather than a global lock. - The alert cooldown is one hour, instead of three days which seemed too much. The delete_messages setting was changed to the more generic remove_context. If it's a nickname event, the context will be removed by applying a superstar infraction to the user. In order to allow filtering nicknames in voice state events, the filter context can now have None in the channel field. Additionally: - Fixes a bug when ignoring filters in message edits. - Makes the invites list keep track of message edits. - The FakeContext class is moved to utils since it's now also needed by remove_context.
-rw-r--r--bot/exts/filtering/_filter_context.py3
-rw-r--r--bot/exts/filtering/_filter_lists/filter_list.py6
-rw-r--r--bot/exts/filtering/_filter_lists/invite.py2
-rw-r--r--bot/exts/filtering/_filter_lists/token.py2
-rw-r--r--bot/exts/filtering/_filters/unique/rich_embed.py2
-rw-r--r--bot/exts/filtering/_settings_types/actions/infraction_and_notification.py44
-rw-r--r--bot/exts/filtering/_settings_types/actions/ping.py2
-rw-r--r--bot/exts/filtering/_settings_types/actions/remove_context.py (renamed from bot/exts/filtering/_settings_types/actions/delete_messages.py)57
-rw-r--r--bot/exts/filtering/_settings_types/validations/channel_scope.py2
-rw-r--r--bot/exts/filtering/_settings_types/validations/filter_dm.py3
-rw-r--r--bot/exts/filtering/_ui/ui.py22
-rw-r--r--bot/exts/filtering/_utils.py35
-rw-r--r--bot/exts/filtering/filtering.py56
13 files changed, 176 insertions, 60 deletions
diff --git a/bot/exts/filtering/_filter_context.py b/bot/exts/filtering/_filter_context.py
index 4a213535a..61f8c9fbc 100644
--- a/bot/exts/filtering/_filter_context.py
+++ b/bot/exts/filtering/_filter_context.py
@@ -18,6 +18,7 @@ class Event(Enum):
MESSAGE = auto()
MESSAGE_EDIT = auto()
+ NICKNAME = auto()
@dataclass
@@ -27,7 +28,7 @@ class FilterContext:
# Input context
event: Event # The type of event
author: User | Member | None # Who triggered the event
- channel: TextChannel | Thread | DMChannel # The channel involved
+ channel: TextChannel | Thread | DMChannel | None # The channel involved
content: str | Iterable # What actually needs filtering
message: Message | None # The message involved
embeds: list = field(default_factory=list) # Any embeds involved
diff --git a/bot/exts/filtering/_filter_lists/filter_list.py b/bot/exts/filtering/_filter_lists/filter_list.py
index b5d6141d7..c829f4a8f 100644
--- a/bot/exts/filtering/_filter_lists/filter_list.py
+++ b/bot/exts/filtering/_filter_lists/filter_list.py
@@ -105,10 +105,10 @@ class AtomicList:
if ctx.event == Event.MESSAGE_EDIT and ctx.message and self.list_type == ListType.DENY:
previously_triggered = ctx.message_cache.get_message_metadata(ctx.message.id)
+ ignore_filters = previously_triggered[self]
+ # This updates the cache. Some filters are ignored, but they're necessary if there's another edit.
+ previously_triggered[self] = relevant_filters
if previously_triggered and self in previously_triggered:
- ignore_filters = previously_triggered[self]
- # This updates the cache. Some filters are ignored, but they're necessary if there's another edit.
- previously_triggered[self] = relevant_filters
relevant_filters = [filter_ for filter_ in relevant_filters if filter_ not in ignore_filters]
return relevant_filters
diff --git a/bot/exts/filtering/_filter_lists/invite.py b/bot/exts/filtering/_filter_lists/invite.py
index 36031f276..dd14d2222 100644
--- a/bot/exts/filtering/_filter_lists/invite.py
+++ b/bot/exts/filtering/_filter_lists/invite.py
@@ -37,7 +37,7 @@ class InviteList(FilterList[InviteFilter]):
def __init__(self, filtering_cog: Filtering):
super().__init__()
- filtering_cog.subscribe(self, Event.MESSAGE)
+ filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT)
def get_filter_type(self, content: str) -> type[Filter]:
"""Get a subclass of filter matching the filter list and the filter's content."""
diff --git a/bot/exts/filtering/_filter_lists/token.py b/bot/exts/filtering/_filter_lists/token.py
index e4dbf4717..f5da28bb5 100644
--- a/bot/exts/filtering/_filter_lists/token.py
+++ b/bot/exts/filtering/_filter_lists/token.py
@@ -32,7 +32,7 @@ class TokensList(FilterList[TokenFilter]):
def __init__(self, filtering_cog: Filtering):
super().__init__()
- filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT)
+ filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT, Event.NICKNAME)
def get_filter_type(self, content: str) -> type[Filter]:
"""Get a subclass of filter matching the filter list and the filter's content."""
diff --git a/bot/exts/filtering/_filters/unique/rich_embed.py b/bot/exts/filtering/_filters/unique/rich_embed.py
index 5c3517e10..00c28e571 100644
--- a/bot/exts/filtering/_filters/unique/rich_embed.py
+++ b/bot/exts/filtering/_filters/unique/rich_embed.py
@@ -21,6 +21,8 @@ class RichEmbedFilter(UniqueFilter):
"""Determine if `msg` contains any rich embeds not auto-generated from a URL."""
if ctx.embeds:
if ctx.event == Event.MESSAGE_EDIT:
+ if not ctx.message.edited_at: # This might happen, apparently.
+ return False
# If the edit delta is less than 100 microseconds, it's probably a double filter trigger.
delta = ctx.message.edited_at - (ctx.before_message.edited_at or ctx.before_message.created_at)
if delta.total_seconds() < 0.0001:
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 b8b463626..f29aee571 100644
--- a/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py
+++ b/bot/exts/filtering/_settings_types/actions/infraction_and_notification.py
@@ -1,4 +1,3 @@
-from dataclasses import dataclass
from datetime import timedelta
from enum import Enum, auto
from typing import ClassVar
@@ -11,45 +10,14 @@ from discord.errors import Forbidden
from pydantic import validator
import bot as bot_module
-from bot.constants import Channels, Guild
+from bot.constants import Channels
from bot.exts.filtering._filter_context import FilterContext
from bot.exts.filtering._settings_types.settings_entry import ActionEntry
+from bot.exts.filtering._utils import FakeContext
log = get_logger(__name__)
-@dataclass
-class FakeContext:
- """
- A class representing a context-like object that can be sent to infraction commands.
-
- The goal is to be able to apply infractions without depending on the existence of a message or an interaction
- (which are the two ways to create a Context), e.g. in API events which aren't message-driven, or in custom filtering
- events.
- """
-
- channel: discord.abc.Messageable
- bot: bot_module.bot.Bot | None = None
- guild: discord.Guild | None = None
- author: discord.Member | discord.User | None = None
- me: discord.Member | None = None
-
- def __post_init__(self):
- """Initialize the missing information."""
- if not self.bot:
- self.bot = bot_module.instance
- if not self.guild:
- self.guild = self.bot.get_guild(Guild.id)
- if not self.me:
- self.me = self.guild.me
- if not self.author:
- self.author = self.me
-
- async def send(self, *args, **kwargs) -> discord.Message:
- """A wrapper for channel.send."""
- return await self.channel.send(*args, **kwargs)
-
-
class Infraction(Enum):
"""An enumeration of infraction types. The lower the value, the higher it is on the hierarchy."""
@@ -79,6 +47,8 @@ class Infraction(Enum):
command = bot_module.instance.get_command(command_name)
if not command:
await alerts_channel.send(f":warning: Could not apply {command_name} to {user.mention}: command not found.")
+ log.warning(f":warning: Could not apply {command_name} to {user.mention}: command not found.")
+ return
ctx = FakeContext(channel)
if self.name in ("KICK", "WARNING", "WATCH", "NOTE"):
@@ -160,8 +130,14 @@ class InfractionAndNotification(ActionEntry):
if not channel:
log.info(f"Could not find a channel with ID {self.infraction_channel}, infracting in mod-alerts.")
channel = alerts_channel
+ elif not ctx.channel:
+ channel = alerts_channel
else:
channel = ctx.channel
+ if not channel: # If somehow it's set to `alerts_channel` and it can't be found.
+ log.error(f"Unable to apply infraction as the context channel {channel} can't be found.")
+ return
+
await self.infraction_type.invoke(
ctx.author, channel, alerts_channel, self.infraction_duration, self.infraction_reason
)
diff --git a/bot/exts/filtering/_settings_types/actions/ping.py b/bot/exts/filtering/_settings_types/actions/ping.py
index 5597bdd59..b3725917c 100644
--- a/bot/exts/filtering/_settings_types/actions/ping.py
+++ b/bot/exts/filtering/_settings_types/actions/ping.py
@@ -35,7 +35,7 @@ class Ping(ActionEntry):
async def action(self, ctx: FilterContext) -> None:
"""Add the stored pings to the alert message content."""
- mentions = self.guild_pings if ctx.channel.guild else self.dm_pings
+ mentions = self.guild_pings if not ctx.channel or ctx.channel.guild else self.dm_pings
new_content = " ".join([resolve_mention(mention) for mention in mentions])
ctx.alert_content = f"{new_content} {ctx.alert_content}"
diff --git a/bot/exts/filtering/_settings_types/actions/delete_messages.py b/bot/exts/filtering/_settings_types/actions/remove_context.py
index 19c0beb95..7eb3db6c4 100644
--- a/bot/exts/filtering/_settings_types/actions/delete_messages.py
+++ b/bot/exts/filtering/_settings_types/actions/remove_context.py
@@ -2,14 +2,24 @@ from collections import defaultdict
from typing import ClassVar
from botcore.utils import scheduling
+from botcore.utils.logging import get_logger
from discord import Message
from discord.errors import HTTPException
+import bot
from bot.constants import Channels
-from bot.exts.filtering._filter_context import FilterContext
+from bot.exts.filtering._filter_context import Event, FilterContext
from bot.exts.filtering._settings_types.settings_entry import ActionEntry
+from bot.exts.filtering._utils import FakeContext
from bot.utils.messages import send_attachments
+log = get_logger(__name__)
+
+SUPERSTAR_REASON = (
+ "Your nickname was found to be in violation of our code of conduct. "
+ "If you believe this is a mistake, please let us know."
+)
+
async def upload_messages_attachments(ctx: FilterContext, messages: list[Message]) -> None:
"""Re-upload the messages' attachments for future logging."""
@@ -21,22 +31,31 @@ async def upload_messages_attachments(ctx: FilterContext, messages: list[Message
ctx.attachments[message.id] = await send_attachments(message, destination, link_large=False)
-class DeleteMessages(ActionEntry):
+class RemoveContext(ActionEntry):
"""A setting entry which tells whether to delete the offending message(s)."""
- name: ClassVar[str] = "delete_messages"
+ name: ClassVar[str] = "remove_context"
description: ClassVar[str] = (
- "A boolean field. If True, the filter being triggered will cause the offending message to be deleted."
+ "A boolean field. If True, the filter being triggered will cause the offending context to be removed. "
+ "An offending message will be deleted, while an offending nickname will be superstarified."
)
- delete_messages: bool
+ remove_context: bool
async def action(self, ctx: FilterContext) -> None:
- """Delete the context message(s)."""
- if not self.delete_messages or not ctx.message:
+ """Remove the offending context."""
+ if not self.remove_context:
return
- if not ctx.message.guild:
+ if ctx.event in (Event.MESSAGE, Event.MESSAGE_EDIT):
+ await self._handle_messages(ctx)
+ elif ctx.event == Event.NICKNAME:
+ await self._handle_nickname(ctx)
+
+ @staticmethod
+ async def _handle_messages(ctx: FilterContext) -> None:
+ """Delete any messages involved in this context."""
+ if not ctx.message or not ctx.message.guild:
return
channel_messages = defaultdict(set) # Duplicates will cause batch deletion to fail.
@@ -68,9 +87,27 @@ class DeleteMessages(ActionEntry):
else:
ctx.action_descriptions.append(f"{success} deleted, {fail} failed to delete")
+ @staticmethod
+ async def _handle_nickname(ctx: FilterContext) -> None:
+ """Apply a superstar infraction to remove the user's nickname."""
+ alerts_channel = bot.instance.get_channel(Channels.mod_alerts)
+ if not alerts_channel:
+ log.error(f"Unable to apply superstar as the context channel {alerts_channel} can't be found.")
+ return
+ command = bot.instance.get_command("superstar")
+ if not command:
+ user = ctx.author
+ await alerts_channel.send(f":warning: Could not apply superstar to {user.mention}: command not found.")
+ log.warning(f":warning: Could not apply superstar to {user.mention}: command not found.")
+ ctx.action_descriptions.append("failed to superstar")
+ return
+
+ await command(FakeContext(alerts_channel), ctx.author, None, reason=SUPERSTAR_REASON)
+ ctx.action_descriptions.append("superstar")
+
def __or__(self, other: ActionEntry):
"""Combines two actions of the same type. Each type of action is executed once per filter."""
- if not isinstance(other, DeleteMessages):
+ if not isinstance(other, RemoveContext):
return NotImplemented
- return DeleteMessages(delete_messages=self.delete_messages or other.delete_messages)
+ return RemoveContext(delete_messages=self.remove_context or other.remove_context)
diff --git a/bot/exts/filtering/_settings_types/validations/channel_scope.py b/bot/exts/filtering/_settings_types/validations/channel_scope.py
index 80f837a15..d37efaa09 100644
--- a/bot/exts/filtering/_settings_types/validations/channel_scope.py
+++ b/bot/exts/filtering/_settings_types/validations/channel_scope.py
@@ -51,6 +51,8 @@ class ChannelScope(ValidationEntry):
"""
channel = ctx.channel
+ if not channel:
+ return True
if not hasattr(channel, "category"): # This is not a guild channel, outside the scope of this setting.
return True
if hasattr(channel, "parent"):
diff --git a/bot/exts/filtering/_settings_types/validations/filter_dm.py b/bot/exts/filtering/_settings_types/validations/filter_dm.py
index b9e566253..9961984d6 100644
--- a/bot/exts/filtering/_settings_types/validations/filter_dm.py
+++ b/bot/exts/filtering/_settings_types/validations/filter_dm.py
@@ -14,4 +14,7 @@ class FilterDM(ValidationEntry):
def triggers_on(self, ctx: FilterContext) -> bool:
"""Return whether the filter should be triggered even if it was triggered in DMs."""
+ if not ctx.channel: # No channel - out of scope for this setting.
+ return True
+
return ctx.channel.guild is not None or self.filter_dm
diff --git a/bot/exts/filtering/_ui/ui.py b/bot/exts/filtering/_ui/ui.py
index 17a933783..ec549725c 100644
--- a/bot/exts/filtering/_ui/ui.py
+++ b/bot/exts/filtering/_ui/ui.py
@@ -2,6 +2,7 @@ from __future__ import annotations
import re
from abc import ABC, abstractmethod
+from collections.abc import Iterable
from enum import EnumMeta
from functools import partial
from typing import Any, Callable, Coroutine, Optional, TypeVar
@@ -72,17 +73,21 @@ async def _build_alert_message_content(ctx: FilterContext, current_message_lengt
return alert_content
-async def build_mod_alert(ctx: FilterContext, triggered_filters: dict[FilterList, list[str]]) -> Embed:
+async def build_mod_alert(ctx: FilterContext, triggered_filters: dict[FilterList, Iterable[str]]) -> Embed:
"""Build an alert message from the filter context."""
embed = Embed(color=Colours.soft_orange)
embed.set_thumbnail(url=ctx.author.display_avatar.url)
triggered_by = f"**Triggered by:** {format_user(ctx.author)}"
- if ctx.channel.guild:
- triggered_in = f"**Triggered in:** {format_channel(ctx.channel)}\n"
+ if ctx.channel:
+ if ctx.channel.guild:
+ triggered_in = f"**Triggered in:** {format_channel(ctx.channel)}\n"
+ else:
+ triggered_in = "**Triggered in:** :warning:**DM**:warning:\n"
+ if len(ctx.related_channels) > 1:
+ triggered_in += f"**Channels:** {', '.join(channel.mention for channel in ctx.related_channels)}\n"
else:
- triggered_in = "**Triggered in:** :warning:**DM**:warning:\n"
- if len(ctx.related_channels) > 1:
- triggered_in += f"**Channels:** {', '.join(channel.mention for channel in ctx.related_channels)}\n"
+ triggered_by += "\n"
+ triggered_in = ""
filters = []
for filter_list, list_message in triggered_filters.items():
@@ -94,7 +99,10 @@ async def build_mod_alert(ctx: FilterContext, triggered_filters: dict[FilterList
actions = "\n**Actions Taken:** " + (", ".join(ctx.action_descriptions) if ctx.action_descriptions else "-")
mod_alert_message = "\n".join(part for part in (triggered_by, triggered_in, filters, matches, actions) if part)
- mod_alert_message += f"\n**[Original Content]({ctx.message.jump_url})**:\n"
+ if ctx.message:
+ mod_alert_message += f"\n**[Original Content]({ctx.message.jump_url})**:\n"
+ else:
+ mod_alert_message += "\n**Original Content**:\n"
mod_alert_message += await _build_alert_message_content(ctx, len(mod_alert_message))
embed.description = mod_alert_message
diff --git a/bot/exts/filtering/_utils.py b/bot/exts/filtering/_utils.py
index 86b6ab101..bd56c1260 100644
--- a/bot/exts/filtering/_utils.py
+++ b/bot/exts/filtering/_utils.py
@@ -4,12 +4,15 @@ import inspect
import pkgutil
from abc import ABC, abstractmethod
from collections import defaultdict
+from dataclasses import dataclass
from functools import cache
from typing import Any, Iterable, TypeVar, Union
+import discord
import regex
import bot
+from bot.bot import Bot
from bot.constants import Guild
VARIATION_SELECTORS = r"\uFE00-\uFE0F\U000E0100-\U000E01EF"
@@ -183,3 +186,35 @@ class FieldRequiring(ABC):
else:
# Add to the set of unique values for that field.
FieldRequiring.__unique_attributes[parent][attribute].add(value)
+
+
+@dataclass
+class FakeContext:
+ """
+ A class representing a context-like object that can be sent to infraction commands.
+
+ The goal is to be able to apply infractions without depending on the existence of a message or an interaction
+ (which are the two ways to create a Context), e.g. in API events which aren't message-driven, or in custom filtering
+ events.
+ """
+
+ channel: discord.abc.Messageable
+ bot: Bot | None = None
+ guild: discord.Guild | None = None
+ author: discord.Member | discord.User | None = None
+ me: discord.Member | None = None
+
+ def __post_init__(self):
+ """Initialize the missing information."""
+ if not self.bot:
+ self.bot = bot.instance
+ if not self.guild:
+ self.guild = self.bot.get_guild(Guild.id)
+ if not self.me:
+ self.me = self.guild.me
+ if not self.author:
+ self.author = self.me
+
+ async def send(self, *args, **kwargs) -> discord.Message:
+ """A wrapper for channel.send."""
+ return await self.channel.send(*args, **kwargs)
diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py
index 05b2339b9..9c9b1eff4 100644
--- a/bot/exts/filtering/filtering.py
+++ b/bot/exts/filtering/filtering.py
@@ -2,13 +2,17 @@ import datetime
import json
import operator
import re
+import unicodedata
from collections import defaultdict
+from collections.abc import Iterable
from functools import partial, reduce
from io import BytesIO
+from operator import attrgetter
from typing import Literal, Optional, get_type_hints
import arrow
import discord
+from async_rediscache import RedisCache
from botcore.site_api import ResponseCodeError
from discord import Colour, Embed, HTTPException, Message, MessageType
from discord.ext import commands, tasks
@@ -37,11 +41,13 @@ from bot.exts.filtering._utils import past_tense, repr_equals, starting_value, t
from bot.log import get_logger
from bot.pagination import LinePaginator
from bot.utils.channel import is_mod_channel
+from bot.utils.lock import lock_arg
from bot.utils.message_cache import MessageCache
log = get_logger(__name__)
CACHE_SIZE = 100
+HOURS_BETWEEN_NICKNAME_ALERTS = 1
WEEKLY_REPORT_ISO_DAY = 3 # 1=Monday, 7=Sunday
@@ -51,6 +57,9 @@ class Filtering(Cog):
# A set of filter list names with missing implementations that already caused a warning.
already_warned = set()
+ # Redis cache mapping a user ID to the last timestamp a bad nickname alert was sent.
+ name_alerts = RedisCache()
+
# region: init
def __init__(self, bot: Bot):
@@ -188,6 +197,10 @@ class Filtering(Cog):
if ctx.send_alert:
await self._send_alert(ctx, list_messages)
+ ctx = FilterContext.from_message(Event.NICKNAME, msg)
+ ctx.content = msg.author.display_name
+ await self._check_bad_name(ctx)
+
@Cog.listener()
async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None:
"""Filter the contents of an edited message. Don't reinvoke filters already invoked on the `before` version."""
@@ -209,6 +222,12 @@ class Filtering(Cog):
if ctx.send_alert:
await self._send_alert(ctx, list_messages)
+ @Cog.listener()
+ async def on_voice_state_update(self, member: discord.Member, *_) -> None:
+ """Checks for bad words in usernames when users join, switch or leave a voice channel."""
+ ctx = FilterContext(Event.NICKNAME, member, None, member.display_name, None)
+ await self._check_bad_name(ctx)
+
# endregion
# region: blacklist commands
@@ -388,7 +407,7 @@ class Filtering(Cog):
A template filter can be specified in the settings area to copy overrides from. The setting name is "--template"
and the value is the filter ID. The template will be used before applying any other override.
- Example: `!filter add denied token "Scaleios is great" delete_messages=True send_alert=False --template=100`
+ Example: `!filter add denied token "Scaleios is great" remove_context=True send_alert=False --template=100`
"""
result = await self._resolve_list_type_and_name(ctx, list_type, list_name)
if result is None:
@@ -809,7 +828,7 @@ class Filtering(Cog):
return result_actions, messages, triggers
- async def _send_alert(self, ctx: FilterContext, triggered_filters: dict[FilterList, list[str]]) -> None:
+ async def _send_alert(self, ctx: FilterContext, triggered_filters: dict[FilterList, Iterable[str]]) -> None:
"""Build an alert message from the filter context, and send it via the alert webhook."""
if not self.webhook:
return
@@ -819,6 +838,39 @@ class Filtering(Cog):
# There shouldn't be more than 10, but if there are it's not very useful to send them all.
await self.webhook.send(username=name, content=ctx.alert_content, embeds=[embed, *ctx.alert_embeds][:10])
+ async def _recently_alerted_name(self, member: discord.Member) -> bool:
+ """When it hasn't been `HOURS_BETWEEN_NICKNAME_ALERTS` since last alert, return False, otherwise True."""
+ if last_alert := await self.name_alerts.get(member.id):
+ last_alert = arrow.get(last_alert)
+ if arrow.utcnow() - last_alert < datetime.timedelta(days=HOURS_BETWEEN_NICKNAME_ALERTS):
+ log.trace(f"Last alert was too recent for {member}'s nickname.")
+ return True
+
+ return False
+
+ @lock_arg("filtering.check_bad_name", "ctx", attrgetter("author.id"))
+ async def _check_bad_name(self, ctx: FilterContext) -> None:
+ """Check filter triggers in the passed context - a member's display name."""
+ if await self._recently_alerted_name(ctx.author):
+ return
+
+ name = ctx.content
+ normalised_name = unicodedata.normalize("NFKC", name)
+ cleaned_normalised_name = "".join([c for c in normalised_name if not unicodedata.combining(c)])
+
+ # Run filters against normalised, cleaned normalised and the original name,
+ # in case there are filters for one but not another.
+ names_to_check = (name, normalised_name, cleaned_normalised_name)
+
+ new_ctx = ctx.replace(content=" ".join(names_to_check))
+ result_actions, list_messages, _ = await self._resolve_action(new_ctx)
+ if result_actions:
+ await result_actions.action(ctx)
+ if ctx.send_alert:
+ await self._send_alert(ctx, list_messages) # `ctx` has the original content.
+ # Update time when alert sent
+ await self.name_alerts.set(ctx.author.id, arrow.utcnow().timestamp())
+
async def _resolve_list_type_and_name(
self, ctx: Context, list_type: ListType | None = None, list_name: str | None = None, *, exclude: str = ""
) -> tuple[ListType, FilterList] | None: