Repository: microsoft/botbuilder-python

Issue:
[Parity] ActivityEx is not present in Botbuilder-Python
See [dotnet source](https://github.com/microsoft/botbuilder-js/issues/1753)


Discussion:
This item is assigned to @JuanAr from Southworks. Having problems with Github at the moment.
@JuanAr @mrivera-ms @gabog  Given that Class Exentions are a C# only feature we don't have a direct file equivalent but the functionality is probably in the actual class or a helper. Is there any specif functionality missing?
@axelsrz I believe that in many cases, the pattern for Python would be to add the missing methods to the Activity class.
That's correct @tracyboehrer, we have ActivityEx in dotnet because that was generated from swagger (and it was a partial class) but moving forward we should just start  adding new methods to the main class and drop the partials for the generated ones (we'll get to that at some point in dotnet)

Resolving patch:
diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py
--- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py
+++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py
@@ -8,12 +8,109 @@
 # Changes may cause incorrect behavior and will be lost if the code is
 # regenerated.
 # --------------------------------------------------------------------------
-from datetime import datetime
 
+from botbuilder.schema._connector_client_enums import ActivityTypes
+from datetime import datetime
 from msrest.serialization import Model
 from msrest.exceptions import HttpOperationError
 
 
+class ConversationReference(Model):
+    """An object relating to a particular point in a conversation.
+
+    :param activity_id: (Optional) ID of the activity to refer to
+    :type activity_id: str
+    :param user: (Optional) User participating in this conversation
+    :type user: ~botframework.connector.models.ChannelAccount
+    :param bot: Bot participating in this conversation
+    :type bot: ~botframework.connector.models.ChannelAccount
+    :param conversation: Conversation reference
+    :type conversation: ~botframework.connector.models.ConversationAccount
+    :param channel_id: Channel ID
+    :type channel_id: str
+    :param locale: A locale name for the contents of the text field.
+        The locale name is a combination of an ISO 639 two- or three-letter
+        culture code associated with a language and an ISO 3166 two-letter
+        subculture code associated with a country or region.
+        The locale name can also correspond to a valid BCP-47 language tag.
+    :type locale: str
+    :param service_url: Service endpoint where operations concerning the
+     referenced conversation may be performed
+    :type service_url: str
+    """
+
+    _attribute_map = {
+        "activity_id": {"key": "activityId", "type": "str"},
+        "user": {"key": "user", "type": "ChannelAccount"},
+        "bot": {"key": "bot", "type": "ChannelAccount"},
+        "conversation": {"key": "conversation", "type": "ConversationAccount"},
+        "channel_id": {"key": "channelId", "type": "str"},
+        "locale": {"key": "locale", "type": "str"},
+        "service_url": {"key": "serviceUrl", "type": "str"},
+    }
+
+    def __init__(
+        self,
+        *,
+        activity_id: str = None,
+        user=None,
+        bot=None,
+        conversation=None,
+        channel_id: str = None,
+        locale: str = None,
+        service_url: str = None,
+        **kwargs
+    ) -> None:
+        super(ConversationReference, self).__init__(**kwargs)
+        self.activity_id = activity_id
+        self.user = user
+        self.bot = bot
+        self.conversation = conversation
+        self.channel_id = channel_id
+        self.locale = locale
+        self.service_url = service_url
+
+
+class Mention(Model):
+    """Mention information (entity type: "mention").
+
+    :param mentioned: The mentioned user
+    :type mentioned: ~botframework.connector.models.ChannelAccount
+    :param text: Sub Text which represents the mention (can be null or empty)
+    :type text: str
+    :param type: Type of this entity (RFC 3987 IRI)
+    :type type: str
+    """
+
+    _attribute_map = {
+        "mentioned": {"key": "mentioned", "type": "ChannelAccount"},
+        "text": {"key": "text", "type": "str"},
+        "type": {"key": "type", "type": "str"},
+    }
+
+    def __init__(
+        self, *, mentioned=None, text: str = None, type: str = None, **kwargs
+    ) -> None:
+        super(Mention, self).__init__(**kwargs)
+        self.mentioned = mentioned
+        self.text = text
+        self.type = type
+
+
+class ResourceResponse(Model):
+    """A response containing a resource ID.
+
+    :param id: Id of the resource
+    :type id: str
+    """
+
+    _attribute_map = {"id": {"key": "id", "type": "str"}}
+
+    def __init__(self, *, id: str = None, **kwargs) -> None:
+        super(ResourceResponse, self).__init__(**kwargs)
+        self.id = id
+
+
 class Activity(Model):
     """An Activity is the basic communication type for the Bot Framework 3.0
     protocol.
@@ -288,9 +385,251 @@ def __init__(
         self.semantic_action = semantic_action
         self.caller_id = caller_id
 
+    def apply_conversation_reference(
+        self, reference: ConversationReference, is_incoming: bool = False
+    ):
+        """
+        Updates this activity with the delivery information from an existing ConversationReference
+
+        :param reference: The existing conversation reference.
+        :param is_incoming: Optional, True to treat the activity as an
+        incoming activity, where the bot is the recipient; otherwise, False.
+        Default is False, and the activity will show the bot as the sender.
+
+        :returns: his activity, updated with the delivery information.
+
+        .. remarks::
+            Call GetConversationReference on an incoming
+            activity to get a conversation reference that you can then use to update an
+            outgoing activity with the correct delivery information.
+        """
+        self.channel_id = reference.channel_id
+        self.service_url = reference.service_url
+        self.conversation = reference.conversation
+
+        if reference.locale is not None:
+            self.locale = reference.locale
+
+        if is_incoming:
+            self.from_property = reference.user
+            self.recipient = reference.bot
+
+            if reference.activity_id is not None:
+                self.id = reference.activity_id
+        else:
+            self.from_property = reference.bot
+            self.recipient = reference.user
+
+            if reference.activity_id is not None:
+                self.reply_to_id = reference.activity_id
+
+        return self
+
+    def as_contact_relation_update_activity(self):
+        """
+        Returns this activity as a ContactRelationUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message activity; or None.
+        """
+        return (
+            self if self.__is_activity(ActivityTypes.contact_relation_update) else None
+        )
+
+    def as_conversation_update_activity(self):
+        """
+        Returns this activity as a ConversationUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a conversation update activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.conversation_update) else None
+
+    def as_end_of_conversation_activity(self):
+        """
+        Returns this activity as an EndOfConversationActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an end of conversation activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.end_of_conversation) else None
+
+    def as_event_activity(self):
+        """
+        Returns this activity as an EventActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an event activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.event) else None
+
+    def as_handoff_activity(self):
+        """
+        Returns this activity as a HandoffActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a handoff activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.handoff) else None
+
+    def as_installation_update_activity(self):
+        """
+        Returns this activity as an InstallationUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an installation update activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.installation_update) else None
+
+    def as_invoke_activity(self):
+        """
+        Returns this activity as an InvokeActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as an invoke activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.invoke) else None
+
+    def as_message_activity(self):
+        """
+        Returns this activity as a MessageActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message) else None
+
+    def as_message_delete_activity(self):
+        """
+        Returns this activity as a MessageDeleteActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message delete request; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message_delete) else None
+
+    def as_message_reaction_activity(self):
+        """
+        Returns this activity as a MessageReactionActivity object;
+        or None, if this is not that type of activity.
+
+        :return: This activity as a message reaction activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message_reaction) else None
+
+    def as_message_update_activity(self):
+        """
+        Returns this activity as an MessageUpdateActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a message update request; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.message_update) else None
+
+    def as_suggestion_activity(self):
+        """
+        Returns this activity as a SuggestionActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a suggestion activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.suggestion) else None
+
+    def as_trace_activity(self):
+        """
+        Returns this activity as a TraceActivity object;
+        or None, if this is not that type of activity.
+
+        :returns: This activity as a trace activity; or None.
+        """
+        return self if self.__is_activity(ActivityTypes.trace) else None
+
+    def as_typing_activity(self):
+        """
+        Returns this activity as a TypingActivity object;
+        or null, if this is not that type of activity.
+
+        :returns: This activity as a typing activity; or null.
+        """
+        return self if self.__is_activity(ActivityTypes.typing) else None
+
+    @staticmethod
+    def create_contact_relation_update_activity():
+        """
+        Creates an instance of the :class:`Activity` class as aContactRelationUpdateActivity object.
+
+        :returns: The new contact relation update activity.
+        """
+        return Activity(type=ActivityTypes.contact_relation_update)
+
+    @staticmethod
+    def create_conversation_update_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a ConversationUpdateActivity object.
+
+        :returns: The new conversation update activity.
+        """
+        return Activity(type=ActivityTypes.conversation_update)
+
+    @staticmethod
+    def create_end_of_conversation_activity():
+        """
+        Creates an instance of the :class:`Activity` class as an EndOfConversationActivity object.
+
+        :returns: The new end of conversation activity.
+        """
+        return Activity(type=ActivityTypes.end_of_conversation)
+
+    @staticmethod
+    def create_event_activity():
+        """
+        Creates an instance of the :class:`Activity` class as an EventActivity object.
+
+        :returns: The new event activity.
+        """
+        return Activity(type=ActivityTypes.event)
+
+    @staticmethod
+    def create_handoff_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a HandoffActivity object.
+
+        :returns: The new handoff activity.
+        """
+        return Activity(type=ActivityTypes.handoff)
+
+    @staticmethod
+    def create_invoke_activity():
+        """
+        Creates an instance of the :class:`Activity` class as an InvokeActivity object.
+
+        :returns: The new invoke activity.
+        """
+        return Activity(type=ActivityTypes.invoke)
+
+    @staticmethod
+    def create_message_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a MessageActivity object.
+
+        :returns: The new message activity.
+        """
+        return Activity(type=ActivityTypes.message)
+
     def create_reply(self, text: str = None, locale: str = None):
+        """
+        Creates a new message activity as a response to this activity.
+
+        :param text: The text of the reply.
+        :param locale: The language code for the text.
+
+        :returns: The new message activity.
+
+        .. remarks::
+            The new activity sets up routing information based on this activity.
+        """
         return Activity(
-            type="message",
+            type=ActivityTypes.message,
             timestamp=datetime.utcnow(),
             from_property=ChannelAccount(
                 id=self.recipient.id if self.recipient else None,
@@ -314,6 +653,192 @@ def create_reply(self, text: str = None, locale: str = None):
             entities=[],
         )
 
+    def create_trace(
+        self, name: str, value: object = None, value_type: str = None, label: str = None
+    ):
+        """
+        Creates a new trace activity based on this activity.
+
+        :param name: The name of the trace operation to create.
+        :param value: Optional, the content for this trace operation.
+        :param value_type: Optional, identifier for the format of the value
+        Default is the name of type of the value.
+        :param label: Optional, a descriptive label for this trace operation.
+
+        :returns: The new trace activity.
+        """
+        if not value_type and value:
+            value_type = type(value)
+
+        return Activity(
+            type=ActivityTypes.trace,
+            timestamp=datetime.utcnow(),
+            from_property=ChannelAccount(
+                id=self.recipient.id if self.recipient else None,
+                name=self.recipient.name if self.recipient else None,
+            ),
+            recipient=ChannelAccount(
+                id=self.from_property.id if self.from_property else None,
+                name=self.from_property.name if self.from_property else None,
+            ),
+            reply_to_id=self.id,
+            service_url=self.service_url,
+            channel_id=self.channel_id,
+            conversation=ConversationAccount(
+                is_group=self.conversation.is_group,
+                id=self.conversation.id,
+                name=self.conversation.name,
+            ),
+            name=name,
+            label=label,
+            value_type=value_type,
+            value=value,
+        ).as_trace_activity()
+
+    @staticmethod
+    def create_trace_activity(
+        name: str, value: object = None, value_type: str = None, label: str = None
+    ):
+        """
+        Creates an instance of the :class:`Activity` class as a TraceActivity object.
+
+        :param name: The name of the trace operation to create.
+        :param value: Optional, the content for this trace operation.
+        :param value_type: Optional, identifier for the format of the value.
+        Default is the name of type of the value.
+        :param label: Optional, a descriptive label for this trace operation.
+
+        :returns: The new trace activity.
+        """
+        if not value_type and value:
+            value_type = type(value)
+
+        return Activity(
+            type=ActivityTypes.trace,
+            name=name,
+            label=label,
+            value_type=value_type,
+            value=value,
+        )
+
+    @staticmethod
+    def create_typing_activity():
+        """
+        Creates an instance of the :class:`Activity` class as a TypingActivity object.
+
+        :returns: The new typing activity.
+        """
+        return Activity(type=ActivityTypes.typing)
+
+    def get_conversation_reference(self):
+        """
+        Creates a ConversationReference based on this activity.
+
+        :returns: A conversation reference for the conversation that contains this activity.
+        """
+        return ConversationReference(
+            activity_id=self.id,
+            user=self.from_property,
+            bot=self.recipient,
+            conversation=self.conversation,
+            channel_id=self.channel_id,
+            locale=self.locale,
+            service_url=self.service_url,
+        )
+
+    def get_mentions(self) -> [Mention]:
+        """
+        Resolves the mentions from the entities of this activity.
+
+        :returns: The array of mentions; or an empty array, if none are found.
+
+        .. remarks::
+            This method is defined on the :class:`Activity` class, but is only intended
+            for use with a message activity, where the activity Activity.Type is set to
+            ActivityTypes.Message.
+        """
+        _list = self.entities
+        return [x for x in _list if str(x.type).lower() == "mention"]
+
+    def get_reply_conversation_reference(
+        self, reply: ResourceResponse
+    ) -> ConversationReference:
+        """
+        Create a ConversationReference based on this Activity's Conversation info
+        and the ResourceResponse from sending an activity.
+
+        :param reply: ResourceResponse returned from send_activity.
+
+        :return: A ConversationReference that can be stored and used later to delete or update the activity.
+        """
+        reference = self.get_conversation_reference()
+        reference.activity_id = reply.id
+        return reference
+
+    def has_content(self) -> bool:
+        """
+        Indicates whether this activity has content.
+
+        :returns: True, if this activity has any content to send; otherwise, false.
+
+        .. remarks::
+            This method is defined on the :class:`Activity` class, but is only intended
+            for use with a message activity, where the activity Activity.Type is set to
+            ActivityTypes.Message.
+        """
+        if self.text and self.text.strip():
+            return True
+
+        if self.summary and self.summary.strip():
+            return True
+
+        if self.attachments and len(self.attachments) > 0:
+            return True
+
+        if self.channel_data:
+            return True
+
+        return False
+
+    def is_from_streaming_connection(self) -> bool:
+        """
+        Determine if the Activity was sent via an Http/Https connection or Streaming
+        This can be determined by looking at the service_url property:
+        (1) All channels that send messages via http/https are not streaming
+        (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https.
+
+        :returns: True if the Activity originated from a streaming connection.
+        """
+        if self.service_url:
+            return not self.service_url.lower().startswith("http")
+        return False
+
+    def __is_activity(self, activity_type: str) -> bool:
+        """
+        Indicates whether this activity is of a specified activity type.
+
+        :param activity_type: The activity type to check for.
+        :return: True if this activity is of the specified activity type; otherwise, False.
+        """
+        if self.type is None:
+            return False
+
+        type_attribute = str(self.type).lower()
+        activity_type = str(activity_type).lower()
+
+        result = type_attribute.startswith(activity_type)
+
+        if result:
+            result = len(type_attribute) == len(activity_type)
+
+            if not result:
+                result = (
+                    len(type_attribute) > len(activity_type)
+                    and type_attribute[len(activity_type)] == "/"
+                )
+
+        return result
+
 
 class AnimationCard(Model):
     """An animation card (Ex: gif or short video clip).
@@ -903,62 +1428,6 @@ def __init__(
         self.tenant_id = tenant_id
 
 
-class ConversationReference(Model):
-    """An object relating to a particular point in a conversation.
-
-    :param activity_id: (Optional) ID of the activity to refer to
-    :type activity_id: str
-    :param user: (Optional) User participating in this conversation
-    :type user: ~botframework.connector.models.ChannelAccount
-    :param bot: Bot participating in this conversation
-    :type bot: ~botframework.connector.models.ChannelAccount
-    :param conversation: Conversation reference
-    :type conversation: ~botframework.connector.models.ConversationAccount
-    :param channel_id: Channel ID
-    :type channel_id: str
-    :param locale: A locale name for the contents of the text field.
-        The locale name is a combination of an ISO 639 two- or three-letter
-        culture code associated with a language and an ISO 3166 two-letter
-        subculture code associated with a country or region.
-        The locale name can also correspond to a valid BCP-47 language tag.
-    :type locale: str
-    :param service_url: Service endpoint where operations concerning the
-     referenced conversation may be performed
-    :type service_url: str
-    """
-
-    _attribute_map = {
-        "activity_id": {"key": "activityId", "type": "str"},
-        "user": {"key": "user", "type": "ChannelAccount"},
-        "bot": {"key": "bot", "type": "ChannelAccount"},
-        "conversation": {"key": "conversation", "type": "ConversationAccount"},
-        "channel_id": {"key": "channelId", "type": "str"},
-        "locale": {"key": "locale", "type": "str"},
-        "service_url": {"key": "serviceUrl", "type": "str"},
-    }
-
-    def __init__(
-        self,
-        *,
-        activity_id: str = None,
-        user=None,
-        bot=None,
-        conversation=None,
-        channel_id: str = None,
-        locale: str = None,
-        service_url: str = None,
-        **kwargs
-    ) -> None:
-        super(ConversationReference, self).__init__(**kwargs)
-        self.activity_id = activity_id
-        self.user = user
-        self.bot = bot
-        self.conversation = conversation
-        self.channel_id = channel_id
-        self.locale = locale
-        self.service_url = service_url
-
-
 class ConversationResourceResponse(Model):
     """A response containing a resource.
 
@@ -1349,32 +1818,6 @@ def __init__(self, *, url: str = None, profile: str = None, **kwargs) -> None:
         self.profile = profile
 
 
-class Mention(Model):
-    """Mention information (entity type: "mention").
-
-    :param mentioned: The mentioned user
-    :type mentioned: ~botframework.connector.models.ChannelAccount
-    :param text: Sub Text which represents the mention (can be null or empty)
-    :type text: str
-    :param type: Type of this entity (RFC 3987 IRI)
-    :type type: str
-    """
-
-    _attribute_map = {
-        "mentioned": {"key": "mentioned", "type": "ChannelAccount"},
-        "text": {"key": "text", "type": "str"},
-        "type": {"key": "type", "type": "str"},
-    }
-
-    def __init__(
-        self, *, mentioned=None, text: str = None, type: str = None, **kwargs
-    ) -> None:
-        super(Mention, self).__init__(**kwargs)
-        self.mentioned = mentioned
-        self.text = text
-        self.type = type
-
-
 class MessageReaction(Model):
     """Message reaction object.
 
@@ -1600,20 +2043,6 @@ def __init__(
         self.tap = tap
 
 
-class ResourceResponse(Model):
-    """A response containing a resource ID.
-
-    :param id: Id of the resource
-    :type id: str
-    """
-
-    _attribute_map = {"id": {"key": "id", "type": "str"}}
-
-    def __init__(self, *, id: str = None, **kwargs) -> None:
-        super(ResourceResponse, self).__init__(**kwargs)
-        self.id = id
-
-
 class SemanticAction(Model):
     """Represents a reference to a programmatic action.
 


Test patch:
diff --git a/libraries/botbuilder-schema/tests/test_activity.py b/libraries/botbuilder-schema/tests/test_activity.py
new file mode 100644
--- /dev/null
+++ b/libraries/botbuilder-schema/tests/test_activity.py
@@ -0,0 +1,745 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+from botbuilder.schema import (
+    Activity,
+    ConversationReference,
+    ConversationAccount,
+    ChannelAccount,
+    Entity,
+    ResourceResponse,
+    Attachment,
+)
+from botbuilder.schema._connector_client_enums import ActivityTypes
+
+
+class TestActivity(aiounittest.AsyncTestCase):
+    def test_constructor(self):
+        # Arrange
+        activity = Activity()
+
+        # Assert
+        self.assertIsNotNone(activity)
+        self.assertIsNone(activity.type)
+        self.assertIsNone(activity.id)
+        self.assertIsNone(activity.timestamp)
+        self.assertIsNone(activity.local_timestamp)
+        self.assertIsNone(activity.local_timezone)
+        self.assertIsNone(activity.service_url)
+        self.assertIsNone(activity.channel_id)
+        self.assertIsNone(activity.from_property)
+        self.assertIsNone(activity.conversation)
+        self.assertIsNone(activity.recipient)
+        self.assertIsNone(activity.text_format)
+        self.assertIsNone(activity.attachment_layout)
+        self.assertIsNone(activity.members_added)
+        self.assertIsNone(activity.members_removed)
+        self.assertIsNone(activity.reactions_added)
+        self.assertIsNone(activity.reactions_removed)
+        self.assertIsNone(activity.topic_name)
+        self.assertIsNone(activity.history_disclosed)
+        self.assertIsNone(activity.locale)
+        self.assertIsNone(activity.text)
+        self.assertIsNone(activity.speak)
+        self.assertIsNone(activity.input_hint)
+        self.assertIsNone(activity.summary)
+        self.assertIsNone(activity.suggested_actions)
+        self.assertIsNone(activity.attachments)
+        self.assertIsNone(activity.entities)
+        self.assertIsNone(activity.channel_data)
+        self.assertIsNone(activity.action)
+        self.assertIsNone(activity.reply_to_id)
+        self.assertIsNone(activity.label)
+        self.assertIsNone(activity.value_type)
+        self.assertIsNone(activity.value)
+        self.assertIsNone(activity.name)
+        self.assertIsNone(activity.relates_to)
+        self.assertIsNone(activity.code)
+        self.assertIsNone(activity.expiration)
+        self.assertIsNone(activity.importance)
+        self.assertIsNone(activity.delivery_mode)
+        self.assertIsNone(activity.listen_for)
+        self.assertIsNone(activity.text_highlights)
+        self.assertIsNone(activity.semantic_action)
+        self.assertIsNone(activity.caller_id)
+
+    def test_apply_conversation_reference(self):
+        # Arrange
+        activity = self.__create_activity()
+        conversation_reference = ConversationReference(
+            channel_id="123",
+            service_url="serviceUrl",
+            conversation=ConversationAccount(id="456"),
+            user=ChannelAccount(id="abc"),
+            bot=ChannelAccount(id="def"),
+            activity_id="12345",
+            locale="en-uS",
+        )
+
+        # Act
+        activity.apply_conversation_reference(reference=conversation_reference)
+
+        # Assert
+        self.assertEqual(conversation_reference.channel_id, activity.channel_id)
+        self.assertEqual(conversation_reference.locale, activity.locale)
+        self.assertEqual(conversation_reference.service_url, activity.service_url)
+        self.assertEqual(
+            conversation_reference.conversation.id, activity.conversation.id
+        )
+        self.assertEqual(conversation_reference.bot.id, activity.from_property.id)
+        self.assertEqual(conversation_reference.user.id, activity.recipient.id)
+        self.assertEqual(conversation_reference.activity_id, activity.reply_to_id)
+
+    def test_apply_conversation_reference_with_is_incoming_true(self):
+        # Arrange
+        activity = self.__create_activity()
+        conversation_reference = ConversationReference(
+            channel_id="cr_123",
+            service_url="cr_serviceUrl",
+            conversation=ConversationAccount(id="cr_456"),
+            user=ChannelAccount(id="cr_abc"),
+            bot=ChannelAccount(id="cr_def"),
+            activity_id="cr_12345",
+            locale="en-uS",
+        )
+
+        # Act
+        activity.apply_conversation_reference(
+            reference=conversation_reference, is_incoming=True
+        )
+
+        # Assert
+        self.assertEqual(conversation_reference.channel_id, activity.channel_id)
+        self.assertEqual(conversation_reference.locale, activity.locale)
+        self.assertEqual(conversation_reference.service_url, activity.service_url)
+        self.assertEqual(
+            conversation_reference.conversation.id, activity.conversation.id
+        )
+        self.assertEqual(conversation_reference.user.id, activity.from_property.id)
+        self.assertEqual(conversation_reference.bot.id, activity.recipient.id)
+        self.assertEqual(conversation_reference.activity_id, activity.id)
+
+    def test_as_contact_relation_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.contact_relation_update
+
+        # Act
+        result = activity.as_contact_relation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.contact_relation_update)
+
+    def test_as_contact_relation_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_contact_relation_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_conversation_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.conversation_update
+
+        # Act
+        result = activity.as_conversation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.conversation_update)
+
+    def test_as_conversation_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_conversation_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_end_of_conversation_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.end_of_conversation
+
+        # Act
+        result = activity.as_end_of_conversation_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.end_of_conversation)
+
+    def test_as_end_of_conversation_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_end_of_conversation_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_event_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.event
+
+        # Act
+        result = activity.as_event_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.event)
+
+    def test_as_event_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_event_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_handoff_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.handoff
+
+        # Act
+        result = activity.as_handoff_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.handoff)
+
+    def test_as_handoff_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_handoff_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_installation_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.installation_update
+
+        # Act
+        result = activity.as_installation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.installation_update)
+
+    def test_as_installation_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_installation_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_invoke_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.invoke
+
+        # Act
+        result = activity.as_invoke_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.invoke)
+
+    def test_as_invoke_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_invoke_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message)
+
+    def test_as_message_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.invoke
+
+        # Act
+        result = activity.as_message_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_activity_type_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = None
+
+        # Act
+        result = activity.as_message_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_delete_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message_delete
+
+        # Act
+        result = activity.as_message_delete_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message_delete)
+
+    def test_as_message_delete_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_delete_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_reaction_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message_reaction
+
+        # Act
+        result = activity.as_message_reaction_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message_reaction)
+
+    def test_as_message_reaction_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_reaction_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_message_update_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message_update
+
+        # Act
+        result = activity.as_message_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message_update)
+
+    def test_as_message_update_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_message_update_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_suggestion_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.suggestion
+
+        # Act
+        result = activity.as_suggestion_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.suggestion)
+
+    def test_as_suggestion_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_suggestion_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_trace_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.trace
+
+        # Act
+        result = activity.as_trace_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+
+    def test_as_trace_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_trace_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_as_typing_activity_return_activity(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.typing
+
+        # Act
+        result = activity.as_typing_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.typing)
+
+    def test_as_typing_activity_return_none(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.type = ActivityTypes.message
+
+        # Act
+        result = activity.as_typing_activity()
+
+        # Assert
+        self.assertIsNone(result)
+
+    def test_create_contact_relation_update_activity(self):
+        # Act
+        result = Activity.create_contact_relation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.contact_relation_update)
+
+    def test_create_conversation_update_activity(self):
+        # Act
+        result = Activity.create_conversation_update_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.conversation_update)
+
+    def test_create_end_of_conversation_activity(self):
+        # Act
+        result = Activity.create_end_of_conversation_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.end_of_conversation)
+
+    def test_create_event_activity(self):
+        # Act
+        result = Activity.create_event_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.event)
+
+    def test_create_handoff_activity(self):
+        # Act
+        result = Activity.create_handoff_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.handoff)
+
+    def test_create_invoke_activity(self):
+        # Act
+        result = Activity.create_invoke_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.invoke)
+
+    def test_create_message_activity(self):
+        # Act
+        result = Activity.create_message_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message)
+
+    def test_create_reply(self):
+        # Arrange
+        activity = self.__create_activity()
+        text = "test reply"
+        locale = "en-us"
+
+        # Act
+        result = activity.create_reply(text=text, locale=locale)
+
+        # Assert
+        self.assertEqual(result.text, text)
+        self.assertEqual(result.locale, locale)
+        self.assertEqual(result.type, ActivityTypes.message)
+
+    def test_create_reply_without_arguments(self):
+        # Arrange
+        activity = self.__create_activity()
+
+        # Act
+        result = activity.create_reply()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.message)
+        self.assertEqual(result.text, "")
+        self.assertEqual(result.locale, activity.locale)
+
+    def test_create_trace(self):
+        # Arrange
+        activity = self.__create_activity()
+        name = "test-activity"
+        value_type = "string"
+        value = "test-value"
+        label = "test-label"
+
+        # Act
+        result = activity.create_trace(
+            name=name, value_type=value_type, value=value, label=label
+        )
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+        self.assertEqual(result.name, name)
+        self.assertEqual(result.value_type, value_type)
+        self.assertEqual(result.value, value)
+        self.assertEqual(result.label, label)
+
+    def test_create_trace_activity_no_recipient(self):
+        # Arrange
+        activity = self.__create_activity()
+        activity.recipient = None
+
+        # Act
+        result = activity.create_trace("test")
+
+        # Assert
+        self.assertIsNone(result.from_property.id)
+        self.assertIsNone(result.from_property.name)
+
+    def test_crete_trace_activity_no_value_type(self):
+        # Arrange
+        name = "test-activity"
+        value = "test-value"
+        label = "test-label"
+
+        # Act
+        result = Activity.create_trace_activity(name=name, value=value, label=label)
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+        self.assertEqual(result.value_type, type(value))
+        self.assertEqual(result.label, label)
+
+    def test_create_trace_activity(self):
+        # Arrange
+        name = "test-activity"
+        value_type = "string"
+        value = "test-value"
+        label = "test-label"
+
+        # Act
+        result = Activity.create_trace_activity(
+            name=name, value_type=value_type, value=value, label=label
+        )
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.trace)
+        self.assertEqual(result.name, name)
+        self.assertEqual(result.value_type, value_type)
+        self.assertEqual(result.label, label)
+
+    def test_create_typing_activity(self):
+        # Act
+        result = Activity.create_typing_activity()
+
+        # Assert
+        self.assertEqual(result.type, ActivityTypes.typing)
+
+    def test_get_conversation_reference(self):
+        # Arrange
+        activity = self.__create_activity()
+
+        # Act
+        result = activity.get_conversation_reference()
+
+        # Assert
+        self.assertEqual(activity.id, result.activity_id)
+        self.assertEqual(activity.from_property.id, result.user.id)
+        self.assertEqual(activity.recipient.id, result.bot.id)
+        self.assertEqual(activity.conversation.id, result.conversation.id)
+        self.assertEqual(activity.channel_id, result.channel_id)
+        self.assertEqual(activity.locale, result.locale)
+        self.assertEqual(activity.service_url, result.service_url)
+
+    def test_get_mentions(self):
+        # Arrange
+        mentions = [Entity(type="mention"), Entity(type="reaction")]
+        activity = Activity(entities=mentions)
+
+        # Act
+        result = Activity.get_mentions(activity)
+
+        # Assert
+        self.assertEqual(len(result), 1)
+        self.assertEqual(result[0].type, "mention")
+
+    def test_get_reply_conversation_reference(self):
+        # Arrange
+        activity = self.__create_activity()
+        reply = ResourceResponse(id="1234")
+
+        # Act
+        result = activity.get_reply_conversation_reference(reply=reply)
+
+        # Assert
+        self.assertEqual(reply.id, result.activity_id)
+        self.assertEqual(activity.from_property.id, result.user.id)
+        self.assertEqual(activity.recipient.id, result.bot.id)
+        self.assertEqual(activity.conversation.id, result.conversation.id)
+        self.assertEqual(activity.channel_id, result.channel_id)
+        self.assertEqual(activity.locale, result.locale)
+        self.assertEqual(activity.service_url, result.service_url)
+
+    def test_has_content_empty(self):
+        # Arrange
+        activity_empty = Activity()
+
+        # Act
+        result_empty = activity_empty.has_content()
+
+        # Assert
+        self.assertEqual(result_empty, False)
+
+    def test_has_content_with_text(self):
+        # Arrange
+        activity_with_text = Activity(text="test-text")
+
+        # Act
+        result_with_text = activity_with_text.has_content()
+
+        # Assert
+        self.assertEqual(result_with_text, True)
+
+    def test_has_content_with_summary(self):
+        # Arrange
+        activity_with_summary = Activity(summary="test-summary")
+
+        # Act
+        result_with_summary = activity_with_summary.has_content()
+
+        # Assert
+        self.assertEqual(result_with_summary, True)
+
+    def test_has_content_with_attachment(self):
+        # Arrange
+        activity_with_attachment = Activity(attachments=[Attachment()])
+
+        # Act
+        result_with_attachment = activity_with_attachment.has_content()
+
+        # Assert
+        self.assertEqual(result_with_attachment, True)
+
+    def test_has_content_with_channel_data(self):
+        # Arrange
+        activity_with_channel_data = Activity(channel_data="test-channel-data")
+
+        # Act
+        result_with_channel_data = activity_with_channel_data.has_content()
+
+        # Assert
+        self.assertEqual(result_with_channel_data, True)
+
+    def test_is_from_streaming_connection(self):
+        # Arrange
+        non_streaming = [
+            "http://yayay.com",
+            "https://yayay.com",
+            "HTTP://yayay.com",
+            "HTTPS://yayay.com",
+        ]
+        streaming = [
+            "urn:botframework:WebSocket:wss://beep.com",
+            "urn:botframework:WebSocket:http://beep.com",
+            "URN:botframework:WebSocket:wss://beep.com",
+            "URN:botframework:WebSocket:http://beep.com",
+        ]
+        activity = self.__create_activity()
+        activity.service_url = None
+
+        # Assert
+        self.assertEqual(activity.is_from_streaming_connection(), False)
+
+        for s in non_streaming:
+            activity.service_url = s
+            self.assertEqual(activity.is_from_streaming_connection(), False)
+
+        for s in streaming:
+            activity.service_url = s
+            self.assertEqual(activity.is_from_streaming_connection(), True)
+
+    @staticmethod
+    def __create_activity() -> Activity:
+        account1 = ChannelAccount(
+            id="ChannelAccount_Id_1",
+            name="ChannelAccount_Name_1",
+            aad_object_id="ChannelAccount_aadObjectId_1",
+            role="ChannelAccount_Role_1",
+        )
+
+        account2 = ChannelAccount(
+            id="ChannelAccount_Id_2",
+            name="ChannelAccount_Name_2",
+            aad_object_id="ChannelAccount_aadObjectId_2",
+            role="ChannelAccount_Role_2",
+        )
+
+        conversation_account = ConversationAccount(
+            conversation_type="a",
+            id="123",
+            is_group=True,
+            name="Name",
+            role="ConversationAccount_Role",
+        )
+
+        activity = Activity(
+            id="123",
+            from_property=account1,
+            recipient=account2,
+            conversation=conversation_account,
+            channel_id="ChannelId123",
+            locale="en-uS",
+            service_url="ServiceUrl123",
+        )
+
+        return activity