kopia lustrzana https://gitlab.com/jaywink/federation
commit
6d67fe05eb
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
"""
|
||||||
|
|
Ładowanie…
Reference in New Issue