Merge pull request #73 from jaywink/diaspora-properties

Support new Diaspora XML payload properties and refactor payload processing
merge-requests/130/head
Jason Robinson 2017-05-06 21:01:54 +03:00 zatwierdzone przez GitHub
commit 5309bea06f
4 zmienionych plików z 190 dodań i 195 usunięć

Wyświetl plik

@ -13,12 +13,15 @@ Additionally, Diaspora entity mappers `message_to_objects` and `element_to_objec
### Other backwards incompatible changes
* A failed payload signature verification now raises a `SignatureVerificationError` instead of a less specific `AssertionError`.
### Other additions
### Added
* Three new attributes added to entities.
* Add protocol name to all entities to attribute `_source_protocol`. This might be useful for applications to know which protocol payload the entity was created from once multiple protocols are implemented.
* Add source payload object to the entity at `_source_object` when processing it.
* Add sender public key to the entity at `_sender_key`, but only if it was used for validating signatures.
* Add support for the new Diaspora payload properties coming in the next protocol version. Old XML payloads are and will be still supported.
### Changed
* Refactor processing of Diaspora payload XML into entities. Diaspora protocol is dropping the `<XML><post></post></XML>` wrapper for the payloads. Payloads with the wrapper will still be parsed as before.
## [0.10.1] - 2017-03-09

Wyświetl plik

@ -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 <xml> 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,22 +111,32 @@ 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 <post> 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 <XML><post></post></XML>, 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):
"""Transform some attribute keys."""
def transform_attributes(attrs, cls):
"""Transform some attribute keys.
:param attrs: Properties from the XML
:type attrs: dict
:param cls: Class of the entity
:type cls: class
"""
transformed = {}
for key, value in attrs.items():
if key in ["raw_message", "text"]:
transformed["raw_content"] = value
elif key in ["diaspora_handle", "sender_handle", "author"]:
transformed["handle"] = value
elif key == "recipient_handle":
elif key in ["recipient_handle", "recipient"]:
transformed["target_handle"] = value
elif key == "parent_guid":
transformed["target_guid"] = value
@ -145,7 +160,7 @@ def transform_attributes(attrs):
transformed["raw_content"] = value
elif key == "searchable":
transformed["public"] = True if value == "true" else False
elif key == "target_type":
elif key in ["target_type", "type"] and cls == DiasporaRetraction:
transformed["entity_type"] = DiasporaRetraction.entity_type_from_remote(value)
elif key == "remote_photo_path":
transformed["remote_path"] = value
@ -156,6 +171,8 @@ def transform_attributes(attrs):
transformed["linked_type"] = "Post"
elif key == "author_signature":
transformed["signature"] = value
elif key == "post_guid":
transformed["target_guid"] = value
elif key in BOOLEAN_KEYS:
transformed[key] = True if value == "true" else False
elif key in DATETIME_KEYS:

Wyświetl plik

@ -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()

Wyświetl plik

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
ENCRYPTED_DIASPORA_PAYLOAD = """<?xml version='1.0'?>
<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
<encrypted_header>{encrypted_header}</encrypted_header>
@ -28,7 +26,7 @@ UNENCRYPTED_DIASPORA_PAYLOAD = """<?xml version='1.0'?>
"""
DIASPORA_POST_SIMPLE = """<XML>
DIASPORA_POST_LEGACY = """<XML>
<post>
<status_message>
<raw_message>((status message))</raw_message>
@ -43,139 +41,115 @@ DIASPORA_POST_SIMPLE = """<XML>
"""
DIASPORA_POST_LEGACY_TIMESTAMP = """<XML>
<post>
<status_message>
<raw_message>((status message))</raw_message>
<guid>((guidguidguidguidguidguidguid))</guid>
<diaspora_handle>alice@alice.diaspora.example.org</diaspora_handle>
<public>false</public>
<created_at>2011-07-20 01:36:07 UTC</created_at>
<provider_display_name>Socialhome</provider_display_name>
</status_message>
</post>
</XML>
DIASPORA_POST_SIMPLE = """
<status_message>
<text>((status message))</text>
<guid>((guidguidguidguidguidguidguid))</guid>
<author>alice@alice.diaspora.example.org</author>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<provider_display_name>Socialhome</provider_display_name>
</status_message>
"""
DIASPORA_POST_WITH_PHOTOS = """<XML>
<post>
<status_message>
<raw_message>((status message))</raw_message>
<guid>((guidguidguidguidguidguidguid))</guid>
<diaspora_handle>alice@alice.diaspora.example.org</diaspora_handle>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<provider_display_name>Socialhome</provider_display_name>
<photo>
<guid>((guidguidguidguidguidguidguif))</guid>
<diaspora_handle>alice@alice.diaspora.example.org</diaspora_handle>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<remote_photo_path>https://alice.diaspora.example.org/uploads/images/</remote_photo_path>
<remote_photo_name>1234.jpg</remote_photo_name>
<text/>
<status_message_guid>((guidguidguidguidguidguidguid))</status_message_guid>
<height>120</height>
<width>120</width>
</photo>
<photo>
<guid>((guidguidguidguidguidguidguig))</guid>
<diaspora_handle>alice@alice.diaspora.example.org</diaspora_handle>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<remote_photo_path>https://alice.diaspora.example.org/uploads/images/</remote_photo_path>
<remote_photo_name>12345.jpg</remote_photo_name>
<text>foobar</text>
<status_message_guid>((guidguidguidguidguidguidguid))</status_message_guid>
<height>120</height>
<width>120</width>
</photo>
</status_message>
</post>
</XML>
DIASPORA_POST_LEGACY_TIMESTAMP = """
<status_message>
<text>((status message))</text>
<guid>((guidguidguidguidguidguidguid))</guid>
<author>alice@alice.diaspora.example.org</author>
<public>false</public>
<created_at>2011-07-20 01:36:07 UTC</created_at>
<provider_display_name>Socialhome</provider_display_name>
</status_message>
"""
DIASPORA_POST_INVALID = """<XML>
<post>
<status_message>
<raw_message>((status message))</raw_message>
<diaspora_handle>alice@alice.diaspora.example.org</diaspora_handle>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<provider_display_name>Socialhome</provider_display_name>
</status_message>
</post>
</XML>
DIASPORA_POST_WITH_PHOTOS = """
<status_message>
<text>((status message))</text>
<guid>((guidguidguidguidguidguidguid))</guid>
<author>alice@alice.diaspora.example.org</author>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<provider_display_name>Socialhome</provider_display_name>
<photo>
<guid>((guidguidguidguidguidguidguif))</guid>
<author>alice@alice.diaspora.example.org</author>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<remote_photo_path>https://alice.diaspora.example.org/uploads/images/</remote_photo_path>
<remote_photo_name>1234.jpg</remote_photo_name>
<text/>
<status_message_guid>((guidguidguidguidguidguidguid))</status_message_guid>
<height>120</height>
<width>120</width>
</photo>
</status_message>
"""
DIASPORA_POST_COMMENT = """<XML>
<post>
<comment>
<guid>((guidguidguidguidguidguid))</guid>
<parent_guid>((parent_guidparent_guidparent_guidparent_guid))</parent_guid>
<author_signature>((base64-encoded data))</author_signature>
<text>((text))</text>
<diaspora_handle>alice@alice.diaspora.example.org</diaspora_handle>
<author_signature>((signature))</author_signature>
</comment>
</post>
</XML>
DIASPORA_POST_INVALID = """
<status_message>
<text>((status message))</text>
<author>alice@alice.diaspora.example.org</author>
<public>false</public>
<created_at>2011-07-20T01:36:07Z</created_at>
<provider_display_name>Socialhome</provider_display_name>
</status_message>
"""
DIASPORA_POST_LIKE = """<XML>
<post>
<like>
<target_type>Post</target_type>
<guid>((guidguidguidguidguidguid))</guid>
<parent_guid>((parent_guidparent_guidparent_guidparent_guid))</parent_guid>
<author_signature>((base64-encoded data))</author_signature>
<positive>true</positive>
<diaspora_handle>alice@alice.diaspora.example.org</diaspora_handle>
<author_signature>((signature))</author_signature>
</like>
</post>
</XML>
DIASPORA_POST_COMMENT = """
<comment>
<guid>((guidguidguidguidguidguid))</guid>
<parent_guid>((parent_guidparent_guidparent_guidparent_guid))</parent_guid>
<author_signature>((base64-encoded data))</author_signature>
<text>((text))</text>
<author>alice@alice.diaspora.example.org</author>
<author_signature>((signature))</author_signature>
</comment>
"""
DIASPORA_REQUEST = """<XML>
<post>
<request>
<sender_handle>bob@example.com</sender_handle>
<recipient_handle>alice@alice.diaspora.example.org</recipient_handle>
</request>
</post>
</XML>
DIASPORA_POST_LIKE = """
<like>
<parent_type>Post</parent_type>
<guid>((guidguidguidguidguidguid))</guid>
<parent_guid>((parent_guidparent_guidparent_guidparent_guid))</parent_guid>
<author_signature>((base64-encoded data))</author_signature>
<positive>true</positive>
<author>alice@alice.diaspora.example.org</author>
<author_signature>((signature))</author_signature>
</like>
"""
DIASPORA_PROFILE = """<XML>
<post>
<profile>
<diaspora_handle>bob@example.com</diaspora_handle>
<first_name>Bob Bobertson</first_name>
<last_name></last_name>
<image_url>https://example.com/uploads/images/thumb_large_c833747578b5.jpg</image_url>
<image_url_small>https://example.com/uploads/images/thumb_small_c8b147578b5.jpg</image_url_small>
<image_url_medium>https://example.com/uploads/images/thumb_medium_c8b1aab04f3.jpg</image_url_medium>
<gender></gender>
<bio>A cool bio</bio>
<location>Helsinki</location>
<searchable>true</searchable>
<nsfw>false</nsfw>
<tag_string>#socialfederation #federation</tag_string>
</profile>
</post>
</XML>
DIASPORA_REQUEST = """
<request>
<author>bob@example.com</author>
<recipient>alice@alice.diaspora.example.org</recipient>
</request>
"""
DIASPORA_RETRACTION = """<XML>
<post>
<retraction>
<author>bob@example.com</author>
<target_guid>xxxxxxxxxxxxxxxx</target_guid>
<target_type>Post</target_type>
</retraction>
</post>
</XML>
DIASPORA_PROFILE = """
<profile>
<author>bob@example.com</author>
<first_name>Bob Bobertson</first_name>
<last_name></last_name>
<image_url>https://example.com/uploads/images/thumb_large_c833747578b5.jpg</image_url>
<image_url_small>https://example.com/uploads/images/thumb_small_c8b147578b5.jpg</image_url_small>
<image_url_medium>https://example.com/uploads/images/thumb_medium_c8b1aab04f3.jpg</image_url_medium>
<gender></gender>
<bio>A cool bio</bio>
<location>Helsinki</location>
<searchable>true</searchable>
<nsfw>false</nsfw>
<tag_string>#socialfederation #federation</tag_string>
</profile>
"""
DIASPORA_RETRACTION = """
<retraction>
<author>bob@example.com</author>
<target_guid>xxxxxxxxxxxxxxxx</target_guid>
<target_type>Post</target_type>
</retraction>
"""