From 8067d4089510f47d3b60fc32ce5b9d7f754464be Mon Sep 17 00:00:00 2001 From: dcaayushd Date: Wed, 8 Jul 2026 18:30:29 +0545 Subject: [PATCH 1/5] feat: add shared_file_urls property to MessageActivity for SharePoint link extraction --- .../api/activities/message/message.py | 21 ++++++++++ .../api/tests/unit/test_message_activities.py | 39 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/api/src/microsoft_teams/api/activities/message/message.py b/packages/api/src/microsoft_teams/api/activities/message/message.py index 034eaec3..9650d537 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message.py @@ -137,6 +137,27 @@ def strip_mentions_text(self, options: Optional[StripMentionsTextOptions] = None self.text = stripped_text return self + @property + def shared_file_urls(self) -> list[str]: + """ + Extract SharePoint/OneDrive file URLs from HTML attachments. + + When a user shares a file via the Teams compose box, Teams embeds + the file link inside a text/html attachment rather than sending + structured file metadata. This property parses those URLs out. + """ + import re + + urls = [] + for attachment in self.attachments or []: + if attachment.content_type == "text/html" and attachment.content: + found = re.findall( + r'https://[^\s"<>]+sharepoint\.com[^\s"<>]*', + str(attachment.content), + ) + urls.extend(found) + return urls + class MessageActivityInput(_MessageBase, ActivityInputBase): """Input model for creating message activities with builder methods.""" diff --git a/packages/api/tests/unit/test_message_activities.py b/packages/api/tests/unit/test_message_activities.py index e95c210d..0ad36071 100644 --- a/packages/api/tests/unit/test_message_activities.py +++ b/packages/api/tests/unit/test_message_activities.py @@ -144,6 +144,45 @@ def test_add_attachments_to_existing(self): assert activity.attachments[0] == existing assert activity.attachments[1] == new_attachment + def test_message_activity_shared_file_urls_extracts_sharepoint_links(self): + payload = { + "type": "message", + "id": "msg-123", + "text": "see attached", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "attachments": [ + { + "contentType": "text/html", + "content": '

see file

', + "contentUrl": None, + "name": None, + } + ], + } + + activity = ActivityTypeAdapter.validate_python(payload) + + urls = activity.shared_file_urls + assert len(urls) == 1 + assert "sharepoint.com" in urls[0] + + def test_message_activity_shared_file_urls_empty_when_no_html(self): + payload = { + "type": "message", + "id": "msg-456", + "text": "plain message", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "attachments": [], + } + + activity = ActivityTypeAdapter.validate_python(payload) + + assert activity.shared_file_urls == [] + def test_add_mention_basic(self): """Test adding a basic mention""" activity = self.create_message_activity("Hello ") From 8ea27300a04531be05e5010bb676d2ddb79c4c2c Mon Sep 17 00:00:00 2001 From: Aayush D C Dangi <89606048+dcaayushd@users.noreply.github.com> Date: Wed, 8 Jul 2026 18:50:02 +0545 Subject: [PATCH 2/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../api/activities/message/message.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/message/message.py b/packages/api/src/microsoft_teams/api/activities/message/message.py index 9650d537..ebc364d7 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message.py @@ -147,15 +147,23 @@ def shared_file_urls(self) -> list[str]: structured file metadata. This property parses those URLs out. """ import re + from urllib.parse import urlparse - urls = [] + urls: list[str] = [] for attachment in self.attachments or []: - if attachment.content_type == "text/html" and attachment.content: - found = re.findall( - r'https://[^\s"<>]+sharepoint\.com[^\s"<>]*', - str(attachment.content), - ) - urls.extend(found) + if attachment.content_type != "text/html" or not attachment.content: + continue + + html = str(attachment.content) + candidates = re.findall(r"href=['\"](https?://[^'\"]+)['\"]", html, flags=re.IGNORECASE) + if not candidates: + candidates = re.findall(r"https?://[^\s\"<>]+", html) + + for url in candidates: + host = urlparse(url).netloc.lower() + if host.endswith("sharepoint.com"): + urls.append(url) + return urls From 5671d476cfebfa850c04c86a61599b60224a4638 Mon Sep 17 00:00:00 2001 From: dcaayushd Date: Thu, 9 Jul 2026 14:45:24 +0545 Subject: [PATCH 3/5] fix: address shared_file_urls review feedback --- .../api/activities/message/message.py | 24 +++-- .../api/tests/unit/test_message_activities.py | 91 ++++++++++++++++++- 2 files changed, 102 insertions(+), 13 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/message/message.py b/packages/api/src/microsoft_teams/api/activities/message/message.py index ebc364d7..4443ac33 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message.py @@ -3,7 +3,10 @@ Licensed under the MIT License. """ +import re +from html import unescape from typing import Any, List, Literal, Optional, Self +from urllib.parse import urlparse from microsoft_teams.cards import AdaptiveCard from microsoft_teams.common.experimental import experimental @@ -140,28 +143,31 @@ def strip_mentions_text(self, options: Optional[StripMentionsTextOptions] = None @property def shared_file_urls(self) -> list[str]: """ - Extract SharePoint/OneDrive file URLs from HTML attachments. + Extract SharePoint-backed file URLs from HTML attachments. When a user shares a file via the Teams compose box, Teams embeds the file link inside a text/html attachment rather than sending - structured file metadata. This property parses those URLs out. + structured file metadata. This property parses those URLs out, + including OneDrive for Business links that use SharePoint hosts. """ - import re - from urllib.parse import urlparse - urls: list[str] = [] for attachment in self.attachments or []: if attachment.content_type != "text/html" or not attachment.content: continue html = str(attachment.content) - candidates = re.findall(r"href=['\"](https?://[^'\"]+)['\"]", html, flags=re.IGNORECASE) + candidates: list[str] = re.findall(r"href=['\"](https?://[^'\"]+)['\"]", html, flags=re.IGNORECASE) if not candidates: candidates = re.findall(r"https?://[^\s\"<>]+", html) - for url in candidates: - host = urlparse(url).netloc.lower() - if host.endswith("sharepoint.com"): + for raw in candidates: + url: str = unescape(raw) + hostname = urlparse(url).hostname + if hostname is None: + continue + + host = hostname.lower() + if host == "sharepoint.com" or host.endswith(".sharepoint.com"): urls.append(url) return urls diff --git a/packages/api/tests/unit/test_message_activities.py b/packages/api/tests/unit/test_message_activities.py index 0ad36071..f97ae6f5 100644 --- a/packages/api/tests/unit/test_message_activities.py +++ b/packages/api/tests/unit/test_message_activities.py @@ -145,6 +145,7 @@ def test_add_attachments_to_existing(self): assert activity.attachments[1] == new_attachment def test_message_activity_shared_file_urls_extracts_sharepoint_links(self): + expected_url = "https://company.sharepoint.com/sites/Team/file.pdf?web=1&download=1" payload = { "type": "message", "id": "msg-123", @@ -155,7 +156,10 @@ def test_message_activity_shared_file_urls_extracts_sharepoint_links(self): "attachments": [ { "contentType": "text/html", - "content": '

see file

', + "content": ( + '

see file

' + ), "contentUrl": None, "name": None, } @@ -164,9 +168,87 @@ def test_message_activity_shared_file_urls_extracts_sharepoint_links(self): activity = ActivityTypeAdapter.validate_python(payload) - urls = activity.shared_file_urls - assert len(urls) == 1 - assert "sharepoint.com" in urls[0] + assert isinstance(activity, MessageActivity) + assert activity.shared_file_urls == [expected_url] + + def test_message_activity_shared_file_urls_extracts_one_drive_for_business_sharepoint_host(self): + expected_url = "https://company-my.sharepoint.com/personal/user_company_com/Documents/file.docx" + payload = { + "type": "message", + "id": "msg-234", + "text": "see attached", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "attachments": [ + { + "contentType": "text/html", + "content": f'

see file

', + "contentUrl": None, + "name": None, + } + ], + } + + activity = ActivityTypeAdapter.validate_python(payload) + + assert isinstance(activity, MessageActivity) + assert activity.shared_file_urls == [expected_url] + + def test_message_activity_shared_file_urls_ignores_non_html_or_empty_attachments(self): + payload = { + "type": "message", + "id": "msg-345", + "text": "plain message", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "attachments": [ + { + "contentType": "text/plain", + "content": "https://company.sharepoint.com/sites/Team/file.pdf", + "contentUrl": None, + "name": None, + }, + { + "contentType": "text/html", + "content": "", + "contentUrl": None, + "name": None, + }, + ], + } + + activity = ActivityTypeAdapter.validate_python(payload) + + assert isinstance(activity, MessageActivity) + assert activity.shared_file_urls == [] + + def test_message_activity_shared_file_urls_rejects_non_sharepoint_hosts(self): + payload = { + "type": "message", + "id": "msg-567", + "text": "see attached", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "attachments": [ + { + "contentType": "text/html", + "content": ( + '

bad' + 'bad

' + ), + "contentUrl": None, + "name": None, + } + ], + } + + activity = ActivityTypeAdapter.validate_python(payload) + + assert isinstance(activity, MessageActivity) + assert activity.shared_file_urls == [] def test_message_activity_shared_file_urls_empty_when_no_html(self): payload = { @@ -181,6 +263,7 @@ def test_message_activity_shared_file_urls_empty_when_no_html(self): activity = ActivityTypeAdapter.validate_python(payload) + assert isinstance(activity, MessageActivity) assert activity.shared_file_urls == [] def test_add_mention_basic(self): From 424ff0ed26a8fb34b64ab91f845f5b687438d8e0 Mon Sep 17 00:00:00 2001 From: dcaayushd Date: Thu, 9 Jul 2026 14:57:21 +0545 Subject: [PATCH 4/5] refactor: split shared file URL filtering branches --- .../api/activities/message/message.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/api/src/microsoft_teams/api/activities/message/message.py b/packages/api/src/microsoft_teams/api/activities/message/message.py index 4443ac33..e6531f63 100644 --- a/packages/api/src/microsoft_teams/api/activities/message/message.py +++ b/packages/api/src/microsoft_teams/api/activities/message/message.py @@ -43,6 +43,24 @@ from ..utils import StripMentionsTextOptions, strip_mentions_text +def _is_sharepoint_url(url: str) -> bool: + hostname = urlparse(url).hostname + if hostname is None: + return False + + host = hostname.lower() + return host == "sharepoint.com" or host.endswith(".sharepoint.com") + + +def _sharepoint_urls_from_candidates(candidates: list[str]) -> list[str]: + urls: list[str] = [] + for raw in candidates: + url: str = unescape(raw) + if _is_sharepoint_url(url): + urls.append(url) + return urls + + class _MessageBase(CustomBaseModel): """Base class containing shared message activity fields (all Optional except type).""" @@ -152,7 +170,9 @@ def shared_file_urls(self) -> list[str]: """ urls: list[str] = [] for attachment in self.attachments or []: - if attachment.content_type != "text/html" or not attachment.content: + if attachment.content_type != "text/html": + continue + if not attachment.content: continue html = str(attachment.content) @@ -160,15 +180,7 @@ def shared_file_urls(self) -> list[str]: if not candidates: candidates = re.findall(r"https?://[^\s\"<>]+", html) - for raw in candidates: - url: str = unescape(raw) - hostname = urlparse(url).hostname - if hostname is None: - continue - - host = hostname.lower() - if host == "sharepoint.com" or host.endswith(".sharepoint.com"): - urls.append(url) + urls.extend(_sharepoint_urls_from_candidates(candidates)) return urls From 6d730aa64b7d6aa912a71d47fd8c1e34862a1644 Mon Sep 17 00:00:00 2001 From: dcaayushd Date: Thu, 9 Jul 2026 15:00:05 +0545 Subject: [PATCH 5/5] test: split shared file attachment skip cases --- .../api/tests/unit/test_message_activities.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/api/tests/unit/test_message_activities.py b/packages/api/tests/unit/test_message_activities.py index f97ae6f5..91b2aa6b 100644 --- a/packages/api/tests/unit/test_message_activities.py +++ b/packages/api/tests/unit/test_message_activities.py @@ -195,7 +195,7 @@ def test_message_activity_shared_file_urls_extracts_one_drive_for_business_share assert isinstance(activity, MessageActivity) assert activity.shared_file_urls == [expected_url] - def test_message_activity_shared_file_urls_ignores_non_html_or_empty_attachments(self): + def test_message_activity_shared_file_urls_ignores_non_html_attachments(self): payload = { "type": "message", "id": "msg-345", @@ -210,6 +210,23 @@ def test_message_activity_shared_file_urls_ignores_non_html_or_empty_attachments "contentUrl": None, "name": None, }, + ], + } + + activity = ActivityTypeAdapter.validate_python(payload) + + assert isinstance(activity, MessageActivity) + assert activity.shared_file_urls == [] + + def test_message_activity_shared_file_urls_ignores_html_attachments_without_content(self): + payload = { + "type": "message", + "id": "msg-346", + "text": "plain message", + "from": {"id": "user-123", "name": "Test User"}, + "conversation": {"id": "conv-456", "conversationType": "personal"}, + "recipient": {"id": "bot-789", "name": "Test Bot"}, + "attachments": [ { "contentType": "text/html", "content": "",