aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2023-04-08 20:04:48 +0100
committerGravatar GitHub <[email protected]>2023-04-08 20:04:48 +0100
commitbfdf41d6d0701275e43a98f64d939e750c75ec50 (patch)
tree1984c17d3c4ae9167a5cf8afacaa1b2e27fd0000
parentMerge pull request #2504 from python-discord/swfarnsworth/ping-on-thread-close (diff)
parentMerge branch 'main' into thread_filter (diff)
Merge pull request #2517 from python-discord/thread_filter
Thread filter
-rw-r--r--bot/exts/filtering/_filter_context.py1
-rw-r--r--bot/exts/filtering/_filter_lists/token.py4
-rw-r--r--bot/exts/filtering/_settings_types/actions/remove_context.py17
-rw-r--r--bot/exts/filtering/filtering.py33
-rw-r--r--bot/exts/help_channels/_channel.py22
-rw-r--r--bot/exts/help_channels/_cog.py24
6 files changed, 64 insertions, 37 deletions
diff --git a/bot/exts/filtering/_filter_context.py b/bot/exts/filtering/_filter_context.py
index 0794a48e4..9d8bbba9a 100644
--- a/bot/exts/filtering/_filter_context.py
+++ b/bot/exts/filtering/_filter_context.py
@@ -21,6 +21,7 @@ class Event(Enum):
MESSAGE = auto()
MESSAGE_EDIT = auto()
NICKNAME = auto()
+ THREAD_NAME = auto()
SNEKBOX = auto()
diff --git a/bot/exts/filtering/_filter_lists/token.py b/bot/exts/filtering/_filter_lists/token.py
index 0c591ac3b..5bb21cfc5 100644
--- a/bot/exts/filtering/_filter_lists/token.py
+++ b/bot/exts/filtering/_filter_lists/token.py
@@ -32,7 +32,9 @@ class TokensList(FilterList[TokenFilter]):
def __init__(self, filtering_cog: Filtering):
super().__init__()
- filtering_cog.subscribe(self, Event.MESSAGE, Event.MESSAGE_EDIT, Event.NICKNAME, Event.SNEKBOX)
+ filtering_cog.subscribe(
+ self, Event.MESSAGE, Event.MESSAGE_EDIT, Event.NICKNAME, Event.THREAD_NAME, Event.SNEKBOX
+ )
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/_settings_types/actions/remove_context.py b/bot/exts/filtering/_settings_types/actions/remove_context.py
index 5ec2613f4..dc01426d8 100644
--- a/bot/exts/filtering/_settings_types/actions/remove_context.py
+++ b/bot/exts/filtering/_settings_types/actions/remove_context.py
@@ -1,7 +1,7 @@
from collections import defaultdict
from typing import ClassVar
-from discord import Message
+from discord import Message, Thread
from discord.errors import HTTPException
from pydis_core.utils import scheduling
from pydis_core.utils.logging import get_logger
@@ -52,6 +52,8 @@ class RemoveContext(ActionEntry):
await self._handle_messages(ctx)
elif ctx.event == Event.NICKNAME:
await self._handle_nickname(ctx)
+ elif ctx.event == Event.THREAD_NAME:
+ await self._handle_thread(ctx)
@staticmethod
async def _handle_messages(ctx: FilterContext) -> None:
@@ -106,7 +108,18 @@ class RemoveContext(ActionEntry):
return
await command(FakeContext(ctx.message, alerts_channel, command), ctx.author, None, reason=SUPERSTAR_REASON)
- ctx.action_descriptions.append("superstar")
+ ctx.action_descriptions.append("superstarred")
+
+ @staticmethod
+ async def _handle_thread(ctx: FilterContext) -> None:
+ """Delete the context thread."""
+ if isinstance(ctx.channel, Thread):
+ try:
+ await ctx.channel.delete()
+ except HTTPException:
+ ctx.action_descriptions.append("failed to delete thread")
+ else:
+ ctx.action_descriptions.append("deleted thread")
def union(self, other: Self) -> Self:
"""Combines two actions of the same type. Each type of action is executed once per filter."""
diff --git a/bot/exts/filtering/filtering.py b/bot/exts/filtering/filtering.py
index 392428bb0..82006c9db 100644
--- a/bot/exts/filtering/filtering.py
+++ b/bot/exts/filtering/filtering.py
@@ -12,7 +12,7 @@ from typing import Literal, Optional, get_type_hints
import arrow
import discord
from async_rediscache import RedisCache
-from discord import Colour, Embed, HTTPException, Message, MessageType
+from discord import Colour, Embed, HTTPException, Message, MessageType, Thread
from discord.ext import commands, tasks
from discord.ext.commands import BadArgument, Cog, Context, command, has_any_role
from pydis_core.site_api import ResponseCodeError
@@ -206,6 +206,11 @@ class Filtering(Cog):
"""Filter the contents of a sent message."""
if msg.author.bot or msg.webhook_id or msg.type == MessageType.auto_moderation_action:
return
+ if msg.type == MessageType.channel_name_change and isinstance(msg.channel, Thread):
+ ctx = FilterContext.from_message(Event.THREAD_NAME, msg)
+ await self._check_bad_name(ctx)
+ return
+
self.message_cache.append(msg)
ctx = FilterContext.from_message(Event.MESSAGE, msg, None, self.message_cache)
@@ -218,7 +223,7 @@ class Filtering(Cog):
nick_ctx = FilterContext.from_message(Event.NICKNAME, msg)
nick_ctx.content = msg.author.display_name
- await self._check_bad_name(nick_ctx)
+ await self._check_bad_display_name(nick_ctx)
await self._maybe_schedule_msg_delete(ctx, result_actions)
self._increment_stats(triggers)
@@ -252,6 +257,12 @@ class Filtering(Cog):
ctx = FilterContext(Event.NICKNAME, member, None, member.display_name, None)
await self._check_bad_name(ctx)
+ @Cog.listener()
+ async def on_thread_create(self, thread: Thread) -> None:
+ """Check for bad words in new thread names."""
+ ctx = FilterContext(Event.THREAD_NAME, thread.owner, thread, thread.name, None)
+ await self._check_bad_name(ctx)
+
async def filter_snekbox_output(
self, stdout: str, files: list[FileAttachment], msg: Message
) -> tuple[bool, set[str]]:
@@ -966,11 +977,17 @@ class Filtering(Cog):
return False
@lock_arg("filtering.check_bad_name", "ctx", attrgetter("author.id"))
- async def _check_bad_name(self, ctx: FilterContext) -> None:
+ async def _check_bad_display_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
+ new_ctx = await self._check_bad_name(ctx)
+ if new_ctx.send_alert:
+ # Update time when alert sent
+ await self.name_alerts.set(ctx.author.id, arrow.utcnow().timestamp())
+ async def _check_bad_name(self, ctx: FilterContext) -> FilterContext:
+ """Check filter triggers for some given name (thread name, a member's display name)."""
name = ctx.content
normalised_name = unicodedata.normalize("NFKC", name)
cleaned_normalised_name = "".join([c for c in normalised_name if not unicodedata.combining(c)])
@@ -981,13 +998,13 @@ class Filtering(Cog):
new_ctx = ctx.replace(content=" ".join(names_to_check))
result_actions, list_messages, triggers = await self._resolve_action(new_ctx)
+ new_ctx = new_ctx.replace(content=ctx.content) # Alert with the original content.
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())
+ await result_actions.action(new_ctx)
+ if new_ctx.send_alert:
+ await self._send_alert(new_ctx, list_messages)
self._increment_stats(triggers)
+ return new_ctx
async def _resolve_list_type_and_name(
self, ctx: Context, list_type: ListType | None = None, list_name: str | None = None, *, exclude: str = ""
diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py
index b1a319145..774e9178e 100644
--- a/bot/exts/help_channels/_channel.py
+++ b/bot/exts/help_channels/_channel.py
@@ -142,11 +142,15 @@ async def help_post_opened(opened_post: discord.Thread, *, reopen: bool = False)
try:
await opened_post.starter_message.pin()
except (discord.HTTPException, AttributeError) as e:
- # Suppress if the message was not found, most likely deleted
+ # Suppress if the message or post were not found, most likely deleted
# The message being deleted could be surfaced as an AttributeError on .starter_message,
# or as an exception from the Discord API, depending on timing and cache status.
- if isinstance(e, discord.HTTPException) and e.code != 10008:
- raise e
+ # The post being deleting would happen if it had a bad name that would cause the filtering system to delete it.
+ if isinstance(e, discord.HTTPException):
+ if e.code == 10003: # Post not found.
+ return
+ elif e.code != 10008: # 10008 - Starter message not found.
+ raise e
await send_opened_post_message(opened_post)
@@ -178,7 +182,11 @@ async def help_post_deleted(deleted_post_event: discord.RawThreadDeleteEvent) ->
_stats.report_post_count()
cached_post = deleted_post_event.thread
if cached_post and not cached_post.archived:
- # If the post is in the bot's cache, and it was not archived before deleting, report a complete session.
+ # If the post is in the bot's cache, and it was not archived before deleting,
+ # report a complete session and remove the cooldown.
+ poster = cached_post.owner
+ cooldown_role = cached_post.guild.get_role(constants.Roles.help_cooldown)
+ await members.handle_role_change(poster, poster.remove_roles, cooldown_role)
await _stats.report_complete_session(cached_post, _stats.ClosingReason.DELETED)
@@ -218,6 +226,12 @@ async def maybe_archive_idle_post(post: discord.Thread, scheduler: scheduling.Sc
If `has_task` is True and rescheduling is required, the extant task to make the post
dormant will first be cancelled.
"""
+ try:
+ await post.guild.fetch_channel(post.id)
+ except discord.HTTPException:
+ log.trace(f"Not closing missing post #{post} ({post.id}).")
+ return
+
if post.locked:
log.trace(f"Not closing already closed post #{post} ({post.id}).")
return
diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py
index 6c8478a3f..dd6dee9ee 100644
--- a/bot/exts/help_channels/_cog.py
+++ b/bot/exts/help_channels/_cog.py
@@ -1,7 +1,5 @@
"""Contains the Cog that receives discord.py events and defers most actions to other files in the module."""
-import typing as t
-
import discord
from discord.ext import commands
from pydis_core.utils import scheduling
@@ -13,9 +11,6 @@ from bot.log import get_logger
log = get_logger(__name__)
-if t.TYPE_CHECKING:
- from bot.exts.filtering.filtering import Filtering
-
class HelpForum(commands.Cog):
"""
@@ -62,18 +57,6 @@ class HelpForum(commands.Cog):
self.bot.stats.incr("help.dormant_invoke.staff")
return has_role
- async def post_with_disallowed_title_check(self, post: discord.Thread) -> None:
- """Check if the given post has a bad word, alerting moderators if it does."""
- filter_cog: Filtering | None = self.bot.get_cog("Filtering")
- if filter_cog and (match := filter_cog.get_name_match(post.name)):
- mod_alerts = self.bot.get_channel(constants.Channels.mod_alerts)
- await mod_alerts.send(
- f"<@&{constants.Roles.moderators}>\n"
- f"<@{post.owner_id}> ({post.owner_id}) opened the post {post.mention} ({post.id}), "
- "which triggered the token filter with its name!\n"
- f"**Match:** {match.group()}"
- )
-
@commands.group(name="help-forum", aliases=("hf",))
async def help_forum_group(self, ctx: commands.Context) -> None:
"""A group of commands that help manage our help forum system."""
@@ -133,7 +116,7 @@ class HelpForum(commands.Cog):
@commands.Cog.listener("on_message")
async def new_post_listener(self, message: discord.Message) -> None:
- """Defer application of new post logic for posts the help forum to the _channel helper."""
+ """Defer application of new post logic for posts in the help forum to the _channel helper."""
if not isinstance(message.channel, discord.Thread):
return
thread = message.channel
@@ -145,7 +128,6 @@ class HelpForum(commands.Cog):
if thread.parent_id != self.help_forum_channel.id:
return
- # await self.post_with_disallowed_title_check(thread) TODO bring this back with the new filtering system
await _channel.help_post_opened(thread)
delay = min(constants.HelpChannels.deleted_idle_minutes, constants.HelpChannels.idle_minutes) * 60
@@ -162,12 +144,10 @@ class HelpForum(commands.Cog):
return
if not before.archived and after.archived:
await _channel.help_post_archived(after)
- if before.name != after.name:
- await self.post_with_disallowed_title_check(after)
@commands.Cog.listener()
async def on_raw_thread_delete(self, deleted_thread_event: discord.RawThreadDeleteEvent) -> None:
- """Defer application of new post logic for posts the help forum to the _channel helper."""
+ """Defer application of deleted post logic for posts in the help forum to the _channel helper."""
if deleted_thread_event.parent_id == self.help_forum_channel.id:
await _channel.help_post_deleted(deleted_thread_event)