Add support for ActivityPub Announce

Becomes a Share entity.

Refs: https://git.feneas.org/socialhome/socialhome/issues/522
merge-requests/152/head
Jason Robinson 2019-08-05 01:15:11 +03:00
rodzic 91cb60aac5
commit 79f580d01f
8 zmienionych plików z 96 dodań i 36 usunięć

Wyświetl plik

@ -8,9 +8,9 @@ from federation.entities.activitypub.constants import (
CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_SENSITIVE, CONTEXT_HASHTAG,
CONTEXT_LD_SIGNATURES)
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, ActivitypubEntityMixin
from federation.entities.activitypub.objects import ImageObject
from federation.entities.base import Profile, Post, Follow, Accept, Comment, Retraction
from federation.entities.base import Profile, Post, Follow, Accept, Comment, Retraction, Share
from federation.outbound import handle_send
from federation.types import UserType
from federation.utils.text import with_slash
@ -224,3 +224,18 @@ class ActivitypubRetraction(ActivitypubObjectMixin, Retraction):
"published": self.created_at.isoformat(),
}
return as2
class ActivitypubShare(ActivitypubEntityMixin, Share):
_type = ActivityType.ANNOUNCE.value
def to_as2(self) -> Dict:
as2 = {
"@context": CONTEXTS_DEFAULT,
"id": self.activity_id,
"type": self._type,
"actor": self.actor_id,
"object": self.target_id,
"published": self.created_at.isoformat(),
}
return as2

Wyświetl plik

@ -9,6 +9,7 @@ class EnumBase(Enum):
class ActivityType(EnumBase):
ACCEPT = "Accept"
ANNOUNCE = "Announce"
CREATE = "Create"
DELETE = "Delete"
FOLLOW = "Follow"

Wyświetl plik

@ -4,8 +4,8 @@ from typing import List, Callable, Dict, Union, Optional
from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
from federation.entities.activitypub.entities import (
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment,
ActivitypubRetraction)
from federation.entities.base import Follow, Profile, Accept, Post, Comment, Retraction
ActivitypubRetraction, ActivitypubShare)
from federation.entities.base import Follow, Profile, Accept, Post, Comment, Retraction, Share
from federation.entities.mixins import BaseEntity
from federation.types import UserType, ReceiverVariant
@ -14,6 +14,7 @@ logger = logging.getLogger("federation")
MAPPINGS = {
"Accept": ActivitypubAccept,
"Announce": ActivitypubShare,
"Article": ActivitypubPost,
"Delete": ActivitypubRetraction,
"Follow": ActivitypubFollow, # Technically not correct, but for now we support only following profiles
@ -131,7 +132,7 @@ def get_outbound_entity(entity: BaseEntity, private_key):
cls = entity.__class__
if cls in [
ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
ActivitypubRetraction,
ActivitypubRetraction, ActivitypubShare,
]:
# Already fine
outbound = entity
@ -147,6 +148,8 @@ def get_outbound_entity(entity: BaseEntity, private_key):
outbound = ActivitypubRetraction.from_base(entity)
elif cls == Comment:
outbound = ActivitypubComment.from_base(entity)
elif cls == Share:
outbound = ActivitypubShare.from_base(entity)
if not outbound:
raise ValueError("Don't know how to convert this base entity to ActivityPub protocol entities.")
# TODO LDS signing
@ -181,7 +184,7 @@ def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dic
transformed["entity_type"] = "Object"
else:
transformed["id"] = value
elif cls == ActivitypubProfile:
elif cls in (ActivitypubProfile, ActivitypubShare):
transformed["id"] = value
else:
transformed["activity_id"] = value

Wyświetl plik

@ -5,10 +5,10 @@ from dirty_validators.basic import Email
from federation.entities.activitypub.enums import ActivityType
from federation.entities.mixins import (
PublicMixin, TargetIDMixin, ParticipationMixin, CreatedAtMixin, RawContentMixin, OptionalRawContentMixin,
EntityTypeMixin, ProviderDisplayNameMixin, RootTargetIDMixin)
EntityTypeMixin, ProviderDisplayNameMixin, RootTargetIDMixin, BaseEntity)
class Accept(CreatedAtMixin, TargetIDMixin):
class Accept(CreatedAtMixin, TargetIDMixin, BaseEntity):
"""An acceptance message for some target."""
def __init__(self, *args, **kwargs):
@ -17,7 +17,7 @@ class Accept(CreatedAtMixin, TargetIDMixin):
self._required.remove('id')
class Image(PublicMixin, OptionalRawContentMixin, CreatedAtMixin):
class Image(PublicMixin, OptionalRawContentMixin, CreatedAtMixin, BaseEntity):
"""Reflects a single image, possibly linked to another object."""
remote_path = ""
remote_name = ""
@ -34,7 +34,7 @@ class Image(PublicMixin, OptionalRawContentMixin, CreatedAtMixin):
self._required += ["remote_path", "remote_name"]
class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin, RootTargetIDMixin):
class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin, RootTargetIDMixin, BaseEntity):
"""Represents a comment, linked to another object."""
participation = "comment"
url = ""
@ -43,7 +43,7 @@ class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin, RootTargetIDM
_default_activity = ActivityType.CREATE
class Follow(CreatedAtMixin, TargetIDMixin):
class Follow(CreatedAtMixin, TargetIDMixin, BaseEntity):
"""Represents a handle following or unfollowing another handle."""
following = True
@ -54,7 +54,7 @@ class Follow(CreatedAtMixin, TargetIDMixin):
self._required.remove('id')
class Post(RawContentMixin, PublicMixin, CreatedAtMixin, ProviderDisplayNameMixin):
class Post(RawContentMixin, PublicMixin, CreatedAtMixin, ProviderDisplayNameMixin, BaseEntity):
"""Reflects a post, status message, etc, which will be composed from the message or to the message."""
location = ""
url = ""
@ -63,7 +63,7 @@ class Post(RawContentMixin, PublicMixin, CreatedAtMixin, ProviderDisplayNameMixi
_default_activity = ActivityType.CREATE
class Reaction(ParticipationMixin, CreatedAtMixin):
class Reaction(ParticipationMixin, CreatedAtMixin, BaseEntity):
"""Represents a reaction to another object, for example a like."""
participation = "reaction"
reaction = ""
@ -86,7 +86,7 @@ class Reaction(ParticipationMixin, CreatedAtMixin):
))
class Relationship(CreatedAtMixin, TargetIDMixin):
class Relationship(CreatedAtMixin, TargetIDMixin, BaseEntity):
"""Represents a relationship between two handles."""
relationship = ""
@ -104,7 +104,7 @@ class Relationship(CreatedAtMixin, TargetIDMixin):
))
class Profile(CreatedAtMixin, OptionalRawContentMixin, PublicMixin):
class Profile(CreatedAtMixin, OptionalRawContentMixin, PublicMixin, BaseEntity):
"""Represents a profile for a user."""
atom_url = ""
email = ""
@ -141,7 +141,7 @@ class Profile(CreatedAtMixin, OptionalRawContentMixin, PublicMixin):
raise ValueError("Email is not valid")
class Retraction(CreatedAtMixin, TargetIDMixin, EntityTypeMixin):
class Retraction(CreatedAtMixin, TargetIDMixin, EntityTypeMixin, BaseEntity):
"""Represents a retraction of content by author."""
def __init__(self, *args, **kwargs):
@ -151,7 +151,7 @@ class Retraction(CreatedAtMixin, TargetIDMixin, EntityTypeMixin):
class Share(CreatedAtMixin, TargetIDMixin, EntityTypeMixin, OptionalRawContentMixin, PublicMixin,
ProviderDisplayNameMixin):
ProviderDisplayNameMixin, BaseEntity):
"""Represents a share of another entity.
``entity_type`` defaults to "Post" but can be any base entity class name. It should be the class name of the

Wyświetl plik

@ -26,6 +26,17 @@ class TestEntitiesConvertToAS2:
},
}
def test_accounce_to_as2(self, activitypubannounce):
result = activitypubannounce.to_as2()
assert result == {
"@context": CONTEXTS_DEFAULT,
"id": "http://127.0.0.1:8000/post/123456/#create",
"type": "Announce",
"actor": "http://127.0.0.1:8000/profile/123456/",
"object": "http://127.0.0.1:8000/post/012345/",
'published': '2019-08-05T00:00:00',
}
def test_comment_to_as2(self, activitypubcomment):
result = activitypubcomment.to_as2()
assert result == {

Wyświetl plik

@ -4,12 +4,12 @@ import pytest
from federation.entities.activitypub.entities import (
ActivitypubFollow, ActivitypubAccept, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
ActivitypubRetraction)
ActivitypubRetraction, ActivitypubShare)
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_RETRACTION)
ACTIVITYPUB_COMMENT, ACTIVITYPUB_RETRACTION, ACTIVITYPUB_SHARE)
from federation.types import UserType, ReceiverVariant
@ -19,6 +19,18 @@ class TestActivitypubEntityMappersReceive:
message_to_objects(ACTIVITYPUB_FOLLOW, "https://example.com/actor")
assert mock_post_receive.called
def test_message_to_objects__announce(self):
entities = message_to_objects(ACTIVITYPUB_SHARE, "https://mastodon.social/users/jaywink")
assert len(entities) == 1
entity = entities[0]
assert isinstance(entity, ActivitypubShare)
assert entity.actor_id == "https://mastodon.social/users/jaywink"
assert entity.target_id == "https://mastodon.social/users/Gargron/statuses/102559779793316012"
assert entity.id == "https://mastodon.social/users/jaywink/statuses/102560701449465612/activity"
assert entity.public is True
assert entity.entity_type == "Post"
assert entity.raw_content == ""
def test_message_to_objects__follow(self):
entities = message_to_objects(ACTIVITYPUB_FOLLOW, "https://example.com/actor")
assert len(entities) == 1
@ -148,7 +160,7 @@ class TestActivitypubEntityMappersReceive:
"https://diaspodon.fr/users/jaywink",
)
entity = entities[0]
assert set(entity._receivers) == {
UserType(
id='https://diaspodon.fr/users/jaywink', receiver_variant=ReceiverVariant.FOLLOWERS,
@ -168,21 +180,6 @@ class TestActivitypubEntityMappersReceive:
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):
entities = message_to_objects(DIASPORA_RESHARE, "alice@example.org")
assert len(entities) == 1
entity = entities[0]
assert isinstance(entity, DiasporaReshare)
assert entity.handle == "alice@example.org"
assert entity.guid == "a0b53e5029f6013487753131731751e9"
assert entity.provider_display_name == ""
assert entity.target_handle == "bob@example.com"
assert entity.target_guid == "a0b53bc029f6013487753131731751e9"
assert entity.public is True
assert entity.entity_type == "Post"
assert entity.raw_content == ""
@pytest.mark.skip
def test_message_to_objects_reshare_extra_properties(self):
entities = message_to_objects(DIASPORA_RESHARE_WITH_EXTRA_PROPERTIES, "alice@example.org")

Wyświetl plik

@ -3,7 +3,7 @@ from freezegun import freeze_time
from federation.entities.activitypub.entities import (
ActivitypubPost, ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubComment,
ActivitypubRetraction)
ActivitypubRetraction, ActivitypubShare)
from federation.entities.base import Profile
from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
@ -14,6 +14,15 @@ from federation.tests.fixtures.keys import PUBKEY
from federation.tests.fixtures.payloads import DIASPORA_PUBLIC_PAYLOAD
@pytest.fixture
def activitypubannounce():
with freeze_time("2019-08-05"):
return ActivitypubShare(
activity_id="http://127.0.0.1:8000/post/123456/#create",
actor_id="http://127.0.0.1:8000/profile/123456/",
target_id="http://127.0.0.1:8000/post/012345/",
)
@pytest.fixture
def activitypubcomment():
with freeze_time("2019-04-27"):

Wyświetl plik

@ -183,6 +183,30 @@ ACTIVITYPUB_RETRACTION = {
},
}
ACTIVITYPUB_SHARE = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mastodon.social/users/jaywink/statuses/102560701449465612/activity',
'type': 'Announce',
'actor': 'https://mastodon.social/users/jaywink',
'published': '2019-08-04T20:55:09Z',
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'cc': [
'https://mastodon.social/users/Gargron',
'https://mastodon.social/users/jaywink/followers',
],
'object': 'https://mastodon.social/users/Gargron/statuses/102559779793316012',
'atomUri': 'https://mastodon.social/users/jaywink/statuses/102560701449465612/activity',
'signature': {
'type': 'RsaSignature2017',
'creator': 'https://mastodon.social/users/jaywink#main-key',
'created': '2019-08-04T20:55:09Z',
'signatureValue': 'fBW+hqP4ZslMf+1ZebqwuYAhQHvE5atsD/DLzda0eLY8xdf5XdROtoMHfVow5ZSq34w5CIPKOUUPo6aYx5bbLSd'
'JqwhoKOuwbtAmq3UvUp3vsiX671Cc4AL2b7sRL2sH0XfMtl5vpVaZM4LnpzGE3py91tQPCKY+azg6XUxJKOn6Kt'
'bo47LSpXZmzNacsfiiEmF48FlPojRZniz1wKNV+MIvvThIQlaahKAvPYHSF9INwMtlJpnVjc9T+9IkeSuHbNY4x'
'R9huLESZc3iZQk1OPIUsbqmMYVRm1G/WEnPpQwl4rH64YNptpxq8oxvtkECcK1ulT9+XxoCFaLg7pHr9Q==',
},
}
ACTIVITYPUB_UNDO_FOLLOW = {
"@context": [
"https://www.w3.org/ns/activitystreams",