From a7ee31c1169158f6324109293a48c61d6f62118f Mon Sep 17 00:00:00 2001 From: Ganapathi Diddi Date: Mon, 15 Jul 2024 00:10:55 +0530 Subject: [PATCH 1/4] [Teams] Add support for meeting participants added/removed events --- .../core/teams/teams_activity_handler.py | 35 +++++++++ .../teams/test_teams_activity_handler.py | 72 +++++++++++++++++++ .../botbuilder/schema/teams/__init__.py | 6 ++ .../botbuilder/schema/teams/_models_py3.py | 46 ++++++++++++ 4 files changed, 159 insertions(+) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 2e5774cc2..5428a3dfd 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -27,6 +27,7 @@ TaskModuleResponse, TabRequest, TabSubmit, + MeetingParticipantsEventDetails, ) from botframework.connector import Channels from ..serializer_helper import deserializer_helper @@ -913,6 +914,14 @@ async def on_event_activity(self, turn_context: TurnContext): return await self.on_teams_meeting_end_event( turn_context.activity.value, turn_context ) + if turn_context.activity.name == "application/vnd.microsoft.meetingParticipantJoin": + return await self.on_teams_meeting_participants_join_event( + turn_context.activity.value, turn_context + ) + if turn_context.activity.name == "application/vnd.microsoft.meetingParticipantLeave": + return await self.on_teams_meeting_participants_leave_event( + turn_context.activity.value, turn_context + ) return await super().on_event_activity(turn_context) @@ -941,3 +950,29 @@ async def on_teams_meeting_end_event( :returns: A task that represents the work queued to execute. """ return + +async def on_teams_meeting_participants_join_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when meeting participants are added. + + :param meeting: The details of the meeting. + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + +async def on_teams_meeting_participants_leave_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when meeting participants are removed. + + :param meeting: The details of the meeting. + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index 32e9f2edb..e953688b2 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -32,6 +32,7 @@ TabRequest, TabSubmit, TabContext, + MeetingParticipantsEventDetails, ) from botframework.connector import Channels from simple_adapter import SimpleAdapter @@ -332,6 +333,22 @@ async def on_teams_meeting_end_event( return await super().on_teams_meeting_end_event( turn_context.activity.value, turn_context ) + + async def on_teams_meeting_participants_join_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): + self.record.append("on_teams_meeting_participants_join_event") + return await super().on_teams_meeting_participants_join_event( + turn_context.activity.value, turn_context + ) + + async def on_teams_meeting_participants_leave_event( + self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext + ): + self.record.append("on_teams_meeting_participants_leave_event") + return await super().on_teams_meeting_participants_leave_event( + turn_context.activity.value, turn_context + ) class NotImplementedAdapter(BotAdapter): @@ -1157,3 +1174,58 @@ async def test_on_teams_meeting_end_event(self): assert len(bot.record) == 2 assert bot.record[0] == "on_event_activity" assert bot.record[1] == "on_teams_meeting_end_event" + + async def test_on_teams_meeting_participants_join_event(self): + # arrange + activity = Activity( + type=ActivityTypes.invoke, + channel_id=Channels.ms_teams, + name="application/vnd.microsoft.meetingParticipantJoin", + value={ + "members": [ + { + "user": {"id": "id", "name": "name"}, + "meeting": {"role": "role", "in_meeting": True} + }] + , + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_meeting_participants_join_event" + + async def test_on_teams_meeting_participants_leave_event(self): + # arrange + activity = Activity( + type=ActivityTypes.invoke, + channel_id=Channels.ms_teams, + name="application/vnd.microsoft.meetingParticipantLeave", + value={ + "members": [ + { + "user": {"id": "id", "name": "name"}, + "meeting": {"role": "role", "in_meeting": True} + }] + , + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + print(bot.record) + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_meeting_participants_leave_event" diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 7824e4571..55901f7a4 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -77,6 +77,9 @@ from ._models_py3 import TabSubmitData from ._models_py3 import TabSuggestedActions from ._models_py3 import TaskModuleCardResponse +from ._models_py3 import UserMeetingDetails +from ._models_py3 import TeamsMeetingMember +from ._models_py3 import MeetingParticipantsEventDetails __all__ = [ "AppBasedLinkQuery", @@ -155,4 +158,7 @@ "TabSubmitData", "TabSuggestedActions", "TaskModuleCardResponse", + "UserMeetingDetails", + "TeamsMeetingMember", + "MeetingParticipantsEventDetails", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index bb860c8da..61699d6cc 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2506,3 +2506,49 @@ class MeetingEndEventDetails(MeetingDetailsBase): def __init__(self, *, end_time: str = None, **kwargs): super(MeetingEndEventDetails, self).__init__(**kwargs) self.end_time = end_time + +class UserMeetingDetails(Model): + """Specific details of a user in a Teams meeting.""" + + def __init__(self, *, in_meeting: bool = None, role: str = None, **kwargs) -> None: + super(UserMeetingDetails, self).__init__(**kwargs) + self.in_meeting = in_meeting + self.role = role + + +class TeamsMeetingMember(Model): + """Data about the meeting participants. + + :param user: The channel user data. + :type user: ~botbuilder.schema.models.TeamsChannelAccount + :param meeting: The user meeting details. + :type meeting: ~botbuilder.schema.models.UserMeetingDetails + """ + + _attribute_map = { + "user": {"key": "TeamsChannelAccount", "type": "[TeamsChannelAccount]"}, + "meeting": {"key": "UserMeetingDetails", "type": "[UserMeetingDetails]"} + } + + def __init__( + self, *, user: TeamsChannelAccount = None, meeting: UserMeetingDetails = None, **kwargs + ) -> None: + super(TeamsMeetingMember, self).__init__(**kwargs) + self.user = user + self.meeting = meeting + +class MeetingParticipantsEventDetails(Model): + """Data about the meeting participants. + + :param members: The members involved in the meeting event. + :type members: list[~botframework.connector.teams.models.TeamsMeetingMember] + """ + + _attribute_map = { + "conversations": {"key": "members", "type": "[TeamsMeetingMember]"}, + } + + def __init__(self, *, members=None, **kwargs) -> None: + super(MeetingParticipantsEventDetails, self).__init__(**kwargs) + self.members = members + From 876ed19c4771c3a4dee3a49133357083891a1350 Mon Sep 17 00:00:00 2001 From: Ganapathi Diddi Date: Tue, 16 Jul 2024 19:06:36 +0530 Subject: [PATCH 2/4] test case issue and code format fix --- .../core/teams/teams_activity_handler.py | 14 +++++--- .../teams/test_teams_activity_handler.py | 33 +++++++++-------- .../botbuilder/schema/teams/_models_py3.py | 36 +++++++++++++------ 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 5428a3dfd..33b4e419c 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -914,11 +914,17 @@ async def on_event_activity(self, turn_context: TurnContext): return await self.on_teams_meeting_end_event( turn_context.activity.value, turn_context ) - if turn_context.activity.name == "application/vnd.microsoft.meetingParticipantJoin": + if ( + turn_context.activity.name + == "application/vnd.microsoft.meetingParticipantJoin" + ): return await self.on_teams_meeting_participants_join_event( turn_context.activity.value, turn_context ) - if turn_context.activity.name == "application/vnd.microsoft.meetingParticipantLeave": + if ( + turn_context.activity.name + == "application/vnd.microsoft.meetingParticipantLeave" + ): return await self.on_teams_meeting_participants_leave_event( turn_context.activity.value, turn_context ) @@ -951,7 +957,7 @@ async def on_teams_meeting_end_event( """ return -async def on_teams_meeting_participants_join_event( + async def on_teams_meeting_participants_join_event( self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext ): # pylint: disable=unused-argument """ @@ -964,7 +970,7 @@ async def on_teams_meeting_participants_join_event( """ return -async def on_teams_meeting_participants_leave_event( + async def on_teams_meeting_participants_leave_event( self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext ): # pylint: disable=unused-argument """ diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index e953688b2..390df6191 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -333,7 +333,7 @@ async def on_teams_meeting_end_event( return await super().on_teams_meeting_end_event( turn_context.activity.value, turn_context ) - + async def on_teams_meeting_participants_join_event( self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext ): @@ -341,7 +341,7 @@ async def on_teams_meeting_participants_join_event( return await super().on_teams_meeting_participants_join_event( turn_context.activity.value, turn_context ) - + async def on_teams_meeting_participants_leave_event( self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext ): @@ -1178,18 +1178,18 @@ async def test_on_teams_meeting_end_event(self): async def test_on_teams_meeting_participants_join_event(self): # arrange activity = Activity( - type=ActivityTypes.invoke, + type=ActivityTypes.event, channel_id=Channels.ms_teams, name="application/vnd.microsoft.meetingParticipantJoin", value={ "members": [ { - "user": {"id": "id", "name": "name"}, - "meeting": {"role": "role", "in_meeting": True} - }] - , + "user": {"id": "123", "name": "name"}, + "meeting": {"role": "role", "in_meeting": True}, + } + ], }, - ) + ) turn_context = TurnContext(SimpleAdapter(), activity) @@ -1199,24 +1199,24 @@ async def test_on_teams_meeting_participants_join_event(self): # Assert assert len(bot.record) == 2 - assert bot.record[0] == "on_invoke_activity" + assert bot.record[0] == "on_event_activity" assert bot.record[1] == "on_teams_meeting_participants_join_event" async def test_on_teams_meeting_participants_leave_event(self): # arrange activity = Activity( - type=ActivityTypes.invoke, + type=ActivityTypes.event, channel_id=Channels.ms_teams, name="application/vnd.microsoft.meetingParticipantLeave", value={ "members": [ { - "user": {"id": "id", "name": "name"}, - "meeting": {"role": "role", "in_meeting": True} - }] - , + "user": {"id": "id", "name": "name"}, + "meeting": {"role": "role", "in_meeting": True}, + } + ], }, - ) + ) turn_context = TurnContext(SimpleAdapter(), activity) @@ -1225,7 +1225,6 @@ async def test_on_teams_meeting_participants_leave_event(self): await bot.on_turn(turn_context) # Assert - print(bot.record) assert len(bot.record) == 2 - assert bot.record[0] == "on_invoke_activity" + assert bot.record[0] == "on_event_activity" assert bot.record[1] == "on_teams_meeting_participants_leave_event" diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 61699d6cc..260442cf8 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2507,10 +2507,22 @@ def __init__(self, *, end_time: str = None, **kwargs): super(MeetingEndEventDetails, self).__init__(**kwargs) self.end_time = end_time + class UserMeetingDetails(Model): - """Specific details of a user in a Teams meeting.""" + """Specific details of a user in a Teams meeting. - def __init__(self, *, in_meeting: bool = None, role: str = None, **kwargs) -> None: + :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(UserMeetingDetails, self).__init__(**kwargs) self.in_meeting = in_meeting self.role = role @@ -2520,35 +2532,39 @@ class TeamsMeetingMember(Model): """Data about the meeting participants. :param user: The channel user data. - :type user: ~botbuilder.schema.models.TeamsChannelAccount + :type user: TeamsChannelAccount :param meeting: The user meeting details. - :type meeting: ~botbuilder.schema.models.UserMeetingDetails + :type meeting: UserMeetingDetails """ _attribute_map = { - "user": {"key": "TeamsChannelAccount", "type": "[TeamsChannelAccount]"}, - "meeting": {"key": "UserMeetingDetails", "type": "[UserMeetingDetails]"} + "user": {"key": "user", "type": "TeamsChannelAccount"}, + "meeting": {"key": "meeting", "type": "UserMeetingDetails"}, } def __init__( - self, *, user: TeamsChannelAccount = None, meeting: UserMeetingDetails = None, **kwargs + self, + *, + user: TeamsChannelAccount = None, + meeting: UserMeetingDetails = None, + **kwargs ) -> None: super(TeamsMeetingMember, self).__init__(**kwargs) self.user = user self.meeting = meeting + class MeetingParticipantsEventDetails(Model): """Data about the meeting participants. :param members: The members involved in the meeting event. - :type members: list[~botframework.connector.teams.models.TeamsMeetingMember] + :type members: list[~botframework.connector.models.TeamsMeetingMember] """ _attribute_map = { "conversations": {"key": "members", "type": "[TeamsMeetingMember]"}, } - def __init__(self, *, members=None, **kwargs) -> None: + def __init__(self, *, members: List[TeamsMeetingMember] = None, **kwargs) -> None: super(MeetingParticipantsEventDetails, self).__init__(**kwargs) self.members = members - From c161fe6389200426a1b8b99499cf22ad2596bf19 Mon Sep 17 00:00:00 2001 From: Ganapathi Diddi Date: Wed, 17 Jul 2024 21:23:18 +0530 Subject: [PATCH 3/4] code format fix for microsoft_app_credentials.py --- .../connector/auth/microsoft_app_credentials.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py index 6056fec5e..8f0724256 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -56,8 +56,14 @@ def get_access_token(self, force_refresh: bool = False) -> str: return auth_token["access_token"] else: error = auth_token["error"] if "error" in auth_token else "Unknown error" - error_description = auth_token["error_description"] if "error_description" in auth_token else "Unknown error description" - raise PermissionError(f"Failed to get access token with error: {error}, error_description: {error_description}") + error_description = ( + auth_token["error_description"] + if "error_description" in auth_token + else "Unknown error description" + ) + raise PermissionError( + f"Failed to get access token with error: {error}, error_description: {error_description}" + ) def __get_msal_app(self): if not self.app: From c317d0fb6a5babdb993f754a55098a9ea31b6451 Mon Sep 17 00:00:00 2001 From: Ganapathi Diddi Date: Wed, 17 Jul 2024 23:00:32 +0530 Subject: [PATCH 4/4] removing unnecessary else --- .../auth/microsoft_app_credentials.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py index 8f0724256..523977b08 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -54,16 +54,15 @@ def get_access_token(self, force_refresh: bool = False) -> str: auth_token = self.__get_msal_app().acquire_token_for_client(scopes=scopes) if "access_token" in auth_token: return auth_token["access_token"] - else: - error = auth_token["error"] if "error" in auth_token else "Unknown error" - error_description = ( - auth_token["error_description"] - if "error_description" in auth_token - else "Unknown error description" - ) - raise PermissionError( - f"Failed to get access token with error: {error}, error_description: {error_description}" - ) + error = auth_token["error"] if "error" in auth_token else "Unknown error" + error_description = ( + auth_token["error_description"] + if "error_description" in auth_token + else "Unknown error description" + ) + raise PermissionError( + f"Failed to get access token with error: {error}, error_description: {error_description}" + ) def __get_msal_app(self): if not self.app: