From b27ecc5223aef1b9b0cfc46e6e1e13fdfb636182 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Sat, 6 May 2017 20:19:40 +0300 Subject: [PATCH] Refactor processing Diaspora payload xml New protocol version is dropping the xml/post wrapper elements. Support parsing these payloads plus legacy ones. Refs: #60 --- federation/entities/diaspora/mappers.py | 111 ++++++++++-------- .../tests/entities/diaspora/test_mappers.py | 33 +++--- 2 files changed, 77 insertions(+), 67 deletions(-) diff --git a/federation/entities/diaspora/mappers.py b/federation/entities/diaspora/mappers.py index 4a735ea..66da0d5 100644 --- a/federation/entities/diaspora/mappers.py +++ b/federation/entities/diaspora/mappers.py @@ -21,19 +21,24 @@ MAPPINGS = { "retraction": DiasporaRetraction, } -BOOLEAN_KEYS = [ +TAGS = [ + # Order is important. Any top level tags should be before possibly child tags + "status_message", "comment", "like", "request", "profile", "retraction", "photo", +] + +BOOLEAN_KEYS = ( "public", "nsfw", -] +) -DATETIME_KEYS = [ +DATETIME_KEYS = ( "created_at", -] +) -INTEGER_KEYS = [ +INTEGER_KEYS = ( "height", "width", -] +) def xml_children_as_dict(node): """Turn the children of node into a dict, keyed by tag name. @@ -43,8 +48,8 @@ def xml_children_as_dict(node): return dict((e.tag, e.text) for e in node) -def element_to_objects(tree, sender_key_fetcher=None): - """Transform an Element tree to a list of entities recursively. +def element_to_objects(element, sender_key_fetcher=None): + """Transform an Element to a list of entities recursively. Possible child entities are added to each entity `_children` list. @@ -54,45 +59,45 @@ def element_to_objects(tree, sender_key_fetcher=None): :returns: list of entities """ entities = [] - for element in tree: - cls = MAPPINGS.get(element.tag, None) - if not cls: - continue + cls = MAPPINGS.get(element.tag, None) + if not cls: + return [] - attrs = xml_children_as_dict(element) - transformed = transform_attributes(attrs) - if hasattr(cls, "fill_extra_attributes"): - transformed = cls.fill_extra_attributes(transformed) - entity = cls(**transformed) - # Add protocol name - entity._source_protocol = "diaspora" - # Save element object to entity for possible later use - entity._source_object = element - # If relayable, fetch sender key for validation - if issubclass(cls, DiasporaRelayableMixin): - if sender_key_fetcher: - entity._sender_key = sender_key_fetcher(entity.handle) - else: - profile = retrieve_and_parse_profile(entity.handle) - if profile: - entity._sender_key = profile.public_key - try: - entity.validate() - except ValueError as ex: - logger.error("Failed to validate entity %s: %s", entity, ex, extra={ - "attrs": attrs, - "transformed": transformed, - }) - continue - # Do child elements - entity._children = element_to_objects(element) - # Add to entities list - entities.append(entity) - if cls == DiasporaRequest: - # We support sharing/following separately, so also generate base Relationship for the following part - transformed.update({"relationship": "following"}) - relationship = Relationship(**transformed) - entities.append(relationship) + attrs = xml_children_as_dict(element) + transformed = transform_attributes(attrs, cls) + if hasattr(cls, "fill_extra_attributes"): + transformed = cls.fill_extra_attributes(transformed) + entity = cls(**transformed) + # Add protocol name + entity._source_protocol = "diaspora" + # Save element object to entity for possible later use + entity._source_object = element + # If relayable, fetch sender key for validation + if issubclass(cls, DiasporaRelayableMixin): + if sender_key_fetcher: + entity._sender_key = sender_key_fetcher(entity.handle) + else: + profile = retrieve_and_parse_profile(entity.handle) + if profile: + entity._sender_key = profile.public_key + try: + entity.validate() + except ValueError as ex: + logger.error("Failed to validate entity %s: %s", entity, ex, extra={ + "attrs": attrs, + "transformed": transformed, + }) + return [] + # Do child elements + for child in element: + entity._children = element_to_objects(child) + # Add to entities list + entities.append(entity) + if cls == DiasporaRequest: + # We support sharing/following separately, so also generate base Relationship for the following part + transformed.update({"relationship": "following"}) + relationship = Relationship(**transformed) + entities.append(relationship) return entities @@ -106,11 +111,15 @@ def message_to_objects(message, sender_key_fetcher=None): :returns: list of entities """ doc = etree.fromstring(message) - if doc[0].tag == "post": - # Skip the top element if it exists - doc = doc[0] - entities = element_to_objects(doc, sender_key_fetcher) - return entities + # Future Diaspora protocol version contains the element at top level + if doc.tag in TAGS: + return element_to_objects(doc, sender_key_fetcher) + # Legacy Diaspora protocol wraps the element in , so find the right element + for tag in TAGS: + element = doc.find(".//%s" % tag) + if element is not None: + return element_to_objects(element, sender_key_fetcher) + return [] def transform_attributes(attrs, cls): diff --git a/federation/tests/entities/diaspora/test_mappers.py b/federation/tests/entities/diaspora/test_mappers.py index 0dddc99..848cfc8 100644 --- a/federation/tests/entities/diaspora/test_mappers.py +++ b/federation/tests/entities/diaspora/test_mappers.py @@ -15,7 +15,7 @@ from federation.tests.fixtures.keys import get_dummy_private_key from federation.tests.fixtures.payloads import ( DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID, DIASPORA_RETRACTION, - DIASPORA_POST_WITH_PHOTOS, DIASPORA_POST_LEGACY_TIMESTAMP) + DIASPORA_POST_WITH_PHOTOS, DIASPORA_POST_LEGACY_TIMESTAMP, DIASPORA_POST_LEGACY) def mock_fill(attributes): @@ -23,7 +23,7 @@ def mock_fill(attributes): return attributes -class TestDiasporaEntityMappersReceive(object): +class TestDiasporaEntityMappersReceive(): def test_message_to_objects_simple_post(self): entities = message_to_objects(DIASPORA_POST_SIMPLE) assert len(entities) == 1 @@ -37,6 +37,20 @@ class TestDiasporaEntityMappersReceive(object): assert post.created_at == datetime(2011, 7, 20, 1, 36, 7) assert post.provider_display_name == "Socialhome" + def test_message_to_objects_post_legacy(self): + # This is the previous XML schema used before renewal of protocol + entities = message_to_objects(DIASPORA_POST_LEGACY) + assert len(entities) == 1 + post = entities[0] + assert isinstance(post, DiasporaPost) + assert isinstance(post, Post) + assert post.raw_content == "((status message))" + assert post.guid == "((guidguidguidguidguidguidguid))" + assert post.handle == "alice@alice.diaspora.example.org" + assert post.public == False + assert post.created_at == datetime(2011, 7, 20, 1, 36, 7) + assert post.provider_display_name == "Socialhome" + def test_message_to_objects_legact_timestamp(self): entities = message_to_objects(DIASPORA_POST_LEGACY_TIMESTAMP) post = entities[0] @@ -60,19 +74,6 @@ class TestDiasporaEntityMappersReceive(object): assert photo.handle == "alice@alice.diaspora.example.org" assert photo.public == False assert photo.created_at == datetime(2011, 7, 20, 1, 36, 7) - photo = post._children[1] - assert isinstance(photo, Image) - assert photo.remote_path == "https://alice.diaspora.example.org/uploads/images/" - assert photo.remote_name == "12345.jpg" - assert photo.raw_content == "foobar" - assert photo.linked_type == "Post" - assert photo.linked_guid == "((guidguidguidguidguidguidguid))" - assert photo.height == 120 - assert photo.width == 120 - assert photo.guid == "((guidguidguidguidguidguidguig))" - assert photo.handle == "alice@alice.diaspora.example.org" - assert photo.public == False - assert photo.created_at == datetime(2011, 7, 20, 1, 36, 7) @patch("federation.entities.diaspora.mappers.DiasporaComment._validate_signatures") def test_message_to_objects_comment(self, mock_validate): @@ -171,7 +172,7 @@ class TestDiasporaEntityMappersReceive(object): mock_retrieve.assert_called_once_with("alice@alice.diaspora.example.org") -class TestGetOutboundEntity(object): +class TestGetOutboundEntity(): def test_already_fine_entities_are_returned_as_is(self): dummy_key = get_dummy_private_key() entity = DiasporaPost()