From 1e757ca43b27d0da89f772d0858a0cfaf92e67cf Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Mon, 15 Jul 2019 00:09:53 +0300 Subject: [PATCH] Add support for root parent for Comment entity Added support for Diaspora `Comment` entity `thread_parent_guid` attribute. Added `root_target_id` and `root_target_guid` to `Comment` base entity. This allows referring to a parent object up the hierarchy chain for threaded comments. --- CHANGELOG.md | 4 +++ federation/entities/base.py | 4 +-- federation/entities/diaspora/entities.py | 3 ++- federation/entities/diaspora/mappers.py | 3 +++ federation/entities/mixins.py | 6 +++++ .../tests/entities/diaspora/test_entities.py | 25 +++++++++++++++---- .../tests/entities/diaspora/test_mappers.py | 23 ++++++++++++++++- federation/tests/fixtures/entities.py | 16 ++++++++++++ .../tests/fixtures/payloads/diaspora.py | 12 +++++++++ 9 files changed, 87 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0ee9b..cf4c362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ ActivityPub profiles will parse these values from incoming profile documents. Diaspora entities will default to the inboxes in the specification. +* Added support for Diaspora `Comment` entity `thread_parent_guid` attribute. + +* Added `root_target_id` and `root_target_guid` to `Comment` base entity. This allows referring to a parent object up the hierarchy chain for threaded comments. + ### Changed * **Backwards incompatible.** Lowest compatible Python version is now 3.6. diff --git a/federation/entities/base.py b/federation/entities/base.py index 32a272d..8835c57 100644 --- a/federation/entities/base.py +++ b/federation/entities/base.py @@ -5,7 +5,7 @@ from dirty_validators.basic import Email from federation.entities.activitypub.enums import ActivityType from federation.entities.mixins import ( PublicMixin, TargetIDMixin, ParticipationMixin, CreatedAtMixin, RawContentMixin, OptionalRawContentMixin, - EntityTypeMixin, ProviderDisplayNameMixin) + EntityTypeMixin, ProviderDisplayNameMixin, RootTargetIDMixin) class Accept(CreatedAtMixin, TargetIDMixin): @@ -34,7 +34,7 @@ class Image(PublicMixin, OptionalRawContentMixin, CreatedAtMixin): self._required += ["remote_path", "remote_name"] -class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin): +class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin, RootTargetIDMixin): """Represents a comment, linked to another object.""" participation = "comment" url = "" diff --git a/federation/entities/diaspora/entities.py b/federation/entities/diaspora/entities.py index 2879a1a..659f848 100644 --- a/federation/entities/diaspora/entities.py +++ b/federation/entities/diaspora/entities.py @@ -15,7 +15,8 @@ class DiasporaComment(DiasporaRelayableMixin, Comment): element = etree.Element(self._tag_name) struct_to_xml(element, [ {"guid": self.guid}, - {"parent_guid": self.target_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}, diff --git a/federation/entities/diaspora/mappers.py b/federation/entities/diaspora/mappers.py index 482c08c..9ad25b9 100644 --- a/federation/entities/diaspora/mappers.py +++ b/federation/entities/diaspora/mappers.py @@ -188,6 +188,9 @@ def transform_attributes(attrs, cls): 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] diff --git a/federation/entities/mixins.py b/federation/entities/mixins.py index 07e2470..5f06803 100644 --- a/federation/entities/mixins.py +++ b/federation/entities/mixins.py @@ -137,6 +137,12 @@ class TargetIDMixin(BaseEntity): self._required += ["target_id"] +class RootTargetIDMixin(BaseEntity): + root_target_id = "" + root_target_handle = "" + root_target_guid = "" + + class ParticipationMixin(TargetIDMixin): """Reflects a participation to something.""" participation = "" diff --git a/federation/tests/entities/diaspora/test_entities.py b/federation/tests/entities/diaspora/test_entities.py index 587ae64..1daee01 100644 --- a/federation/tests/entities/diaspora/test_entities.py +++ b/federation/tests/entities/diaspora/test_entities.py @@ -27,6 +27,19 @@ class TestEntitiesConvertToXML: assert len(result.find("created_at").text) > 0 result.find("created_at").text = "" # timestamp makes testing painful converted = b"guidtarget_guid" \ + b"target_guid" \ + b"signature" \ + b"raw_contentalice@example.com" \ + b"" + assert etree.tostring(result) == converted + + def test_nested_comment_to_xml(self, diasporanestedcomment): + result = diasporanestedcomment.to_xml() + assert result.tag == "comment" + assert len(result.find("created_at").text) > 0 + result.find("created_at").text = "" # timestamp makes testing painful + converted = b"guidtarget_guid" \ + b"thread_target_guid" \ b"signature" \ b"raw_contentalice@example.com" \ b"" @@ -122,13 +135,15 @@ class TestDiasporaRelayableMixin: guid="guid", target_id="target_guid", target_guid="target_guid", + root_target_id="target_guid", + root_target_guid="target_guid", ) entity.sign(get_dummy_private_key()) - assert entity.signature == "OWvW/Yxw4uCnx0WDn0n5/B4uhyZ8Pr6h3FZaw8J7PCXyPluOfYXFoHO21bykP8c2aVnuJNHe+lmeAkUC" \ - "/kHnl4yxk/jqe3uroW842OWvsyDRQ11vHxhIqNMjiepFPkZmXX3vqrYYh5FrC/tUsZrEc8hHoOIHXFR2" \ - "kGD0gPV+4EEG6pbMNNZ+SBVun0hvruX8iKQVnBdc/+zUI9+T/MZmLyqTq/CvuPxDyHzQPSHi68N9rJyr" \ - "4Xa1K+R33Xq8eHHxs8LVNRqzaHGeD3DX8yBu/vP9TYmZsiWlymbuGwLCa4Yfv/VS1hQZovhg6YTxV4CR" \ - "v4ToGL+CAJ7UHEugRRBwDw==" + assert entity.signature == "XZYggFdQHOicguZ0ReVJkYiK5othHgBgAtwnSmm4NR31qeLa76Ur/i2B5Xi9dtopDlNS8EbFy+MLJ1ds" \ + "ovDjPsVC1nLZrL57y0v+HtwJas6hQqNbvmEyr1q6X+0p1i93eINzt/7bxcP5uEGxy8J4ItsJzbDVLlC5" \ + "3ZtIg7pmhR0ltqNqBHrgL8WDokfGKFlXqANchbD+Xeyv2COGbI78LwplVdYjHW1+jefjpYhMCxayIvMv" \ + "WS8TV1hMTqUz+zSqoCHU04RgjjGW8e8vINDblQwMfEMeJ5T6OP5RiU3zCqDc3uL2zxHHh9IGC+clVuhP" \ + "HTv8tHUHNLgc2vIzRtGh6w==" def test_signing_like_works(self): entity = DiasporaLike( diff --git a/federation/tests/entities/diaspora/test_mappers.py b/federation/tests/entities/diaspora/test_mappers.py index e0af5a5..c17d44a 100644 --- a/federation/tests/entities/diaspora/test_mappers.py +++ b/federation/tests/entities/diaspora/test_mappers.py @@ -18,7 +18,7 @@ from federation.tests.fixtures.payloads import ( DIASPORA_POST_WITH_PHOTOS, DIASPORA_CONTACT, DIASPORA_PROFILE_EMPTY_TAGS, DIASPORA_RESHARE, DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES, DIASPORA_POST_SIMPLE_WITH_MENTION, - DIASPORA_PROFILE_FIRST_NAME_ONLY) + DIASPORA_PROFILE_FIRST_NAME_ONLY, DIASPORA_POST_COMMENT_NESTED) class TestDiasporaEntityMappersReceive: @@ -71,6 +71,7 @@ class TestDiasporaEntityMappersReceive: assert isinstance(comment, DiasporaComment) assert isinstance(comment, Comment) assert comment.target_guid == "((parent_guidparent_guidparent_guidparent_guid))" + assert comment.root_target_guid == "" assert comment.guid == "((guidguidguidguidguidguid))" assert comment.handle == "alice@alice.diaspora.example.org" assert comment.participation == "comment" @@ -81,6 +82,26 @@ class TestDiasporaEntityMappersReceive: ] mock_validate.assert_called_once_with() + @patch("federation.entities.diaspora.mappers.DiasporaComment._validate_signatures") + def test_message_to_objects_nested_comment(self, mock_validate): + entities = message_to_objects(DIASPORA_POST_COMMENT_NESTED, "alice@alice.diaspora.example.org", + sender_key_fetcher=Mock()) + assert len(entities) == 1 + comment = entities[0] + assert isinstance(comment, DiasporaComment) + assert isinstance(comment, Comment) + assert comment.target_guid == "((parent_guidparent_guidparent_guidparent_guid))" + assert comment.root_target_guid == "((threadparentguid))" + assert comment.guid == "((guidguidguidguidguidguid))" + assert comment.handle == "alice@alice.diaspora.example.org" + assert comment.participation == "comment" + assert comment.raw_content == "((text))" + assert comment.signature == "((signature))" + assert comment._xml_tags == [ + "guid", "parent_guid", "thread_parent_guid", "text", "author", + ] + mock_validate.assert_called_once_with() + @patch("federation.entities.diaspora.mappers.DiasporaLike._validate_signatures") def test_message_to_objects_like(self, mock_validate): entities = message_to_objects( diff --git a/federation/tests/fixtures/entities.py b/federation/tests/fixtures/entities.py index fb2c34a..69c5068 100644 --- a/federation/tests/fixtures/entities.py +++ b/federation/tests/fixtures/entities.py @@ -118,6 +118,22 @@ def diasporacomment(): ) +@pytest.fixture +def diasporanestedcomment(): + return DiasporaComment( + raw_content="raw_content", + signature="signature", + id="guid", + guid="guid", + actor_id="alice@example.com", + handle="alice@example.com", + target_id="thread_target_guid", + target_guid="thread_target_guid", + root_target_id="target_guid", + root_target_guid="target_guid", + ) + + @pytest.fixture def diasporacontact(): return DiasporaContact( diff --git a/federation/tests/fixtures/payloads/diaspora.py b/federation/tests/fixtures/payloads/diaspora.py index 8bd3669..005c863 100644 --- a/federation/tests/fixtures/payloads/diaspora.py +++ b/federation/tests/fixtures/payloads/diaspora.py @@ -91,6 +91,18 @@ DIASPORA_POST_COMMENT = """ """ +DIASPORA_POST_COMMENT_NESTED = """ + + ((guidguidguidguidguidguid)) + ((parent_guidparent_guidparent_guidparent_guid)) + ((threadparentguid)) + ((base64-encoded data)) + ((text)) + alice@alice.diaspora.example.org + ((signature)) + +""" + DIASPORA_POST_LIKE = """ Post