Merge branch 'images-from-ap' into 'master'

Extract images from ActivityPub payloads

See merge request jaywink/federation!154
merge-requests/155/merge
Jason Robinson 2019-08-17 15:15:53 +00:00
commit 9617737993
8 zmienionych plików z 112 dodań i 45 usunięć

Wyświetl plik

@ -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:

Wyświetl plik

@ -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 ""

Wyświetl plik

@ -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):

Wyświetl plik

@ -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:

Wyświetl plik

@ -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")

Wyświetl plik

@ -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")

Wyświetl plik

@ -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):

Wyświetl plik

@ -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=='}}