kopia lustrzana https://gitlab.com/jaywink/federation
Add support for ActivityPub Announce
Becomes a Share entity. Refs: https://git.feneas.org/socialhome/socialhome/issues/522merge-requests/152/head
rodzic
91cb60aac5
commit
79f580d01f
|
@ -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
|
||||
|
|
|
@ -9,6 +9,7 @@ class EnumBase(Enum):
|
|||
|
||||
class ActivityType(EnumBase):
|
||||
ACCEPT = "Accept"
|
||||
ANNOUNCE = "Announce"
|
||||
CREATE = "Create"
|
||||
DELETE = "Delete"
|
||||
FOLLOW = "Follow"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 == {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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",
|
||||
|
|
Ładowanie…
Reference in New Issue