From 7c02bfce899716a10ec32dd9fbb60885c7ddcfd4 Mon Sep 17 00:00:00 2001 From: Drexuxux Date: Tue, 4 Aug 2026 21:47:53 +0300 Subject: [PATCH] fix(slack): insert resolved display names literally when humanizing mentions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _humanize_user_mentions rewrites <@UID> to @DisplayName by passing the resolved name as re.sub's replacement, where re parses it as a template. A display name is arbitrary user-set text, so the escapes in it are the user's characters, not regex syntax: dev\ops -> re.error: bad escape \o a\1b -> re.error: invalid group reference 1 \g<0> -> expands to the whole match, silently putting the opaque <@UID> back — the token this method exists to remove The trigger-text call site sits in _handle_slack_message outside any try, and both Bolt event handlers await it bare, so the raise takes the whole inbound message down: every message mentioning that person is dropped. Pass the replacement as a function instead — re does no template parsing on the return value, so the name lands verbatim. Same shape the Matrix adapter already uses for its outbound mention rewrite. --- plugins/platforms/slack/adapter.py | 12 +++++- .../test_slack_mention_humanization.py | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index dc4b94ead0..ecc90bb969 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -3922,7 +3922,17 @@ class SlackAdapter(BasePlatformAdapter): # (keeps the message intact rather than emptying a mention). display = (name or uid).strip() or uid # Replace both the bare and labelled forms of this exact ID. - text = re.sub(rf"<@{uid}(?:\|[^>]*)?>", f"@{display}", text) + # The replacement goes in as a *function* so the resolved name is + # inserted verbatim: as a template string, ``re`` parses backslash + # escapes in it, and a display name is arbitrary user-set text. + # ``dev\ops`` raises ``re.error: bad escape \o``, ``a\1b`` raises + # on the group reference, and ``\g<0>`` silently re-injects the + # raw ``<@UID>`` this method exists to remove. + text = re.sub( + rf"<@{uid}(?:\|[^>]*)?>", + lambda _m, _name=f"@{display}": _name, + text, + ) return text def _build_identity_prompt(self, team_id: str = "") -> str: diff --git a/tests/gateway/test_slack_mention_humanization.py b/tests/gateway/test_slack_mention_humanization.py index f08b127880..f77da13d26 100644 --- a/tests/gateway/test_slack_mention_humanization.py +++ b/tests/gateway/test_slack_mention_humanization.py @@ -95,6 +95,46 @@ async def test_handles_labelled_mention_form(): assert out == "@Alice Example hi" +@pytest.mark.asyncio +async def test_backslash_in_display_name_does_not_raise(): + """A display name is arbitrary user-set text. Fed to ``re.sub`` as a + replacement template it is parsed for escapes, so ``dev\\ops`` blew up + with ``re.error: bad escape \\o`` and the inbound message was lost.""" + adapter = _adapter_with_names({"U07DEV": r"dev\ops"}) + out = await adapter._humanize_user_mentions("ping <@U07DEV> please", chat_id="C1") + assert out == r"ping @dev\ops please" + + +@pytest.mark.asyncio +async def test_group_reference_in_display_name_is_literal(): + """``\\1`` in a name is a group reference in a replacement template — with + no groups in the pattern it raised ``invalid group reference``.""" + adapter = _adapter_with_names({"U07ODD": r"a\1b"}) + out = await adapter._humanize_user_mentions("hi <@U07ODD>", chat_id="C1") + assert out == r"hi @a\1b" + + +@pytest.mark.asyncio +async def test_named_group_reference_does_not_reinject_the_raw_id(): + """``\\g<0>`` expands to the whole match, silently putting the opaque + ``<@UID>`` back — the exact token this method exists to remove.""" + adapter = _adapter_with_names({"U07ODD": r"\g<0>"}) + out = await adapter._humanize_user_mentions("hi <@U07ODD>", chat_id="C1") + assert "<@" not in out + assert out == r"hi @\g<0>" + + +@pytest.mark.asyncio +async def test_one_odd_name_does_not_break_the_other_mentions(): + adapter = _adapter_with_names( + {"U07DEV": r"dev\ops", "U07ALICE": "Alice Example"} + ) + out = await adapter._humanize_user_mentions( + "<@U07DEV> and <@U07ALICE> ship it", chat_id="C1" + ) + assert out == r"@dev\ops and @Alice Example ship it" + + # ----- _build_identity_prompt -------------------------------------------------- def test_identity_prompt_names_the_bot():