From 72adad3f6360305b58a05b7882caade8e5155047 Mon Sep 17 00:00:00 2001 From: Amrit10737726 Date: Thu, 1 Aug 2024 19:03:14 +0530 Subject: [PATCH 1/7] Fix class structure for meeting notification feature extensibility --- .../core/teams/teams_activity_handler.py | 8 +- .../botbuilder/core/teams/teams_info.py | 36 ++- .../teams/test_teams_activity_handler.py | 4 +- .../tests/teams/test_teams_info.py | 254 ++++++++++++++++++ .../botbuilder/schema/teams/__init__.py | 26 ++ .../botbuilder/schema/teams/content_type.py | 7 + .../schema/teams/meeting_notification.py | 27 ++ .../schema/teams/meeting_notification_base.py | 24 ++ .../meeting_notification_channel_data.py | 14 + ...ing_notification_recipient_failure_info.py | 27 ++ .../teams/meeting_notification_response.py | 21 ++ .../schema/teams/meeting_stage_surface.py | 13 + .../schema/teams/meeting_tab_icon_surface.py | 22 ++ .../botbuilder/schema/teams/on_behalf_of.py | 27 ++ .../botbuilder/schema/teams/surface.py | 34 +++ .../botbuilder/schema/teams/surface_type.py | 14 + .../teams/targeted_meeting_notification.py | 34 +++ .../targeted_meeting_notification_value.py | 26 ++ .../teams/operations/teams_operations.py | 60 ++++- .../operations/teams_operations_extensions.py | 71 +++++ 20 files changed, 741 insertions(+), 8 deletions(-) create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/content_type.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_response.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/surface.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/surface_type.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py create mode 100644 libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py create mode 100644 libraries/botframework-connector/botframework/connector/teams/operations/teams_operations_extensions.py 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 33b4e419c..aa568752d 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -709,7 +709,7 @@ async def on_teams_team_unarchived( # pylint: disable=unused-argument async def on_teams_members_added_dispatch( # pylint: disable=unused-argument self, - members_added: [ChannelAccount], + members_added: ChannelAccount, team_info: TeamInfo, turn_context: TurnContext, ): @@ -762,7 +762,7 @@ async def on_teams_members_added_dispatch( # pylint: disable=unused-argument async def on_teams_members_added( # pylint: disable=unused-argument self, - teams_members_added: [TeamsChannelAccount], + teams_members_added: TeamsChannelAccount, team_info: TeamInfo, turn_context: TurnContext, ): @@ -785,7 +785,7 @@ async def on_teams_members_added( # pylint: disable=unused-argument async def on_teams_members_removed_dispatch( # pylint: disable=unused-argument self, - members_removed: [ChannelAccount], + members_removed: ChannelAccount, team_info: TeamInfo, turn_context: TurnContext, ): @@ -816,7 +816,7 @@ async def on_teams_members_removed_dispatch( # pylint: disable=unused-argument async def on_teams_members_removed( # pylint: disable=unused-argument self, - teams_members_removed: [TeamsChannelAccount], + teams_members_removed: TeamsChannelAccount, team_info: TeamInfo, turn_context: TurnContext, ): diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index f70f6cccc..032f11998 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -1,8 +1,12 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import List, Tuple +from typing import Any, List, Optional, Tuple +from botbuilder.schema.teams.meeting_notification_base import MeetingNotificationBase +from botbuilder.schema.teams.meeting_notification_response import ( + MeetingNotificationResponse, +) from botframework.connector import Channels from botframework.connector.aio import ConnectorClient from botframework.connector.teams import TeamsConnectorClient @@ -26,6 +30,9 @@ TeamsPagedMembersResult, TeamsMeetingParticipant, ) +from botframework.connector.teams.operations.teams_operations_extensions import ( + TeamsOperationsExtensions, +) class TeamsInfo: @@ -401,3 +408,30 @@ async def _get_member( return TeamsChannelAccount().deserialize( dict(member.serialize(), **member.additional_properties) ) + + @staticmethod + async def send_meeting_notification_async( + turn_context: TurnContext, + notification: MeetingNotificationBase, + meeting_id: Optional[str] = None, + cancellation_token: Optional[Any] = None, + ) -> MeetingNotificationResponse: + if meeting_id is None: + meeting_id = turn_context.activity.id + else: + raise ValueError( + "This method is only valid within the scope of a MS Teams Meeting." + ) + + if not notification: + raise Exception(f"{notification} is required.") + + teams_client = TeamsInfo.get_teams_connector_client(turn_context) + + try: + return await TeamsOperationsExtensions.send_meeting_notification_async( + meeting_id, notification, notification + ) + + finally: + await teams_client.close() 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 390df6191..a1e68865c 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -50,7 +50,7 @@ async def on_conversation_update_activity(self, turn_context: TurnContext): async def on_teams_members_added( # pylint: disable=unused-argument self, - teams_members_added: [TeamsChannelAccount], + teams_members_added: TeamsChannelAccount, team_info: TeamInfo, turn_context: TurnContext, ): @@ -61,7 +61,7 @@ async def on_teams_members_added( # pylint: disable=unused-argument async def on_teams_members_removed( self, - teams_members_removed: [TeamsChannelAccount], + teams_members_removed: TeamsChannelAccount, team_info: TeamInfo, turn_context: TurnContext, ): diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py index dea57030c..8cccb7ec4 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_info.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py @@ -1,9 +1,29 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from http.server import BaseHTTPRequestHandler import aiounittest from botframework.connector import Channels +import json +from botbuilder.schema._models_py3 import ErrorResponse +from botbuilder.schema.teams._models_py3 import ( + TaskModuleContinueResponse, + TaskModuleTaskInfo, +) +from botbuilder.schema.teams.meeting_stage_surface import MeetingStageSurface +from botbuilder.schema.teams.targeted_meeting_notification_value import ( + TargetedMeetingNotificationValue, +) +from simple_adapter_with_create_conversation import SimpleAdapterWithCreateConversation +from botbuilder.schema.teams.on_behalf_of import OnBehalfOf +from botbuilder.schema.teams.meeting_notification_channel_data import ( + MeetingNotificationChannelData, +) +from botbuilder.schema.teams.targeted_meeting_notification import ( + TargetedMeetingNotification, +) + from botbuilder.core import TurnContext, MessageFactory from botbuilder.core.teams import TeamsInfo, TeamsActivityHandler from botbuilder.schema import ( @@ -251,3 +271,237 @@ async def call_send_message_to_teams(self, turn_context: TurnContext): assert reference[0].activity_id == "new_conversation_id" assert reference[1] == "reference123" + + @staticmethod + def get_targeted_meeting_notification(from_user) -> TargetedMeetingNotification: + recipients = [from_user.id] + + if from_user.name == "207": + recipients.append("failingid") + + surface = MeetingStageSurface[TaskModuleContinueResponse]() + surface.content = TaskModuleContinueResponse( + value=TaskModuleTaskInfo(title="title here", height=3, width=2) + ) + surface.content_type = "Task" + + value = TargetedMeetingNotificationValue( + recipients=recipients, surfaces=[surface] + ) + + obo = OnBehalfOf(display_name=from_user.name, mri=from_user.id) + channel_data = MeetingNotificationChannelData(on_behalf_of_list=[obo]) + + return TargetedMeetingNotification( + value=value, + channel_data=channel_data + ) + + @staticmethod + async def call_send_meeting_notification_async(turn_context: TurnContext): + from_user = turn_context.activity.from_property + + try: + failed_participants = await TeamsInfo.send_meeting_notification_async( + turn_context, + TestTeamsActivityHandler.get_targeted_meeting_notification(from_user), + "meeting-id", + ) + + if from_user.name == "207": + assert ( + "failingid" + == failed_participants.recipients_failure_info[0].recipient_mri + ) + elif from_user.name == "202": + assert failed_participants is None + else: + raise ValueError( + f"Expected HttpOperationException with response status code {from_user.name}" + ) + + except Exception as ex: + assert from_user.name == str(ex.response.status_code) + error_response = ErrorResponse.from_json(ex.response.content) + + if from_user.name == "400": + assert "BadSyntax" == error_response.error.code + elif from_user.name == "403": + assert "BotNotInConversationRoster" == error_response.error.code + else: + raise ValueError( + f"Expected HttpOperationException with response status code {from_user.name}" + ) + + +class RosterHttpMessageHandler(BaseHTTPRequestHandler): + async def send_async(self): + # Set response headers + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + + # Route handling based on path + path_handlers = { + "team-id": self.handle_team_id, + "v3/conversations": self.handle_v3_conversations, + "team-id/conversations": self.handle_team_id_conversations, + "team-id/members": self.handle_team_id_members, + "conversation-id/members": self.handle_conversation_id_members, + "team-id/members/id-1": self.handle_member_id, + "conversation-id/members/id-1": self.handle_member_id, + "v1/meetings/meetingId-1/participants/participantId-1?tenantId=tenantId-1": self.handle_meeting_participant, + "v1/meetings/meeting-id": self.handle_meeting_id, + "v1/meetings/meeting-id/notification": self.handle_meeting_notification, + } + + path = self.path + handler = next( + (handler for key, handler in path_handlers.items() if path.endswith(key)), + None, + ) + if handler: + response = await handler() + self.wfile.write(response.encode("utf-8")) + else: + self.send_response(404) + self.wfile.write(b"Not Found") + + async def handle_team_id(self): + content = { + "id": "team-id", + "name": "team-name", + "aadGroupId": "team-aadgroupid", + } + return json.dumps(content) + + async def handle_v3_conversations(self): + content = { + "id": "id123", + "serviceUrl": "https://serviceUrl/", + "activityId": "activityId123", + } + return json.dumps(content) + + async def handle_team_id_conversations(self): + content = { + "conversations": [ + {"id": "channel-id-1"}, + {"id": "channel-id-2", "name": "channel-name-2"}, + {"id": "channel-id-3", "name": "channel-name-3"}, + ] + } + return json.dumps(content) + + async def handle_team_id_members(self): + content = [ + { + "id": "id-1", + "objectId": "objectId-1", + "name": "name-1", + "givenName": "givenName-1", + "surname": "surname-1", + "email": "email-1", + "userPrincipalName": "userPrincipalName-1", + "tenantId": "tenantId-1", + }, + { + "id": "id-2", + "objectId": "objectId-2", + "name": "name-2", + "givenName": "givenName-2", + "surname": "surname-2", + "email": "email-2", + "userPrincipalName": "userPrincipalName-2", + "tenantId": "tenantId-2", + }, + ] + return json.dumps(content) + + async def handle_conversation_id_members(self): + content = [ + { + "id": "id-3", + "objectId": "objectId-3", + "name": "name-3", + "givenName": "givenName-3", + "surname": "surname-3", + "email": "email-3", + "userPrincipalName": "userPrincipalName-3", + "tenantId": "tenantId-3", + }, + { + "id": "id-4", + "objectId": "objectId-4", + "name": "name-4", + "givenName": "givenName-4", + "surname": "surname-4", + "email": "email-4", + "userPrincipalName": "userPrincipalName-4", + "tenantId": "tenantId-4", + }, + ] + return json.dumps(content) + + async def handle_member_id(self): + content = { + "id": "id-1", + "objectId": "objectId-1", + "name": "name-1", + "givenName": "givenName-1", + "surname": "surname-1", + "email": "email-1", + "userPrincipalName": "userPrincipalName-1", + "tenantId": "tenantId-1", + } + return json.dumps(content) + + async def handle_meeting_participant(self): + content = { + "user": {"userPrincipalName": "userPrincipalName-1"}, + "meeting": {"role": "Organizer"}, + "conversation": {"Id": "meetigConversationId-1"}, + } + return json.dumps(content) + + async def handle_meeting_id(self): + content = { + "details": {"id": "meeting-id"}, + "organizer": {"id": "organizer-id"}, + "conversation": {"id": "meetingConversationId-1"}, + } + return json.dumps(content) + + async def handle_meeting_notification(self): + content_length = int(self.headers["Content-Length"]) + response_body = self.rfile.read(content_length).decode("utf-8") + notification = json.loads(response_body) + obo = notification["ChannelData"]["OnBehalfOfList"][0] + + # hack displayname as expected status code, for testing + display_name = obo["DisplayName"] + if display_name == "207": + recipient_failure_info = { + "RecipientMri": next( + r + for r in notification["Value"]["Recipients"] + if r.lower() != obo["Mri"].lower() + ) + } + infos = {"RecipientsFailureInfo": [recipient_failure_info]} + response = json.dumps(infos) + status_code = 207 + elif display_name == "403": + response = json.dumps({"error": {"code": "BotNotInConversationRoster"}}) + status_code = 403 + elif display_name == "400": + response = json.dumps({"error": {"code": "BadSyntax"}}) + status_code = 400 + else: + response = "" + status_code = 202 + + self.send_response(status_code) + self.send_header("Content-type", "application/json") + self.end_headers() + return response diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 55901f7a4..e58116f10 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -80,6 +80,20 @@ from ._models_py3 import UserMeetingDetails from ._models_py3 import TeamsMeetingMember from ._models_py3 import MeetingParticipantsEventDetails +from .meeting_notification_base import MeetingNotificationBase +from .meeting_notification_channel_data import MeetingNotificationChannelData +from .meeting_notification_recipient_failure_info import ( + MeetingNotificationRecipientFailureInfo, +) +from .meeting_notification_response import MeetingNotificationResponse +from .meeting_notification import MeetingNotification +from .meeting_stage_surface import MeetingStageSurface +from .meeting_tab_icon_surface import MeetingTabIconSurface +from .on_behalf_of import OnBehalfOf +from .surface_type import SurfaceType +from .surface import Surface +from .targeted_meeting_notification import TargetedMeetingNotification +from .targeted_meeting_notification_value import TargetedMeetingNotificationValue __all__ = [ "AppBasedLinkQuery", @@ -161,4 +175,16 @@ "UserMeetingDetails", "TeamsMeetingMember", "MeetingParticipantsEventDetails", + "MeetingNotificationBase", + "MeetingNotificationChannelData", + "MeetingNotificationRecipientFailureInfo", + "MeetingNotificationResponse", + "MeetingNotification", + "MeetingStageSurface", + "MeetingTabIconSurface", + "OnBehalfOf", + "SurfaceType", + "Surface", + "TargetedMeetingNotification", + "TargetedMeetingNotificationValue", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/content_type.py b/libraries/botbuilder-schema/botbuilder/schema/teams/content_type.py new file mode 100644 index 000000000..9f5f41e75 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/content_type.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class ContentType(str, Enum): + + UNKNOWN = "unknown" + TASK = "task" diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py new file mode 100644 index 000000000..a2a77091e --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass, field +from typing import Optional, TypeVar, Generic +import json +from .meeting_notification_base import MeetingNotificationBase + +T = TypeVar("T") + + +@dataclass +class MeetingNotification(Generic[T], MeetingNotificationBase): + """ + Specifies Bot meeting notification including meeting notification value. + """ + + value: Optional[T] = field(default=None, metadata={"json": "value"}) + + def to_json(self) -> str: + """ + Converts the MeetingNotification object to JSON. + :return: JSON representation of the MeetingNotification object. + """ + return json.dumps( + self, + default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + sort_keys=True, + indent=4, + ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py new file mode 100644 index 000000000..427be4847 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass, field +from typing import Optional +import json + + +@dataclass +class MeetingNotificationBase: + """ + Specifies Bot meeting notification base including channel data and type. + """ + + type: Optional[str] = field(default=None) + + def to_json(self) -> str: + """ + Converts the MeetingNotificationBase object to JSON. + :return: JSON representation of the MeetingNotificationBase object. + """ + return json.dumps( + self, + default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + sort_keys=True, + indent=4, + ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py new file mode 100644 index 000000000..0da4c3697 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass, field +from typing import List +from .on_behalf_of import OnBehalfOf + + +@dataclass +class MeetingNotificationChannelData: + """ + Specify Teams Bot meeting notification channel data. + """ + + on_behalf_of_list: List[OnBehalfOf] = field( + default_factory=list, metadata={"json": "OnBehalfOf"} + ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py new file mode 100644 index 000000000..0fe95d5f3 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py @@ -0,0 +1,27 @@ +import json +from dataclasses import dataclass + + +@dataclass +class MeetingNotificationRecipientFailureInfo: + recipient_mri: str + error_code: str + failure_reason: str + + def to_json(self): + return json.dumps( + { + "recipientMri": self.recipient_mri, + "errorcode": self.error_code, + "failureReason": self.failure_reason, + } + ) + + @staticmethod + def from_json(json_str): + data = json.loads(json_str) + return MeetingNotificationRecipientFailureInfo( + recipient_mri=data.get("recipientMri"), + error_code=data.get("errorcode"), + failure_reason=data.get("failureReason"), + ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_response.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_response.py new file mode 100644 index 000000000..7fced9050 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_response.py @@ -0,0 +1,21 @@ +import json +from typing import List + +from botbuilder.schema.teams.meeting_notification_recipient_failure_info import ( + MeetingNotificationRecipientFailureInfo, +) + + +class MeetingNotificationResponse: + def __init__(self): + self.recipients_failure_info: List[MeetingNotificationRecipientFailureInfo] = [] + + @property + def recipients_failure_info(self) -> List[MeetingNotificationRecipientFailureInfo]: + return self._recipients_failure_info + + @recipients_failure_info.setter + def recipients_failure_info( + self, value: List[MeetingNotificationRecipientFailureInfo] + ): + self._recipients_failure_info = value diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py new file mode 100644 index 000000000..620af2d8b --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py @@ -0,0 +1,13 @@ +from typing import Generic, TypeVar, Optional +from botbuilder.schema.teams.content_type import ContentType +from botbuilder.schema.teams.surface import Surface +from botbuilder.schema.teams.surface_type import SurfaceType + +T = TypeVar("T") + + +class MeetingStageSurface(Generic[T], Surface): + def __init__(self): + super().__init__(self, SurfaceType.MEETING_STAGE) + self.content_type = ContentType.TASK + self.content: Optional[T] = None diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py new file mode 100644 index 000000000..c7034c7ad --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from .surface import Surface +from .surface_type import SurfaceType + + +@dataclass +class MeetingTabIconSurface(Surface): + """ + Specifies meeting tab icon surface. + """ + + tab_entity_id: str = None + + def __init__(self): + super().__init__(SurfaceType.MEETING_TAB_ICON) + + def to_dict(self): + """ + Converts the MeetingTabIconSurface object to a dictionary. + :return: Dictionary representation of the MeetingTabIconSurface object. + """ + return {"type": self.type.value, "tabEntityId": self.tab_entity_id} diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py b/libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py new file mode 100644 index 000000000..11e18db0b --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass, field +import json + + +@dataclass +class OnBehalfOf: + """ + Specifies attribution for notifications. + """ + + item_id: int = field(default=0, metadata={"json": "itemid"}) + mention_type: str = field(default="person", metadata={"json": "mentionType"}) + mri: str = field(default=None, metadata={"json": "mri"}) + display_name: str = field(default=None, metadata={"json": "displayName"}) + + def to_json(self) -> str: + """ + Converts the OnBehalfOf object to JSON. + + :return: JSON representation of the OnBehalfOf object. + """ + return json.dumps( + self, + default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + sort_keys=True, + indent=4, + ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/surface.py b/libraries/botbuilder-schema/botbuilder/schema/teams/surface.py new file mode 100644 index 000000000..be911d3af --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/surface.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from enum import Enum +from typing import Type +import json +from botbuilder.schema.teams.surface_type import SurfaceType + + +class Surface: + + def __init__(self, type: SurfaceType): + """ + Initializes a new instance of the Surface class. + + :param type: Type of Surface. + """ + self.type = type + + @property + def type(self) -> SurfaceType: + return self._type + + @type.setter + def type(self, value: SurfaceType): + self._type = value + + def to_json(self) -> str: + """ + Converts the Surface object to JSON. + + :return: JSON representation of the Surface object. + """ + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/surface_type.py b/libraries/botbuilder-schema/botbuilder/schema/teams/surface_type.py new file mode 100644 index 000000000..2674285ed --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/surface_type.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from enum import Enum + + +class SurfaceType(str, Enum): + """ + Defines Teams Surface type for use with a Surface object. + """ + + UNKNOWN = "Unknown" + MEETING_STAGE = "MeetingStage" + MEETING_TAB_ICON = "MeetingTabIcon" diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py new file mode 100644 index 000000000..80f25d91e --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass, field +from typing import Optional +import json +from .meeting_notification import MeetingNotification +from .targeted_meeting_notification_value import TargetedMeetingNotificationValue +from .meeting_notification_channel_data import MeetingNotificationChannelData + + +@dataclass +class TargetedMeetingNotification( + MeetingNotification[TargetedMeetingNotificationValue] +): + """ + Specifies Teams targeted meeting notification. + """ + value: Optional[TargetedMeetingNotificationValue] = field( + default=None, metadata={"json": "value"} + ) + channel_data: Optional[MeetingNotificationChannelData] = field( + default=None, metadata={"json": "channelData"} + ) + + def to_json(self) -> str: + """ + Converts the TargetedMeetingNotification object to JSON. + :return: JSON representation of the TargetedMeetingNotification object. + """ + return json.dumps( + self, + default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + sort_keys=True, + indent=4, + ) + \ No newline at end of file diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py new file mode 100644 index 000000000..79dbae416 --- /dev/null +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass, field +from typing import List +import json +from .surface import Surface + + +@dataclass +class TargetedMeetingNotificationValue: + """ + Specifies the targeted meeting notification value, including recipients and surfaces. + """ + + recipients: List[str] = field(default_factory=list, metadata={"json": "recipients"}) + surfaces: List[Surface] = field(default_factory=list, metadata={"json": "surfaces"}) + + def to_json(self) -> str: + """ + Converts the TargetedMeetingNotificationValue object to JSON. + :return: JSON representation of the TargetedMeetingNotificationValue object. + """ + return json.dumps( + self, + default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + sort_keys=True, + indent=4, + ) 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 ff1bdb18c..9d2cb929e 100644 --- a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py @@ -5,9 +5,17 @@ # license information. # -------------------------------------------------------------------------- +import logging from msrest.pipeline import ClientRawResponse from msrest.exceptions import HttpOperationError +from botbuilder.schema.teams.meeting_notification_base import MeetingNotificationBase +from typing import Any, Dict, List, Optional +from botbuilder.schema.teams import MeetingNotificationBase +from botbuilder.schema.teams.meeting_notification_response import ( + MeetingNotificationResponse, +) + from ... import models @@ -148,7 +156,7 @@ def fetch_participant( tenant_id: str, custom_headers=None, raw=False, - **operation_config + **operation_config, ): """Fetches Teams meeting participant details. @@ -266,3 +274,53 @@ def fetch_meeting( return deserialized fetch_participant.metadata = {"url": "/v1/meetings/{meetingId}"} + + @staticmethod + async def send_meeting_notification_message_async( + meeting_id: str, + notification: MeetingNotificationBase, + custom_headers: Optional[Dict[str, List[str]]] = None, + ) -> MeetingNotificationResponse: + if meeting_id is None: + raise ValueError("meeting_id cannot be null") + + # Using static method for trace_activity + invocation_id = TeamsOperations.trace_activity( + "SendMeetingNotification", {"meeting_id": meeting_id} + ) + + # Construct URL + url = f"v1/meetings/{meeting_id}/notification" + + response = await TeamsOperations.get_response_async( + url, + "POST", + invocation_id, + content=notification, + custom_headers=custom_headers, + ) + return response + + @staticmethod + def trace_activity(operation_name: str, content: Any) -> str: + logging.basicConfig(level=logging.DEBUG) + invocation_id = None + tracing_parameters: Dict[str, Any] = {} + for attr in dir(content): + if not attr.startswith("__") and not callable(getattr(content, attr)): + tracing_parameters[attr] = getattr(content, attr) + logging.debug( + "Operation: %s, Parameters: %s", operation_name, tracing_parameters + ) + return invocation_id + + @staticmethod + async def get_response_async( + api_url: str, + http_method: str, + invocation_id: Optional[str] = None, + content: Optional[Any] = None, + custom_headers: Optional[Dict[str, List[str]]] = None, + ) -> None: + should_trace = invocation_id is not None + return None diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations_extensions.py b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations_extensions.py new file mode 100644 index 000000000..655281aa0 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations_extensions.py @@ -0,0 +1,71 @@ +import aiohttp +from typing import Optional +from botbuilder.schema.teams._models_py3 import ( + ConversationList, + MeetingInfo, + TeamDetails, + TeamsMeetingParticipant, +) +from botbuilder.schema.teams.meeting_notification_base import MeetingNotificationBase +from botbuilder.schema.teams.meeting_notification_response import ( + MeetingNotificationResponse, +) +from botframework.connector.teams.operations.teams_operations import TeamsOperations + + +class TeamsOperationsExtensions: + @staticmethod + async def fetch_channel_list_async( + operations: TeamsOperations, team_id: str + ) -> ConversationList: + result = await operations.get_teams_channels(team_id) + return result.body + + @staticmethod + async def fetch_team_details_async( + operations: TeamsOperations, team_id: str + ) -> TeamDetails: + result = await operations.get_team_details(team_id) + return result.body + + @staticmethod + async def fetch_meeting_info_async( + operations: TeamsOperations, meeting_id: str + ) -> MeetingInfo: + if not isinstance(operations, TeamsOperations): + raise ValueError( + "TeamsOperations with GetMeetingInfoWithHttpMessages is required for FetchMeetingInfo." + ) + result = await operations.fetch_meeting(meeting_id) + return result.body + + @staticmethod + async def fetch_participant_async( + operations: TeamsOperations, + meeting_id: str, + participant_id: str, + tenant_id: str, + ) -> TeamsMeetingParticipant: + if not isinstance(operations, TeamsOperations): + raise ValueError( + "TeamsOperations with GetParticipantWithHttpMessages is required for FetchParticipant." + ) + result = await operations.fetch_participant( + meeting_id, participant_id, tenant_id + ) + return result.body + + @staticmethod + async def send_meeting_notification_async( + operations: TeamsOperations, + meeting_id: str, + notification: MeetingNotificationBase, + ) -> MeetingNotificationResponse: + if not isinstance(operations, TeamsOperations): + raise ValueError( + "TeamsOperations with SendMeetingNotificationWithHttpMessages is required for SendMeetingNotification." + ) + result = await operations.send_meeting_notification_message_async( + meeting_id, notification + ) + return result.body From 2cc24ccebaf03a949feb732d00a38b0e22be731c Mon Sep 17 00:00:00 2001 From: Amrit10737726 Date: Tue, 6 Aug 2024 22:04:40 +0530 Subject: [PATCH 2/7] added test cases realated changes --- .../botbuilder/core/teams/teams_info.py | 1 - .../tests/teams/test_teams_info.py | 87 ++++++++++++++----- .../schema/teams/meeting_stage_surface.py | 2 +- .../teams/targeted_meeting_notification.py | 2 +- 4 files changed, 65 insertions(+), 27 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index 032f11998..fc87305e4 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -414,7 +414,6 @@ async def send_meeting_notification_async( turn_context: TurnContext, notification: MeetingNotificationBase, meeting_id: Optional[str] = None, - cancellation_token: Optional[Any] = None, ) -> MeetingNotificationResponse: if meeting_id is None: meeting_id = turn_context.activity.id diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py index 8cccb7ec4..6ed83b547 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_info.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py @@ -2,8 +2,14 @@ # Licensed under the MIT License. from http.server import BaseHTTPRequestHandler +from typing import List, Self +from unittest.mock import AsyncMock, MagicMock, patch +from urllib.parse import urlparse +from aioresponses import aioresponses import aiounittest +from botbuilder.schema.teams.meeting_notification_base import MeetingNotificationBase from botframework.connector import Channels +from botbuilder.schema.teams.content_type import ContentType import json from botbuilder.schema._models_py3 import ErrorResponse @@ -15,6 +21,11 @@ from botbuilder.schema.teams.targeted_meeting_notification_value import ( TargetedMeetingNotificationValue, ) +from botframework.connector.aio._connector_client_async import ConnectorClient +from botframework.connector.auth.microsoft_app_credentials import ( + MicrosoftAppCredentials, +) +import pytest from simple_adapter_with_create_conversation import SimpleAdapterWithCreateConversation from botbuilder.schema.teams.on_behalf_of import OnBehalfOf from botbuilder.schema.teams.meeting_notification_channel_data import ( @@ -31,7 +42,8 @@ ChannelAccount, ConversationAccount, ) -from simple_adapter_with_create_conversation import SimpleAdapterWithCreateConversation + +# from simple_adapter_with_create_conversation import SimpleAdapterWithCreateConversation ACTIVITY = Activity( id="1234", @@ -254,6 +266,38 @@ async def test_get_meeting_info(self): handler = TeamsActivityHandler() await handler.on_turn(turn_context) + @pytest.mark.asyncio + @pytest.mark.parametrize("status_code", ["202", "207", "400", "403"]) + async def test_send_meeting_notification_async( + status_code, + ): # pylint: disable=no-self-argument + base_uri = "https://test.coffee" + + with aioresponses() as m: + # Mock the HTTP response + m.post( + f"{base_uri}/v1/meetings/meeting-id/notification", + status=status_code, + payload={}, + ) + connector_client = ConnectorClient( + credentials=MicrosoftAppCredentials("", ""), base_url=base_uri + ) + + activity = Activity( + type="targetedMeetingNotification", + text="test_send_meeting_notification", + channel_id=Channels.ms_teams, + service_url="https://test.coffee", + from_property=ChannelAccount(id="id-1", name=status_code), + conversation=ConversationAccount(id="conversation-id"), + ) + + turn_context = TurnContext(SimpleAdapterWithCreateConversation(), activity) + turn_context.turn_state[ConnectorClient] = connector_client + handler = TestTeamsActivityHandler() + await handler.on_turn(turn_context) + class TestTeamsActivityHandler(TeamsActivityHandler): async def on_turn(self, turn_context: TurnContext): @@ -261,6 +305,8 @@ async def on_turn(self, turn_context: TurnContext): if turn_context.activity.text == "test_send_message_to_teams_channel": await self.call_send_message_to_teams(turn_context) + elif turn_context.activity.text == "test_send_meeting_notification": + await self.call_send_meeting_notification_async(turn_context) async def call_send_message_to_teams(self, turn_context: TurnContext): msg = MessageFactory.text("call_send_message_to_teams") @@ -272,9 +318,11 @@ async def call_send_message_to_teams(self, turn_context: TurnContext): assert reference[0].activity_id == "new_conversation_id" assert reference[1] == "reference123" - @staticmethod - def get_targeted_meeting_notification(from_user) -> TargetedMeetingNotification: - recipients = [from_user.id] + def get_targeted_meeting_notification( + self, from_user: ChannelAccount + ) -> MeetingNotificationBase: + # Create a list of recipients + recipients: List[str] = [from_user.id] if from_user.name == "207": recipients.append("failingid") @@ -283,28 +331,25 @@ def get_targeted_meeting_notification(from_user) -> TargetedMeetingNotification: surface.content = TaskModuleContinueResponse( value=TaskModuleTaskInfo(title="title here", height=3, width=2) ) - surface.content_type = "Task" + surface.content_type = ContentType.TASK value = TargetedMeetingNotificationValue( recipients=recipients, surfaces=[surface] ) obo = OnBehalfOf(display_name=from_user.name, mri=from_user.id) + channel_data = MeetingNotificationChannelData(on_behalf_of_list=[obo]) - return TargetedMeetingNotification( - value=value, - channel_data=channel_data - ) + return TargetedMeetingNotification(value=value, channel_data=channel_data) - @staticmethod - async def call_send_meeting_notification_async(turn_context: TurnContext): + async def call_send_meeting_notification_async(self, turn_context: TurnContext): from_user = turn_context.activity.from_property try: failed_participants = await TeamsInfo.send_meeting_notification_async( turn_context, - TestTeamsActivityHandler.get_targeted_meeting_notification(from_user), + self.get_targeted_meeting_notification(from_user), "meeting-id", ) @@ -320,18 +365,12 @@ async def call_send_meeting_notification_async(turn_context: TurnContext): f"Expected HttpOperationException with response status code {from_user.name}" ) - except Exception as ex: - assert from_user.name == str(ex.response.status_code) - error_response = ErrorResponse.from_json(ex.response.content) - - if from_user.name == "400": - assert "BadSyntax" == error_response.error.code - elif from_user.name == "403": - assert "BotNotInConversationRoster" == error_response.error.code - else: - raise ValueError( - f"Expected HttpOperationException with response status code {from_user.name}" - ) + except ValueError as ve: + # Handle specific ValueError for invalid meeting ID + assert ( + str(ve) + == "This method is only valid within the scope of a MS Teams Meeting." + ) class RosterHttpMessageHandler(BaseHTTPRequestHandler): diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py index 620af2d8b..2a62f99b0 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_stage_surface.py @@ -8,6 +8,6 @@ class MeetingStageSurface(Generic[T], Surface): def __init__(self): - super().__init__(self, SurfaceType.MEETING_STAGE) + super().__init__(SurfaceType.MEETING_STAGE) self.content_type = ContentType.TASK self.content: Optional[T] = None diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py index 80f25d91e..03a9920d1 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py @@ -13,6 +13,7 @@ class TargetedMeetingNotification( """ Specifies Teams targeted meeting notification. """ + value: Optional[TargetedMeetingNotificationValue] = field( default=None, metadata={"json": "value"} ) @@ -31,4 +32,3 @@ def to_json(self) -> str: sort_keys=True, indent=4, ) - \ No newline at end of file From 4fce280752c27e5788f9314cfcfefbd75100e18f Mon Sep 17 00:00:00 2001 From: Amrit10737726 Date: Tue, 6 Aug 2024 23:38:19 +0530 Subject: [PATCH 3/7] revert the changes from single to list --- .../botbuilder/core/teams/teams_activity_handler.py | 8 ++++---- .../tests/teams/test_teams_activity_handler.py | 4 ++-- 2 files changed, 6 insertions(+), 6 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 aa568752d..33b4e419c 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -709,7 +709,7 @@ async def on_teams_team_unarchived( # pylint: disable=unused-argument async def on_teams_members_added_dispatch( # pylint: disable=unused-argument self, - members_added: ChannelAccount, + members_added: [ChannelAccount], team_info: TeamInfo, turn_context: TurnContext, ): @@ -762,7 +762,7 @@ async def on_teams_members_added_dispatch( # pylint: disable=unused-argument async def on_teams_members_added( # pylint: disable=unused-argument self, - teams_members_added: TeamsChannelAccount, + teams_members_added: [TeamsChannelAccount], team_info: TeamInfo, turn_context: TurnContext, ): @@ -785,7 +785,7 @@ async def on_teams_members_added( # pylint: disable=unused-argument async def on_teams_members_removed_dispatch( # pylint: disable=unused-argument self, - members_removed: ChannelAccount, + members_removed: [ChannelAccount], team_info: TeamInfo, turn_context: TurnContext, ): @@ -816,7 +816,7 @@ async def on_teams_members_removed_dispatch( # pylint: disable=unused-argument async def on_teams_members_removed( # pylint: disable=unused-argument self, - teams_members_removed: TeamsChannelAccount, + teams_members_removed: [TeamsChannelAccount], team_info: TeamInfo, turn_context: TurnContext, ): 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 a1e68865c..390df6191 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -50,7 +50,7 @@ async def on_conversation_update_activity(self, turn_context: TurnContext): async def on_teams_members_added( # pylint: disable=unused-argument self, - teams_members_added: TeamsChannelAccount, + teams_members_added: [TeamsChannelAccount], team_info: TeamInfo, turn_context: TurnContext, ): @@ -61,7 +61,7 @@ async def on_teams_members_added( # pylint: disable=unused-argument async def on_teams_members_removed( self, - teams_members_removed: TeamsChannelAccount, + teams_members_removed: [TeamsChannelAccount], team_info: TeamInfo, turn_context: TurnContext, ): From 8e59ae210790fedfdb9819509c7b9f56fd494ef1 Mon Sep 17 00:00:00 2001 From: Amrit10737726 Date: Wed, 7 Aug 2024 17:16:48 +0530 Subject: [PATCH 4/7] replace @dataclass with _init_ and related changes --- .../botbuilder/core/teams/teams_info.py | 12 +++------ .../tests/teams/test_teams_info.py | 2 -- .../schema/teams/meeting_notification.py | 15 ++++++----- .../schema/teams/meeting_notification_base.py | 13 +++++----- .../meeting_notification_channel_data.py | 24 ++++++++++++----- ...ing_notification_recipient_failure_info.py | 2 -- .../schema/teams/meeting_tab_icon_surface.py | 2 -- .../botbuilder/schema/teams/on_behalf_of.py | 26 +++++++++++++------ .../teams/targeted_meeting_notification.py | 25 +++++++++++------- .../targeted_meeting_notification_value.py | 19 +++++++++----- 10 files changed, 83 insertions(+), 57 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index fc87305e4..c1264c660 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -425,12 +425,8 @@ async def send_meeting_notification_async( if not notification: raise Exception(f"{notification} is required.") - teams_client = TeamsInfo.get_teams_connector_client(turn_context) + teams_client = await TeamsInfo.get_teams_connector_client(turn_context) - try: - return await TeamsOperationsExtensions.send_meeting_notification_async( - meeting_id, notification, notification - ) - - finally: - await teams_client.close() + return await teams_client.teams.send_meeting_notification_async( + meeting_id, notification, notification + ) diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py index 6ed83b547..2bf50286c 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_info.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py @@ -43,8 +43,6 @@ ConversationAccount, ) -# from simple_adapter_with_create_conversation import SimpleAdapterWithCreateConversation - ACTIVITY = Activity( id="1234", type="message", diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py index a2a77091e..f750d4ad3 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass, field from typing import Optional, TypeVar, Generic import json from .meeting_notification_base import MeetingNotificationBase @@ -6,22 +5,26 @@ T = TypeVar("T") -@dataclass class MeetingNotification(Generic[T], MeetingNotificationBase): """ Specifies Bot meeting notification including meeting notification value. """ - value: Optional[T] = field(default=None, metadata={"json": "value"}) + def __init__(self, value: Optional[T] = None, type: Optional[str] = None): + super().__init__(type=type) + self.value = value def to_json(self) -> str: """ Converts the MeetingNotification object to JSON. :return: JSON representation of the MeetingNotification object. """ + value_dict = self.value.to_dict() if self.value and hasattr(self.value, 'to_dict') else self.value return json.dumps( - self, - default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + { + "type": self.type, + "value": value_dict + }, sort_keys=True, - indent=4, + indent=4 ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py index 427be4847..71c4ff4b4 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py @@ -1,15 +1,14 @@ -from dataclasses import dataclass, field from typing import Optional import json -@dataclass class MeetingNotificationBase: """ Specifies Bot meeting notification base including channel data and type. """ - type: Optional[str] = field(default=None) + def __init__(self, type: Optional[str] = None): + self.type = type def to_json(self) -> str: """ @@ -17,8 +16,10 @@ def to_json(self) -> str: :return: JSON representation of the MeetingNotificationBase object. """ return json.dumps( - self, - default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + { + "type": self.type + }, sort_keys=True, - indent=4, + indent=4 ) + \ No newline at end of file diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py index 0da4c3697..1615b9e7e 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_channel_data.py @@ -1,14 +1,26 @@ -from dataclasses import dataclass, field -from typing import List +import json +from typing import List, Optional from .on_behalf_of import OnBehalfOf -@dataclass class MeetingNotificationChannelData: """ Specify Teams Bot meeting notification channel data. """ - on_behalf_of_list: List[OnBehalfOf] = field( - default_factory=list, metadata={"json": "OnBehalfOf"} - ) + def __init__(self, on_behalf_of_list: Optional[List[OnBehalfOf]] = None): + self.on_behalf_of_list = ( + on_behalf_of_list if on_behalf_of_list is not None else [] + ) + + def to_json(self) -> str: + """ + Converts the MeetingNotificationChannelData object to JSON. + + :return: JSON representation of the MeetingNotificationChannelData object. + """ + return json.dumps( + {"OnBehalfOf": [item.to_dict() for item in self.on_behalf_of_list]}, + sort_keys=True, + indent=4, + ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py index 0fe95d5f3..1d96a25b0 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_recipient_failure_info.py @@ -1,8 +1,6 @@ import json -from dataclasses import dataclass -@dataclass class MeetingNotificationRecipientFailureInfo: recipient_mri: str error_code: str diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py index c7034c7ad..ff67f312c 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_tab_icon_surface.py @@ -1,9 +1,7 @@ -from dataclasses import dataclass from .surface import Surface from .surface_type import SurfaceType -@dataclass class MeetingTabIconSurface(Surface): """ Specifies meeting tab icon surface. diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py b/libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py index 11e18db0b..bb42863ba 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/on_behalf_of.py @@ -1,17 +1,23 @@ -from dataclasses import dataclass, field import json +from typing import Optional -@dataclass class OnBehalfOf: """ Specifies attribution for notifications. """ - item_id: int = field(default=0, metadata={"json": "itemid"}) - mention_type: str = field(default="person", metadata={"json": "mentionType"}) - mri: str = field(default=None, metadata={"json": "mri"}) - display_name: str = field(default=None, metadata={"json": "displayName"}) + def __init__( + self, + item_id: int = 0, + mention_type: str = "person", + mri: Optional[str] = None, + display_name: Optional[str] = None, + ): + self.item_id = item_id + self.mention_type = mention_type + self.mri = mri + self.display_name = display_name def to_json(self) -> str: """ @@ -20,8 +26,12 @@ def to_json(self) -> str: :return: JSON representation of the OnBehalfOf object. """ return json.dumps( - self, - default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + { + "itemid": self.item_id, + "mentionType": self.mention_type, + "mri": self.mri, + "displayName": self.display_name, + }, sort_keys=True, indent=4, ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py index 03a9920d1..92f1422dd 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass, field from typing import Optional import json from .meeting_notification import MeetingNotification @@ -6,7 +5,6 @@ from .meeting_notification_channel_data import MeetingNotificationChannelData -@dataclass class TargetedMeetingNotification( MeetingNotification[TargetedMeetingNotificationValue] ): @@ -14,12 +12,14 @@ class TargetedMeetingNotification( Specifies Teams targeted meeting notification. """ - value: Optional[TargetedMeetingNotificationValue] = field( - default=None, metadata={"json": "value"} - ) - channel_data: Optional[MeetingNotificationChannelData] = field( - default=None, metadata={"json": "channelData"} - ) + def __init__( + self, + value: Optional[TargetedMeetingNotificationValue] = None, + channel_data: Optional[MeetingNotificationChannelData] = None, + type: Optional[str] = None + ): + super().__init__(value=value, type=type) + self.channel_data = channel_data def to_json(self) -> str: """ @@ -27,8 +27,13 @@ def to_json(self) -> str: :return: JSON representation of the TargetedMeetingNotification object. """ return json.dumps( - self, - default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + { + "type": self.type, + "value": self.value.to_dict() if self.value else None, + "channelData": ( + self.channel_data.to_dict() if self.channel_data else None + ), + }, sort_keys=True, indent=4, ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py index 79dbae416..cb0823ab2 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification_value.py @@ -1,17 +1,20 @@ -from dataclasses import dataclass, field -from typing import List +from typing import List, Optional import json from .surface import Surface -@dataclass class TargetedMeetingNotificationValue: """ Specifies the targeted meeting notification value, including recipients and surfaces. """ - recipients: List[str] = field(default_factory=list, metadata={"json": "recipients"}) - surfaces: List[Surface] = field(default_factory=list, metadata={"json": "surfaces"}) + def __init__( + self, + recipients: Optional[List[str]] = None, + surfaces: Optional[List[Surface]] = None, + ): + self.recipients = recipients if recipients is not None else [] + self.surfaces = surfaces if surfaces is not None else [] def to_json(self) -> str: """ @@ -19,8 +22,10 @@ def to_json(self) -> str: :return: JSON representation of the TargetedMeetingNotificationValue object. """ return json.dumps( - self, - default=lambda o: {k: v for k, v in o.__dict__.items() if v is not None}, + { + "recipients": self.recipients, + "surfaces": [surface.to_dict() for surface in self.surfaces], + }, sort_keys=True, indent=4, ) From 49aa631016db304579efabb43b04d8eeafedc8db Mon Sep 17 00:00:00 2001 From: Amrit10737726 <167064182+Amrit10737726@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:27:05 +0530 Subject: [PATCH 5/7] Update teams_info.py --- libraries/botbuilder-core/botbuilder/core/teams/teams_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index c1264c660..0c12a2456 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Any, List, Optional, Tuple +from typing import Any,List,Optional, Tuple from botbuilder.schema.teams.meeting_notification_base import MeetingNotificationBase from botbuilder.schema.teams.meeting_notification_response import ( From 522bb0faeb78471d5d16875f28cb9b5e137cf808 Mon Sep 17 00:00:00 2001 From: Amrit10737726 Date: Wed, 28 Aug 2024 17:59:25 +0530 Subject: [PATCH 6/7] minor chnages --- .../botbuilder-core/botbuilder/core/teams/teams_info.py | 7 +++---- libraries/botbuilder-core/tests/teams/test_teams_info.py | 6 ++++-- .../connector/teams/operations/teams_operations.py | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index 0c12a2456..e2c5481a4 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -413,7 +413,7 @@ async def _get_member( async def send_meeting_notification_async( turn_context: TurnContext, notification: MeetingNotificationBase, - meeting_id: Optional[str] = None, + meeting_id: str = None, ) -> MeetingNotificationResponse: if meeting_id is None: meeting_id = turn_context.activity.id @@ -421,12 +421,11 @@ async def send_meeting_notification_async( raise ValueError( "This method is only valid within the scope of a MS Teams Meeting." ) - if not notification: raise Exception(f"{notification} is required.") teams_client = await TeamsInfo.get_teams_connector_client(turn_context) - return await teams_client.teams.send_meeting_notification_async( - meeting_id, notification, notification + return await teams_client.teams.send_meeting_notification_message_async( + meeting_id, notification ) diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py index 2bf50286c..dc4e98c81 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_info.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from http.server import BaseHTTPRequestHandler +from http.server import BaseHTTPRequestHandler, HTTPServer +import socket from typing import List, Self -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from urllib.parse import urlparse from aioresponses import aioresponses import aiounittest @@ -26,6 +27,7 @@ MicrosoftAppCredentials, ) import pytest +from simple_adapter import SimpleAdapter from simple_adapter_with_create_conversation import SimpleAdapterWithCreateConversation from botbuilder.schema.teams.on_behalf_of import OnBehalfOf from botbuilder.schema.teams.meeting_notification_channel_data import ( 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 9d2cb929e..d9e6f1cd0 100644 --- a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py @@ -279,7 +279,7 @@ def fetch_meeting( async def send_meeting_notification_message_async( meeting_id: str, notification: MeetingNotificationBase, - custom_headers: Optional[Dict[str, List[str]]] = None, + custom_headers = None, ) -> MeetingNotificationResponse: if meeting_id is None: raise ValueError("meeting_id cannot be null") @@ -320,7 +320,7 @@ async def get_response_async( http_method: str, invocation_id: Optional[str] = None, content: Optional[Any] = None, - custom_headers: Optional[Dict[str, List[str]]] = None, + custom_header = None, ) -> None: should_trace = invocation_id is not None return None From 04f6fb42ea587185021b9cdfd551676e2c7be504 Mon Sep 17 00:00:00 2001 From: Ganapathi Diddi Date: Thu, 19 Sep 2024 20:01:10 +0530 Subject: [PATCH 7/7] black issue fix --- .../botbuilder/core/teams/teams_info.py | 2 +- .../botbuilder/schema/teams/meeting_notification.py | 13 ++++++------- .../schema/teams/meeting_notification_base.py | 9 +-------- .../schema/teams/targeted_meeting_notification.py | 2 +- .../connector/teams/operations/teams_operations.py | 4 ++-- 5 files changed, 11 insertions(+), 19 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index e2c5481a4..92cf64888 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Any,List,Optional, Tuple +from typing import Any, List, Optional, Tuple from botbuilder.schema.teams.meeting_notification_base import MeetingNotificationBase from botbuilder.schema.teams.meeting_notification_response import ( diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py index f750d4ad3..dfd000aea 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification.py @@ -19,12 +19,11 @@ def to_json(self) -> str: Converts the MeetingNotification object to JSON. :return: JSON representation of the MeetingNotification object. """ - value_dict = self.value.to_dict() if self.value and hasattr(self.value, 'to_dict') else self.value + value_dict = ( + self.value.to_dict() + if self.value and hasattr(self.value, "to_dict") + else self.value + ) return json.dumps( - { - "type": self.type, - "value": value_dict - }, - sort_keys=True, - indent=4 + {"type": self.type, "value": value_dict}, sort_keys=True, indent=4 ) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py index 71c4ff4b4..ea325ca5c 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/meeting_notification_base.py @@ -15,11 +15,4 @@ def to_json(self) -> str: Converts the MeetingNotificationBase object to JSON. :return: JSON representation of the MeetingNotificationBase object. """ - return json.dumps( - { - "type": self.type - }, - sort_keys=True, - indent=4 - ) - \ No newline at end of file + return json.dumps({"type": self.type}, sort_keys=True, indent=4) diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py index 92f1422dd..280000744 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/targeted_meeting_notification.py @@ -16,7 +16,7 @@ def __init__( self, value: Optional[TargetedMeetingNotificationValue] = None, channel_data: Optional[MeetingNotificationChannelData] = None, - type: Optional[str] = None + type: Optional[str] = None, ): super().__init__(value=value, type=type) self.channel_data = channel_data 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 d9e6f1cd0..a2c7a45a6 100644 --- a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py @@ -279,7 +279,7 @@ def fetch_meeting( async def send_meeting_notification_message_async( meeting_id: str, notification: MeetingNotificationBase, - custom_headers = None, + custom_headers=None, ) -> MeetingNotificationResponse: if meeting_id is None: raise ValueError("meeting_id cannot be null") @@ -320,7 +320,7 @@ async def get_response_async( http_method: str, invocation_id: Optional[str] = None, content: Optional[Any] = None, - custom_header = None, + custom_header=None, ) -> None: should_trace = invocation_id is not None return None