Repository: microsoft/botbuilder-python

Issue:
Teams - meeting start/end events (Python)
See [parent](https://github.com/microsoft/botframework-sdk/issues/6305)


Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py
--- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py
+++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py
@@ -14,6 +14,8 @@
     TeamInfo,
     ChannelInfo,
     FileConsentCardResponse,
+    MeetingStartEventDetails,
+    MeetingEndEventDetails,
     TeamsChannelData,
     TeamsChannelAccount,
     MessagingExtensionAction,
@@ -877,3 +879,64 @@ async def on_teams_channel_restored(  # pylint: disable=unused-argument
         :returns: A task that represents the work queued to execute.
         """
         return
+
+    async def on_event_activity(self, turn_context: TurnContext):
+        """
+        Invoked when an event activity is received from the connector when the base behavior of
+        :meth:`on_turn()` is used.
+
+        :param turn_context: The context object for this turn
+        :type turn_context: :class:`botbuilder.core.TurnContext`
+
+        :returns: A task that represents the work queued to execute
+
+        .. remarks::
+            When the :meth:`on_turn()` method receives an event activity, it calls this method.
+            If the activity name is `tokens/response`, it calls :meth:`on_token_response_event()`;
+            otherwise, it calls :meth:`on_event()`.
+
+            In a derived class, override this method to add logic that applies to all event activities.
+            Add logic to apply before the specific event-handling logic before the call to this base class method.
+            Add logic to apply after the specific event-handling logic after the call to this base class method.
+
+            Event activities communicate programmatic information from a client or channel to a bot.
+            The meaning of an event activity is defined by the event activity name property, which is meaningful within
+            the scope of a channel.
+        """
+        if turn_context.activity.channel_id == Channels.ms_teams:
+            if turn_context.activity.name == "application/vnd.microsoft.meetingStart":
+                return await self.on_teams_meeting_start_event(
+                    turn_context.activity.value, turn_context
+                )
+            if turn_context.activity.name == "application/vnd.microsoft.meetingEnd":
+                return await self.on_teams_meeting_end_event(
+                    turn_context.activity.value, turn_context
+                )
+
+        return await super().on_event_activity(turn_context)
+
+    async def on_teams_meeting_start_event(
+        self, meeting: MeetingStartEventDetails, turn_context: TurnContext
+    ):  # pylint: disable=unused-argument
+        """
+        Override this in a derived class to provide logic for when a Teams meeting start event is received.
+
+        :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_end_event(
+        self, meeting: MeetingEndEventDetails, turn_context: TurnContext
+    ):  # pylint: disable=unused-argument
+        """
+        Override this in a derived class to provide logic for when a Teams meeting end event is received.
+
+        :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-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py
--- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py
+++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py
@@ -11,6 +11,8 @@
 from ._models_py3 import FileUploadInfo
 from ._models_py3 import MeetingDetails
 from ._models_py3 import MeetingInfo
+from ._models_py3 import MeetingStartEventDetails
+from ._models_py3 import MeetingEndEventDetails
 from ._models_py3 import MessageActionsPayload
 from ._models_py3 import MessageActionsPayloadApp
 from ._models_py3 import MessageActionsPayloadAttachment
@@ -87,6 +89,8 @@
     "FileUploadInfo",
     "MeetingDetails",
     "MeetingInfo",
+    "MeetingStartEventDetails",
+    "MeetingEndEventDetails",
     "MessageActionsPayload",
     "MessageActionsPayloadApp",
     "MessageActionsPayloadAttachment",
diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py
--- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py
+++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py
@@ -2372,54 +2372,65 @@ def _custom_init(self):
         return
 
 
-class MeetingDetails(Model):
+class MeetingDetailsBase(Model):
     """Specific details of a Teams meeting.
 
     :param id: The meeting's Id, encoded as a BASE64 string.
     :type id: str
+    :param join_url: The URL used to join the meeting.
+    :type join_url: str
+    :param title: The title of the meeting.
+    :type title: str
+    """
+
+    _attribute_map = {
+        "id": {"key": "uniqueId", "type": "str"},
+        "join_url": {"key": "joinUrl", "type": "str"},
+        "title": {"key": "title", "type": "str"},
+    }
+
+    def __init__(
+        self, *, id: str = None, join_url: str = None, title: str = None, **kwargs
+    ) -> None:
+        super(MeetingDetailsBase, self).__init__(**kwargs)
+        self.id = id
+        self.join_url = join_url
+        self.title = title
+
+
+class MeetingDetails(MeetingDetailsBase):
+    """Specific details of a Teams meeting.
+
     :param ms_graph_resource_id: The MsGraphResourceId, used specifically for MS Graph API calls.
     :type ms_graph_resource_id: str
     :param scheduled_start_time: The meeting's scheduled start time, in UTC.
     :type scheduled_start_time: str
     :param scheduled_end_time: The meeting's scheduled end time, in UTC.
     :type scheduled_end_time: str
-    :param join_url: The URL used to join the meeting.
-    :type join_url: str
-    :param title: The title of the meeting.
-    :type title: str
     :param type: The meeting's type.
     :type type: str
     """
 
     _attribute_map = {
-        "id": {"key": "uniqueId", "type": "str"},
         "ms_graph_resource_id": {"key": "msGraphResourceId", "type": "str"},
         "scheduled_start_time": {"key": "scheduledStartTime", "type": "str"},
         "scheduled_end_time": {"key": "scheduledEndTime", "type": "str"},
-        "join_url": {"key": "joinUrl", "type": "str"},
-        "title": {"key": "title", "type": "str"},
         "type": {"key": "type", "type": "str"},
     }
 
     def __init__(
         self,
         *,
-        id: str = None,
         ms_graph_resource_id: str = None,
         scheduled_start_time: str = None,
         scheduled_end_time: str = None,
-        join_url: str = None,
-        title: str = None,
         type: str = None,
         **kwargs
     ) -> None:
         super(MeetingDetails, self).__init__(**kwargs)
-        self.id = id
         self.ms_graph_resource_id = ms_graph_resource_id
         self.scheduled_start_time = scheduled_start_time
         self.scheduled_end_time = scheduled_end_time
-        self.join_url = join_url
-        self.title = title
         self.type = type
 
 
@@ -2452,3 +2463,45 @@ def __init__(
         self.details = details
         self.conversation = conversation
         self.organizer = organizer
+
+
+class MeetingEventDetails(MeetingDetailsBase):
+    """Base class for Teams meting start and end events.
+
+    :param meeting_type: The meeting's type.
+    :type meeting_type: str
+    """
+
+    _attribute_map = {"meeting_type": {"key": "MeetingType", "type": "str"}}
+
+    def __init__(self, *, meeting_type: str = None, **kwargs):
+        super(MeetingEventDetails, self).__init__(**kwargs)
+        self.meeting_type = meeting_type
+
+
+class MeetingStartEventDetails(MeetingDetailsBase):
+    """Specific details of a Teams meeting start event.
+
+    :param start_time: Timestamp for meeting start, in UTC.
+    :type start_time: str
+    """
+
+    _attribute_map = {"start_time": {"key": "StartTime", "type": "str"}}
+
+    def __init__(self, *, start_time: str = None, **kwargs):
+        super(MeetingStartEventDetails, self).__init__(**kwargs)
+        self.start_time = start_time
+
+
+class MeetingEndEventDetails(MeetingDetailsBase):
+    """Specific details of a Teams meeting end event.
+
+    :param end_time: Timestamp for meeting end, in UTC.
+    :type end_time: str
+    """
+
+    _attribute_map = {"end_time": {"key": "EndTime", "type": "str"}}
+
+    def __init__(self, *, end_time: str = None, **kwargs):
+        super(MeetingEndEventDetails, self).__init__(**kwargs)
+        self.end_time = end_time


Test patch:
diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
--- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
+++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
@@ -19,6 +19,8 @@
     AppBasedLinkQuery,
     ChannelInfo,
     FileConsentCardResponse,
+    MeetingStartEventDetails,
+    MeetingEndEventDetails,
     MessageActionsPayload,
     MessagingExtensionAction,
     MessagingExtensionQuery,
@@ -309,6 +311,26 @@ async def on_teams_tab_submit(
         self.record.append("on_teams_tab_submit")
         return await super().on_teams_tab_submit(turn_context, tab_submit)
 
+    async def on_event_activity(self, turn_context: TurnContext):
+        self.record.append("on_event_activity")
+        return await super().on_event_activity(turn_context)
+
+    async def on_teams_meeting_start_event(
+        self, meeting: MeetingStartEventDetails, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_meeting_start_event")
+        return await super().on_teams_meeting_start_event(
+            turn_context.activity.value, turn_context
+        )
+
+    async def on_teams_meeting_end_event(
+        self, meeting: MeetingEndEventDetails, turn_context: TurnContext
+    ):
+        self.record.append("on_teams_meeting_end_event")
+        return await super().on_teams_meeting_end_event(
+            turn_context.activity.value, turn_context
+        )
+
 
 class NotImplementedAdapter(BotAdapter):
     async def delete_activity(
@@ -1064,3 +1086,37 @@ async def test_typing_activity(self):
 
         assert len(bot.record) == 1
         assert bot.record[0] == "on_typing_activity"
+
+    async def test_on_teams_meeting_start_event(self):
+        activity = Activity(
+            type=ActivityTypes.event,
+            name="application/vnd.microsoft.meetingStart",
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_event_activity"
+        assert bot.record[1] == "on_teams_meeting_start_event"
+
+    async def test_on_teams_meeting_end_event(self):
+        activity = Activity(
+            type=ActivityTypes.event,
+            name="application/vnd.microsoft.meetingEnd",
+            channel_id=Channels.ms_teams,
+        )
+
+        turn_context = TurnContext(SimpleAdapter(), activity)
+
+        # Act
+        bot = TestingTeamsActivityHandler()
+        await bot.on_turn(turn_context)
+
+        assert len(bot.record) == 2
+        assert bot.record[0] == "on_event_activity"
+        assert bot.record[1] == "on_teams_meeting_end_event"