diff --git a/nanobot/channels/msteams.py b/nanobot/channels/msteams.py index fc25f2fbb..d2addacca 100644 --- a/nanobot/channels/msteams.py +++ b/nanobot/channels/msteams.py @@ -70,6 +70,7 @@ class ConversationRef: activity_id: str | None = None conversation_type: str | None = None tenant_id: str | None = None + updated_at: float | None = None class MSTeamsChannel(BaseChannel): @@ -288,7 +289,9 @@ class MSTeamsChannel(BaseChannel): activity_id=activity_id or None, conversation_type=conversation_type or None, tenant_id=str((channel_data.get("tenant") or {}).get("id") or "") or None, + updated_at=time.time(), ) + self._save_refs() await self._handle_message( @@ -493,6 +496,14 @@ class MSTeamsChannel(BaseChannel): def _save_refs(self) -> None: """Persist conversation references.""" try: + stale_keys = [ + key + for key, ref in self._conversation_refs.items() + if self._is_stale_or_unsupported_ref(ref) + ] + for key in stale_keys: + self._conversation_refs.pop(key, None) + data = { key: { "service_url": ref.service_url, @@ -501,6 +512,7 @@ class MSTeamsChannel(BaseChannel): "activity_id": ref.activity_id, "conversation_type": ref.conversation_type, "tenant_id": ref.tenant_id, + "updated_at": ref.updated_at, } for key, ref in self._conversation_refs.items() } @@ -508,6 +520,21 @@ class MSTeamsChannel(BaseChannel): except Exception as e: logger.warning("Failed to save MSTeams conversation refs: {}", e) + def _is_stale_or_unsupported_ref(self, ref: ConversationRef) -> bool: + """Reject unsupported refs and prune old refs.""" + service_url = (ref.service_url or "").strip().lower() + conversation_type = (ref.conversation_type or "").strip().lower() + updated_at = ref.updated_at or 0.0 + max_age_seconds = 30 * 24 * 60 * 60 + + if "webchat.botframework.com" in service_url: + return True + if conversation_type and conversation_type != "personal": + return True + if updated_at and updated_at < time.time() - max_age_seconds: + return True + return False + async def _get_access_token(self) -> str: """Fetch an access token for Bot Framework / Azure Bot auth.""" diff --git a/tests/test_msteams.py b/tests/test_msteams.py index b4ed59092..3dbfdfb2f 100644 --- a/tests/test_msteams.py +++ b/tests/test_msteams.py @@ -1,4 +1,5 @@ import json +import time import pytest @@ -551,6 +552,38 @@ async def test_start_logs_install_hint_when_pyjwt_missing(make_channel, monkeypa assert errors == ["PyJWT not installed. Run: pip install nanobot-ai[msteams]"] +def test_save_refs_prunes_webchat_and_stale_refs(make_channel): + ch = make_channel() + now = time.time() + ch._conversation_refs = { + "teams-good": ConversationRef( + service_url="https://smba.trafficmanager.net/amer/", + conversation_id="teams-good", + conversation_type="personal", + updated_at=now, + ), + "webchat-bad": ConversationRef( + service_url="https://webchat.botframework.com/", + conversation_id="webchat-bad", + conversation_type=None, + updated_at=now, + ), + "teams-stale": ConversationRef( + service_url="https://smba.trafficmanager.net/amer/", + conversation_id="teams-stale", + conversation_type="personal", + updated_at=now - (31 * 24 * 60 * 60), + ), + } + + ch._save_refs() + + assert set(ch._conversation_refs) == {"teams-good"} + saved = json.loads(ch._refs_path.read_text(encoding="utf-8")) + assert set(saved) == {"teams-good"} + assert saved["teams-good"]["updated_at"] == pytest.approx(now) + + def test_msteams_default_config_includes_restart_notify_fields(): cfg = MSTeamsChannel.default_config()