Merge ImageObject into the base Image entity

No point having a separate object type when the features
of it match the entity Image type.

Also send out Image instead of Document for image type
attachments. Lets see if Mastodon and others are fine
with this.

Refs: https://git.feneas.org/socialhome/socialhome/issues/522
refactor-imageobject
Jason Robinson 2019-09-08 00:09:32 +03:00
rodzic 54ecc5aea5
commit c4e54d0027
8 zmienionych plików z 108 dodań i 113 usunięć

Wyświetl plik

@ -1,16 +1,15 @@
import logging
import re
import uuid
from typing import Dict, List
import attr
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 ActivitypubEntityMixin, CleanContentMixin, AttachImagesMixin
from federation.entities.activitypub.objects import ImageObject
from federation.entities.base import Profile, Post, Follow, Accept, Comment, Retraction, Share
from federation.entities.base import Profile, Post, Follow, Accept, Comment, Retraction, Share, Image
from federation.entities.mixins import RawContentMixin, BaseEntity
from federation.entities.utils import get_base_attributes
from federation.outbound import handle_send
from federation.types import UserType
from federation.utils.django import get_configuration
@ -19,6 +18,55 @@ from federation.utils.text import with_slash
logger = logging.getLogger("federation")
class AttachImagesMixin(RawContentMixin):
def pre_send(self) -> None:
"""
Attach any embedded images from raw_content.
"""
if self._media_type != "text/markdown":
return
regex = r"!\[([\w ]*)\]\((https?://[\w\d\-\./]+\.[\w]*((?<=jpg)|(?<=gif)|(?<=png)|(?<=jpeg)))\)"
matches = re.finditer(regex, self.raw_content, re.MULTILINE | re.IGNORECASE)
for match in matches:
groups = match.groups()
self._children.append(
ActivitypubImage(
url=groups[1],
name=groups[0] or "",
inline=True,
)
)
class ActivitypubEntityMixin(BaseEntity):
_type = None
@classmethod
def from_base(cls, entity):
# noinspection PyArgumentList
return cls(**get_base_attributes(entity))
def to_string(self):
# noinspection PyUnresolvedReferences
return str(self.to_as2())
class CleanContentMixin(RawContentMixin):
def post_receive(self) -> None:
"""
Make linkified tags normal tags.
"""
def cleaner(match):
return f"#{match.groups()[0]}"
self.raw_content = re.sub(
r'\[#([\w\-_]+)\]\(http?s://[a-zA-Z0-9/._-]+\)',
cleaner,
self.raw_content,
re.MULTILINE,
)
class ActivitypubAccept(ActivitypubEntityMixin, Accept):
_type = ActivityType.ACCEPT.value
object: Dict = None
@ -91,17 +139,7 @@ class ActivitypubNoteMixin(AttachImagesMixin, CleanContentMixin, ActivitypubEnti
if len(self._children):
as2["object"]["attachment"] = []
for child in self._children:
image = ImageObject(url=child.url)
if image.mediaType:
attachment = {
"type": "Document",
"mediaType": image.mediaType,
"name": child.name,
"url": child.url,
}
if child.inline:
attachment["pyfed:inlineImage"] = True
as2["object"]["attachment"].append(attachment)
as2["object"]["attachment"].append(child.to_as2())
as2["object"]["tag"] = self.add_object_tags()
return as2
@ -189,6 +227,19 @@ class ActivitypubFollow(ActivitypubEntityMixin, Follow):
return as2
class ActivitypubImage(ActivitypubEntityMixin, Image):
_type = ObjectType.IMAGE.value
def to_as2(self) -> Dict:
return {
"type": self._type,
"url": self.url,
"mediaType": self.media_type,
"name": self.name,
"pyfed:inlineImage": self.inline,
}
class ActivitypubPost(ActivitypubNoteMixin, Post):
pass
@ -227,7 +278,9 @@ class ActivitypubProfile(ActivitypubEntityMixin, Profile):
as2['summary'] = self.raw_content
if self.image_urls.get('large'):
try:
as2['icon'] = attr.asdict(ImageObject(url=self.image_urls.get('large')))
profile_icon = ActivitypubImage(url=self.image_urls.get('large'))
if profile_icon.media_type:
as2['icon'] = profile_icon.to_as2()
except Exception as ex:
logger.warning("ActivitypubProfile.to_as2 - failed to set profile icon: %s", ex)
return as2

Wyświetl plik

@ -22,5 +22,6 @@ class ActorType(EnumBase):
class ObjectType(EnumBase):
IMAGE = "Image"
NOTE = "Note"
TOMBSTONE = "Tombstone"

Wyświetl plik

@ -7,7 +7,6 @@ from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
from federation.entities.activitypub.entities import (
ActivitypubFollow, ActivitypubProfile, ActivitypubAccept, ActivitypubPost, ActivitypubComment,
ActivitypubRetraction, ActivitypubShare)
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
@ -111,7 +110,8 @@ def extract_attachments(payload: Dict) -> List[Image]:
"""
attachments = []
for item in payload.get('attachment', []):
if item.get("type") == "Document" and item.get("mediaType") in IMAGE_TYPES:
# noinspection PyProtectedMember
if item.get("type") == "Document" and item.get("mediaType") in Image._valid_media_types:
if item.get('pyfed:inlineImage', False):
# Skip this image as it's indicated to be inline in content and source already
continue
@ -119,6 +119,7 @@ def extract_attachments(payload: Dict) -> List[Image]:
Image(
url=item.get('url'),
name=item.get('name') or "",
media_type=item.get("mediaType"),
)
)
return attachments

Wyświetl plik

@ -1,54 +0,0 @@
import re
from federation.entities.base import Image
from federation.entities.mixins import BaseEntity, RawContentMixin
from federation.entities.utils import get_base_attributes
class AttachImagesMixin(RawContentMixin):
def pre_send(self) -> None:
"""
Attach any embedded images from raw_content.
"""
if self._media_type != "text/markdown":
return
regex = r"!\[([\w ]*)\]\((https?://[\w\d\-\./]+\.[\w]*((?<=jpg)|(?<=gif)|(?<=png)|(?<=jpeg)))\)"
matches = re.finditer(regex, self.raw_content, re.MULTILINE | re.IGNORECASE)
for match in matches:
groups = match.groups()
self._children.append(
Image(
url=groups[1],
name=groups[0] or "",
inline=True,
)
)
class ActivitypubEntityMixin(BaseEntity):
_type = None
@classmethod
def from_base(cls, entity):
# noinspection PyArgumentList
return cls(**get_base_attributes(entity))
def to_string(self):
# noinspection PyUnresolvedReferences
return str(self.to_as2())
class CleanContentMixin(RawContentMixin):
def post_receive(self) -> None:
"""
Make linkified tags normal tags.
"""
def cleaner(match):
return f"#{match.groups()[0]}"
self.raw_content = re.sub(
r'\[#([\w\-_]+)\]\(http?s://[a-zA-Z0-9/._-]+\)',
cleaner,
self.raw_content,
re.MULTILINE,
)

Wyświetl plik

@ -1,26 +0,0 @@
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.
"""
url: str = attr.ib()
type: str = attr.ib(default="Image")
mediaType: str = attr.ib()
@mediaType.default
def cache_media_type(self):
content_type = fetch_content_type(self.url)
if content_type in IMAGE_TYPES:
return content_type
return ""

Wyświetl plik

@ -1,4 +1,4 @@
from typing import Dict
from typing import Dict, Tuple
from dirty_validators.basic import Email
@ -6,6 +6,7 @@ from federation.entities.activitypub.enums import ActivityType
from federation.entities.mixins import (
PublicMixin, TargetIDMixin, ParticipationMixin, CreatedAtMixin, RawContentMixin, OptionalRawContentMixin,
EntityTypeMixin, ProviderDisplayNameMixin, RootTargetIDMixin, BaseEntity)
from federation.utils.network import fetch_content_type
class Accept(CreatedAtMixin, TargetIDMixin, BaseEntity):
@ -19,17 +20,33 @@ class Accept(CreatedAtMixin, TargetIDMixin, BaseEntity):
class Image(OptionalRawContentMixin, CreatedAtMixin, BaseEntity):
"""Reflects a single image, possibly linked to another object."""
url = ""
name = ""
height = 0
width = 0
inline = False
url: str = ""
name: str = ""
height: int = 0
width: int = 0
inline: bool = False
media_type: str = ""
_default_activity = ActivityType.CREATE
_valid_media_types: Tuple[str] = (
"image/jpeg",
"image/png",
"image/gif",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._required += ["url"]
self._required.remove("id")
self._required.remove("actor_id")
if self.url and not self.media_type:
self.media_type = self.get_media_type()
def get_media_type(self) -> str:
media_type = fetch_content_type(self.url)
if media_type in self._valid_media_types:
return media_type
return ""
class Comment(RawContentMixin, ParticipationMixin, CreatedAtMixin, RootTargetIDMixin, BaseEntity):

Wyświetl plik

@ -170,8 +170,7 @@ class TestEntitiesConvertToAS2:
'published': '2019-04-27T00:00:00',
}
@patch("federation.entities.activitypub.objects.fetch_content_type", return_value="image/jpeg")
def test_post_to_as2__with_images(self, mock_fetch, activitypubpost_images):
def test_post_to_as2__with_images(self, activitypubpost_images):
result = activitypubpost_images.to_as2()
assert result == {
'@context': [
@ -197,16 +196,18 @@ class TestEntitiesConvertToAS2:
'url': '',
'attachment': [
{
'type': 'Document',
'type': 'Image',
'mediaType': 'image/jpeg',
'name': '',
'url': 'foobar',
'pyfed:inlineImage': False,
},
{
'type': 'Document',
'type': 'Image',
'mediaType': 'image/jpeg',
'name': 'spam and eggs',
'url': 'barfoo',
'pyfed:inlineImage': False,
},
],
'source': {
@ -217,7 +218,7 @@ class TestEntitiesConvertToAS2:
'published': '2019-04-27T00:00:00',
}
@patch("federation.entities.activitypub.objects.fetch_content_type", return_value="image/jpeg")
@patch("federation.entities.base.fetch_content_type", return_value="image/jpeg")
def test_profile_to_as2(self, mock_fetch, activitypubprofile):
result = activitypubprofile.to_as2()
assert result == {
@ -247,6 +248,8 @@ class TestEntitiesConvertToAS2:
"type": "Image",
"url": "urllarge",
"mediaType": "image/jpeg",
"name": "",
"pyfed:inlineImage": False,
}
}

Wyświetl plik

@ -3,7 +3,7 @@ from freezegun import freeze_time
from federation.entities.activitypub.entities import (
ActivitypubPost, ActivitypubAccept, ActivitypubFollow, ActivitypubProfile, ActivitypubComment,
ActivitypubRetraction, ActivitypubShare)
ActivitypubRetraction, ActivitypubShare, ActivitypubImage)
from federation.entities.base import Profile, Image
from federation.entities.diaspora.entities import (
DiasporaPost, DiasporaComment, DiasporaLike, DiasporaProfile, DiasporaRetraction,
@ -81,8 +81,8 @@ def activitypubpost_images():
activity_id=f"http://127.0.0.1:8000/post/123456/#create",
actor_id=f"http://127.0.0.1:8000/profile/123456/",
_children=[
Image(url="foobar"),
Image(url="barfoo", name="spam and eggs"),
ActivitypubImage(url="foobar", media_type="image/jpeg"),
ActivitypubImage(url="barfoo", name="spam and eggs", media_type="image/jpeg"),
],
)