CONTEXTS_DEFAULT = [
    CONTEXT_ACTIVITYSTREAMS,
    CONTEXT_PYTHON_FEDERATION,
] CONTEXT_MANUALLY_APPROVES_FOLLOWERS = {"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"} CONTEXT_SENSITIVE = {"sensitive": "as:sensitive"} CONTEXT_HASHTAG = {"Hashtag": "as:Hashtag"} CONTEXT_LD_SIGNATURES = "https://w3id.org/security/v1" CONTEXT_DIASPORA = {"diaspora": "https://diasporafoundation.org/ns/"} 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 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 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 Accept(CreatedAtMixin, TargetIDMixin, BaseEntity):
    """An acceptance message for some target."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # ID not required for accept
        self._required.remove('id') 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 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 Image(OptionalRawContentMixin, CreatedAtMixin, BaseEntity):
    """Reflects a single image, possibly linked to another object."""
    url: str = ""
    name: str = ""
    height: int = 0
    width: int = 0
    inline: bool = False
    media_type: str = ""

    _default_activity = ActivityType.CREATE
    _valid_media_types: Tuple[str] = (
        "image/jpeg",
        "image/png",
        "image/gif",
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._required += ["url"]
        self._required.remove("id")
        self._required.remove("actor_id")
        if self.url and not self.media_type:
            self.media_type = self.get_media_type()

    def get_media_type(self) -> str:
        media_type = fetch_content_type(self.url)
        if media_type in self._valid_media_types:
            return media_type
        return "" def get_base_attributes(entity):
    """Build a dict of attributes of an entity.

    Returns attributes and their values, ignoring any properties, functions and anything that starts
    with an underscore.
    """
    attributes = {}
    cls = entity.__class__
    for attr, _ in inspect.getmembers(cls, lambda o: not isinstance(o, property) and not inspect.isroutine(o)):
        if not attr.startswith("_"):
            attributes[attr] = getattr(entity, attr)
    return attributes def handle_send(
        entity: BaseEntity,
        author_user: UserType,
        recipients: List[Dict],
        parent_user: UserType = None,
        payload_logger: callable = None,
) -> None:
    """Send an entity to remote servers.

    Using this we will build a list of payloads per protocol. After that, each recipient will get the generated
    protocol payload delivered. Delivery to the same endpoint will only be done once so it's ok to include
    the same endpoint as a receiver multiple times.

    Any given user arguments must have ``private_key`` and ``fid`` attributes.

    :arg entity: Entity object to send. Can be a base entity or a protocol specific one.
    :arg author_user: User authoring the object.
    :arg recipients: A list of recipients to delivery to. Each recipient is a dict
                     containing at minimum the "endpoint", "fid", "public" and "protocol" keys.

                     For ActivityPub and Diaspora payloads, "endpoint" should be an URL of the endpoint to deliver to.

                     The "fid" can be empty for Diaspora payloads. For ActivityPub it should be the recipient
                     federation ID should the delivery be non-private.

                     The "protocol" should be a protocol name that is known for this recipient.

                     The "public" value should be a boolean to indicate whether the payload should be flagged as a
                     public payload.

                     TODO: support guessing the protocol over networks? Would need caching of results

                     For private deliveries to Diaspora protocol recipients, "public_key" is also required.

                     For example
                     [
                        {
                            "endpoint": "https://domain.tld/receive/users/1234-5678-0123-4567",
                            "fid": "",
                            "protocol": "diaspora",
                            "public": False,
                            "public_key": <RSAPublicKey object> | str,
                        },
                        {
                            "endpoint": "https://domain2.tld/receive/public",
                            "fid": "",
                            "protocol": "diaspora",
                            "public": True,
                        },
                        {
                            "endpoint": "https://domain4.tld/sharedinbox/",
                            "fid": "https://domain4.tld/profiles/jack/",
                            "protocol": "activitypub",
                            "public": True,
                        },
                        {
                            "endpoint": "https://domain4.tld/profiles/jill/inbox",
                            "fid": "https://domain4.tld/profiles/jill",
                            "protocol": "activitypub",
                            "public": False,
                        },
                        {
                            "endpoint": "https://matrix.domain.tld",
                            "fid": "#@user:domain.tld",
                            "protocol": "matrix",
                            "public": True,
                        }
                     ]
    :arg parent_user: (Optional) User object of the parent object, if there is one. This must be given for the
                      Diaspora protocol if a parent object exists, so that a proper ``parent_author_signature`` can
                      be generated. If given, the payload will be sent as this user.
    :arg payload_logger: (Optional) Function to log the payloads with.
    """
    payloads = []
    ready_payloads = {
        "activitypub": {
            "auth": None,
            "headers": {},
            "payload": None,
            "urls": set(),
        },
        "diaspora": {
            "auth": None,
            "headers": {},
            "payload": None,
            "urls": set(),
        },
        "matrix": {
            "auth": None,
            "headers": {},
            "payload": None,
            "urls": set(),
        },
    }
    skip_ready_payload = {
        "activitypub": False,
        "diaspora": False,
        "matrix": False,
    }
    logger.debug('handle_send - length of recipients: %s', len(recipients))
    # Flatten to unique recipients
    # TODO supply a callable that empties "fid" in the case that public=True
    unique_recipients = list(unique_everseen(recipients))
    logger.debug('handle_send - length of unique_recipients: %s', len(unique_recipients))
    logger.debug('handle_send / unique_recipients - %s', unique_recipients)

    matrix_config = None

    # Generate payloads and collect urls
    for recipient in unique_recipients:
        payload = None
        endpoint = recipient["endpoint"]
        fid = recipient["fid"]
        public_key = recipient.get("public_key")
        if isinstance(public_key, str):
            public_key = RSA.importKey(public_key)
        protocol = recipient["protocol"]
        public = recipient["public"]

        if protocol == "activitypub":
            if skip_ready_payload["activitypub"]:
                logger.debug('Skipping activitypub payload as skip_ready_payload set')
                continue
            if entity.__class__.__name__.startswith("Diaspora") or entity.__class__.__name__.startswith("Matrix"):
                # Don't try to do anything with these entities currently
                skip_ready_payload["activitypub"] = True
                logger.debug('Skipping activitypub payload as payload is diaspora or matrix')
                continue
            # noinspection PyBroadException
            try:
                if not ready_payloads[protocol]["payload"]:
                    try:
                        # noinspection PyTypeChecker
                        ready_payloads[protocol]["payload"] = handle_create_payload(
                            entity, author_user, protocol, parent_user=parent_user, payload_logger=payload_logger,
                        )
                    except ValueError as ex:
                        # No point continuing for this protocol
                        skip_ready_payload["activitypub"] = True
                        logger.warning("handle_send - skipping activitypub due to failure to generate payload: %s", ex)
                        continue
                payload = copy.copy(ready_payloads[protocol]["payload"])
                if public:
                    payload["to"] = [NAMESPACE_PUBLIC]
                    payload["cc"] = [fid]
                    if isinstance(payload.get("object"), dict):
                        payload["object"]["to"] = [NAMESPACE_PUBLIC]
                        payload["object"]["cc"] = [fid]
                else:
                    payload["to"] = [fid]
                    if isinstance(payload.get("object"), dict):
                        payload["object"]["to"] = [fid]
                rendered_payload = json.dumps(payload).encode("utf-8")
            except Exception:
                logger.error(
                    "handle_send - failed to generate activitypub payload for %s, %s: %s",
                    fid, endpoint, traceback.format_exc(),
                    extra={
                        "recipient": recipient,
                        "unique_recipients": list(unique_recipients),
                        "payload": payload,
                        "payloads": payloads,
                        "ready_payloads": ready_payloads,
                        "entity": entity,
                        "author": author_user.id,
                        "parent_user": parent_user.id if parent_user else None,
                    }
                )
                continue
            payloads.append({
                "auth": get_http_authentication(author_user.rsa_private_key, f"{author_user.id}#main-key"),
                "headers": {
                    "Content-Type": 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
                },
                "payload": rendered_payload,
                "urls": {endpoint},
            })
        elif protocol == "diaspora":
            if entity.__class__.__name__.startswith("Activitypub") or entity.__class__.__name__.startswith("Matrix"):
                # Don't try to do anything with these entities currently
                skip_ready_payload["diaspora"] = True
                logger.debug('Skipping diaspora payload as payload is activitypub or matrix')
                continue
            if public:
                if skip_ready_payload["diaspora"]:
                    logger.debug('Skipping diaspora payload as skip_ready_payload set')
                    continue
                if public_key:
                    logger.warning("handle_send - Diaspora recipient cannot be public and use encrypted delivery")
                    continue
                if not ready_payloads[protocol]["payload"]:
                    try:
                        # noinspection PyTypeChecker
                        ready_payloads[protocol]["payload"] = handle_create_payload(
                            entity, author_user, protocol, parent_user=parent_user, payload_logger=payload_logger,
                        )
                    except Exception as ex:
                        # No point continuing for this protocol
                        skip_ready_payload["diaspora"] = True
                        logger.warning("handle_send - skipping diaspora due to failure to generate payload: %s", ex)
                        continue
                ready_payloads["diaspora"]["urls"].add(endpoint)
            else:
                if not public_key:
                    logger.warning("handle_send - Diaspora recipient cannot be private without a public key for "
                                   "encrypted delivery")
                    continue
                # Private payload
                try:
                    payload = handle_create_payload(
                        entity, author_user, "diaspora", to_user_key=public_key, parent_user=parent_user,
                        payload_logger=payload_logger,
                    )
                    payload = json.dumps(payload)
                except Exception as ex:
                    logger.error("handle_send - failed to generate private payload for %s: %s", endpoint, ex)
                    continue
                payloads.append({
                    "auth": None,
                    "headers": {
                        "Content-Type": "application/json",
                    },
                    "payload": payload,
                    "urls": {endpoint},
                })
        elif protocol == "matrix":
            if skip_ready_payload["matrix"]:
                logger.debug('Skipping matrix payload as skip_ready_payload set')
                continue
            if entity.__class__.__name__.startswith("Activitypub") or entity.__class__.__name__.startswith("Diaspora"):
                # Don't try to do anything with these entities currently
                skip_ready_payload["matrix"] = True
                logger.debug('Skipping matrix payload as payload is activitypub or diaspora')
                continue
            payload_info = []
            # noinspection PyBroadException
            try:
                try:
                    # For matrix we actually might get multiple payloads and endpoints
                    payload_info = handle_create_payload(
                        entity, author_user, protocol, parent_user=parent_user, payload_logger=payload_logger,
                    )
                except ValueError as ex:
                    # No point continuing for this protocol
                    skip_ready_payload["matrix"] = True
                    logger.warning("handle_send - skipping matrix due to failure to generate payload: %s", ex)
                    continue
                if not matrix_config:
                    matrix_config = get_matrix_configuration()
                for payload in payload_info:
                    rendered_payload = json.dumps(payload["payload"]).encode("utf-8")
                    payloads.append({
                        "auth": None,
                        "headers": {
                            "Authorization": f"Bearer {matrix_config['appservice']['token']}",
                            "Content-Type": "application/json",
                        },
                        "payload": rendered_payload,
                        "urls": {payload["endpoint"]},
                        "method": payload.get("method"),
                    })
            except Exception:
                logger.error(
                    "handle_send - failed to generate matrix payload for %s, %s: %s",
                    fid, endpoint, traceback.format_exc(),
                    extra={
                        "recipient": recipient,
                        "unique_recipients": list(unique_recipients),
                        "payload_info": payload_info,
                        "payloads": payloads,
                        "ready_payloads": ready_payloads,
                        "entity": entity,
                        "author": author_user.id,
                        "parent_user": parent_user.id if parent_user else None,
                    }
                )
                logger.debug('Continuing from matrix payload after error')
                continue

    # Add public diaspora payload
    if ready_payloads["diaspora"]["payload"]:
        payloads.append({
            "auth": None,
            "headers": {
                "Content-Type": "application/magic-envelope+xml",
            },
            "payload": ready_payloads["diaspora"]["payload"],
            "urls": ready_payloads["diaspora"]["urls"],
        })

    logger.debug("handle_send - %s", payloads)

    # Do actual sending
    for payload in payloads:
        for url in payload["urls"]:
            try:
                # TODO send_document and fetch_document need to handle rate limits
                send_document(
                    url,
                    payload["payload"],
                    auth=payload.get("auth"),
                    headers=payload.get("headers"),
                    method=payload.get("method"),
                )
            except Exception as ex:
                logger.error("handle_send - failed to send payload to %s: %s, payload: %s", url, ex, payload["payload"]) 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 def get_configuration():
    """
    Combine defaults with the Django configuration.
    """
    configuration = {
        "get_object_function": None,
        "hcard_path": "/hcard/users/",
        "nodeinfo2_function": None,
        "process_payload_function": None,
        "search_path": None,
        "tags_path": None,
        # TODO remove or default to True once AP support is more ready
        "activitypub": False,
    }
    try:
        configuration.update(settings.FEDERATION)
    except ImproperlyConfigured:
        # Django is not properly configured, return defaults
        return configuration
    if not all([
        "get_private_key_function" in configuration,
        "get_profile_function" in configuration,
        "base_url" in configuration,
    ]):
        raise ImproperlyConfigured("Missing required FEDERATION settings, please check documentation.")
    return configuration def with_slash(url):
    if url.endswith('/'):
        return url
    return f"{url}/" def validate_handle(handle):
    """
    Very basic handle validation as per
    https://diaspora.github.io/diaspora_federation/federation/types.html#diaspora-id
    """
    return re.match(r"[a-z0-9\-_.]+@[^@/]+\.[^@/]+", handle, flags=re.IGNORECASE) is not None
import logging
import uuid
import bleach
from typing import Dict, List
from federation.entities.activitypub.constants import (
    CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_SENSITIVE, CONTEXT_HASHTAG,
    CONTEXT_LD_SIGNATURES, CONTEXT_DIASPORA)
from federation.entities.activitypub.enums import ActorType, ObjectType, ActivityType
from federation.entities.base import Profile, Post, Follow, Accept, Comment, Retraction, Share, Image
from federation.entities.mixins import RawContentMixin, BaseEntity, PublicMixin, CreatedAtMixin
from federation.entities.utils import get_base_attributes
from federation.outbound import handle_send
from federation.types import UserType
from federation.utils.django import get_configuration
from federation.utils.text import with_slash, validate_handle
        from federation.utils.activitypub import retrieve_and_parse_profile  # Circulars
            from federation.utils.django import get_function_from_config
        if self.guid:
            as2["@context"].append(CONTEXT_DIASPORA)
            as2["object"]["diaspora:guid"] = self.guid

        return as2


class ActivitypubComment(ActivitypubNoteMixin, Comment):
    def to_as2(self) -> Dict:
        as2 = super().to_as2()
        as2["object"]["inReplyTo"] = self.target_id
        return as2


class ActivitypubFollow(ActivitypubEntityMixin, Follow):
    _type = ActivityType.FOLLOW.value

    def post_receive(self) -> None:
        """
        Post receive hook - send back follow ack.
        """
        super().post_receive()
        if not self.following:
            return

        try:
        except ImportError:
            logger.warning("ActivitypubFollow.post_receive - Unable to send automatic Accept back, only supported on "
                           "Django currently")
            return
        get_private_key_function = get_function_from_config("get_private_key_function")
        key = get_private_key_function(self.target_id)
        if not key:
            logger.warning("ActivitypubFollow.post_receive - Failed to send automatic Accept back: could not find "
                           "profile to sign it with")
            return
        accept = ActivitypubAccept(
            activity_id=f"{self.target_id}#accept-{uuid.uuid4()}",
            actor_id=self.target_id,
            target_id=self.activity_id,
            object=self.to_as2(),
        )
        # noinspection PyBroadException
        try:
            profile = retrieve_and_parse_profile(self.actor_id)
        except Exception:
            profile = None
        if not profile:
            logger.warning("ActivitypubFollow.post_receive - Failed to fetch remote profile for sending back Accept")
            return
        # noinspection PyBroadException
        try:
            handle_send(
                accept,
                UserType(id=self.target_id, private_key=key),
                recipients=[{
                    "endpoint": profile.inboxes["private"],
                    "fid": self.actor_id,
                    "protocol": "activitypub",
                    "public": False,
                }],
            )
        except Exception:
            logger.exception("ActivitypubFollow.post_receive - Failed to send Accept back")

    def to_as2(self) -> Dict:
        if self.following:
            as2 = {
                "@context": CONTEXTS_DEFAULT,
                "id": self.activity_id,
                "type": self._type,
                "actor": self.actor_id,
                "object": self.target_id,
            }
        else:
            as2 = {
                "@context": CONTEXTS_DEFAULT,
                "id": self.activity_id,
                "type": ActivityType.UNDO.value,
                "actor": self.actor_id,
                "object": {
                    "id": f"{self.actor_id}#follow-{uuid.uuid4()}",
                    "type": self._type,
                    "actor": self.actor_id,
                    "object": self.target_id,
                },
            }
        return as2


class ActivitypubImage(ActivitypubEntityMixin, Image):
    _type = ObjectType.IMAGE.value

    def to_as2(self) -> Dict:
        return {
            "type": self._type,
            "url": self.url,
            "mediaType": self.media_type,
            "name": self.name,
            "pyfed:inlineImage": self.inline,
        }


class ActivitypubPost(ActivitypubNoteMixin, Post):
    pass


class ActivitypubProfile(ActivitypubEntityMixin, Profile):
    _type = ActorType.PERSON.value
    public = True

    def to_as2(self) -> Dict:
        as2 = {
            "@context": CONTEXTS_DEFAULT + [
                CONTEXT_LD_SIGNATURES,
                CONTEXT_MANUALLY_APPROVES_FOLLOWERS,
            ],
            "endpoints": {
                "sharedInbox": self.inboxes["public"],
            },

            "followers": f"{with_slash(self.id)}followers/",