Convert outbound entities to correct protocol types

When sending an entity, first convert it to the correct entity using the protocol entities. If a suitable entity is not found, raise an error.

Closes #27
merge-requests/130/head
Jason Robinson 2016-07-18 23:26:25 +03:00
rodzic 8b880f89a0
commit bf348e9544
8 zmienionych plików z 128 dodań i 11 usunięć

Wyświetl plik

@ -5,6 +5,7 @@
## Changed
- Unlock most of the direct dependencies to a certain version range. Unlock all of test requirements to any version.
- Entities passed to `federation.controllers.handle_create_payload` are now converted from the base entity types (Post, Comment, Reaction, etc) to Diaspora entity types (DiasporaPost, DiasporaComment, DiasporaLike, etc). This ensures actual payload generation has the correct methods available (for example `to_xml`) whatever entity is passed in.
### Fixes
- Fix fetching sender handle from Diaspora protocol private messages. As it is not contained in the header, it needs to be read from the message content itself.

Wyświetl plik

@ -1,5 +1,6 @@
import importlib
from federation.entities.diaspora.mappers import get_outbound_entity
from federation.exceptions import NoSuitableProtocolFoundError
from federation.protocols.diaspora.protocol import Protocol
@ -51,6 +52,8 @@ def handle_create_payload(from_user, to_user, entity):
`from_user` must have `private_key` and `handle` attributes.
`to_user` must have `key` attribute.
"""
# Just use Diaspora protocol for now
protocol = Protocol()
data = protocol.build_send(from_user=from_user, to_user=to_user, entity=entity)
outbound_entity = get_outbound_entity(entity)
data = protocol.build_send(from_user=from_user, to_user=to_user, entity=outbound_entity)
return data

Wyświetl plik

@ -2,10 +2,16 @@
from lxml import etree
from federation.entities.base import Comment, Post, Reaction, Relationship
from federation.entities.diaspora.utils import format_dt, struct_to_xml
from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes
class DiasporaComment(Comment):
class DiasporaEntityMixin(object):
@classmethod
def from_base(cls, entity):
return cls(**get_base_attributes(entity))
class DiasporaComment(DiasporaEntityMixin, Comment):
"""Diaspora comment."""
author_signature = ""
@ -21,7 +27,7 @@ class DiasporaComment(Comment):
return element
class DiasporaPost(Post):
class DiasporaPost(DiasporaEntityMixin, Post):
"""Diaspora post, ie status message."""
def to_xml(self):
"""Convert to XML message."""
@ -36,7 +42,7 @@ class DiasporaPost(Post):
return element
class DiasporaLike(Reaction):
class DiasporaLike(DiasporaEntityMixin, Reaction):
"""Diaspora like."""
author_signature = ""
reaction = "like"
@ -55,7 +61,7 @@ class DiasporaLike(Reaction):
return element
class DiasporaRequest(Relationship):
class DiasporaRequest(DiasporaEntityMixin, Relationship):
"""Diaspora relationship request."""
relationship = "sharing"

Wyświetl plik

@ -3,7 +3,7 @@ from datetime import datetime
from lxml import etree
from federation.entities.base import Image, Relationship
from federation.entities.base import Image, Relationship, Post, Reaction, Comment
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest
MAPPINGS = {
@ -69,3 +69,33 @@ def transform_attributes(attrs):
else:
transformed[key] = value
return transformed
def get_outbound_entity(entity):
"""Get the correct outbound entity for this protocol.
We might have to look at entity values to decide the correct outbound entity.
If we cannot find one, we should raise as conversion cannot be guaranteed to the given protocol.
Args:
entity - any of the base entity types from federation.entities.base
Returns:
An instance of the correct protocol specific entity.
"""
cls = entity.__class__
if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike]:
# Already fine
return entity
elif cls == Post:
return DiasporaPost.from_base(entity)
elif cls == Comment:
return DiasporaComment.from_base(entity)
elif cls == Reaction:
if entity.reaction == "like":
return DiasporaLike.from_base(entity)
elif cls == Relationship:
if entity.relationship in ["sharing", "following"]:
# Unfortunately we must send out in both cases since in Diaspora they are the same thing
return DiasporaRequest.from_base(entity)
raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.")

Wyświetl plik

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import inspect
from dateutil.tz import tzlocal, tzutc
from lxml import etree
@ -32,3 +34,17 @@ def struct_to_xml(node, struct):
for obj in struct:
for k, v in obj.items():
etree.SubElement(node, k).text = v
def get_base_attributes(entity):
"""Build a dict of attributes of an entity.
Returns attributes and their values, ignoring any properties, functions and anything that starts
with an underscore.
"""
attributes = {}
cls = entity.__class__
for attr, _ in inspect.getmembers(cls, lambda o: not isinstance(o, property) and not inspect.isroutine(o)):
if not attr.startswith("_"):
attributes[attr] = getattr(entity, attr)
return attributes

Wyświetl plik

@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
from datetime import datetime
import pytest
from federation.entities.base import Comment, Post, Reaction, Relationship
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest
from federation.entities.diaspora.mappers import message_to_objects
from federation.entities.diaspora.mappers import message_to_objects, get_outbound_entity
from federation.tests.fixtures.payloads import DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, \
DIASPORA_REQUEST
class TestDiasporaEntityMappersReceive(object):
def test_message_to_objects_simple_post(self):
entities = message_to_objects(DIASPORA_POST_SIMPLE)
assert len(entities) == 1
@ -61,3 +62,43 @@ class TestDiasporaEntityMappersReceive(object):
assert following.target_handle == "alice@alice.diaspora.example.org"
assert sharing.relationship == "sharing"
assert following.relationship == "following"
class TestGetOutboundEntity(object):
def test_already_fine_entities_are_returned_as_is(self):
entity = DiasporaPost()
assert get_outbound_entity(entity) == entity
entity = DiasporaLike()
assert get_outbound_entity(entity) == entity
entity = DiasporaComment()
assert get_outbound_entity(entity) == entity
entity = DiasporaRequest()
assert get_outbound_entity(entity) == entity
def test_post_is_converted_to_diasporapost(self):
entity = Post()
assert isinstance(get_outbound_entity(entity), DiasporaPost)
def test_comment_is_converted_to_diasporacomment(self):
entity = Comment()
assert isinstance(get_outbound_entity(entity), DiasporaComment)
def test_reaction_of_like_is_converted_to_diasporaplike(self):
entity = Reaction(reaction="like")
assert isinstance(get_outbound_entity(entity), DiasporaLike)
def test_relationship_of_sharing_or_following_is_converted_to_diasporarequest(self):
entity = Relationship(relationship="sharing")
assert isinstance(get_outbound_entity(entity), DiasporaRequest)
entity = Relationship(relationship="following")
assert isinstance(get_outbound_entity(entity), DiasporaRequest)
def test_other_reaction_raises(self):
entity = Reaction(reaction="foo")
with pytest.raises(ValueError):
get_outbound_entity(entity)
def test_other_relation_raises(self):
entity = Relationship(relationship="foo")
with pytest.raises(ValueError):
get_outbound_entity(entity)

Wyświetl plik

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
from federation.entities.base import Post
from federation.entities.diaspora.utils import get_base_attributes
class TestGetBaseAttributes(object):
def test_get_base_attributes_returns_only_intended_attributes(self):
entity = Post()
attrs = get_base_attributes(entity).keys()
assert set(attrs) == {
'created_at', 'guid', 'handle', 'location', 'photos', 'provider_display_name', 'public', 'raw_content'
}

Wyświetl plik

@ -11,7 +11,6 @@ from federation.tests.fixtures.payloads import UNENCRYPTED_DIASPORA_PAYLOAD
class TestHandleReceiveProtocolIdentification(object):
def test_handle_receive_routes_to_identified_protocol(self):
payload = UNENCRYPTED_DIASPORA_PAYLOAD
with patch.object(
@ -31,7 +30,6 @@ class TestHandleReceiveProtocolIdentification(object):
class TestHandleCreatePayloadBuildsAPayload(object):
def test_handle_create_payload_builds_an_xml(self):
from_user = Mock(private_key=RSA.generate(2048), handle="foobar@domain.tld")
to_user = Mock(key=RSA.generate(2048).publickey())
@ -42,3 +40,13 @@ class TestHandleCreatePayloadBuildsAPayload(object):
assert len(parts) == 2
assert parts[0] == "xml"
assert len(parts[1]) > 0
@patch("federation.controllers.get_outbound_entity")
def test_handle_create_payload_calls_get_outbound_entity(self, mock_get_outbound_entity):
mock_get_outbound_entity.return_value = DiasporaPost()
from_user = Mock(private_key=RSA.generate(2048), handle="foobar@domain.tld")
to_user = Mock(key=RSA.generate(2048).publickey())
entity = DiasporaPost()
handle_create_payload(from_user, to_user, entity)
assert mock_get_outbound_entity.called