From 80c4e433d70da457372ce2a8b32e6a58a2c279c1 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Sun, 17 Mar 2019 16:09:34 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 4 ++ federation/entities/activitypub/entities.py | 4 +- federation/entities/activitypub/mappers.py | 48 +++++++++++-------- federation/entities/base.py | 7 +++ federation/entities/diaspora/entities.py | 8 ++++ .../tests/entities/diaspora/test_mappers.py | 4 +- .../tests/entities/diaspora/test_utils.py | 1 + federation/tests/test_outbound.py | 8 +++- 8 files changed, 58 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4fef3..d2f27fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/federation/entities/activitypub/entities.py b/federation/entities/activitypub/entities.py index d3bebee..d9f77b7 100644 --- a/federation/entities/activitypub/entities.py +++ b/federation/entities/activitypub/entities.py @@ -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/", diff --git a/federation/entities/activitypub/mappers.py b/federation/entities/activitypub/mappers.py index bf3435d..87446e3 100644 --- a/federation/entities/activitypub/mappers.py +++ b/federation/entities/activitypub/mappers.py @@ -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 diff --git a/federation/entities/base.py b/federation/entities/base.py index c46a035..3c26f7b 100644 --- a/federation/entities/base.py +++ b/federation/entities/base.py @@ -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` diff --git a/federation/entities/diaspora/entities.py b/federation/entities/diaspora/entities.py index 283f253..2879a1a 100644 --- a/federation/entities/diaspora/entities.py +++ b/federation/entities/diaspora/entities.py @@ -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) diff --git a/federation/tests/entities/diaspora/test_mappers.py b/federation/tests/entities/diaspora/test_mappers.py index 59ecd1e..e0af5a5 100644 --- a/federation/tests/entities/diaspora/test_mappers.py +++ b/federation/tests/entities/diaspora/test_mappers.py @@ -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): diff --git a/federation/tests/entities/diaspora/test_utils.py b/federation/tests/entities/diaspora/test_utils.py index 8840347..272d321 100644 --- a/federation/tests/entities/diaspora/test_utils.py +++ b/federation/tests/entities/diaspora/test_utils.py @@ -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", } diff --git a/federation/tests/test_outbound.py b/federation/tests/test_outbound.py index 487ed9c..329a7b0 100644 --- a/federation/tests/test_outbound.py +++ b/federation/tests/test_outbound.py @@ -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("