Add Retraction entity

With DiasporaRetraction counterpart.

Closes #8
merge-requests/130/head
Jason Robinson 2016-10-01 15:51:07 +03:00
rodzic 42fce2493f
commit c7b741913e
8 zmienionych plików z 164 dodań i 14 usunięć

Wyświetl plik

@ -1,3 +1,8 @@
## [unreleased]
### Added
* Added `Retraction` entity with `DiasporaRetraction` counterpart.
## [0.7.0] - 2016-09-15
### Backwards incompatible changes

Wyświetl plik

@ -5,6 +5,11 @@ import warnings
from dirty_validators.basic import Email
__all__ = (
"Post", "Image", "Comment", "Reaction", "Relationship", "Profile", "Retraction"
)
class BaseEntity(object):
_required = []
@ -75,6 +80,18 @@ class GUIDMixin(BaseEntity):
raise ValueError("GUID must be at least 16 characters")
class TargetGUIDMixin(BaseEntity):
target_guid = ""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._required += ["target_guid"]
def validate_target_guid(self):
if len(self.target_guid) < 16:
raise ValueError("Target GUID must be at least 16 characters")
class HandleMixin(BaseEntity):
handle = ""
@ -143,7 +160,7 @@ class Image(GUIDMixin, HandleMixin, PublicMixin, CreatedAtMixin, BaseEntity):
self._required += ["remote_path", "remote_name"]
class ParticipationMixin(BaseEntity):
class ParticipationMixin(TargetGUIDMixin):
"""Reflects a participation to something."""
target_guid = ""
participation = ""
@ -152,7 +169,7 @@ class ParticipationMixin(BaseEntity):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._required += ["target_guid", "participation"]
self._required += ["participation"]
def validate_participation(self):
"""Ensure participation is of a certain type."""
@ -231,3 +248,17 @@ class Profile(CreatedAtMixin, HandleMixin, RawContentMixin, PublicMixin, GUIDMix
validator = Email()
if not validator.is_valid(self.email):
raise ValueError("Email is not valid")
class Retraction(CreatedAtMixin, HandleMixin, TargetGUIDMixin):
"""Represents a retraction of content by author."""
entity_type = ""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._required += ["entity_type"]
def validate_entity_type(self):
"""Ensure type is some entity we know of."""
if self.entity_type not in __all__:
raise ValueError("Entity type %s not recognized." % self.entity_type)

Wyświetl plik

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from lxml import etree
from federation.entities.base import Comment, Post, Reaction, Relationship, Profile
from federation.entities.base import Comment, Post, Reaction, Relationship, Profile, Retraction
from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes
from federation.utils.diaspora import retrieve_and_parse_profile
@ -121,3 +121,37 @@ class DiasporaProfile(DiasporaEntityMixin, Profile):
profile = retrieve_and_parse_profile(attributes.get("handle"))
attributes["guid"] = profile.guid
return attributes
class DiasporaRetraction(DiasporaEntityMixin, Retraction):
"""Diaspora Retraction."""
mapped = {
"Like": "Reaction",
"Photo": "Image",
}
def to_xml(self):
"""Convert to XML message."""
element = etree.Element("retraction")
struct_to_xml(element, [
{"author": self.handle},
{"target_guid": self.target_guid},
{"target_type": DiasporaRetraction.entity_type_to_remote(self.entity_type)},
])
return element
@staticmethod
def entity_type_from_remote(value):
"""Convert entity type between Diaspora names and our Entity names."""
if value in DiasporaRetraction.mapped:
return DiasporaRetraction.mapped[value]
return value
@staticmethod
def entity_type_to_remote(value):
"""Convert entity type between our Entity names and Diaspora names."""
if value in DiasporaRetraction.mapped.values():
values = list(DiasporaRetraction.mapped.values())
index = values.index(value)
return list(DiasporaRetraction.mapped.keys())[index]
return value

Wyświetl plik

@ -4,9 +4,9 @@ from datetime import datetime
from lxml import etree
from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile
from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile, Retraction
from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile)
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction)
logger = logging.getLogger("social-federation")
@ -18,6 +18,7 @@ MAPPINGS = {
"like": DiasporaLike,
"request": DiasporaRequest,
"profile": DiasporaProfile,
"retraction": DiasporaRetraction,
}
BOOLEAN_KEYS = [
@ -73,7 +74,7 @@ def transform_attributes(attrs):
for key, value in attrs.items():
if key in ["raw_message", "text"]:
transformed["raw_content"] = value
elif key in ["diaspora_handle", "sender_handle"]:
elif key in ["diaspora_handle", "sender_handle", "author"]:
transformed["handle"] = value
elif key == "recipient_handle":
transformed["target_handle"] = value
@ -99,6 +100,8 @@ def transform_attributes(attrs):
transformed["raw_content"] = value
elif key == "searchable":
transformed["public"] = True if value == "true" else False
elif key == "target_type":
transformed["entity_type"] = DiasporaRetraction.entity_type_from_remote(value)
elif key in BOOLEAN_KEYS:
transformed[key] = True if value == "true" else False
elif key in DATETIME_KEYS:
@ -137,4 +140,6 @@ def get_outbound_entity(entity):
return DiasporaRequest.from_base(entity)
elif cls == Profile:
return DiasporaProfile.from_base(entity)
elif cls == Retraction:
return DiasporaRetraction.from_base(entity)
raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.")

Wyświetl plik

@ -6,7 +6,7 @@ from lxml import etree
from federation.entities.base import Profile
from federation.entities.diaspora.entities import DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, \
DiasporaProfile
DiasporaProfile, DiasporaRetraction
class TestEntitiesConvertToXML(object):
@ -66,6 +66,14 @@ class TestEntitiesConvertToXML(object):
b"<nsfw>false</nsfw><tag_string>#socialfederation #federation</tag_string></profile>"
assert etree.tostring(result) == converted
def test_retraction_to_xml(self):
entity = DiasporaRetraction(handle="bob@example.com", target_guid="x" * 16, entity_type="Post")
result = entity.to_xml()
assert result.tag == "retraction"
converted = b"<retraction><author>bob@example.com</author>" \
b"<target_guid>xxxxxxxxxxxxxxxx</target_guid><target_type>Post</target_type></retraction>"
assert etree.tostring(result) == converted
class TestDiasporaProfileFillExtraAttributes(object):
def test_raises_if_no_handle(self):
@ -79,3 +87,17 @@ class TestDiasporaProfileFillExtraAttributes(object):
attrs = {"handle": "foo"}
attrs = DiasporaProfile.fill_extra_attributes(attrs)
assert attrs == {"handle": "foo", "guid": "guidguidguidguid"}
class TestDiasporaRetractionEntityConverters(object):
def test_entity_type_from_remote(self):
assert DiasporaRetraction.entity_type_from_remote("Post") == "Post"
assert DiasporaRetraction.entity_type_from_remote("Like") == "Reaction"
assert DiasporaRetraction.entity_type_from_remote("Photo") == "Image"
assert DiasporaRetraction.entity_type_from_remote("Comment") == "Comment"
def test_entity_type_to_remote(self):
assert DiasporaRetraction.entity_type_to_remote("Post") == "Post"
assert DiasporaRetraction.entity_type_to_remote("Reaction") == "Like"
assert DiasporaRetraction.entity_type_to_remote("Image") == "Photo"
assert DiasporaRetraction.entity_type_to_remote("Comment") == "Comment"

Wyświetl plik

@ -4,12 +4,16 @@ from unittest.mock import patch
import pytest
from federation.entities.base import Comment, Post, Reaction, Relationship, Profile
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, \
DiasporaProfile
from federation.entities.base import Comment, Post, Reaction, Relationship, Profile, Retraction
from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest,
DiasporaProfile, DiasporaRetraction
)
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, DIASPORA_PROFILE, DIASPORA_POST_INVALID
from federation.tests.fixtures.payloads import (
DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE,
DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID, DIASPORA_RETRACTION
)
def mock_fill(attributes):
@ -90,6 +94,15 @@ class TestDiasporaEntityMappersReceive(object):
assert profile.nsfw == False
assert profile.tag_list == ["socialfederation", "federation"]
def test_message_to_objects_retraction(self):
entities = message_to_objects(DIASPORA_RETRACTION)
assert len(entities) == 1
entity = entities[0]
assert isinstance(entity, Retraction)
assert entity.handle == "bob@example.com"
assert entity.target_guid == "x" * 16
assert entity.entity_type == "Post"
@patch("federation.entities.diaspora.mappers.logger.error")
def test_invalid_entity_logs_an_error(self, mock_logger):
entities = message_to_objects(DIASPORA_POST_INVALID)
@ -118,7 +131,7 @@ class TestGetOutboundEntity(object):
entity = Comment()
assert isinstance(get_outbound_entity(entity), DiasporaComment)
def test_reaction_of_like_is_converted_to_diasporaplike(self):
def test_reaction_of_like_is_converted_to_diasporalike(self):
entity = Reaction(reaction="like")
assert isinstance(get_outbound_entity(entity), DiasporaLike)
@ -141,3 +154,7 @@ class TestGetOutboundEntity(object):
entity = Relationship(relationship="foo")
with pytest.raises(ValueError):
get_outbound_entity(entity)
def test_retraction_is_converted_to_diasporaretraction(self):
entity = Retraction()
assert isinstance(get_outbound_entity(entity), DiasporaRetraction)

Wyświetl plik

@ -4,7 +4,7 @@ from unittest.mock import Mock
import pytest
from federation.entities.base import BaseEntity, Relationship, Profile, RawContentMixin, GUIDMixin, HandleMixin, \
PublicMixin, Image
PublicMixin, Image, Retraction
from federation.tests.factories.entities import TaggedPostFactory, PostFactory
@ -113,3 +113,28 @@ class TestImageEntity(object):
)
with pytest.raises(ValueError):
entity.validate()
class TestRetractionEntity(object):
def test_instance_creation(self):
entity = Retraction(
handle="foo@example.com", target_guid="x"*16, entity_type="Post"
)
entity.validate()
def test_required_validates(self):
entity = Retraction(
handle="fooexample.com", target_guid="x" * 16, entity_type="Post"
)
with pytest.raises(ValueError):
entity.validate()
entity = Retraction(
handle="foo@example.com", target_guid="x" * 15, entity_type="Post"
)
with pytest.raises(ValueError):
entity.validate()
entity = Retraction(
handle="foo@example.com", target_guid="x" * 16, entity_type="Foo"
)
with pytest.raises(ValueError):
entity.validate()

Wyświetl plik

@ -111,3 +111,14 @@ DIASPORA_PROFILE = """<XML>
</post>
</XML>
"""
DIASPORA_RETRACTION = """<XML>
<post>
<retraction>
<author>bob@example.com</author>
<target_guid>xxxxxxxxxxxxxxxx</target_guid>
<target_type>Post</target_type>
</retraction>
</post>
</XML>
"""