Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/chat_sdk/adapters/slack/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1651,7 +1651,7 @@ async def _resolve_outgoing_mentions(self, text: str, thread_id: str) -> str:
return text
state = self._chat.get_state()

mention_pattern = re.compile(r"@(\w+)")
mention_pattern = re.compile(r"(?<![\w<])@(\w+)")
mentions: dict[str, list[str]] = {}

for match in mention_pattern.finditer(text):
Expand Down
4 changes: 2 additions & 2 deletions src/chat_sdk/adapters/slack/format_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def flush_text() -> None:

def _convert_mentions_to_slack(self, text: str) -> str:
"""Convert @mentions to Slack format: @name -> <@name>."""
return re.sub(r"(?<!<)@(\w+)", r"<@\1>", text)
return re.sub(r"(?<![\w<])@(\w+)", r"<@\1>", text)

def _node_to_mrkdwn(self, node: Content) -> str:
"""Convert a single AST node to Slack mrkdwn."""
Expand All @@ -215,7 +215,7 @@ def _node_to_mrkdwn(self, node: Content) -> str:

if node_type == "text":
value = node.get("value", "")
return re.sub(r"(?<!<)@(\w+)", r"<@\1>", value)
return re.sub(r"(?<![\w<])@(\w+)", r"<@\1>", value)

if node_type == "strong":
content = "".join(self._node_to_mrkdwn(c) for c in children)
Expand Down
17 changes: 17 additions & 0 deletions tests/test_slack_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,23 @@ def test_from_markdown_no_double_wrap(self):
result = self.converter.from_markdown("Hey <@U12345>")
assert result == "Hey <@U12345>"

def test_does_not_wrap_at_in_email_localpart(self):
"""`@` preceded by a word char is part of an email address, not a mention.

Without the lookbehind tightening, `alice@example.com` was rewritten to
`alice<@example>.com`, surfacing a broken-looking mention in messages
that quote email addresses from upstream APIs.
"""
result = self.converter.render_postable("Contact alice@example.com or bob@example.org")
assert "<@example>" not in result
assert "alice@example.com" in result
assert "bob@example.org" in result

def test_does_not_wrap_at_in_email_localpart_from_markdown_ast(self):
result = self.converter.render_postable({"markdown": "Contact alice@example.com"})
assert "<@example>" not in result
assert "alice@example.com" in result


# ---------------------------------------------------------------------------
# toPlainText
Expand Down
Loading