Merge branch 'activitypub-retractions' into 'master'

Add support for Retraction to/from ActivityPub

See merge request jaywink/federation!150
merge-requests/151/head
Jason Robinson 2019-07-20 23:03:18 +00:00
commit d9637a6ae5
8 zmienionych plików z 118 dodań i 15 usunięć

Wyświetl plik

@ -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.
* `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
* **Backwards incompatible.** Support for Legacy Diaspora payloads have been removed to reduce the amount of code needed to maintain while refactoring for ActivityPub.

Wyświetl plik

@ -10,7 +10,7 @@ from federation.entities.activitypub.constants import (
from federation.entities.activitypub.enums import ActorType, ObjectType, ActivityType
from federation.entities.activitypub.mixins import ActivitypubObjectMixin, ActivitypubActorMixin
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.types import UserType
from federation.utils.text import with_slash
@ -206,3 +206,21 @@ class ActivitypubProfile(ActivitypubActorMixin, Profile):
except Exception as ex:
logger.warning("ActivitypubProfile.to_as2 - failed to set profile icon: %s", ex)
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

Wyświetl plik

@ -22,3 +22,4 @@ class ActorType(EnumBase):
class ObjectType(EnumBase):
NOTE = "Note"
TOMBSTONE = "Tombstone"

Wyświetl plik

@ -3,8 +3,9 @@ from typing import List, Callable, Dict, Union
from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
from federation.entities.activitypub.entities import (
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment)
from federation.entities.base import Follow, Profile, Accept, Post, Comment
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment,
ActivitypubRetraction)
from federation.entities.base import Follow, Profile, Accept, Post, Comment, Retraction
from federation.entities.mixins import BaseEntity
from federation.types import UserType
@ -14,6 +15,7 @@ logger = logging.getLogger("federation")
MAPPINGS = {
"Accept": ActivitypubAccept,
"Article": ActivitypubPost,
"Delete": ActivitypubRetraction,
"Follow": ActivitypubFollow, # Technically not correct, but for now we support only following profiles
"Note": ActivitypubPost,
"Page": ActivitypubPost,
@ -27,7 +29,9 @@ def element_to_objects(payload: Dict) -> List:
Transform an Element to a list of entities recursively.
"""
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"):
cls = ActivitypubComment
else:
@ -80,7 +84,10 @@ def get_outbound_entity(entity: BaseEntity, private_key):
return entity
outbound = None
cls = entity.__class__
if cls in [ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubPost, ActivitypubComment]:
if cls in [
ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
ActivitypubRetraction,
]:
# Already fine
outbound = entity
elif cls == Accept:
@ -91,6 +98,8 @@ def get_outbound_entity(entity: BaseEntity, private_key):
outbound = ActivitypubPost.from_base(entity)
elif cls == Profile:
outbound = ActivitypubProfile.from_base(entity)
elif cls == Retraction:
outbound = ActivitypubRetraction.from_base(entity)
elif cls == Comment:
outbound = ActivitypubComment.from_base(entity)
if not outbound:
@ -121,7 +130,13 @@ def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dic
if value is None:
value = ""
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
else:
transformed["activity_id"] = value

Wyświetl plik

@ -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:
@patch("federation.utils.activitypub.retrieve_and_parse_profile", autospec=True)

Wyświetl plik

@ -3,12 +3,13 @@ from unittest.mock import patch
import pytest
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.base import Accept, Follow, Profile, Post, Comment
from federation.tests.fixtures.payloads import (
ACTIVITYPUB_FOLLOW, ACTIVITYPUB_PROFILE, ACTIVITYPUB_PROFILE_INVALID, ACTIVITYPUB_UNDO_FOLLOW, ACTIVITYPUB_POST,
ACTIVITYPUB_COMMENT)
ACTIVITYPUB_COMMENT, ACTIVITYPUB_RETRACTION)
class TestActivitypubEntityMappersReceive:
@ -150,15 +151,14 @@ class TestActivitypubEntityMappersReceive:
entity = entities[0]
assert entity._receiving_actor_id == "bob@example.com"
@pytest.mark.skip
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
entity = entities[0]
assert isinstance(entity, DiasporaRetraction)
assert entity.handle == "bob@example.com"
assert entity.target_guid == "x" * 16
assert entity.entity_type == "Post"
assert isinstance(entity, ActivitypubRetraction)
assert entity.actor_id == "https://friendica.feneas.org/profile/jaywink"
assert entity.target_id == "https://friendica.feneas.org/objects/76158462-165d-3386-aa23-ba2090614385"
assert entity.entity_type == "Object"
@pytest.mark.skip
def test_message_to_objects_accounce(self):

Wyświetl plik

@ -2,7 +2,8 @@ import pytest
from freezegun import freeze_time
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.diaspora.entities import (
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
def activitypubundofollow():
return ActivitypubFollow(

Wyświetl plik

@ -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 = {
"@context": [
"https://www.w3.org/ns/activitystreams",