class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin, RootTargetIDMixin, BaseEntity):
    """Represents a comment, linked to another object."""
    participation = "comment"
    url = ""

    _allowed_children = (Image,)
    _default_activity = ActivityType.CREATE class Follow(CreatedAtMixin, TargetIDMixin, BaseEntity):
    """Represents a handle following or unfollowing another handle."""
    following = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._required += ["following"]
        # ID not required for follow
        self._required.remove('id') class Post(RawContentMixin, PublicMixin, CreatedAtMixin, ProviderDisplayNameMixin, BaseEntity):
    """Reflects a post, status message, etc, which will be composed from the message or to the message."""
    location = ""
    url = ""

    _allowed_children = (Image,)
    _default_activity = ActivityType.CREATE class Profile(CreatedAtMixin, OptionalRawContentMixin, PublicMixin, BaseEntity):
    """Represents a profile for a user."""
    atom_url = ""
    email = ""
    gender = ""
    image_urls = None
    location = ""
    name = ""
    nsfw = False
    public_key = ""
    tag_list = None
    url = ""
    username = ""
    inboxes: Dict = None

    _allowed_children = (Image,)

    def __init__(self, *args, **kwargs):
        self.image_urls = {
            "small": "", "medium": "", "large": ""
        }
        self.inboxes = {
            "private": None,
            "public": None,
        }
        self.tag_list = []
        super().__init__(*args, **kwargs)
        # As an exception, a Profile does not require to have an `actor_id`
        self._required.remove('actor_id')

    def validate_email(self):
        if self.email:
            validator = Email()
            if not validator.is_valid(self.email):
                raise ValueError("Email is not valid") class Reaction(ParticipationMixin, CreatedAtMixin, BaseEntity):
    """Represents a reaction to another object, for example a like."""
    participation = "reaction"
    reaction = ""

    _default_activity = ActivityType.CREATE
    _reaction_valid_values = ["like"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._required += ["reaction"]

    def validate_reaction(self):
        """Ensure reaction is of a certain type.

        Mainly for future expansion.
        """
        if self.reaction not in self._reaction_valid_values:
            raise ValueError("reaction should be one of: {valid}".format(
                valid=", ".join(self._reaction_valid_values)
            )) class Retraction(CreatedAtMixin, TargetIDMixin, EntityTypeMixin, BaseEntity):
    """Represents a retraction of content by author."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # ID not required for retraction
        self._required.remove('id') class Share(CreatedAtMixin, TargetIDMixin, EntityTypeMixin, OptionalRawContentMixin, PublicMixin,
            ProviderDisplayNameMixin, BaseEntity):
    """Represents a share of another entity.

    ``entity_type`` defaults to "Post" but can be any base entity class name. It should be the class name of the
    entity that was shared.

    The optional ``raw_content`` can be used for a "quoted share" case where the sharer adds their own note to the
    share.
    """
    entity_type = "Post" class DiasporaComment(DiasporaRelayableMixin, Comment):
    """Diaspora comment."""
    _tag_name = "comment"

    def to_xml(self):
        element = etree.Element(self._tag_name)
        properties = [
            {"guid": self.guid},
            {"parent_guid": self.root_target_guid or self.target_guid},
            {"thread_parent_guid": self.target_guid},
            {"author_signature": self.signature},
            {"parent_author_signature": self.parent_signature},
            {"text": self.raw_content},
            {"author": self.handle},
            {"created_at": format_dt(self.created_at)},
        ]
        if self.id and self.id.startswith("http"):
            properties.append({
                "activitypub_id": self.id,
            })
        struct_to_xml(element, properties)
        return element class DiasporaContact(DiasporaEntityMixin, Follow):
    """Diaspora contact.

    Note we don't implement 'sharing' at the moment so just send it as the same as 'following'.
    """
    _tag_name = "contact"

    def to_xml(self):
        """Convert to XML message."""
        element = etree.Element(self._tag_name)
        struct_to_xml(element, [
            {"author": self.handle},
            {"recipient": self.target_handle},
            {"following": "true" if self.following else "false"},
            {"sharing": "true" if self.following else "false"},
        ])
        return element class DiasporaLike(DiasporaRelayableMixin, Reaction):
    """Diaspora like."""
    _tag_name = "like"
    reaction = "like"

    def to_xml(self):
        """Convert to XML message."""
        element = etree.Element(self._tag_name)
        struct_to_xml(element, [
            {"parent_type": "Post"},
            {"guid": self.guid},
            {"parent_guid": self.target_guid},
            {"author_signature": self.signature},
            {"parent_author_signature": self.parent_signature},
            {"positive": "true"},
            {"author": self.handle},
        ])
        return element class DiasporaPost(DiasporaEntityMixin, Post):
    """Diaspora post, ie status message."""
    _tag_name = "status_message"

    def to_xml(self):
        """Convert to XML message."""
        element = etree.Element(self._tag_name)
        properties = [
            {"text": self.raw_content},
            {"guid": self.guid},
            {"author": self.handle},
            {"public": "true" if self.public else "false"},
            {"created_at": format_dt(self.created_at)},
            {"provider_display_name": self.provider_display_name},
        ]
        if self.id and self.id.startswith("http"):
            properties.append({
                "activitypub_id": self.id,
            })
        struct_to_xml(element, properties)
        return element class DiasporaProfile(DiasporaEntityMixin, Profile):
    """Diaspora profile."""
    _tag_name = "profile"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.inboxes = {
            "private": get_private_endpoint(self.handle, self.guid),
            "public": get_public_endpoint(self.handle),
        }

    def to_xml(self):
        """Convert to XML message."""
        element = etree.Element(self._tag_name)
        properties = [
            {"author": self.handle},
            {"first_name": self.name},
            {"last_name": ""},  # We only have one field - splitting it would be artificial
            {"image_url": self.image_urls["large"]},
            {"image_url_small": self.image_urls["small"]},
            {"image_url_medium": self.image_urls["medium"]},
            {"gender": self.gender},
            {"bio": self.raw_content},
            {"location": self.location},
            {"searchable": "true" if self.public else "false"},
            {"nsfw": "true" if self.nsfw else "false"},
            {"tag_string": " ".join(["#%s" % tag for tag in self.tag_list])},
        ]
        if self.id and self.id.startswith("http"):
            properties.append({
                "activitypub_id": self.id,
            })
        struct_to_xml(element, properties)
        return element class DiasporaReshare(DiasporaEntityMixin, Share):
    """Diaspora Reshare."""
    _tag_name = "reshare"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._required += ["target_guid", "target_handle"]

    @staticmethod
    def fill_extra_attributes(attributes):
        """If `public` is missing, add it as True.

        Diaspora removed this from protocol version 0.2.2 so assume all future reshares are public.
        """
        if attributes.get("public") is None:
            attributes["public"] = True
        return attributes

    def to_xml(self):
        element = etree.Element(self._tag_name)
        struct_to_xml(element, [
            {"author": self.handle},
            {"guid": self.guid},
            {"created_at": format_dt(self.created_at)},
            {"root_author": self.target_handle},
            {"root_guid": self.target_guid},
            {"provider_display_name": self.provider_display_name},
            {"public": "true" if self.public else "false"},
            # Some of our own not in Diaspora protocol
            {"text": self.raw_content},
            {"entity_type": self.entity_type},
        ])
        return element class DiasporaRetraction(DiasporaEntityMixin, Retraction):
    """Diaspora Retraction."""
    _tag_name = "retraction"
    mapped = {
        "Like": "Reaction",
        "Photo": "Image",
        "Person": "Profile",
    }

    def to_xml(self):
        """Convert to XML message."""
        element = etree.Element(self._tag_name)
        struct_to_xml(element, [
            {"author": self.handle},
            {"target_guid": self.target_guid},
            {"target_type": DiasporaRetraction.entity_type_to_remote(self.entity_type)},
        ])
        return element

    @staticmethod
    def entity_type_from_remote(value):
        """Convert entity type between Diaspora names and our Entity names."""
        if value in DiasporaRetraction.mapped:
            return DiasporaRetraction.mapped[value]
        return value

    @staticmethod
    def entity_type_to_remote(value):
        """Convert entity type between our Entity names and Diaspora names."""
        if value in DiasporaRetraction.mapped.values():
            values = list(DiasporaRetraction.mapped.values())
            index = values.index(value)
            return list(DiasporaRetraction.mapped.keys())[index]
        return value class DiasporaImage(DiasporaEntityMixin, Image):
    _tag_name = "photo" class DiasporaRelayableMixin(DiasporaEntityMixin):
    _xml_tags = []
    parent_signature = ""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._required += ["signature"]

    def _validate_signatures(self):
        super()._validate_signatures()
        if not self._sender_key:
            raise SignatureVerificationError("Cannot verify entity signature - no sender key available")
        source_doc = etree.fromstring(self._source_object)
        if not verify_relayable_signature(self._sender_key, source_doc, self.signature):
            raise SignatureVerificationError("Signature verification failed.")

    def sign(self, private_key: RSA) -> None:
        self.signature = create_relayable_signature(private_key, self.to_xml())

    def sign_with_parent(self, private_key):
        if self._source_object:
            doc = etree.fromstring(self._source_object)
        else:
            doc = self.to_xml()
        self.parent_signature = create_relayable_signature(private_key, doc)
        add_element_to_doc(doc, "parent_author_signature", self.parent_signature)
        self.outbound_doc = doc def get_element_child_info(doc, attr):
    """Get information from child elements of this elementas a list since order is important.

    Don't include signature tags.

    :param doc: XML element
    :param attr: Attribute to get from the elements, for example "tag" or "text".
    """
    props = []
    for child in doc:
        if child.tag not in ["author_signature", "parent_author_signature"]:
            props.append(getattr(child, attr))
    return props class UserType:
    id: str = attr.ib()
    private_key: Optional[Union[RsaKey, str]] = attr.ib(default=None)
    receiver_variant: Optional[ReceiverVariant] = attr.ib(default=None)

    # Required only if sending to Diaspora protocol platforms
    handle: Optional[str] = attr.ib(default=None)
    guid: Optional[str] = attr.ib(default=None)

    # Required only if sending to Matrix protocol
    mxid: Optional[str] = attr.ib(default=None)
    # TODO needed?
    variant: Optional[UserVariant] = attr.ib(default=None)

    @property
    def rsa_private_key(self) -> RsaKey:
        if isinstance(self.private_key, str):
            return RSA.importKey(self.private_key)
        return self.private_key class ReceiverVariant(Enum):
    # Indicates this receiver is a single actor
    ACTOR = "actor"
    # Indicates this receiver is the followers of this actor
    FOLLOWERS = "followers"
import logging
from datetime import datetime
from typing import Callable, List
from Crypto.PublicKey.RSA import RsaKey
from lxml import etree
from federation.entities.base import Comment, Follow, Post, Profile, Reaction, Retraction, Share
from federation.entities.diaspora.entities import (
    DiasporaComment, DiasporaContact, DiasporaLike, DiasporaPost,
    DiasporaProfile, DiasporaReshare, DiasporaRetraction,
    DiasporaImage)
from federation.entities.diaspora.mixins import DiasporaRelayableMixin
from federation.entities.mixins import BaseEntity
from federation.protocols.diaspora.signatures import get_element_child_info
from federation.types import UserType, ReceiverVariant
from federation.utils.diaspora import retrieve_and_parse_profile
) -> List:
    """Takes in a message extracted by a protocol and maps it to entities.

    :param message: XML payload
    :type message: str
    :param sender: Payload sender id
    :type message: str
    :param sender_key_fetcher: Function to fetch sender public key. If not given, key will always be fetched
        over network. The function should take sender handle as the only parameter.
    :param user: Optional receiving user object. If given, should have a `handle`.
    :returns: list of entities
    """
    doc = etree.fromstring(message)
    if doc.tag in TAGS:
        return element_to_objects(doc, sender, sender_key_fetcher, user)
    return []


def transform_attributes(attrs, cls):
    """Transform some attribute keys.

    :param attrs: Properties from the XML
    :type attrs: dict
    :param cls: Class of the entity
    :type cls: class
    """
    transformed = {}
    for key, value in attrs.items():
        if value is None:
            value = ""
        if key == "text":
            transformed["raw_content"] = value
        elif key == "activitypub_id":
            transformed["id"] = value
        elif key == "author":
            if cls == DiasporaProfile:
                # Diaspora Profile XML message contains no GUID. We need the guid. Fetch it.
                profile = retrieve_and_parse_profile(value)
                transformed['id'] = value
                transformed["guid"] = profile.guid
            else:
                transformed["actor_id"] = value
            transformed["handle"] = value
        elif key == 'guid':
            if cls != DiasporaProfile:
                transformed["id"] = value
                transformed["guid"] = value
        elif key in ("root_author", "recipient"):
            transformed["target_id"] = value
            transformed["target_handle"] = value
        elif key in ("target_guid", "root_guid", "parent_guid"):
            transformed["target_id"] = value
            transformed["target_guid"] = value
        elif key == "thread_parent_guid":
            transformed["root_target_id"] = value
            transformed["root_target_guid"] = value
        elif key in ("first_name", "last_name"):
            values = [attrs.get('first_name'), attrs.get('last_name')]
            values = [v for v in values if v]
            transformed["name"] = " ".join(values)
        elif key == "image_url":
            if "image_urls" not in transformed:
                transformed["image_urls"] = {}
            transformed["image_urls"]["large"] = value
        elif key == "image_url_small":
            if "image_urls" not in transformed:
                transformed["image_urls"] = {}
            transformed["image_urls"]["small"] = value
        elif key == "image_url_medium":
            if "image_urls" not in transformed:
                transformed["image_urls"] = {}
            transformed["image_urls"]["medium"] = value
        elif key == "tag_string":
            if value:
                transformed["tag_list"] = value.replace("#", "").split(" ")
        elif key == "bio":
            transformed["raw_content"] = value
        elif key == "searchable":
            transformed["public"] = True if value == "true" else False
        elif key in ["target_type"] and cls == DiasporaRetraction:
            transformed["entity_type"] = DiasporaRetraction.entity_type_from_remote(value)
        elif key == "remote_photo_path":
            transformed["url"] = f"{value}{attrs.get('remote_photo_name')}"
        elif key == "author_signature":
            transformed["signature"] = value
        elif key in BOOLEAN_KEYS:
            transformed[key] = True if value == "true" else False
        elif key in DATETIME_KEYS:
            transformed[key] = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
        elif key in INTEGER_KEYS:
            transformed[key] = int(value)
        else:
            transformed[key] = value
    return transformed


def get_outbound_entity(entity: BaseEntity, private_key: RsaKey):
    """Get the correct outbound entity for this protocol.

    We might have to look at entity values to decide the correct outbound entity.
    If we cannot find one, we should raise as conversion cannot be guaranteed to the given protocol.

    Private key of author is needed to be passed for signing the outbound entity.

    :arg entity: An entity instance which can be of a base or protocol entity class.
    :arg private_key: Private key of sender as an RSA object
    :returns: Protocol specific entity class instance.
    :raises ValueError: If conversion cannot be done.
    """
    if getattr(entity, "outbound_doc", None):
        # If the entity already has an outbound doc, just return the entity as is
        return entity
    outbound = None
    cls = entity.__class__
    if cls in [DiasporaPost, DiasporaImage, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
               DiasporaContact, DiasporaReshare]:
        # Already fine
        outbound = entity
    elif cls == Post:
        outbound = DiasporaPost.from_base(entity)

    elif cls == Comment: