From 3f3d8a7b2478a157f6232a632af57f66da25cda9 Mon Sep 17 00:00:00 2001 From: Teknium Date: Fri, 17 Apr 2026 06:38:00 -0700 Subject: [PATCH] fix(discord): strip mention syntax from auto-thread names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously a message like `<@&1490963422786093149> help` would spawn a thread literally named `<@&1490963422786093149> help`, exposing raw Discord mention markers in the thread list. Only user mentions (`<@id>`) were being stripped upstream — role mentions (`<@&id>`) and channel mentions (`<#id>`) leaked through. Fix: strip all three mention patterns in `_auto_create_thread` before building the thread name. Collapse runs of whitespace left by the removal. If the entire content was mention-only, fall back to 'Hermes' instead of an empty title. Fixes #6336. Tests: two new regression guards in test_discord_slash_commands.py covering mixed-mention content and mention-only content. --- gateway/platforms/discord.py | 9 ++++- tests/gateway/test_discord_slash_commands.py | 42 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index b746c7ea598..a53908145d8 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -2407,8 +2407,15 @@ class DiscordAdapter(BasePlatformAdapter): Returns the created thread object, or ``None`` on failure. """ - # Build a short thread name from the message + # Build a short thread name from the message. Strip Discord mention + # syntax (users / roles / channels) so thread titles don't end up + # showing raw <@id>, <@&id>, or <#id> markers — the ID isn't + # meaningful to humans glancing at the thread list (#6336). content = (message.content or "").strip() + # <@123>, <@!123>, <@&123>, <#123> — collapse to empty; normalize spaces. + content = re.sub(r"<@[!&]?\d+>", "", content) + content = re.sub(r"<#\d+>", "", content) + content = re.sub(r"\s+", " ", content).strip() thread_name = content[:80] if content else "Hermes" if len(content) > 80: thread_name = thread_name[:77] + "..." diff --git a/tests/gateway/test_discord_slash_commands.py b/tests/gateway/test_discord_slash_commands.py index 2302e49ef51..1c3ec262538 100644 --- a/tests/gateway/test_discord_slash_commands.py +++ b/tests/gateway/test_discord_slash_commands.py @@ -414,6 +414,48 @@ async def test_auto_create_thread_uses_message_content_as_name(adapter): assert call_kwargs["auto_archive_duration"] == 1440 +@pytest.mark.asyncio +async def test_auto_create_thread_strips_mention_syntax_from_name(adapter): + """Thread names must not contain raw <@id>, <@&id>, or <#id> markers. + + Regression guard for #6336 — previously a message like + ``<@&1490963422786093149> help`` would spawn a thread literally + named ``<@&1490963422786093149> help``. + """ + thread = SimpleNamespace(id=999, name="help") + message = SimpleNamespace( + content="<@&1490963422786093149> <@555> please help <#123>", + create_thread=AsyncMock(return_value=thread), + channel=SimpleNamespace(send=AsyncMock()), + author=SimpleNamespace(display_name="Jezza"), + ) + + await adapter._auto_create_thread(message) + + name = message.create_thread.await_args[1]["name"] + assert "<@" not in name, f"role/user mention leaked: {name!r}" + assert "<#" not in name, f"channel mention leaked: {name!r}" + assert name == "please help" + + +@pytest.mark.asyncio +async def test_auto_create_thread_falls_back_to_hermes_when_only_mentions(adapter): + """If a message contains only mention syntax, the stripped content is + empty — fall back to the 'Hermes' default rather than ''.""" + thread = SimpleNamespace(id=999, name="Hermes") + message = SimpleNamespace( + content="<@&1490963422786093149>", + create_thread=AsyncMock(return_value=thread), + channel=SimpleNamespace(send=AsyncMock()), + author=SimpleNamespace(display_name="Jezza"), + ) + + await adapter._auto_create_thread(message) + + name = message.create_thread.await_args[1]["name"] + assert name == "Hermes" + + @pytest.mark.asyncio async def test_auto_create_thread_truncates_long_names(adapter): long_text = "a" * 200