def send_profile(profile_id, recipients=None): """Handle sending a Profile object out via the federation layer. :param profile_id: Profile.id of profile to send :param recipients: Optional list of recipients, see `federation.outbound.handle_send` parameters """ try: profile = Profile.objects.get(id=profile_id, user__isnull=False) except Profile.DoesNotExist: logger.warning("send_profile - No local profile found with id %s", profile_id) return entity = make_federable_profile(profile) if not entity: logger.warning("send_profile - No entity for %s", profile) return if settings.DEBUG: # Don't send in development mode return if not recipients: recipients = _get_remote_followers(profile, profile.visibility) logger.debug("send_profile - sending to recipients: %s", recipients) handle_send(entity, profile.federable, recipients, payload_logger=get_outbound_payload_logger())
def test_no_error_for_diaspora_entities_on_activitypub_recipients( self, mock_logger, mock_send, diasporacomment): key = get_dummy_private_key() diasporacomment.outbound_doc = diasporacomment.to_xml() recipients = [{ "endpoint": "https://example.com/receive/public", "public": True, "protocol": "diaspora", "fid": "", }, { "endpoint": "https://example.net/inbox", "fid": "https://example.net/foobar", "public": True, "protocol": "activitypub", }] author = UserType( private_key=key, id="*****@*****.**", handle="*****@*****.**", ) handle_send(diasporacomment, author, recipients) # Ensure first call is a public diaspora payload args, kwargs = mock_send.call_args_list[0] assert args[0] == "https://example.com/receive/public" # Should only be one call assert mock_send.call_count == 1 # Ensure no error logged assert mock_logger.call_count == 0
def test_calls_handle_create_payload(self, mock_send, diasporapost): key = get_dummy_private_key() recipients = [ ("diaspora://[email protected]/profile/xyz", key.publickey()), ("diaspora://foo@localhost/profile/abc", None), "diaspora://[email protected]/profile/zzz", "diaspora://[email protected]/profile/qwerty", # Same host twice to ensure one delivery only per host # for public payloads ] mock_author = Mock(private_key=key, handle="*****@*****.**") handle_send(diasporapost, mock_author, recipients) # Ensure first call is a private payload args, kwargs = mock_send.call_args_list[0] assert args[0] == "https://127.0.0.1/receive/users/xyz" assert "aes_key" in args[1] assert "encrypted_magic_envelope" in args[1] assert kwargs['headers'] == {'Content-Type': 'application/json'} # Ensure public payloads and recipients, one per unique host args1, kwargs1 = mock_send.call_args_list[1] args2, kwargs2 = mock_send.call_args_list[2] public_endpoints = {args1[0], args2[0]} assert public_endpoints == { "https://example.net/receive/public", "https://localhost/receive/public" } assert args1[1].startswith("<me:env xmlns:me=") assert args2[1].startswith("<me:env xmlns:me=") assert kwargs1['headers'] == { 'Content-Type': 'application/magic-envelope+xml' } assert kwargs2['headers'] == { 'Content-Type': 'application/magic-envelope+xml' }
def send_reply(content_id): """Handle sending a Content object that is a reply out via the federation layer. Currently we only deliver public content. """ try: content = Content.objects.get(id=content_id, visibility=Visibility.PUBLIC, parent_id__isnull=False) except Content.DoesNotExist: logger.warning("No content found with id %s", content_id) return if not content.is_local: return entity = make_federable_entity(content) if entity: if settings.DEBUG: # Don't send in development mode return recipients = [ (settings.SOCIALHOME_RELAY_DOMAIN, "diaspora"), ] recipients.extend(_get_remote_participants_for_parent(content.parent)) recipients.extend(_get_remote_followers(content.author)) handle_send(entity, content.author, recipients) else: logger.warning("No entity for %s", content)
def forward_relayable(entity, parent_id): """Handle forwarding of a relayable object. Currently only for public content. """ try: parent = Content.objects.get(id=parent_id, visibility=Visibility.PUBLIC) except Content.DoesNotExist: logger.warning("No public content found with id %s", parent_id) return try: content = Content.objects.get(guid=entity.guid, visibility=Visibility.PUBLIC) except Content.DoesNotExist: logger.warning("forward_relayable - No content found with guid %s", entity.guid) return if settings.DEBUG: # Don't send in development mode return recipients = [ (settings.SOCIALHOME_RELAY_DOMAIN, "diaspora"), ] recipients.extend( _get_remote_participants_for_parent(parent, exclude=entity.handle)) recipients.extend( _get_remote_followers(parent.author, exclude=entity.handle)) handle_send(entity, content.author, recipients, parent_user=parent.author)
def send_follow_change(profile_id, followed_id, follow): """Handle sending of a local follow of a remote profile.""" try: profile = Profile.objects.get(id=profile_id, user__isnull=False) except Profile.DoesNotExist: logger.warning( "send_follow_change - No local profile %s found to send follow with", profile_id) return try: remote_profile = Profile.objects.get(id=followed_id, user__isnull=True) except Profile.DoesNotExist: logger.warning( "send_follow_change - No remote profile %s found to send follow for", followed_id) return if settings.DEBUG: # Don't send in development mode return entity = base.Follow( actor_id=profile.fid, target_id=remote_profile.fid, following=follow, handle=profile.handle, target_handle=remote_profile.handle, ) recipients = [ # TODO fid or handle? (remote_profile.handle, remote_profile.key, remote_profile.guid), ] logger.debug("send_follow_change - sending to recipients: %s", recipients) handle_send(entity, profile.federable, recipients) # Also trigger a profile send # TODO fid or handle? send_profile(profile_id, recipients=[remote_profile.handle])
def send_share(content_id): """Handle sending a share of a Content object to the federation layer. Currently we only deliver public shares. """ try: content = Content.objects.get(id=content_id, visibility=Visibility.PUBLIC, content_type=ContentType.SHARE, local=True) except Content.DoesNotExist: logger.warning("No local share found with id %s", content_id) return entity = make_federable_content(content) if entity: if settings.DEBUG: # Don't send in development mode return recipients = _get_remote_followers(content.author) if not content.share_of.local: # Send to original author recipients.append( # TODO fid or handle? content.share_of.author.handle, ) logger.debug("send_share - sending to recipients: %s", recipients) handle_send(entity, content.author.federable, recipients) else: logger.warning("send_share - No entity for %s", content)
def forward_entity(entity, target_content_id): """Handle forwarding of an entity related to a target content. For example: remote replies on local content, remote shares on local content. """ try: target_content = Content.objects.get( id=target_content_id, visibility__in=(Visibility.PUBLIC, Visibility.LIMITED), local=True, ) except Content.DoesNotExist: logger.warning("forward_entity - No local content found with id %s", target_content_id) return try: content = Content.objects.fed(entity.id, visibility__in=(Visibility.PUBLIC, Visibility.LIMITED)).get() except Content.DoesNotExist: logger.warning("forward_entity - No content found with uuid %s", entity.id) return if settings.DEBUG: # Don't send in development mode return if target_content.visibility == Visibility.PUBLIC: recipients = _get_remote_participants_for_content(target_content, exclude=entity.actor_id) recipients.extend(_get_remote_followers( target_content.author, target_content.visibility, exclude=entity.actor_id, )) elif target_content.visibility == Visibility.LIMITED and content.content_type == ContentType.REPLY: recipients = _get_limited_recipients(entity.actor_id, target_content) else: return logger.debug("forward_entity - sending to recipients: %s", recipients) handle_send(entity, content.author.federable, recipients, parent_user=target_content.author.federable)
def send_profile_retraction(profile): """Handle sending of retractions for profiles. Only sent for public and limited profiles. Reason: we might actually leak user information outside for profiles which were never federated outside if we send for example SELF or SITE profile retractions. This must be called as a pre_delete signal or it will fail. """ if profile.visibility not in (Visibility.PUBLIC, Visibility.LIMITED) or not profile.is_local: return entity = make_federable_retraction(profile) if entity: if settings.DEBUG: # Don't send in development mode return if profile.visibility == Visibility.PUBLIC: recipients = [settings.SOCIALHOME_RELAY_ID] else: recipients = [] recipients.extend(_get_remote_followers(profile)) logger.debug("send_profile_retraction - sending to recipients: %s", recipients) handle_send(entity, profile.federable, recipients) else: logger.warning("send_profile_retraction - No retraction entity for %s", profile)
def forward_entity(entity, target_content_id): """Handle forwarding of an entity related to a target content. For example: remote replies on local content, remote shares on local content. Currently only for public content. """ try: target_content = Content.objects.get(id=target_content_id, visibility=Visibility.PUBLIC, local=True) except Content.DoesNotExist: logger.warning( "forward_entity - No public local content found with id %s", target_content_id) return try: content = Content.objects.get(guid=entity.guid, visibility=Visibility.PUBLIC) except Content.DoesNotExist: logger.warning("forward_entity - No content found with guid %s", entity.guid) return if settings.DEBUG: # Don't send in development mode return recipients = _get_remote_participants_for_content(target_content, exclude=entity.handle) recipients.extend( _get_remote_followers(target_content.author, exclude=entity.handle)) handle_send(entity, content.author, recipients, parent_user=target_content.author)
def send_content(content_id): """Handle sending a Content object out via the federation layer. Currently we only deliver public content. """ try: content = Content.objects.get(id=content_id, visibility=Visibility.PUBLIC, content_type=ContentType.CONTENT, local=True) except Content.DoesNotExist: logger.warning("No local content found with id %s", content_id) return entity = make_federable_content(content) if entity: if settings.DEBUG: # Don't send in development mode return recipients = [ (settings.SOCIALHOME_RELAY_DOMAIN, "diaspora"), ] recipients.extend(_get_remote_followers(content.author)) handle_send(entity, content.author, recipients) else: logger.warning("send_content - No entity for %s", content)
def send_follow_change(profile_id, followed_id, follow): """Handle sending of a local follow of a remote profile.""" try: profile = Profile.objects.get(id=profile_id, user__isnull=False) except Profile.DoesNotExist: logger.warning("send_follow_change - No local profile %s found to send follow with", profile_id) return try: remote_profile = Profile.objects.get(id=followed_id, user__isnull=True) except Profile.DoesNotExist: logger.warning("send_follow_change - No remote profile %s found to send follow for", followed_id) return if settings.DEBUG: # Don't send in development mode return entity = base.Follow( activity_id=f'{profile.fid}#follow-{uuid4()}', actor_id=profile.fid, target_id=remote_profile.fid, following=follow, handle=profile.handle, target_handle=remote_profile.handle, ) # Explicitly use limited visibility to force private endpoint recipients = [remote_profile.get_recipient_for_visibility(Visibility.LIMITED)] logger.debug("send_follow_change - sending to recipients: %s", recipients) handle_send(entity, profile.federable, recipients) # Also trigger a profile send send_profile(profile_id, recipients=recipients)
def send_reply(content_id): """Handle sending a Content object that is a reply out via the federation layer. Currently we only deliver public content. """ try: content = Content.objects.get(id=content_id, visibility=Visibility.PUBLIC, content_type=ContentType.REPLY, local=True) except Content.DoesNotExist: logger.warning("No content found with id %s", content_id) return entity = make_federable_content(content) if not entity: logger.warning("send_reply - No entity for %s", content) if settings.DEBUG: # Don't send in development mode return # Send directly (remote parent) or as a relayable (local parent) if content.parent.local: forward_entity(entity, content.parent.id) else: # We only need to send to the original author recipients = [ (content.parent.author.handle, None), ] handle_send(entity, content.author, recipients)
def send_profile(profile_id, recipients=None): """Handle sending a Profile object out via the federation layer. :param profile_id: Profile.id of profile to send :param recipients: Optional list of recipients, see `federation.outbound.handle_send` parameters """ try: profile = Profile.objects.get(id=profile_id, user__isnull=False) except Profile.DoesNotExist: logger.warning("send_profile - No local profile found with id %s", profile_id) return entity = make_federable_profile(profile) if not entity: logger.warning("send_profile - No entity for %s", profile) return if settings.DEBUG: # Don't send in development mode return if not recipients: # If we have Matrix support enabled, also add the appservice if settings.SOCIALHOME_MATRIX_ENABLED: recipients = [profile.get_recipient_for_matrix_appservice()] else: recipients = [] recipients.extend(_get_remote_followers(profile, profile.visibility)) logger.debug("send_profile - sending to recipients: %s", recipients) handle_send(entity, profile.federable, recipients, payload_logger=get_outbound_payload_logger())
def send_content_retraction(content, author_id): """ Handle sending of retractions for content. """ if content.visibility not in (Visibility.PUBLIC, Visibility.LIMITED) or not content.local: return author = Profile.objects.get(id=author_id) entity = make_federable_retraction(content, author) if entity: if settings.DEBUG: # Don't send in development mode return if content.visibility == Visibility.PUBLIC: recipients = [settings.SOCIALHOME_RELAY_ID] recipients.extend(_get_remote_followers(author)) else: recipients = _get_limited_recipients(author.uuid, content) logger.debug("send_content_retraction - sending to recipients: %s", recipients) handle_send(entity, author.federable, recipients) else: logger.warning("send_content_retraction - No retraction entity for %s", content)
def send_content(content_id, activity_fid, recipient_id=None): """ Handle sending a Content object out via the federation layer. """ try: content = Content.objects.get( id=content_id, visibility__in=(Visibility.PUBLIC, Visibility.LIMITED), content_type=ContentType.CONTENT, local=True, ) except Content.DoesNotExist: logger.warning("No local content found with id %s", content_id) return if recipient_id: try: recipient = Profile.objects.get(id=recipient_id, user__isnull=True) except Profile.DoesNotExist: logger.warning("No remote recipient found with id %s", recipient_id) return else: recipient = None entity = make_federable_content(content) if entity: entity.activity_id = activity_fid if settings.DEBUG: # Don't send in development mode return recipients = [] if recipient: recipients.append( recipient.get_recipient_for_visibility(content.visibility)) else: if content.visibility == Visibility.PUBLIC: # If we have Matrix support enabled, also add the appservice if settings.SOCIALHOME_MATRIX_ENABLED: recipients.append( content.author.get_recipient_for_matrix_appservice()) if settings.SOCIALHOME_RELAY_ID: recipients.append({ "endpoint": settings.SOCIALHOME_RELAY_ID, "fid": "", "public": True, "protocol": "diaspora" }) recipients.extend( _get_remote_followers(content.author, content.visibility)) logger.debug("send_content - sending to recipients: %s", recipients) handle_send(entity, content.author.federable, recipients, payload_logger=get_outbound_payload_logger()) else: logger.warning("send_content - No entity for %s", content)
def test_calls_send_document(self, mock_send, mock_create, diasporapost): recipients = [("[email protected]", "diaspora"), ("localhost", None)] mock_from_user = Mock() handle_send(diasporapost, mock_from_user, recipients) call_args_list = [ call("https://127.0.0.1/receive/public", "payload"), call("https://localhost/receive/public", "payload"), ] assert call_args_list[0] in mock_send.call_args_list assert call_args_list[1] in mock_send.call_args_list
def post_receive(self) -> None: """ Post receive hook - send back follow ack. """ super().post_receive() if not self.following: return from federation.utils.activitypub import retrieve_and_parse_profile # Circulars try: from federation.utils.django import get_function_from_config 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(), ) 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 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 send_reply(content_id, activity_fid): """ Handle sending a Content object that is a reply out via the federation layer. """ try: content = Content.objects.get( id=content_id, visibility__in=(Visibility.PUBLIC, Visibility.LIMITED), content_type=ContentType.REPLY, local=True, ) except Content.DoesNotExist: logger.warning("No content found with id %s", content_id) return entity = make_federable_content(content) if not entity: logger.warning("send_reply - No entity for %s", content) entity.activity_id = activity_fid if settings.DEBUG: # Don't send in development mode return recipients = [] if not content.root_parent.author.is_local: recipients.append( content.root_parent.author.get_recipient_for_visibility( content.visibility)) if content.visibility == Visibility.PUBLIC: recipients.extend( _get_remote_participants_for_content(content, exclude=content.author.fid, include_remote=True), ) recipients.extend( _get_remote_followers( content.author, content.visibility, exclude=content.author.fid, )) elif content.visibility == Visibility.LIMITED: recipients.extend(_get_limited_recipients(content.author.fid, content)) else: return if not recipients: logger.debug("send_reply - no remote recipients for content: %s", content.id) return logger.debug("send_reply - sending to recipients: %s", recipients) handle_send(entity, content.author.federable, recipients, payload_logger=get_outbound_payload_logger())
def test_calls_handle_create_payload(self, mock_send, mock_create, diasporapost): recipients = [("[email protected]", "diaspora"), ("localhost", None)] mock_author = Mock() handle_send(diasporapost, mock_author, recipients) mock_create.assert_called_once_with(diasporapost, mock_author, parent_user=None) mock_create.reset_mock() handle_send(diasporapost, mock_author, recipients, parent_user="******") mock_create.assert_called_once_with(diasporapost, mock_author, parent_user="******")
def send_content_retraction(content, author_id): """Handle sending of retractions. Currently only for public content. """ if not content.visibility == Visibility.PUBLIC or not content.is_local: return author = Profile.objects.get(id=author_id) entity = make_federable_retraction(content, author) if entity: if settings.DEBUG: # Don't send in development mode return recipients = [ (settings.SOCIALHOME_RELAY_DOMAIN, "diaspora"), ] recipients.extend(_get_remote_followers(author)) handle_send(entity, author, recipients) else: logger.warning("No retraction entity for %s", content)
def send_content(content_id, recipient_id=None): """ Handle sending a Content object out via the federation layer. """ try: content = Content.objects.get( id=content_id, visibility__in=(Visibility.PUBLIC, Visibility.LIMITED), content_type=ContentType.CONTENT, local=True, ) except Content.DoesNotExist: logger.warning("No local content found with id %s", content_id) return if recipient_id: try: recipient = Profile.objects.get(id=recipient_id, user__isnull=True) except Profile.DoesNotExist: logger.warning("No remote recipient found with id %s", recipient_id) return else: recipient = None entity = make_federable_content(content) if entity: if settings.DEBUG: # Don't send in development mode return if recipient: recipients = [ # TODO fid or handle? (recipient.handle, recipient.key, recipient.guid), ] else: recipients = [settings.SOCIALHOME_RELAY_ID] recipients.extend(_get_remote_followers(content.author)) logger.debug("send_content - sending to recipients: %s", recipients) handle_send(entity, content.author.federable, recipients) else: logger.warning("send_content - No entity for %s", content)
def test_survives_sending_share_if_diaspora_payload_cannot_be_created( self, mock_send, share): key = get_dummy_private_key() share.target_handle = None # Ensure diaspora payload fails recipients = [{ "endpoint": "https://example.com/receive/public", "public": True, "protocol": "diaspora", "fid": "", }, { "endpoint": "https://example.tld/receive/public", "public": True, "protocol": "diaspora", "fid": "", }, { "endpoint": "https://example.net/inbox", "fid": "https://example.net/foobar", "public": True, "protocol": "activitypub", }] author = UserType( private_key=key, id="*****@*****.**", handle="*****@*****.**", ) handle_send(share, author, recipients) # Ensure first call is a public activitypub payload args, kwargs = mock_send.call_args_list[0] assert args[0] == "https://example.net/inbox" assert kwargs['headers'] == { 'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', } assert encode_if_text( "https://www.w3.org/ns/activitystreams#Public") in args[1] # Should only be one call assert mock_send.call_count == 1
def send_reply(content_id): """ Handle sending a Content object that is a reply out via the federation layer. """ try: content = Content.objects.get( id=content_id, visibility__in=(Visibility.PUBLIC, Visibility.LIMITED), content_type=ContentType.REPLY, local=True, ) except Content.DoesNotExist: logger.warning("No content found with id %s", content_id) return entity = make_federable_content(content) if not entity: logger.warning("send_reply - No entity for %s", content) if settings.DEBUG: # Don't send in development mode return # Send directly (remote parent) or as a relayable (local parent) if content.parent.local: forward_entity(entity, content.parent.id) else: # We only need to send to the original author parent_author = content.parent.author if content.visibility == Visibility.PUBLIC: recipients = [ # TODO fid or handle? parent_author.handle, ] else: recipients = [ # TODO fid or handle? (parent_author.handle, parent_author.key, parent_author.guid), ] logger.debug("send_reply - sending to recipients: %s", recipients) handle_send(entity, content.author.federable, recipients)
def test_calls_handle_create_payload(self, mock_send, profile): key = get_dummy_private_key() recipients = [ { "endpoint": "https://127.0.0.1/receive/users/1234", "public_key": key.publickey(), "public": False, "protocol": "diaspora", "fid": "", }, { "endpoint": "https://example.com/receive/public", "public": True, "protocol": "diaspora", "fid": "", }, { "endpoint": "https://example.net/receive/public", "public": True, "protocol": "diaspora", "fid": "", }, # Same twice to ensure one delivery only per unique { "endpoint": "https://example.net/receive/public", "public": True, "protocol": "diaspora", "fid": "", }, { "endpoint": "https://example.net/foobar/inbox", "fid": "https://example.net/foobar", "public": False, "protocol": "activitypub", }, { "endpoint": "https://example.net/inbox", "fid": "https://example.net/foobar", "public": True, "protocol": "activitypub", } ] author = UserType( private_key=key, id="*****@*****.**", handle="*****@*****.**", ) handle_send(profile, author, recipients) # Ensure first call is a private diaspora payload args, kwargs = mock_send.call_args_list[0] assert args[0] == "https://127.0.0.1/receive/users/1234" assert "aes_key" in args[1] assert "encrypted_magic_envelope" in args[1] assert kwargs['headers'] == {'Content-Type': 'application/json'} # Ensure second call is a private activitypub payload args, kwargs = mock_send.call_args_list[1] assert args[0] == "https://example.net/foobar/inbox" assert kwargs['headers'] == { 'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', } assert encode_if_text( "https://www.w3.org/ns/activitystreams#Public") not in args[1] # Ensure third call is a public activitypub payload args, kwargs = mock_send.call_args_list[2] assert args[0] == "https://example.net/inbox" assert kwargs['headers'] == { 'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', } assert encode_if_text( "https://www.w3.org/ns/activitystreams#Public") in args[1] # Ensure diaspora public payloads and recipients, one per unique host args3, kwargs3 = mock_send.call_args_list[3] args4, kwargs4 = mock_send.call_args_list[4] public_endpoints = {args3[0], args4[0]} assert public_endpoints == { "https://example.net/receive/public", "https://example.com/receive/public", } assert args3[1].startswith("<me:env xmlns:me=") assert args4[1].startswith("<me:env xmlns:me=") assert kwargs3['headers'] == { 'Content-Type': 'application/magic-envelope+xml' } assert kwargs4['headers'] == { 'Content-Type': 'application/magic-envelope+xml' } with pytest.raises(IndexError): # noinspection PyStatementEffect mock_send.call_args_list[5]