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..4fd6d4ee1 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -28,6 +28,7 @@ TabRequest, TabSubmit, MeetingParticipantsEventDetails, + ReadReceiptInfo, ) from botframework.connector import Channels from ..serializer_helper import deserializer_helper @@ -906,6 +907,10 @@ async def on_event_activity(self, turn_context: TurnContext): the scope of a channel. """ if turn_context.activity.channel_id == Channels.ms_teams: + if turn_context.activity.name == "application/vnd.microsoft.readReceipt": + return await self.on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) if turn_context.activity.name == "application/vnd.microsoft.meetingStart": return await self.on_teams_meeting_start_event( turn_context.activity.value, turn_context @@ -931,6 +936,19 @@ async def on_event_activity(self, turn_context: TurnContext): return await super().on_event_activity(turn_context) + async def on_teams_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when the bot receives a read receipt event. + + :param read_receipt_info: Information regarding the read receipt. i.e. Id of the message last read by the user. + :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_start_event( self, meeting: MeetingStartEventDetails, 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 390df6191..af9bcd4ab 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -33,6 +33,7 @@ TabSubmit, TabContext, MeetingParticipantsEventDetails, + ReadReceiptInfo, ) from botframework.connector import Channels from simple_adapter import SimpleAdapter @@ -318,6 +319,14 @@ 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_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): + self.record.append("on_teams_read_receipt_event") + return await super().on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) + async def on_teams_meeting_start_event( self, meeting: MeetingStartEventDetails, turn_context: TurnContext ): @@ -1141,6 +1150,24 @@ async def test_typing_activity(self): assert len(bot.record) == 1 assert bot.record[0] == "on_typing_activity" + async def test_on_teams_read_receipt_event(self): + activity = Activity( + type=ActivityTypes.event, + name="application/vnd.microsoft.readReceipt", + channel_id=Channels.ms_teams, + value={"lastReadMessageId": "10101010"}, + ) + + 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_read_receipt_event" + async def test_on_teams_meeting_start_event(self): activity = Activity( type=ActivityTypes.event, diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 55901f7a4..0bac60e96 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -80,6 +80,7 @@ from ._models_py3 import UserMeetingDetails from ._models_py3 import TeamsMeetingMember from ._models_py3 import MeetingParticipantsEventDetails +from ._models_py3 import ReadReceiptInfo __all__ = [ "AppBasedLinkQuery", @@ -161,4 +162,5 @@ "UserMeetingDetails", "TeamsMeetingMember", "MeetingParticipantsEventDetails", + "ReadReceiptInfo", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 260442cf8..a507467cc 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -2568,3 +2568,54 @@ class MeetingParticipantsEventDetails(Model): def __init__(self, *, members: List[TeamsMeetingMember] = None, **kwargs) -> None: super(MeetingParticipantsEventDetails, self).__init__(**kwargs) self.members = members + + +class ReadReceiptInfo(Model): + """General information about a read receipt. + + :param last_read_message_id: The id of the last read message. + :type last_read_message_id: str + """ + + _attribute_map = { + "last_read_message_id": {"key": "lastReadMessageId", "type": "str"}, + } + + def __init__(self, *, last_read_message_id: str = None, **kwargs) -> None: + super(ReadReceiptInfo, self).__init__(**kwargs) + self.last_read_message_id = last_read_message_id + + @staticmethod + def is_message_read(compare_message_id, last_read_message_id): + """ + Helper method useful for determining if a message has been read. + This method converts the strings to integers. If the compare_message_id is + less than or equal to the last_read_message_id, then the message has been read. + + :param compare_message_id: The id of the message to compare. + :param last_read_message_id: The id of the last message read by the user. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + if not compare_message_id or not last_read_message_id: + return False + + try: + compare_message_id_long = int(compare_message_id) + last_read_message_id_long = int(last_read_message_id) + except ValueError: + return False + + return compare_message_id_long <= last_read_message_id_long + + def is_message_read_instance(self, compare_message_id): + """ + Helper method useful for determining if a message has been read. + If the compare_message_id is less than or equal to the last_read_message_id, + then the message has been read. + + :param compare_message_id: The id of the message to compare. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + return ReadReceiptInfo.is_message_read( + compare_message_id, self.last_read_message_id + ) diff --git a/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py new file mode 100644 index 000000000..e6aad9bf3 --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ReadReceiptInfo + + +class TestReadReceiptInfo(aiounittest.AsyncTestCase): + def test_read_receipt_info(self): + # Arrange + test_cases = [ + ("1000", "1000", True), + ("1001", "1000", True), + ("1000", "1001", False), + ("1000", None, False), + (None, "1000", False), + ] + + for last_read, compare, is_read in test_cases: + # Act + info = ReadReceiptInfo(last_read_message_id=last_read) + + # Assert + self.assertEqual(info.last_read_message_id, last_read) + self.assertEqual(info.is_message_read_instance(compare), is_read) + self.assertEqual( + ReadReceiptInfo.is_message_read(compare, last_read), is_read + )