diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py index e95d5d68a..7b9c2fd0a 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py @@ -2,7 +2,22 @@ # Licensed under the MIT License. from botbuilder.schema import Activity -from botbuilder.schema.teams import NotificationInfo, TeamsChannelData, TeamInfo +from botbuilder.schema.teams import ( + NotificationInfo, + TeamsChannelData, + TeamInfo, + TeamsMeetingInfo, +) + + +def teams_get_channel_data(activity: Activity) -> TeamsChannelData: + if not activity: + return None + + if activity.channel_data: + return TeamsChannelData().deserialize(activity.channel_data) + + return None def teams_get_channel_id(activity: Activity) -> str: @@ -41,3 +56,14 @@ def teams_notify_user( channel_data.notification.alert_in_meeting = alert_in_meeting channel_data.notification.external_resource_url = external_resource_url activity.channel_data = channel_data + + +def teams_get_meeting_info(activity: Activity) -> TeamsMeetingInfo: + if not activity: + return None + + if activity.channel_data: + channel_data = TeamsChannelData().deserialize(activity.channel_data) + return channel_data.meeting + + return None diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index e781f4696..6533f38d6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -2,7 +2,14 @@ # Licensed under the MIT License. from typing import List, Tuple + +from botframework.connector.aio import ConnectorClient +from botframework.connector.teams.teams_connector_client import TeamsConnectorClient from botbuilder.schema import ConversationParameters, ConversationReference +from botbuilder.core.teams.teams_activity_extensions import ( + teams_get_meeting_info, + teams_get_channel_data, +) from botbuilder.core.turn_context import Activity, TurnContext from botbuilder.schema.teams import ( ChannelInfo, @@ -10,9 +17,8 @@ TeamsChannelData, TeamsChannelAccount, TeamsPagedMembersResult, + TeamsMeetingParticipant, ) -from botframework.connector.aio import ConnectorClient -from botframework.connector.teams.teams_connector_client import TeamsConnectorClient class TeamsInfo: @@ -177,6 +183,48 @@ async def get_member( return await TeamsInfo.get_team_member(turn_context, team_id, member_id) + @staticmethod + async def get_meeting_participant( + turn_context: TurnContext, + meeting_id: str = None, + participant_id: str = None, + tenant_id: str = None, + ) -> TeamsMeetingParticipant: + meeting_id = ( + meeting_id + if meeting_id + else teams_get_meeting_info(turn_context.activity).id + ) + if meeting_id is None: + raise TypeError( + "TeamsInfo._get_meeting_participant: method requires a meeting_id" + ) + + participant_id = ( + participant_id + if participant_id + else turn_context.activity.from_property.aad_object_id + ) + if participant_id is None: + raise TypeError( + "TeamsInfo._get_meeting_participant: method requires a participant_id" + ) + + tenant_id = ( + tenant_id + if tenant_id + else teams_get_channel_data(turn_context.activity).tenant.id + ) + if tenant_id is None: + raise TypeError( + "TeamsInfo._get_meeting_participant: method requires a tenant_id" + ) + + connector_client = await TeamsInfo.get_teams_connector_client(turn_context) + return connector_client.teams.fetch_participant( + meeting_id, participant_id, tenant_id + ) + @staticmethod async def get_teams_connector_client( turn_context: TurnContext, diff --git a/libraries/botbuilder-core/tests/teams/test_teams_extension.py b/libraries/botbuilder-core/tests/teams/test_teams_extension.py index e4ebb4449..98c1ee829 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_extension.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_extension.py @@ -10,6 +10,7 @@ teams_get_team_info, teams_notify_user, ) +from botbuilder.core.teams.teams_activity_extensions import teams_get_meeting_info class TestTeamsActivityHandler(aiounittest.AsyncTestCase): @@ -149,3 +150,13 @@ def test_teams_notify_user_with_no_channel_data(self): # Assert assert activity.channel_data.notification.alert assert activity.id == "id123" + + def test_teams_meeting_info(self): + # Arrange + activity = Activity(channel_data={"meeting": {"id": "meeting123"}}) + + # Act + meeting_id = teams_get_meeting_info(activity).id + + # Assert + assert meeting_id == "meeting123" diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py index 9ddc5662c..5c044e6ca 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_info.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py @@ -2,7 +2,7 @@ # Licensed under the MIT License. import aiounittest - +from botframework.connector import Channels from botbuilder.core import TurnContext, MessageFactory from botbuilder.core.teams import TeamsInfo, TeamsActivityHandler @@ -199,6 +199,25 @@ def create_conversation(): else: assert False, "should have raise TypeError" + async def test_get_participant(self): + adapter = SimpleAdapterWithCreateConversation() + + activity = Activity( + type="message", + text="Test-get_participant", + channel_id=Channels.ms_teams, + from_property=ChannelAccount(aad_object_id="participantId-1"), + channel_data={ + "meeting": {"id": "meetingId-1"}, + "tenant": {"id": "tenantId-1"}, + }, + service_url="https://test.coffee", + ) + + turn_context = TurnContext(adapter, activity) + handler = TeamsActivityHandler() + await handler.on_turn(turn_context) + class TestTeamsActivityHandler(TeamsActivityHandler): async def on_turn(self, turn_context: TurnContext): diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index a6d384feb..75a454851 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -58,6 +58,9 @@ from ._models_py3 import TeamsChannelData from ._models_py3 import TeamsPagedMembersResult from ._models_py3 import TenantInfo +from ._models_py3 import TeamsMeetingInfo +from ._models_py3 import TeamsMeetingParticipant +from ._models_py3 import MeetingParticipantInfo __all__ = [ "AppBasedLinkQuery", @@ -117,4 +120,7 @@ "TeamsChannelData", "TeamsPagedMembersResult", "TenantInfo", + "TeamsMeetingInfo", + "TeamsMeetingParticipant", + "MeetingParticipantInfo", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 150f69d5e..98214bbd6 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2,7 +2,12 @@ # Licensed under the MIT License. from msrest.serialization import Model -from botbuilder.schema import Activity, Attachment, ChannelAccount, PagedMembersResult +from botbuilder.schema import ( + Attachment, + ChannelAccount, + PagedMembersResult, + ConversationAccount, +) class TaskModuleRequest(Model): @@ -1898,6 +1903,8 @@ class TeamsChannelData(Model): :type notification: ~botframework.connector.teams.models.NotificationInfo :param tenant: Information about the tenant in which the message was sent :type tenant: ~botframework.connector.teams.models.TenantInfo + :param meeting: Information about the meeting in which the message was sent + :type meeting: ~botframework.connector.teams.models.TeamsMeetingInfo """ _attribute_map = { @@ -1906,6 +1913,7 @@ class TeamsChannelData(Model): "team": {"key": "team", "type": "TeamInfo"}, "notification": {"key": "notification", "type": "NotificationInfo"}, "tenant": {"key": "tenant", "type": "TenantInfo"}, + "meeting": {"key": "meeting", "type": "TeamsMeetingInfo"}, } def __init__( @@ -1916,6 +1924,7 @@ def __init__( team=None, notification=None, tenant=None, + meeting=None, **kwargs ) -> None: super(TeamsChannelData, self).__init__(**kwargs) @@ -1925,6 +1934,7 @@ def __init__( self.team = team self.notification = notification self.tenant = tenant + self.meeting = meeting class TenantInfo(Model): @@ -1941,3 +1951,70 @@ class TenantInfo(Model): def __init__(self, *, id: str = None, **kwargs) -> None: super(TenantInfo, self).__init__(**kwargs) self.id = id + + +class TeamsMeetingInfo(Model): + """Describes a Teams Meeting. + + :param id: Unique identifier representing a meeting + :type id: str + """ + + _attribute_map = { + "id": {"key": "id", "type": "str"}, + } + + def __init__(self, *, id: str = None, **kwargs) -> None: + super(TeamsMeetingInfo, self).__init__(**kwargs) + self.id = id + + +class MeetingParticipantInfo(Model): + """Teams meeting participant details. + + :param role: Role of the participant in the current meeting. + :type role: str + :param in_meeting: True, if the participant is in the meeting. + :type in_meeting: bool + """ + + _attribute_map = { + "role": {"key": "role", "type": "str"}, + "in_meeting": {"key": "inMeeting", "type": "bool"}, + } + + def __init__(self, *, role: str = None, in_meeting: bool = None, **kwargs) -> None: + super(MeetingParticipantInfo, self).__init__(**kwargs) + self.role = role + self.in_meeting = in_meeting + + +class TeamsMeetingParticipant(Model): + """Teams participant channel account detailing user Azure Active Directory and meeting participant details. + + :param user: Teams Channel Account information for this meeting participant + :type user: TeamsChannelAccount + :param meeting: >Information specific to this participant in the specific meeting. + :type meeting: MeetingParticipantInfo + :param conversation: Conversation Account for the meeting. + :type conversation: ConversationAccount + """ + + _attribute_map = { + "user": {"key": "user", "type": "TeamsChannelAccount"}, + "meeting": {"key": "meeting", "type": "MeetingParticipantInfo"}, + "conversation": {"key": "conversation", "type": "ConversationAccount"}, + } + + def __init__( + self, + *, + user: TeamsChannelAccount = None, + meeting: MeetingParticipantInfo = None, + conversation: ConversationAccount = None, + **kwargs + ) -> None: + super(TeamsMeetingParticipant, self).__init__(**kwargs) + self.user = user + self.meeting = meeting + self.conversation = conversation diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py index e6a2d909d..5c61086b0 100644 --- a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py @@ -145,3 +145,74 @@ def get_team_details( return deserialized get_team_details.metadata = {"url": "/v3/teams/{teamId}"} + + def fetch_participant( + self, + meeting_id: str, + participant_id: str, + tenant_id: str, + custom_headers=None, + raw=False, + **operation_config + ): + """Fetches Teams meeting participant details. + + :param meeting_id: Teams meeting id + :type meeting_id: str + :param participant_id: Teams meeting participant id + :type participant_id: str + :param tenant_id: Teams meeting tenant id + :type tenant_id: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: TeamsMeetingParticipant or ClientRawResponse if raw=true + :rtype: ~botframework.connector.teams.models.TeamsParticipantChannelAccount or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + + # Construct URL + url = self.fetch_participant.metadata["url"] + path_format_arguments = { + "meetingId": self._serialize.url("meeting_id", meeting_id, "str"), + "participantId": self._serialize.url( + "participant_id", participant_id, "str" + ), + "tenantId": self._serialize.url("tenant_id", tenant_id, "str"), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters["Accept"] = "application/json" + if custom_headers: + header_parameters.update(custom_headers) + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise HttpOperationError(self._deserialize, response) + + deserialized = None + + if response.status_code == 200: + deserialized = self._deserialize("TeamsMeetingParticipant", response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + fetch_participant.metadata = { + "url": "/v1/meetings/{meetingId}/participants/{participantId}?tenantId={tenantId}" + }