kopia lustrzana https://gitlab.com/jaywink/federation
Merge branch 'images-from-ap' into 'master'
Extract images from ActivityPub payloads See merge request jaywink/federation!154merge-requests/155/merge
commit
9617737993
|
@ -5,7 +5,8 @@ from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
|
|||
from federation.entities.activitypub.entities import (
|
||||
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment,
|
||||
ActivitypubRetraction, ActivitypubShare)
|
||||
from federation.entities.base import Follow, Profile, Accept, Post, Comment, Retraction, Share
|
||||
from federation.entities.activitypub.objects import IMAGE_TYPES
|
||||
from federation.entities.base import Follow, Profile, Accept, Post, Comment, Retraction, Share, Image
|
||||
from federation.entities.mixins import BaseEntity
|
||||
from federation.types import UserType, ReceiverVariant
|
||||
|
||||
|
@ -35,6 +36,7 @@ UNDO_MAPPINGS = {
|
|||
"Announce": ActivitypubRetraction,
|
||||
}
|
||||
|
||||
|
||||
def element_to_objects(payload: Dict) -> List:
|
||||
"""
|
||||
Transform an Element to a list of entities.
|
||||
|
@ -65,6 +67,9 @@ def element_to_objects(payload: Dict) -> List:
|
|||
entity._source_object = payload
|
||||
# Extract receivers
|
||||
entity._receivers = extract_receivers(payload)
|
||||
# Extract children
|
||||
if payload.get("object") and isinstance(payload.get("object"), dict):
|
||||
entity._children = extract_attachments(payload.get("object"))
|
||||
|
||||
if hasattr(entity, "post_receive"):
|
||||
entity.post_receive()
|
||||
|
@ -84,6 +89,25 @@ def element_to_objects(payload: Dict) -> List:
|
|||
return entities
|
||||
|
||||
|
||||
def extract_attachments(payload: Dict) -> List[Image]:
|
||||
"""
|
||||
Extract images from attachments.
|
||||
|
||||
There could be other attachments, but currently we only extract images.
|
||||
"""
|
||||
attachments = []
|
||||
for item in payload.get('attachment', []):
|
||||
# noinspection PyProtectedMember
|
||||
if item.get("type") == "Document" and item.get("mediaType") in IMAGE_TYPES:
|
||||
attachments.append(
|
||||
Image(
|
||||
url=item.get('url'),
|
||||
name=item.get('name') or "",
|
||||
)
|
||||
)
|
||||
return attachments
|
||||
|
||||
|
||||
def extract_receiver(payload: Dict, receiver: str) -> Optional[UserType]:
|
||||
"""
|
||||
Transform a single receiver ID to a UserType.
|
||||
|
@ -239,7 +263,7 @@ def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dic
|
|||
elif key == "inReplyTo":
|
||||
transformed["target_id"] = value
|
||||
elif key == "name":
|
||||
transformed["name"] = value
|
||||
transformed["name"] = value or ""
|
||||
elif key == "object" and not is_object:
|
||||
if isinstance(value, dict):
|
||||
if cls == ActivitypubAccept:
|
||||
|
|
|
@ -2,17 +2,18 @@ import attr
|
|||
|
||||
from federation.utils.network import fetch_content_type
|
||||
|
||||
IMAGE_TYPES = (
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class ImageObject:
|
||||
"""
|
||||
An Image object for AS2 serialization.
|
||||
"""
|
||||
_allowed_types = (
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
)
|
||||
url: str = attr.ib()
|
||||
type: str = attr.ib(default="Image")
|
||||
mediaType: str = attr.ib()
|
||||
|
@ -20,6 +21,6 @@ class ImageObject:
|
|||
@mediaType.default
|
||||
def cache_media_type(self):
|
||||
content_type = fetch_content_type(self.url)
|
||||
if content_type in self._allowed_types:
|
||||
if content_type in IMAGE_TYPES:
|
||||
return content_type
|
||||
return ""
|
||||
|
|
|
@ -17,21 +17,18 @@ class Accept(CreatedAtMixin, TargetIDMixin, BaseEntity):
|
|||
self._required.remove('id')
|
||||
|
||||
|
||||
class Image(PublicMixin, OptionalRawContentMixin, CreatedAtMixin, BaseEntity):
|
||||
class Image(OptionalRawContentMixin, CreatedAtMixin, BaseEntity):
|
||||
"""Reflects a single image, possibly linked to another object."""
|
||||
remote_path = ""
|
||||
remote_name = ""
|
||||
linked_type = ""
|
||||
linked_guid = ""
|
||||
url = ""
|
||||
name = ""
|
||||
height = 0
|
||||
width = 0
|
||||
url = ""
|
||||
|
||||
_default_activity = ActivityType.CREATE
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._required += ["remote_path", "remote_name"]
|
||||
self._required += ["url"]
|
||||
|
||||
|
||||
class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin, RootTargetIDMixin, BaseEntity):
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
from datetime import datetime
|
||||
from typing import Callable, List
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
from Crypto.PublicKey.RSA import RsaKey
|
||||
from lxml import etree
|
||||
|
||||
|
@ -131,6 +132,7 @@ def element_to_objects(
|
|||
entity._mentions = entity.extract_mentions()
|
||||
# Do child elements
|
||||
for child in element:
|
||||
# noinspection PyProtectedMember
|
||||
entity._children.extend(element_to_objects(child, sender, user=user))
|
||||
# Add to entities list
|
||||
entities.append(entity)
|
||||
|
@ -219,12 +221,7 @@ def transform_attributes(attrs, cls):
|
|||
elif key in ["target_type"] and cls == DiasporaRetraction:
|
||||
transformed["entity_type"] = DiasporaRetraction.entity_type_from_remote(value)
|
||||
elif key == "remote_photo_path":
|
||||
transformed["remote_path"] = value
|
||||
elif key == "remote_photo_name":
|
||||
transformed["remote_name"] = value
|
||||
elif key == "status_message_guid":
|
||||
transformed["linked_guid"] = value
|
||||
transformed["linked_type"] = "Post"
|
||||
transformed["url"] = f"{value}{attrs.get('remote_photo_name')}"
|
||||
elif key == "author_signature":
|
||||
transformed["signature"] = value
|
||||
elif key in BOOLEAN_KEYS:
|
||||
|
|
|
@ -6,10 +6,11 @@ from federation.entities.activitypub.entities import (
|
|||
ActivitypubFollow, ActivitypubAccept, ActivitypubProfile, ActivitypubPost, ActivitypubComment,
|
||||
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.entities.base import Accept, Follow, Profile, Post, Comment, Image
|
||||
from federation.tests.fixtures.payloads import (
|
||||
ACTIVITYPUB_FOLLOW, ACTIVITYPUB_PROFILE, ACTIVITYPUB_PROFILE_INVALID, ACTIVITYPUB_UNDO_FOLLOW, ACTIVITYPUB_POST,
|
||||
ACTIVITYPUB_COMMENT, ACTIVITYPUB_RETRACTION, ACTIVITYPUB_SHARE, ACTIVITYPUB_RETRACTION_SHARE)
|
||||
ACTIVITYPUB_COMMENT, ACTIVITYPUB_RETRACTION, ACTIVITYPUB_SHARE, ACTIVITYPUB_RETRACTION_SHARE,
|
||||
ACTIVITYPUB_POST_IMAGES)
|
||||
from federation.types import UserType, ReceiverVariant
|
||||
|
||||
|
||||
|
@ -71,25 +72,22 @@ class TestActivitypubEntityMappersReceive:
|
|||
assert post.public is True
|
||||
assert getattr(post, "target_id", None) is None
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_message_to_objects_post_with_photos(self):
|
||||
entities = message_to_objects(DIASPORA_POST_WITH_PHOTOS, "alice@alice.diaspora.example.org")
|
||||
entities = message_to_objects(ACTIVITYPUB_POST_IMAGES, "https://mastodon.social/users/jaywink")
|
||||
assert len(entities) == 1
|
||||
post = entities[0]
|
||||
assert isinstance(post, DiasporaPost)
|
||||
assert isinstance(post, ActivitypubPost)
|
||||
assert len(post._children) == 1
|
||||
photo = post._children[0]
|
||||
assert isinstance(photo, DiasporaImage)
|
||||
assert photo.remote_path == "https://alice.diaspora.example.org/uploads/images/"
|
||||
assert photo.remote_name == "1234.jpg"
|
||||
assert isinstance(photo, Image)
|
||||
assert photo.url == "https://files.mastodon.social/media_attachments/files/017/642/079/original/" \
|
||||
"f51b0aee0ee1f2e1.jpg"
|
||||
assert photo.name == ""
|
||||
assert photo.raw_content == ""
|
||||
assert photo.linked_type == "Post"
|
||||
assert photo.linked_guid == "((guidguidguidguidguidguidguid))"
|
||||
assert photo.height == 120
|
||||
assert photo.width == 120
|
||||
assert photo.guid == "((guidguidguidguidguidguidguif))"
|
||||
assert photo.handle == "alice@alice.diaspora.example.org"
|
||||
assert photo.public == False
|
||||
assert photo.created_at == datetime(2011, 7, 20, 1, 36, 7)
|
||||
assert photo.height == 0
|
||||
assert photo.width == 0
|
||||
assert photo.guid == ""
|
||||
assert photo.handle == ""
|
||||
|
||||
def test_message_to_objects_comment(self):
|
||||
entities = message_to_objects(ACTIVITYPUB_COMMENT, "https://diaspodon.fr/users/jaywink")
|
||||
|
|
|
@ -51,16 +51,13 @@ class TestDiasporaEntityMappersReceive:
|
|||
assert isinstance(post, DiasporaPost)
|
||||
photo = post._children[0]
|
||||
assert isinstance(photo, DiasporaImage)
|
||||
assert photo.remote_path == "https://alice.diaspora.example.org/uploads/images/"
|
||||
assert photo.remote_name == "1234.jpg"
|
||||
assert photo.url == "https://alice.diaspora.example.org/uploads/images/1234.jpg"
|
||||
assert photo.name == ""
|
||||
assert photo.raw_content == ""
|
||||
assert photo.linked_type == "Post"
|
||||
assert photo.linked_guid == "((guidguidguidguidguidguidguid))"
|
||||
assert photo.height == 120
|
||||
assert photo.width == 120
|
||||
assert photo.guid == "((guidguidguidguidguidguidguif))"
|
||||
assert photo.handle == "alice@alice.diaspora.example.org"
|
||||
assert photo.public == False
|
||||
assert photo.created_at == datetime(2011, 7, 20, 1, 36, 7)
|
||||
|
||||
@patch("federation.entities.diaspora.mappers.DiasporaComment._validate_signatures")
|
||||
|
|
|
@ -59,12 +59,12 @@ class DiasporaPostFactory(PostFactory):
|
|||
model = DiasporaPost
|
||||
|
||||
|
||||
class ImageFactory(ActorIDMixinFactory, IDMixinFactory, PublicMixinFactory, factory.Factory):
|
||||
class ImageFactory(ActorIDMixinFactory, IDMixinFactory, factory.Factory):
|
||||
class Meta:
|
||||
model = Image
|
||||
|
||||
remote_path = factory.Faker('uri')
|
||||
remote_name = factory.Faker('file_path', extension='jpg')
|
||||
url = factory.Faker('uri')
|
||||
name = factory.Faker('slug')
|
||||
|
||||
|
||||
class ProfileFactory(IDMixinFactory, RawContentMixinFactory, factory.Factory):
|
||||
|
|
|
@ -319,3 +319,56 @@ ACTIVITYPUB_POST_OBJECT = {
|
|||
'partOf': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237/replies',
|
||||
'items': []}},
|
||||
}
|
||||
|
||||
ACTIVITYPUB_POST_IMAGES = {'@context': ['https://www.w3.org/ns/activitystreams',
|
||||
{'ostatus': 'http://ostatus.org#',
|
||||
'atomUri': 'ostatus:atomUri',
|
||||
'inReplyToAtomUri': 'ostatus:inReplyToAtomUri',
|
||||
'conversation': 'ostatus:conversation',
|
||||
'sensitive': 'as:sensitive',
|
||||
'Hashtag': 'as:Hashtag',
|
||||
'toot': 'http://joinmastodon.org/ns#',
|
||||
'Emoji': 'toot:Emoji',
|
||||
'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
|
||||
'blurhash': 'toot:blurhash'}],
|
||||
'id': 'https://mastodon.social/users/jaywink/statuses/102611770245850345/activity',
|
||||
'type': 'Create',
|
||||
'actor': 'https://mastodon.social/users/jaywink',
|
||||
'published': '2019-08-13T21:22:37Z',
|
||||
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
'cc': ['https://mastodon.social/users/jaywink/followers'],
|
||||
'object': {'id': 'https://mastodon.social/users/jaywink/statuses/102611770245850345',
|
||||
'type': 'Note',
|
||||
'summary': None,
|
||||
'inReplyTo': None,
|
||||
'published': '2019-08-13T21:22:37Z',
|
||||
'url': 'https://mastodon.social/@jaywink/102611770245850345',
|
||||
'attributedTo': 'https://mastodon.social/users/jaywink',
|
||||
'to': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
'cc': ['https://mastodon.social/users/jaywink/followers'],
|
||||
'sensitive': False,
|
||||
'atomUri': 'https://mastodon.social/users/jaywink/statuses/102611770245850345',
|
||||
'inReplyToAtomUri': None,
|
||||
'conversation': 'tag:mastodon.social,2019-08-13:objectId=119290371:objectType=Conversation',
|
||||
'content': '<p>image test</p>',
|
||||
'contentMap': {'en': '<p>image test</p>'},
|
||||
'attachment': [{'type': 'Document',
|
||||
'mediaType': 'image/jpeg',
|
||||
'url': 'https://files.mastodon.social/media_attachments/files/017/642/079/original/f51b0aee0ee1f2e1.jpg',
|
||||
'name': None,
|
||||
'blurhash': 'UaH1x+IpD*RktToft6s:0f%2tQj@xsWWRkNG'},
|
||||
{'type': 'Document',
|
||||
'mediaType': 'video/mp4',
|
||||
'url': 'https://files.mastodon.social/media_attachments/files/017/642/084/original/e18dda257e5e7078.mp4',
|
||||
'name': None,
|
||||
'blurhash': 'UH9jv0ay00Rj%MM{IU%M%MWBRjofxuayM{t7'}],
|
||||
'tag': [],
|
||||
'replies': {'id': 'https://mastodon.social/users/jaywink/statuses/102611770245850345/replies',
|
||||
'type': 'Collection',
|
||||
'first': {'type': 'CollectionPage',
|
||||
'partOf': 'https://mastodon.social/users/jaywink/statuses/102611770245850345/replies',
|
||||
'items': []}}},
|
||||
'signature': {'type': 'RsaSignature2017',
|
||||
'creator': 'https://mastodon.social/users/jaywink#main-key',
|
||||
'created': '2019-08-13T21:22:37Z',
|
||||
'signatureValue': 'Ia61wdHHIy9gCY5YwqlPtd80eJ2liT9Yi3yHdRdP+fQ5/9np3wHJKNPa7gdzP/BiRzh6aOa2dHWJjB8mOnHYrYBn6Fl3RlCniqousVTDue/ek0zvcFWmlhfja02meDiva+t61O/6Ul1l4tQObMorSf7GbEPePlQiozr/SR/5HIj3SDP0Y8JmlTvhSFgiH6obdroaIYEMQAoYZVcYofGeQUEhotDRp0OGQ4UaPBli4WyzVOUqHMW6pw90QQzZF9XpimwAemk9oAgPmGEPkugFeHfrWt1l84KLdwqwWD8FRIep7gCtu6MpCA8TX4JC5yJvyQ9GbZLZfJSQ6t5wSrcafw=='}}
|
||||
|
|
Ładowanie…
Reference in New Issue