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