From a2784ab7ec18c8800ea9ba362b32053c8d9345c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:48:08 -0700 Subject: [PATCH 1/3] Bump picomatch from 4.0.3 to 4.0.4 in /examples/tab/Web (#328) Bumps [picomatch](https://github.com/micromatch/picomatch) from 4.0.3 to 4.0.4.
Release notes

Sourced from picomatch's releases.

4.0.4

This is a security release fixing several security relevant issues.

What's Changed

Full Changelog: https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=picomatch&package-manager=npm_and_yarn&previous-version=4.0.3&new-version=4.0.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/teams.py/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/tab/Web/package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/tab/Web/package-lock.json b/examples/tab/Web/package-lock.json index 7a4811e8..969cfa17 100644 --- a/examples/tab/Web/package-lock.json +++ b/examples/tab/Web/package-lock.json @@ -2085,10 +2085,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, From d0a9da56a645074dbaf8fe7a9888f99b8ddfaa57 Mon Sep 17 00:00:00 2001 From: Lily Du Date: Fri, 27 Mar 2026 16:50:04 -0700 Subject: [PATCH 2/3] feat: add missing endpoints (Paged Members, Meeting Notifs) & address client gaps (#327) - went through the list of gaps and differences betw our clients vs. backend implementation (started with @singhk97 's list and then had Claude re-verify or re-test). - added in the missing endpoints from BF, verified that these exist in backend codebase, and manually had Claude verify them w/ my test bot **Issues Addressed** - `role` in TeamsChannelAccount comes in as `userRole` instead. updated to be `str` instead of fixed value, matching BE. - `aadObjectId` is either returned as `aadObjectId` or `objectId` (scenario dependent) - for `GET v3/teams/{id}/conversations`, needed to index into `response.json()["conversations"].` I manually tested this. - missing `tenant_id` in `TeamDetails` - No DELETE member route for` DELETE v3/conversations/{id}/members/{id}` - `GET v3/conversations/{id}/activities/{id}/members` returns `TeamsChannelAccount`, not `Account` - Extra parameters `topic` and `bot` in `CreateConversationParams` for `{service_url}/v3/conversations` - No `GET v3/conversations route` - removed `is_group` from `CreateConversationParams` - ignored on backend **New Endpoints** 1) **Paged Conversation Members** Added get_paged() to ConversationMemberClient. Supports optional page_size and continuation_token query params. For pageSize to work, the minimum is 50, by default its 200, and max is 500. 2) **Meeting Notifications** Added send_notification() to MeetingClient. Sends a targeted in-meeting notification to specific recipients on specified surfaces (e.g. meetingTabIcon, meetingStage). This is different from targeted messages, this was introduced in 2022. Requires this RSC permission `OnlineMeetingNotification.Send.Chat` and ECS flag enabled for the tenant/bot. --------- Co-authored-by: lilydu --- .../api/clients/conversation/__init__.py | 4 +- .../api/clients/conversation/activity.py | 8 +- .../api/clients/conversation/client.py | 33 ++------ .../api/clients/conversation/member.py | 33 +++++--- .../api/clients/conversation/params.py | 33 +------- .../api/clients/meeting/client.py | 27 ++++++- .../api/clients/team/__init__.py | 3 +- .../api/clients/team/client.py | 3 +- .../api/clients/team/params.py | 15 ++++ .../src/microsoft_teams/api/models/account.py | 14 +++- .../api/models/conversation/__init__.py | 3 +- .../conversation/paged_members_result.py | 23 ++++++ .../api/models/meetings/__init__.py | 12 +++ .../models/meetings/meeting_notification.py | 75 +++++++++++++++++++ .../api/models/team_details.py | 3 + packages/api/tests/conftest.py | 58 ++++++++------ .../tests/unit/test_conversation_client.py | 72 +++++++++--------- .../api/tests/unit/test_meeting_client.py | 68 ++++++++++++++++- .../apps/contexts/function_context.py | 2 - .../apps/routing/activity_context.py | 2 - 20 files changed, 340 insertions(+), 151 deletions(-) create mode 100644 packages/api/src/microsoft_teams/api/clients/team/params.py create mode 100644 packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py create mode 100644 packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py b/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py index dc421bc9..8dba1468 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py @@ -6,13 +6,11 @@ from .activity import ConversationActivityClient from .client import ConversationClient from .member import ConversationMemberClient -from .params import CreateConversationParams, GetConversationsParams, GetConversationsResponse +from .params import CreateConversationParams __all__ = [ "ConversationActivityClient", "ConversationClient", "ConversationMemberClient", "CreateConversationParams", - "GetConversationsParams", - "GetConversationsResponse", ] diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py index e72d1104..a1ee86ef 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py @@ -9,7 +9,7 @@ from microsoft_teams.common.http import Client from ...activities import ActivityParams, SentActivity -from ...models import Account +from ...models import TeamsChannelAccount from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -111,7 +111,7 @@ async def delete(self, conversation_id: str, activity_id: str) -> None: """ await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}") - async def get_members(self, conversation_id: str, activity_id: str) -> List[Account]: + async def get_members(self, conversation_id: str, activity_id: str) -> List[TeamsChannelAccount]: """ Get the members associated with an activity. @@ -120,12 +120,12 @@ async def get_members(self, conversation_id: str, activity_id: str) -> List[Acco activity_id: The ID of the activity Returns: - List of Account objects representing the activity members + List of TeamsChannelAccount objects representing the activity members """ response = await self.http.get( f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}/members" ) - return [Account.model_validate(member) for member in response.json()] + return [TeamsChannelAccount.model_validate(member) for member in response.json()] @experimental("ExperimentalTeamsTargeted") async def create_targeted(self, conversation_id: str, activity: ActivityParams) -> SentActivity: diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/client.py b/packages/api/src/microsoft_teams/api/clients/conversation/client.py index 7ad5aa01..1c42a103 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/client.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/client.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from typing import Dict, Optional, Union +from typing import Optional, Union from microsoft_teams.common.http import Client, ClientOptions @@ -12,11 +12,7 @@ from ..base_client import BaseClient from .activity import ActivityParams, ConversationActivityClient from .member import ConversationMemberClient -from .params import ( - CreateConversationParams, - GetConversationsParams, - GetConversationsResponse, -) +from .params import CreateConversationParams class ConversationOperations: @@ -64,12 +60,12 @@ class MemberOperations(ConversationOperations): async def get_all(self): return await self._client.members_client.get(self._conversation_id) + async def get_paged(self, page_size: Optional[int] = None, continuation_token: Optional[str] = None): + return await self._client.members_client.get_paged(self._conversation_id, page_size, continuation_token) + async def get(self, member_id: str): return await self._client.members_client.get_by_id(self._conversation_id, member_id) - async def delete(self, member_id: str) -> None: - await self._client.members_client.delete(self._conversation_id, member_id) - class ConversationClient(BaseClient): """Client for managing Teams conversations.""" @@ -137,25 +133,6 @@ def members(self, conversation_id: str) -> MemberOperations: """ return MemberOperations(self, conversation_id) - async def get(self, params: Optional[GetConversationsParams] = None) -> GetConversationsResponse: - """Get a list of conversations. - - Args: - params: Optional parameters for getting conversations. - - Returns: - A response containing the list of conversations and a continuation token. - """ - query_params: Dict[str, str] = {} - if params and params.continuation_token: - query_params["continuationToken"] = params.continuation_token - - response = await self.http.get( - f"{self.service_url}/v3/conversations", - params=query_params, - ) - return GetConversationsResponse.model_validate(response.json()) - async def create(self, params: CreateConversationParams) -> ConversationResource: """Create a new conversation. diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/member.py b/packages/api/src/microsoft_teams/api/clients/conversation/member.py index f46692d3..78a7a958 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/member.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/member.py @@ -8,6 +8,7 @@ from microsoft_teams.common.http import Client from ...models import TeamsChannelAccount +from ...models.conversation import PagedMembersResult from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -47,6 +48,28 @@ async def get(self, conversation_id: str) -> List[TeamsChannelAccount]: response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members") return [TeamsChannelAccount.model_validate(member) for member in response.json()] + async def get_paged( + self, + conversation_id: str, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> PagedMembersResult: + """ + Get a page of members in a conversation. + + Args: + conversation_id: The ID of the conversation. + page_size: Optional maximum number of members to return per page. + continuation_token: Optional token from a previous call to fetch the next page. + + Returns: + PagedMembersResult containing the members and an optional continuation token + for fetching subsequent pages. + """ + url = f"{self.service_url}/v3/conversations/{conversation_id}/pagedMembers" + response = await self.http.get(url, params={"pageSize": page_size, "continuationToken": continuation_token}) + return PagedMembersResult.model_validate(response.json()) + async def get_by_id(self, conversation_id: str, member_id: str) -> TeamsChannelAccount: """ Get a specific member in a conversation. @@ -60,13 +83,3 @@ async def get_by_id(self, conversation_id: str, member_id: str) -> TeamsChannelA """ response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") return TeamsChannelAccount.model_validate(response.json()) - - async def delete(self, conversation_id: str, member_id: str) -> None: - """ - Remove a member from a conversation. - - Args: - conversation_id: The ID of the conversation - member_id: The ID of the member to remove - """ - await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/params.py b/packages/api/src/microsoft_teams/api/clients/conversation/params.py index c1f3c380..3d35407c 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/params.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/params.py @@ -5,35 +5,17 @@ from typing import Any, Dict, List, Optional -from ...models import Account, Conversation, CustomBaseModel +from ...models import Account, CustomBaseModel from .activity import ActivityParams -class GetConversationsParams(CustomBaseModel): - """Parameters for getting conversations.""" - - continuation_token: Optional[str] = None - - class CreateConversationParams(CustomBaseModel): """Parameters for creating a conversation.""" - is_group: bool = False - """ - Whether this is a group conversation. - """ - bot: Optional[Account] = None - """ - The bot account to add to the conversation. - """ members: Optional[List[Account]] = None """ The members to add to the conversation. """ - topic_name: Optional[str] = None - """ - The topic name for the conversation. - """ tenant_id: Optional[str] = None """ The tenant ID for the conversation. @@ -46,16 +28,3 @@ class CreateConversationParams(CustomBaseModel): """ The channel-specific data for the conversation. """ - - -class GetConversationsResponse(CustomBaseModel): - """Response from getting conversations.""" - - continuation_token: Optional[str] = None - """ - Token for getting the next page of conversations. - """ - conversations: List[Conversation] = [] - """ - List of conversations. - """ diff --git a/packages/api/src/microsoft_teams/api/clients/meeting/client.py b/packages/api/src/microsoft_teams/api/clients/meeting/client.py index 7b467fbd..cea7277b 100644 --- a/packages/api/src/microsoft_teams/api/clients/meeting/client.py +++ b/packages/api/src/microsoft_teams/api/clients/meeting/client.py @@ -8,6 +8,7 @@ from microsoft_teams.common.http import Client, ClientOptions from ...models import MeetingInfo, MeetingParticipant +from ...models.meetings.meeting_notification import MeetingNotificationParams, MeetingNotificationResponse from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -57,7 +58,31 @@ async def get_participant(self, meeting_id: str, id: str, tenant_id: str) -> Mee Returns: MeetingParticipant: The meeting participant information. """ - url = f"{self.service_url}/v1/meetings/{meeting_id}/participants/{id}?tenantId={tenant_id}" response = await self.http.get(url) return MeetingParticipant.model_validate(response.json()) + + async def send_notification( + self, meeting_id: str, params: MeetingNotificationParams + ) -> Optional[MeetingNotificationResponse]: + """ + Send a targeted meeting notification to participants. + + Returns None on full success (HTTP 202). Returns a MeetingNotificationResponse + with failure details on partial success (HTTP 207). + + Args: + meeting_id: The BASE64-encoded meeting ID. + params: The notification parameters including recipients and surfaces. + + Returns: + None if all notifications were sent successfully, or a MeetingNotificationResponse + with per-recipient failure details on partial success. + """ + response = await self.http.post( + f"{self.service_url}/v1/meetings/{meeting_id}/notification", + json=params.model_dump(by_alias=True, exclude_none=True), + ) + if not response.text: + return None + return MeetingNotificationResponse.model_validate(response.json()) diff --git a/packages/api/src/microsoft_teams/api/clients/team/__init__.py b/packages/api/src/microsoft_teams/api/clients/team/__init__.py index 730d78ca..f05df8a2 100644 --- a/packages/api/src/microsoft_teams/api/clients/team/__init__.py +++ b/packages/api/src/microsoft_teams/api/clients/team/__init__.py @@ -4,5 +4,6 @@ """ from .client import TeamClient +from .params import GetTeamConversationsResponse -__all__ = ["TeamClient"] +__all__ = ["GetTeamConversationsResponse", "TeamClient"] diff --git a/packages/api/src/microsoft_teams/api/clients/team/client.py b/packages/api/src/microsoft_teams/api/clients/team/client.py index ddda3b10..7ffca2e9 100644 --- a/packages/api/src/microsoft_teams/api/clients/team/client.py +++ b/packages/api/src/microsoft_teams/api/clients/team/client.py @@ -10,6 +10,7 @@ from ...models import ChannelInfo, TeamDetails from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient +from .params import GetTeamConversationsResponse class TeamClient(BaseClient): @@ -56,4 +57,4 @@ async def get_conversations(self, id: str) -> List[ChannelInfo]: List of channel information. """ response = await self.http.get(f"{self.service_url}/v3/teams/{id}/conversations") - return [ChannelInfo.model_validate(channel) for channel in response.json()] + return GetTeamConversationsResponse.model_validate(response.json()).conversations diff --git a/packages/api/src/microsoft_teams/api/clients/team/params.py b/packages/api/src/microsoft_teams/api/clients/team/params.py new file mode 100644 index 00000000..23a038eb --- /dev/null +++ b/packages/api/src/microsoft_teams/api/clients/team/params.py @@ -0,0 +1,15 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List + +from ...models import ChannelInfo, CustomBaseModel + + +class GetTeamConversationsResponse(CustomBaseModel): + """Response model for getting team conversations.""" + + conversations: List[ChannelInfo] = [] + """List of conversations in the team.""" diff --git a/packages/api/src/microsoft_teams/api/models/account.py b/packages/api/src/microsoft_teams/api/models/account.py index 5f3100c5..4b8deff6 100644 --- a/packages/api/src/microsoft_teams/api/models/account.py +++ b/packages/api/src/microsoft_teams/api/models/account.py @@ -5,9 +5,11 @@ from typing import Any, Dict, Literal, Optional +from pydantic import AliasChoices, Field + from .custom_base_model import CustomBaseModel -AccountRole = Literal["user", "bot"] +AccountRole = Literal["user", "bot", "skill"] class Account(CustomBaseModel): @@ -59,13 +61,17 @@ class TeamsChannelAccount(CustomBaseModel): """ Display-friendly name of the user or bot. """ - aad_object_id: Optional[str] = None + aad_object_id: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("aadObjectId", "objectId"), + serialization_alias="aadObjectId", + ) """ The user's Object ID in Azure Active Directory (AAD). """ - role: Optional[AccountRole] = None + user_role: Optional[str] = None """ - Role of the user (e.g., 'user' or 'bot'). + Role of the user in the conversation. """ given_name: Optional[str] = None """ diff --git a/packages/api/src/microsoft_teams/api/models/conversation/__init__.py b/packages/api/src/microsoft_teams/api/models/conversation/__init__.py index ccd35b43..fc336f5c 100644 --- a/packages/api/src/microsoft_teams/api/models/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/conversation/__init__.py @@ -6,5 +6,6 @@ from .conversation import Conversation, ConversationType from .conversation_reference import ConversationReference from .conversation_resource import ConversationResource +from .paged_members_result import PagedMembersResult -__all__ = ["Conversation", "ConversationReference", "ConversationResource", "ConversationType"] +__all__ = ["Conversation", "ConversationReference", "ConversationResource", "ConversationType", "PagedMembersResult"] diff --git a/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py b/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py new file mode 100644 index 00000000..89aa7afc --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py @@ -0,0 +1,23 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Optional + +from pydantic import Field + +from ..account import TeamsChannelAccount +from ..custom_base_model import CustomBaseModel + + +class PagedMembersResult(CustomBaseModel): + """ + Result of a paged members request. + """ + + members: List[TeamsChannelAccount] = Field(default_factory=list[TeamsChannelAccount]) + "The members in this page." + + continuation_token: Optional[str] = None + "Token to fetch the next page of members. None if this is the last page." diff --git a/packages/api/src/microsoft_teams/api/models/meetings/__init__.py b/packages/api/src/microsoft_teams/api/models/meetings/__init__.py index 8e718367..1a07032e 100644 --- a/packages/api/src/microsoft_teams/api/models/meetings/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/meetings/__init__.py @@ -6,11 +6,23 @@ from .meeting import Meeting from .meeting_details import MeetingDetails from .meeting_info import MeetingInfo +from .meeting_notification import ( + MeetingNotificationParams, + MeetingNotificationRecipientFailure, + MeetingNotificationResponse, + MeetingNotificationSurface, + MeetingNotificationValue, +) from .meeting_participant import MeetingParticipant __all__ = [ "Meeting", "MeetingDetails", "MeetingInfo", + "MeetingNotificationParams", + "MeetingNotificationRecipientFailure", + "MeetingNotificationResponse", + "MeetingNotificationSurface", + "MeetingNotificationValue", "MeetingParticipant", ] diff --git a/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py b/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py new file mode 100644 index 00000000..71d2effd --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py @@ -0,0 +1,75 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, List, Optional + +from ..custom_base_model import CustomBaseModel + + +class MeetingNotificationSurface(CustomBaseModel): + """ + A surface target for a meeting notification. + """ + + surface: str + "The surface type. E.g. 'meetingStage', 'meetingTabIcon', 'meetingCopilotPane'." + + content_type: Optional[str] = None + "The content type for surfaces that carry content. E.g. 'task'." + + content: Optional[Dict[str, Any]] = None + "The content payload for the surface." + + tab_entity_id: Optional[str] = None + "The tab entity ID, required for 'meetingTabIcon' surfaces." + + +class MeetingNotificationValue(CustomBaseModel): + """ + The value of a targeted meeting notification. + """ + + recipients: List[str] + "AAD object IDs of the meeting participants to notify." + + surfaces: List[MeetingNotificationSurface] + "The surfaces to send the notification to." + + +class MeetingNotificationParams(CustomBaseModel): + """ + Parameters for sending a meeting notification. + """ + + type: str = "targetedMeetingNotification" + "The notification type." + + value: MeetingNotificationValue + "The notification value containing recipients and surfaces." + + +class MeetingNotificationRecipientFailure(CustomBaseModel): + """ + Information about a recipient that failed to receive a meeting notification. + """ + + recipient_mri: Optional[str] = None + "The MRI of the recipient." + + error_code: Optional[str] = None + "The error code." + + failure_reason: Optional[str] = None + "The reason for the failure." + + +class MeetingNotificationResponse(CustomBaseModel): + """ + Response from a meeting notification request when some or all recipients failed (HTTP 207). + None is returned when all notifications were sent successfully (HTTP 202). + """ + + recipients_failure_info: Optional[List[MeetingNotificationRecipientFailure]] = None + "Information about recipients that failed to receive the notification." diff --git a/packages/api/src/microsoft_teams/api/models/team_details.py b/packages/api/src/microsoft_teams/api/models/team_details.py index 70d30ba0..527cbaa4 100644 --- a/packages/api/src/microsoft_teams/api/models/team_details.py +++ b/packages/api/src/microsoft_teams/api/models/team_details.py @@ -30,3 +30,6 @@ class TeamDetails(CustomBaseModel): member_count: Optional[int] = None "Count of members in the team." + + tenant_id: Optional[str] = None + "Azure Active Directory (AAD) tenant ID for the team." diff --git a/packages/api/tests/conftest.py b/packages/api/tests/conftest.py index 70196944..90efb2ff 100644 --- a/packages/api/tests/conftest.py +++ b/packages/api/tests/conftest.py @@ -86,18 +86,41 @@ def handler(request: httpx.Request) -> httpx.Response: "tokenExchangeResource": {"id": "mock_resource_id"}, } elif "/v3/teams/" in str(request.url) and "/conversations" in str(request.url): - response_data = [ - { - "id": "mock_channel_id_1", - "name": "General", - "type": "standard", - }, - { - "id": "mock_channel_id_2", - "name": "Random", - "type": "standard", - }, - ] + response_data = { + "conversations": [ + { + "id": "mock_channel_id_1", + "name": "General", + "type": "standard", + }, + { + "id": "mock_channel_id_2", + "name": "Random", + "type": "standard", + }, + ] + } + elif "/conversations/" in str(request.url) and "/pagedMembers" in str(request.url): + response_data = { + "members": [ + { + "id": "mock_member_id", + "name": "Mock Member", + "aadObjectId": "mock_aad_object_id", + } + ], + "continuationToken": "mock_continuation_token", + } + elif "/notification" in str(request.url) and request.method == "POST": + response_data = { + "recipientsFailureInfo": [ + { + "recipientMri": "8:orgid:mock_recipient", + "errorCode": "BadArgument", + "failureReason": "Invalid recipient", + } + ] + } elif "/conversations/" in str(request.url) and str(request.url).endswith("/members"): response_data = [ { @@ -112,17 +135,6 @@ def handler(request: httpx.Request) -> httpx.Response: "name": "Mock Member", "aadObjectId": "mock_aad_object_id", } - elif "/conversations" in str(request.url) and request.method == "GET": - response_data = { - "conversations": [ - { - "id": "mock_conversation_id", - "conversationType": "personal", - "isGroup": True, - } - ], - "continuationToken": "mock_continuation_token", - } elif "/conversations" in str(request.url) and request.method == "POST": # Parse request body to check if activity is present try: diff --git a/packages/api/tests/unit/test_conversation_client.py b/packages/api/tests/unit/test_conversation_client.py index 86053b3d..bc468e20 100644 --- a/packages/api/tests/unit/test_conversation_client.py +++ b/packages/api/tests/unit/test_conversation_client.py @@ -4,13 +4,13 @@ """ # pyright: basic +from unittest.mock import AsyncMock, patch + +import httpx import pytest from microsoft_teams.api.clients.conversation import ConversationClient -from microsoft_teams.api.clients.conversation.params import ( - CreateConversationParams, - GetConversationsParams, -) -from microsoft_teams.api.models import ConversationResource, TeamsChannelAccount +from microsoft_teams.api.clients.conversation.params import CreateConversationParams +from microsoft_teams.api.models import ConversationResource, PagedMembersResult, TeamsChannelAccount from microsoft_teams.common.http import Client, ClientOptions @@ -47,30 +47,6 @@ def test_conversation_client_initialization_with_options(self): assert client.http is not None assert client.service_url == service_url - @pytest.mark.asyncio - async def test_get_conversations(self, mock_http_client): - """Test getting conversations.""" - service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) - - params = GetConversationsParams(continuation_token="test_token") - response = await client.get(params) - - assert response.conversations is not None - assert isinstance(response.conversations, list) - assert response.continuation_token is not None - - @pytest.mark.asyncio - async def test_get_conversations_without_params(self, mock_http_client): - """Test getting conversations without parameters.""" - service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) - - response = await client.get() - - assert response.conversations is not None - assert isinstance(response.conversations, list) - @pytest.mark.asyncio async def test_create_conversation(self, mock_http_client, mock_account, mock_activity): """Test creating a conversation with an activity.""" @@ -79,9 +55,7 @@ async def test_create_conversation(self, mock_http_client, mock_account, mock_ac params = CreateConversationParams( is_group=True, - bot=mock_account, members=[mock_account], - topic_name="Test Conversation", tenant_id="test_tenant_id", activity=mock_activity, channel_data={"custom": "data"}, @@ -101,9 +75,7 @@ async def test_create_conversation_without_activity(self, mock_http_client, mock params = CreateConversationParams( is_group=True, - bot=mock_account, members=[mock_account], - topic_name="Test Conversation", tenant_id="test_tenant_id", ) @@ -292,17 +264,41 @@ async def test_member_get(self, mock_http_client): assert result.name == "Mock Member" assert result.aad_object_id == "mock_aad_object_id" - async def test_member_delete(self, mock_http_client): - """Test deleting a member.""" + async def test_member_get_paged(self, mock_http_client): + """Test getting a page of members returns PagedMembersResult.""" + service_url = "https://test.service.url" client = ConversationClient(service_url, mock_http_client) conversation_id = "test_conversation_id" - member_id = "test_member_id" members = client.members(conversation_id) - # Should not raise an exception - await members.delete(member_id) + result = await members.get_paged() + + assert isinstance(result, PagedMembersResult) + assert len(result.members) == 1 + assert isinstance(result.members[0], TeamsChannelAccount) + assert result.members[0].id == "mock_member_id" + assert result.continuation_token == "mock_continuation_token" + + async def test_member_get_paged_with_token(self, mock_http_client): + """Test get_paged passes continuation_token and page_size.""" + + service_url = "https://test.service.url" + client = ConversationClient(service_url, mock_http_client) + members = client.members("test_conversation_id") + + mock_response = httpx.Response( + 200, + json={"members": [], "continuationToken": None}, + headers={"content-type": "application/json"}, + ) + with patch.object(mock_http_client, "get", new_callable=AsyncMock, return_value=mock_response) as mock_get: + await members.get_paged(page_size=50, continuation_token="some_token") + + called_params = mock_get.call_args.kwargs.get("params", {}) + assert called_params.get("pageSize") == 50 + assert called_params.get("continuationToken") == "some_token" @pytest.mark.unit diff --git a/packages/api/tests/unit/test_meeting_client.py b/packages/api/tests/unit/test_meeting_client.py index 7a2701e8..4f4e7d18 100644 --- a/packages/api/tests/unit/test_meeting_client.py +++ b/packages/api/tests/unit/test_meeting_client.py @@ -4,9 +4,19 @@ """ # pyright: basic +from unittest.mock import AsyncMock, patch + +import httpx import pytest from microsoft_teams.api.clients.meeting import MeetingClient -from microsoft_teams.api.models import MeetingInfo, MeetingParticipant +from microsoft_teams.api.models import ( + MeetingInfo, + MeetingNotificationParams, + MeetingNotificationResponse, + MeetingNotificationSurface, + MeetingNotificationValue, + MeetingParticipant, +) from microsoft_teams.common.http import Client, ClientOptions @@ -57,3 +67,59 @@ def test_meeting_client_strips_trailing_slash(self, mock_http_client): client = MeetingClient(service_url, mock_http_client) assert client.service_url == "https://test.service.url" + + @pytest.mark.asyncio + async def test_send_notification_partial_failure(self, mock_http_client): + """Test send_notification returns MeetingNotificationResponse on partial failure (HTTP 207).""" + + service_url = "https://test.service.url" + client = MeetingClient(service_url, mock_http_client) + + params = MeetingNotificationParams( + value=MeetingNotificationValue( + recipients=["mock_aad_oid"], + surfaces=[MeetingNotificationSurface(surface="meetingTabIcon", tab_entity_id="test")], + ) + ) + + partial_failure_response = httpx.Response( + 207, + json={ + "recipientsFailureInfo": [ + { + "recipientMri": "8:orgid:mock_recipient", + "errorCode": "BadArgument", + "failureReason": "Invalid recipient", + } + ] + }, + headers={"content-type": "application/json"}, + ) + with patch.object(mock_http_client, "post", new_callable=AsyncMock, return_value=partial_failure_response): + result = await client.send_notification("mock_meeting_id", params) + + assert isinstance(result, MeetingNotificationResponse) + assert result.recipients_failure_info is not None + assert len(result.recipients_failure_info) == 1 + assert result.recipients_failure_info[0].error_code == "BadArgument" + + @pytest.mark.asyncio + async def test_send_notification_full_success(self, mock_http_client): + """Test send_notification returns None on full success (HTTP 202, empty body).""" + import httpx + + service_url = "https://test.service.url" + client = MeetingClient(service_url, mock_http_client) + + params = MeetingNotificationParams( + value=MeetingNotificationValue( + recipients=["mock_aad_oid"], + surfaces=[MeetingNotificationSurface(surface="meetingTabIcon", tab_entity_id="test")], + ) + ) + + empty_response = httpx.Response(202, content=b"", headers={"content-type": "application/json"}) + with patch.object(mock_http_client, "post", new_callable=AsyncMock, return_value=empty_response): + result = await client.send_notification("mock_meeting_id", params) + + assert result is None diff --git a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py index eb209ec5..b86a5e16 100644 --- a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py +++ b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py @@ -120,10 +120,8 @@ async def _resolve_conversation_id(self, activity: str | ActivityParams | Adapti or return a pre-existing one.""" try: conversation_params = CreateConversationParams( - bot=Account(id=self.id, name=self.name, role="bot"), # type: ignore members=[Account(id=self.user_id, role="user", name=self.user_name)], tenant_id=self.tenant_id, - is_group=False, ) conversation = await self.api.conversations.create(conversation_params) self._resolved_conversation_id = conversation.id diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index 243463b3..b8677b00 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -273,8 +273,6 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str one_on_one_conversation = await self.api.conversations.create( CreateConversationParams( tenant_id=self.activity.conversation.tenant_id, - is_group=False, - bot=self.activity.recipient, members=[self.activity.from_], ) ) From c04f80761a37dda97b43e4ed7eeecdf7fd3c46ab Mon Sep 17 00:00:00 2001 From: Aamir Jawaid <48929123+heyitsaamir@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:44:03 -0700 Subject: [PATCH 3/3] fix: include devtools package in publish pipeline (#330) ## Summary - The `devtools` package was accidentally left in the `ExcludePackageFolders` variable in the publish pipeline, preventing it from being published - Clears the exclusion list so all packages are included in both internal and public releases Fixes #329 Co-authored-by: Claude Opus 4.6 (1M context) --- .azdo/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azdo/publish.yml b/.azdo/publish.yml index 9276439b..a6105de6 100644 --- a/.azdo/publish.yml +++ b/.azdo/publish.yml @@ -20,7 +20,7 @@ parameters: variables: - group: TeamsSDK-Release - name: ExcludePackageFolders - value: 'devtools' # Space-separated list of distribution name fragments to exclude (matched as microsoft_teams_*) + value: '' # Space-separated list of distribution name fragments to exclude (matched as microsoft_teams_*) resources: repositories: