kopia lustrzana https://gitlab.com/jaywink/federation
Merge branch 'activitypub-retractions' into 'master'
Add support for Retraction to/from ActivityPub See merge request jaywink/federation!150merge-requests/151/head
commit
d9637a6ae5
|
@ -49,6 +49,8 @@
|
||||||
|
|
||||||
* Network helper utility `fetch_document` can now also take a dictionary of `headers`. They will be passed to the underlying `requests` method call as is.
|
* Network helper utility `fetch_document` can now also take a dictionary of `headers`. They will be passed to the underlying `requests` method call as is.
|
||||||
|
|
||||||
|
* `Retraction` entity can now also have an `entity_type` of `Object`. Receivers will need to find the correct object using `target_id` only. This is currently only relevant for ActivityPub where retraction messages do not refer to object type.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
* **Backwards incompatible.** Support for Legacy Diaspora payloads have been removed to reduce the amount of code needed to maintain while refactoring for ActivityPub.
|
* **Backwards incompatible.** Support for Legacy Diaspora payloads have been removed to reduce the amount of code needed to maintain while refactoring for ActivityPub.
|
||||||
|
|
|
@ -10,7 +10,7 @@ from federation.entities.activitypub.constants import (
|
||||||
from federation.entities.activitypub.enums import ActorType, ObjectType, ActivityType
|
from federation.entities.activitypub.enums import ActorType, ObjectType, ActivityType
|
||||||
from federation.entities.activitypub.mixins import ActivitypubObjectMixin, ActivitypubActorMixin
|
from federation.entities.activitypub.mixins import ActivitypubObjectMixin, ActivitypubActorMixin
|
||||||
from federation.entities.activitypub.objects import ImageObject
|
from federation.entities.activitypub.objects import ImageObject
|
||||||
from federation.entities.base import Profile, Post, Follow, Accept, Comment
|
from federation.entities.base import Profile, Post, Follow, Accept, Comment, Retraction
|
||||||
from federation.outbound import handle_send
|
from federation.outbound import handle_send
|
||||||
from federation.types import UserType
|
from federation.types import UserType
|
||||||
from federation.utils.text import with_slash
|
from federation.utils.text import with_slash
|
||||||
|
@ -206,3 +206,21 @@ class ActivitypubProfile(ActivitypubActorMixin, Profile):
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.warning("ActivitypubProfile.to_as2 - failed to set profile icon: %s", ex)
|
logger.warning("ActivitypubProfile.to_as2 - failed to set profile icon: %s", ex)
|
||||||
return as2
|
return as2
|
||||||
|
|
||||||
|
|
||||||
|
class ActivitypubRetraction(ActivitypubObjectMixin, Retraction):
|
||||||
|
_type = ObjectType.TOMBSTONE.value
|
||||||
|
|
||||||
|
def to_as2(self) -> Dict:
|
||||||
|
as2 = {
|
||||||
|
"@context": CONTEXTS_DEFAULT,
|
||||||
|
"id": self.activity_id,
|
||||||
|
"type": ActivityType.DELETE.value,
|
||||||
|
"actor": self.actor_id,
|
||||||
|
"object": {
|
||||||
|
"id": self.target_id,
|
||||||
|
"type": self._type,
|
||||||
|
},
|
||||||
|
"published": self.created_at.isoformat(),
|
||||||
|
}
|
||||||
|
return as2
|
||||||
|
|
|
@ -22,3 +22,4 @@ class ActorType(EnumBase):
|
||||||
|
|
||||||
class ObjectType(EnumBase):
|
class ObjectType(EnumBase):
|
||||||
NOTE = "Note"
|
NOTE = "Note"
|
||||||
|
TOMBSTONE = "Tombstone"
|
||||||
|
|
|
@ -3,8 +3,9 @@ from typing import List, Callable, Dict, Union
|
||||||
|
|
||||||
from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
|
from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
|
||||||
from federation.entities.activitypub.entities import (
|
from federation.entities.activitypub.entities import (
|
||||||
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment)
|
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment,
|
||||||
from federation.entities.base import Follow, Profile, Accept, Post, Comment
|
ActivitypubRetraction)
|
||||||
|
from federation.entities.base import Follow, Profile, Accept, Post, Comment, Retraction
|
||||||
from federation.entities.mixins import BaseEntity
|
from federation.entities.mixins import BaseEntity
|
||||||
from federation.types import UserType
|
from federation.types import UserType
|
||||||
|
|
||||||
|
@ -14,6 +15,7 @@ logger = logging.getLogger("federation")
|
||||||
MAPPINGS = {
|
MAPPINGS = {
|
||||||
"Accept": ActivitypubAccept,
|
"Accept": ActivitypubAccept,
|
||||||
"Article": ActivitypubPost,
|
"Article": ActivitypubPost,
|
||||||
|
"Delete": ActivitypubRetraction,
|
||||||
"Follow": ActivitypubFollow, # Technically not correct, but for now we support only following profiles
|
"Follow": ActivitypubFollow, # Technically not correct, but for now we support only following profiles
|
||||||
"Note": ActivitypubPost,
|
"Note": ActivitypubPost,
|
||||||
"Page": ActivitypubPost,
|
"Page": ActivitypubPost,
|
||||||
|
@ -27,7 +29,9 @@ def element_to_objects(payload: Dict) -> List:
|
||||||
Transform an Element to a list of entities recursively.
|
Transform an Element to a list of entities recursively.
|
||||||
"""
|
"""
|
||||||
entities = []
|
entities = []
|
||||||
if isinstance(payload.get('object'), dict) and payload["object"].get('type'):
|
if payload.get('type') == "Delete":
|
||||||
|
cls = ActivitypubRetraction
|
||||||
|
elif isinstance(payload.get('object'), dict) and payload["object"].get('type'):
|
||||||
if payload["object"]["type"] == "Note" and payload["object"].get("inReplyTo"):
|
if payload["object"]["type"] == "Note" and payload["object"].get("inReplyTo"):
|
||||||
cls = ActivitypubComment
|
cls = ActivitypubComment
|
||||||
else:
|
else:
|
||||||
|
@ -80,7 +84,10 @@ def get_outbound_entity(entity: BaseEntity, private_key):
|
||||||
return entity
|
return entity
|
||||||
outbound = None
|
outbound = None
|
||||||
cls = entity.__class__
|
cls = entity.__class__
|
||||||
if cls in [ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubPost, ActivitypubComment]:
|
if cls in [
|
||||||
|
ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
|
||||||
|
ActivitypubRetraction,
|
||||||
|
]:
|
||||||
# Already fine
|
# Already fine
|
||||||
outbound = entity
|
outbound = entity
|
||||||
elif cls == Accept:
|
elif cls == Accept:
|
||||||
|
@ -91,6 +98,8 @@ def get_outbound_entity(entity: BaseEntity, private_key):
|
||||||
outbound = ActivitypubPost.from_base(entity)
|
outbound = ActivitypubPost.from_base(entity)
|
||||||
elif cls == Profile:
|
elif cls == Profile:
|
||||||
outbound = ActivitypubProfile.from_base(entity)
|
outbound = ActivitypubProfile.from_base(entity)
|
||||||
|
elif cls == Retraction:
|
||||||
|
outbound = ActivitypubRetraction.from_base(entity)
|
||||||
elif cls == Comment:
|
elif cls == Comment:
|
||||||
outbound = ActivitypubComment.from_base(entity)
|
outbound = ActivitypubComment.from_base(entity)
|
||||||
if not outbound:
|
if not outbound:
|
||||||
|
@ -121,7 +130,13 @@ def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dic
|
||||||
if value is None:
|
if value is None:
|
||||||
value = ""
|
value = ""
|
||||||
if key == "id":
|
if key == "id":
|
||||||
if is_object or cls == ActivitypubProfile:
|
if is_object:
|
||||||
|
if cls == ActivitypubRetraction:
|
||||||
|
transformed["target_id"] = value
|
||||||
|
transformed["entity_type"] = "Object"
|
||||||
|
else:
|
||||||
|
transformed["id"] = value
|
||||||
|
elif cls == ActivitypubProfile:
|
||||||
transformed["id"] = value
|
transformed["id"] = value
|
||||||
else:
|
else:
|
||||||
transformed["activity_id"] = value
|
transformed["activity_id"] = value
|
||||||
|
|
|
@ -139,6 +139,22 @@ class TestEntitiesConvertToAS2:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_retraction_to_as2(self, activitypubretraction):
|
||||||
|
result = activitypubretraction.to_as2()
|
||||||
|
assert result == {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
],
|
||||||
|
'type': 'Delete',
|
||||||
|
'id': 'http://127.0.0.1:8000/post/123456/#delete',
|
||||||
|
'actor': 'http://127.0.0.1:8000/profile/123456/',
|
||||||
|
'object': {
|
||||||
|
'id': 'http://127.0.0.1:8000/post/123456/',
|
||||||
|
'type': 'Tombstone',
|
||||||
|
},
|
||||||
|
'published': '2019-04-27T00:00:00',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestEntitiesPostReceive:
|
class TestEntitiesPostReceive:
|
||||||
@patch("federation.utils.activitypub.retrieve_and_parse_profile", autospec=True)
|
@patch("federation.utils.activitypub.retrieve_and_parse_profile", autospec=True)
|
||||||
|
|
|
@ -3,12 +3,13 @@ from unittest.mock import patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from federation.entities.activitypub.entities import (
|
from federation.entities.activitypub.entities import (
|
||||||
ActivitypubFollow, ActivitypubAccept, ActivitypubProfile, ActivitypubPost, ActivitypubComment)
|
ActivitypubFollow, ActivitypubAccept, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
|
||||||
|
ActivitypubRetraction)
|
||||||
from federation.entities.activitypub.mappers import message_to_objects, get_outbound_entity
|
from federation.entities.activitypub.mappers import message_to_objects, get_outbound_entity
|
||||||
from federation.entities.base import Accept, Follow, Profile, Post, Comment
|
from federation.entities.base import Accept, Follow, Profile, Post, Comment
|
||||||
from federation.tests.fixtures.payloads import (
|
from federation.tests.fixtures.payloads import (
|
||||||
ACTIVITYPUB_FOLLOW, ACTIVITYPUB_PROFILE, ACTIVITYPUB_PROFILE_INVALID, ACTIVITYPUB_UNDO_FOLLOW, ACTIVITYPUB_POST,
|
ACTIVITYPUB_FOLLOW, ACTIVITYPUB_PROFILE, ACTIVITYPUB_PROFILE_INVALID, ACTIVITYPUB_UNDO_FOLLOW, ACTIVITYPUB_POST,
|
||||||
ACTIVITYPUB_COMMENT)
|
ACTIVITYPUB_COMMENT, ACTIVITYPUB_RETRACTION)
|
||||||
|
|
||||||
|
|
||||||
class TestActivitypubEntityMappersReceive:
|
class TestActivitypubEntityMappersReceive:
|
||||||
|
@ -150,15 +151,14 @@ class TestActivitypubEntityMappersReceive:
|
||||||
entity = entities[0]
|
entity = entities[0]
|
||||||
assert entity._receiving_actor_id == "bob@example.com"
|
assert entity._receiving_actor_id == "bob@example.com"
|
||||||
|
|
||||||
@pytest.mark.skip
|
|
||||||
def test_message_to_objects_retraction(self):
|
def test_message_to_objects_retraction(self):
|
||||||
entities = message_to_objects(DIASPORA_RETRACTION, "bob@example.com")
|
entities = message_to_objects(ACTIVITYPUB_RETRACTION, "https://friendica.feneas.org/profile/jaywink")
|
||||||
assert len(entities) == 1
|
assert len(entities) == 1
|
||||||
entity = entities[0]
|
entity = entities[0]
|
||||||
assert isinstance(entity, DiasporaRetraction)
|
assert isinstance(entity, ActivitypubRetraction)
|
||||||
assert entity.handle == "bob@example.com"
|
assert entity.actor_id == "https://friendica.feneas.org/profile/jaywink"
|
||||||
assert entity.target_guid == "x" * 16
|
assert entity.target_id == "https://friendica.feneas.org/objects/76158462-165d-3386-aa23-ba2090614385"
|
||||||
assert entity.entity_type == "Post"
|
assert entity.entity_type == "Object"
|
||||||
|
|
||||||
@pytest.mark.skip
|
@pytest.mark.skip
|
||||||
def test_message_to_objects_accounce(self):
|
def test_message_to_objects_accounce(self):
|
||||||
|
|
|
@ -2,7 +2,8 @@ import pytest
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from federation.entities.activitypub.entities import (
|
from federation.entities.activitypub.entities import (
|
||||||
ActivitypubPost, ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubComment)
|
ActivitypubPost, ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubComment,
|
||||||
|
ActivitypubRetraction)
|
||||||
from federation.entities.base import Profile
|
from federation.entities.base import Profile
|
||||||
from federation.entities.diaspora.entities import (
|
from federation.entities.diaspora.entities import (
|
||||||
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
|
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
|
||||||
|
@ -72,6 +73,17 @@ def activitypubprofile():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def activitypubretraction():
|
||||||
|
with freeze_time("2019-04-27"):
|
||||||
|
return ActivitypubRetraction(
|
||||||
|
target_id="http://127.0.0.1:8000/post/123456/",
|
||||||
|
activity_id="http://127.0.0.1:8000/post/123456/#delete",
|
||||||
|
actor_id="http://127.0.0.1:8000/profile/123456/",
|
||||||
|
entity_type="Post",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def activitypubundofollow():
|
def activitypubundofollow():
|
||||||
return ActivitypubFollow(
|
return ActivitypubFollow(
|
||||||
|
|
|
@ -144,6 +144,45 @@ ACTIVITYPUB_PROFILE_INVALID = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ACTIVITYPUB_RETRACTION = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
{
|
||||||
|
'vcard': 'http://www.w3.org/2006/vcard/ns#',
|
||||||
|
'dfrn': 'http://purl.org/macgirvin/dfrn/1.0/',
|
||||||
|
'diaspora': 'https://diasporafoundation.org/ns/',
|
||||||
|
'litepub': 'http://litepub.social/ns#',
|
||||||
|
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
|
||||||
|
'sensitive': 'as:sensitive',
|
||||||
|
'Hashtag': 'as:Hashtag',
|
||||||
|
'directMessage': 'litepub:directMessage',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'id': 'https://friendica.feneas.org/objects/76158462-165d-3386-aa23-ba2090614385#Delete',
|
||||||
|
'type': 'Delete',
|
||||||
|
'actor': 'https://friendica.feneas.org/profile/jaywink',
|
||||||
|
'published': '2019-07-20T21:24:58Z',
|
||||||
|
'instrument': {
|
||||||
|
'type': 'Service',
|
||||||
|
'name': "Friendica 'Dalmatian Bellflower' 2019.06-1313",
|
||||||
|
'url': 'https://friendica.feneas.org',
|
||||||
|
},
|
||||||
|
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
'cc': ['https://friendica.feneas.org/followers/jaywink'],
|
||||||
|
'object': {
|
||||||
|
'id': 'https://friendica.feneas.org/objects/76158462-165d-3386-aa23-ba2090614385',
|
||||||
|
'type': 'Tombstone',
|
||||||
|
},
|
||||||
|
'signature': {
|
||||||
|
'type': 'RsaSignature2017',
|
||||||
|
'nonce': 'de299d5c8074548d8022d31059b4735870f29ea85d78c5214a423038273c5e5c',
|
||||||
|
'creator': 'https://friendica.feneas.org/profile/jaywink#main-key',
|
||||||
|
'created': '2019-07-20T21:39:13Z',
|
||||||
|
'signatureValue': 'lotsoftext',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
ACTIVITYPUB_UNDO_FOLLOW = {
|
ACTIVITYPUB_UNDO_FOLLOW = {
|
||||||
"@context": [
|
"@context": [
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
|
Ładowanie…
Reference in New Issue