Merge pull request #55 from jaywink/retractions

Add Retraction entity
merge-requests/130/head
Jason Robinson 2016-10-01 22:53:48 +03:00 zatwierdzone przez GitHub
commit 6d67fe05eb
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 ## [0.7.0] - 2016-09-15
### Backwards incompatible changes ### Backwards incompatible changes

Wyświetl plik

@ -5,6 +5,11 @@ import warnings
from dirty_validators.basic import Email from dirty_validators.basic import Email
__all__ = (
"Post", "Image", "Comment", "Reaction", "Relationship", "Profile", "Retraction"
)
class BaseEntity(object): class BaseEntity(object):
_required = [] _required = []
@ -75,6 +80,18 @@ class GUIDMixin(BaseEntity):
raise ValueError("GUID must be at least 16 characters") 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): class HandleMixin(BaseEntity):
handle = "" handle = ""
@ -143,7 +160,7 @@ class Image(GUIDMixin, HandleMixin, PublicMixin, CreatedAtMixin, BaseEntity):
self._required += ["remote_path", "remote_name"] self._required += ["remote_path", "remote_name"]
class ParticipationMixin(BaseEntity): class ParticipationMixin(TargetGUIDMixin):
"""Reflects a participation to something.""" """Reflects a participation to something."""
target_guid = "" target_guid = ""
participation = "" participation = ""
@ -152,7 +169,7 @@ class ParticipationMixin(BaseEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._required += ["target_guid", "participation"] self._required += ["participation"]
def validate_participation(self): def validate_participation(self):
"""Ensure participation is of a certain type.""" """Ensure participation is of a certain type."""
@ -231,3 +248,17 @@ class Profile(CreatedAtMixin, HandleMixin, RawContentMixin, PublicMixin, GUIDMix
validator = Email() validator = Email()
if not validator.is_valid(self.email): if not validator.is_valid(self.email):
raise ValueError("Email is not valid") 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 -*- # -*- coding: utf-8 -*-
from lxml import etree 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.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes
from federation.utils.diaspora import retrieve_and_parse_profile 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")) profile = retrieve_and_parse_profile(attributes.get("handle"))
attributes["guid"] = profile.guid attributes["guid"] = profile.guid
return attributes 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 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 ( from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile) DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, DiasporaProfile, DiasporaRetraction)
logger = logging.getLogger("social-federation") logger = logging.getLogger("social-federation")
@ -18,6 +18,7 @@ MAPPINGS = {
"like": DiasporaLike, "like": DiasporaLike,
"request": DiasporaRequest, "request": DiasporaRequest,
"profile": DiasporaProfile, "profile": DiasporaProfile,
"retraction": DiasporaRetraction,
} }
BOOLEAN_KEYS = [ BOOLEAN_KEYS = [
@ -73,7 +74,7 @@ def transform_attributes(attrs):
for key, value in attrs.items(): for key, value in attrs.items():
if key in ["raw_message", "text"]: if key in ["raw_message", "text"]:
transformed["raw_content"] = value transformed["raw_content"] = value
elif key in ["diaspora_handle", "sender_handle"]: elif key in ["diaspora_handle", "sender_handle", "author"]:
transformed["handle"] = value transformed["handle"] = value
elif key == "recipient_handle": elif key == "recipient_handle":
transformed["target_handle"] = value transformed["target_handle"] = value
@ -99,6 +100,8 @@ def transform_attributes(attrs):
transformed["raw_content"] = value transformed["raw_content"] = value
elif key == "searchable": elif key == "searchable":
transformed["public"] = True if value == "true" else False 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: elif key in BOOLEAN_KEYS:
transformed[key] = True if value == "true" else False transformed[key] = True if value == "true" else False
elif key in DATETIME_KEYS: elif key in DATETIME_KEYS:
@ -137,4 +140,6 @@ def get_outbound_entity(entity):
return DiasporaRequest.from_base(entity) return DiasporaRequest.from_base(entity)
elif cls == Profile: elif cls == Profile:
return DiasporaProfile.from_base(entity) 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.") 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.base import Profile
from federation.entities.diaspora.entities import DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, \ from federation.entities.diaspora.entities import DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, \
DiasporaProfile DiasporaProfile, DiasporaRetraction
class TestEntitiesConvertToXML(object): class TestEntitiesConvertToXML(object):
@ -66,6 +66,14 @@ class TestEntitiesConvertToXML(object):
b"<nsfw>false</nsfw><tag_string>#socialfederation #federation</tag_string></profile>" b"<nsfw>false</nsfw><tag_string>#socialfederation #federation</tag_string></profile>"
assert etree.tostring(result) == converted 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): class TestDiasporaProfileFillExtraAttributes(object):
def test_raises_if_no_handle(self): def test_raises_if_no_handle(self):
@ -79,3 +87,17 @@ class TestDiasporaProfileFillExtraAttributes(object):
attrs = {"handle": "foo"} attrs = {"handle": "foo"}
attrs = DiasporaProfile.fill_extra_attributes(attrs) attrs = DiasporaProfile.fill_extra_attributes(attrs)
assert attrs == {"handle": "foo", "guid": "guidguidguidguid"} 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 import pytest
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.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, \ from federation.entities.diaspora.entities import (
DiasporaProfile DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest,
DiasporaProfile, DiasporaRetraction
)
from federation.entities.diaspora.mappers import message_to_objects, get_outbound_entity 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, \ from federation.tests.fixtures.payloads import (
DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE,
DIASPORA_REQUEST, DIASPORA_PROFILE, DIASPORA_POST_INVALID, DIASPORA_RETRACTION
)
def mock_fill(attributes): def mock_fill(attributes):
@ -90,6 +94,15 @@ class TestDiasporaEntityMappersReceive(object):
assert profile.nsfw == False assert profile.nsfw == False
assert profile.tag_list == ["socialfederation", "federation"] 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") @patch("federation.entities.diaspora.mappers.logger.error")
def test_invalid_entity_logs_an_error(self, mock_logger): def test_invalid_entity_logs_an_error(self, mock_logger):
entities = message_to_objects(DIASPORA_POST_INVALID) entities = message_to_objects(DIASPORA_POST_INVALID)
@ -118,7 +131,7 @@ class TestGetOutboundEntity(object):
entity = Comment() entity = Comment()
assert isinstance(get_outbound_entity(entity), DiasporaComment) 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") entity = Reaction(reaction="like")
assert isinstance(get_outbound_entity(entity), DiasporaLike) assert isinstance(get_outbound_entity(entity), DiasporaLike)
@ -141,3 +154,7 @@ class TestGetOutboundEntity(object):
entity = Relationship(relationship="foo") entity = Relationship(relationship="foo")
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_outbound_entity(entity) 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 import pytest
from federation.entities.base import BaseEntity, Relationship, Profile, RawContentMixin, GUIDMixin, HandleMixin, \ from federation.entities.base import BaseEntity, Relationship, Profile, RawContentMixin, GUIDMixin, HandleMixin, \
PublicMixin, Image PublicMixin, Image, Retraction
from federation.tests.factories.entities import TaggedPostFactory, PostFactory from federation.tests.factories.entities import TaggedPostFactory, PostFactory
@ -113,3 +113,28 @@ class TestImageEntity(object):
) )
with pytest.raises(ValueError): with pytest.raises(ValueError):
entity.validate() 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> </post>
</XML> </XML>
""" """
DIASPORA_RETRACTION = """<XML>
<post>
<retraction>
<author>bob@example.com</author>
<target_guid>xxxxxxxxxxxxxxxx</target_guid>
<target_type>Post</target_type>
</retraction>
</post>
</XML>
"""