Entities of type `Profile` now have a dictionary of `inboxes`

With two elements, `private` and `public`. These should be URL's
indicating where to send payloads for the recipient.

ActivityPub profiles will parse these values from incoming profile
documents. Diaspora entities will default to the inboxes in the
specification.
merge-requests/143/head
Jason Robinson 2019-03-17 16:09:34 +02:00
rodzic 9df3fe5c1a
commit 80c4e433d7
8 zmienionych plików z 58 dodań i 26 usunięć

Wyświetl plik

@ -18,6 +18,10 @@
* Added network utility `network.fetch_host_ip` to fetch IP by hostname.
* Entities of type `Profile` now have a dictionary of `inboxes`, with two elements, `private` and `public`. These should be URL's indicating where to send payloads for the recipient.
ActivityPub profiles will parse these values from incoming profile documents. Diaspora entities will default to the inboxes in the specification.
### Changed
* **Backwards incompatible.** Lowest compatible Python version is now 3.6.

Wyświetl plik

@ -107,12 +107,12 @@ class ActivitypubProfile(ActivitypubEntityMixin, Profile):
CONTEXT_MANUALLY_APPROVES_FOLLOWERS,
],
"endpoints": {
"sharedInbox": f"{with_slash(self.base_url)}ap/inbox/", # TODO just get from config
"sharedInbox": self.inboxes["public"],
},
"followers": f"{with_slash(self.id)}followers/",
"following": f"{with_slash(self.id)}following/",
"id": self.id,
"inbox": f"{with_slash(self.id)}inbox/",
"inbox": self.inboxes["private"],
"manuallyApprovesFollowers": False,
"name": self.name,
"outbox": f"{with_slash(self.id)}outbox/",

Wyświetl plik

@ -1,4 +1,4 @@
from typing import List, Callable, Dict
from typing import List, Callable, Dict, Union
from federation.entities.activitypub.entities import ActivitypubFollow, ActivitypubProfile, ActivitypubAccept
from federation.entities.base import Follow, Profile, Accept
@ -85,51 +85,59 @@ def message_to_objects(
return element_to_objects(message, sender, sender_key_fetcher, user)
def transform_attribute(key, value, cls):
def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dict, cls) -> None:
if value is None:
value = ""
if key == "id":
if cls == ActivitypubProfile:
return {"id": value}
transformed["id"] = value
else:
return {"activity_id": value}
transformed["activity_id"] = value
elif key == "actor":
return {"actor_id": value}
transformed["actor_id"] = value
elif key == "inboxes" and isinstance(value, dict):
if "inboxes" not in transformed:
transformed["inboxes"] = {"private": None, "public": None}
transformed["endpoints"]["public"] = value.get("sharedInbox")
elif key == "icon":
# TODO maybe we should ditch these size constants and instead have a more flexible dict for images
# so based on protocol there would either be one url or many by size name
if isinstance(value, dict):
return {"image_urls": {
transformed["image_urls"] = {
"small": value['url'],
"medium": value['url'],
"large": value['url'],
}}
}
else:
return {"image_urls": {
transformed["image_urls"] = {
"small": value,
"medium": value,
"large": value,
}}
}
elif key == "inbox":
if "inboxes" not in transformed:
transformed["inboxes"] = {"private": None, "public": None}
transformed["inboxes"]["private"] = value
elif key == "name":
return {"name": value}
transformed["name"] = value
elif key == "object":
if isinstance(value, dict):
return transform_attributes(value, cls)
transform_attributes(value, cls, transformed)
else:
return {"target_id": value}
transformed["target_id"] = value
elif key == "preferredUsername":
return {"username": value}
transformed["username"] = value
elif key == "publicKey":
return {"public_key": value.get('publicKeyPem', '')}
transformed["public_key"] = value.get('publicKeyPem', '')
elif key == "summary":
return {"raw_content": value}
transformed["raw_content"] = value
elif key == "url":
return {"url": value}
return {}
transformed["url"] = value
def transform_attributes(payload, cls):
transformed = {}
def transform_attributes(payload: Dict, cls, transformed: Dict = None) -> Dict:
if not transformed:
transformed = {}
for key, value in payload.items():
transformed.update(transform_attribute(key, value, cls))
transform_attribute(key, value, transformed, cls)
return transformed

Wyświetl plik

@ -1,3 +1,5 @@
from typing import Dict
from dirty_validators.basic import Email
from federation.entities.mixins import (
@ -105,6 +107,7 @@ class Profile(CreatedAtMixin, OptionalRawContentMixin, PublicMixin):
tag_list = None
url = ""
username = ""
inboxes: Dict = None
_allowed_children = (Image,)
@ -112,6 +115,10 @@ class Profile(CreatedAtMixin, OptionalRawContentMixin, PublicMixin):
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`

Wyświetl plik

@ -4,6 +4,7 @@ from federation.entities.base import (
Comment, Post, Reaction, Profile, Retraction, Follow, Share, Image)
from federation.entities.diaspora.mixins import DiasporaEntityMixin, DiasporaRelayableMixin
from federation.entities.diaspora.utils import format_dt, struct_to_xml
from federation.utils.diaspora import get_private_endpoint, get_public_endpoint
class DiasporaComment(DiasporaRelayableMixin, Comment):
@ -89,6 +90,13 @@ 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)

Wyświetl plik

@ -237,7 +237,7 @@ class TestGetOutboundEntity:
assert get_outbound_entity(entity, private_key) == entity
entity = DiasporaComment()
assert get_outbound_entity(entity, private_key) == entity
entity = DiasporaProfile()
entity = DiasporaProfile(handle="foobar@example.com", guid="1234")
assert get_outbound_entity(entity, private_key) == entity
entity = DiasporaContact()
assert get_outbound_entity(entity, private_key) == entity
@ -258,7 +258,7 @@ class TestGetOutboundEntity:
def test_profile_is_converted_to_diasporaprofile(self, private_key):
entity = Profile()
entity = Profile(handle="foobar@example.com", guid="1234")
assert isinstance(get_outbound_entity(entity, private_key), DiasporaProfile)
def test_other_reaction_raises(self, private_key):

Wyświetl plik

@ -25,6 +25,7 @@ class TestGetBaseAttributes:
"created_at", "name", "email", "gender", "raw_content", "location", "public",
"nsfw", "public_key", "image_urls", "tag_list", "signature", "url", "atom_url",
"base_url", "id", "actor_id", "handle", "handle", "guid", "activity", "activity_id", "username",
"inboxes",
}

Wyświetl plik

@ -43,7 +43,9 @@ class TestHandleSend:
# Ensure second call is a private activitypub payload
args, kwargs = mock_send.call_args_list[1]
assert args[0] == "https://127.0.0.1/foobar"
assert kwargs['headers'] == {'Content-Type': 'application/activity+json'}
assert kwargs['headers'] == {
'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
}
# Ensure public payloads and recipients, one per unique host
args1, kwargs1 = mock_send.call_args_list[2]
@ -57,6 +59,8 @@ class TestHandleSend:
}
assert args2[1].startswith("<me:env xmlns:me=")
assert args3[1].startswith("<me:env xmlns:me=")
assert kwargs1['headers'] == {'Content-Type': 'application/activity+json'}
assert kwargs1['headers'] == {
'Content-Type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
}
assert kwargs2['headers'] == {'Content-Type': 'application/magic-envelope+xml'}
assert kwargs3['headers'] == {'Content-Type': 'application/magic-envelope+xml'}