Add media type and rendered content to entities with raw_content

Entities with `raw_content` now also contain a `_media_type` and
`rendered_content`.

The default `_media_type` is `text/markdown` except for ActivityPub
originating posts it defaults to `text/html`. If the ActivityPub
payload contains a `source`, that mediaType will be used instead.
merge-requests/156/head
Jason Robinson 2019-08-18 22:37:18 +03:00
rodzic b0c6be6cd7
commit 61a0fc442b
7 zmienionych plików z 107 dodań i 9 usunięć

Wyświetl plik

@ -30,6 +30,10 @@
* All ActivityPub payloads are added a `pyfed: https://docs.jasonrobinson.me/ns/python-federation` context to identify payloads sent by this library. * All ActivityPub payloads are added a `pyfed: https://docs.jasonrobinson.me/ns/python-federation` context to identify payloads sent by this library.
* Entities with `raw_content` now also contain a `_media_type` and `rendered_content`.
The default `_media_type` is `text/markdown` except for ActivityPub originating posts it defaults to `text/html`. If the ActivityPub payload contains a `source`, that mediaType will be used instead.
### Changed ### Changed
* **Backwards incompatible.** Lowest compatible Python version is now 3.6. * **Backwards incompatible.** Lowest compatible Python version is now 3.6.

Wyświetl plik

@ -3,7 +3,6 @@ import uuid
from typing import Dict from typing import Dict
import attr import attr
from commonmark import commonmark
from federation.entities.activitypub.constants import ( from federation.entities.activitypub.constants import (
CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_SENSITIVE, CONTEXT_HASHTAG, CONTEXTS_DEFAULT, CONTEXT_MANUALLY_APPROVES_FOLLOWERS, CONTEXT_SENSITIVE, CONTEXT_HASHTAG,
@ -51,7 +50,7 @@ class ActivitypubNoteMixin(AttachImagesMixin, CleanContentMixin, ActivitypubEnti
"id": self.id, "id": self.id,
"type": self._type, "type": self._type,
"attributedTo": self.actor_id, "attributedTo": self.actor_id,
"content": commonmark(self.raw_content).strip(), "content": self.rendered_content,
"published": self.created_at.isoformat(), "published": self.created_at.isoformat(),
"inReplyTo": None, "inReplyTo": None,
"sensitive": True if "nsfw" in self.tags else False, "sensitive": True if "nsfw" in self.tags else False,
@ -60,7 +59,7 @@ class ActivitypubNoteMixin(AttachImagesMixin, CleanContentMixin, ActivitypubEnti
"url": self.url, "url": self.url,
'source': { 'source': {
'content': self.raw_content, 'content': self.raw_content,
'mediaType': 'text/markdown', 'mediaType': self._media_type,
}, },
}, },
"published": self.created_at.isoformat(), "published": self.created_at.isoformat(),

Wyświetl plik

@ -218,7 +218,9 @@ def message_to_objects(
return element_to_objects(message) return element_to_objects(message)
def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dict, cls, is_object: bool) -> None: def transform_attribute(
key: str, value: Union[str, Dict, int], transformed: Dict, cls, is_object: bool, payload: Dict,
) -> None:
if value is None: if value is None:
value = "" value = ""
if key == "id": if key == "id":
@ -236,8 +238,15 @@ def transform_attribute(key: str, value: Union[str, Dict, int], transformed: Dic
transformed["actor_id"] = value transformed["actor_id"] = value
elif key == "attributedTo" and is_object: elif key == "attributedTo" and is_object:
transformed["actor_id"] = value transformed["actor_id"] = value
elif key == "content": elif key in ("content", "source"):
if payload.get('source') and isinstance(payload.get("source"), dict):
transformed["raw_content"] = payload.get('source').get('content')
transformed["_media_type"] = payload.get('source').get('mediaType')
transformed["_rendered_content"] = payload.get('content')
else:
transformed["raw_content"] = value transformed["raw_content"] = value
# Assume HTML by convention
transformed["_media_type"] = "text/html"
elif key == "inboxes" and isinstance(value, dict): elif key == "inboxes" and isinstance(value, dict):
if "inboxes" not in transformed: if "inboxes" not in transformed:
transformed["inboxes"] = {"private": None, "public": None} transformed["inboxes"] = {"private": None, "public": None}
@ -300,5 +309,5 @@ def transform_attributes(payload: Dict, cls, transformed: Dict = None, is_object
if not transformed: if not transformed:
transformed = {} transformed = {}
for key, value in payload.items(): for key, value in payload.items():
transform_attribute(key, value, transformed, cls, is_object) transform_attribute(key, value, transformed, cls, is_object, payload)
return transformed return transformed

Wyświetl plik

@ -10,6 +10,8 @@ class AttachImagesMixin(RawContentMixin):
""" """
Attach any embedded images from raw_content. 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)))\)" regex = r"!\[([\w ]*)\]\((https?://[\w\d\-\./]+\.[\w]*((?<=jpg)|(?<=gif)|(?<=png)|(?<=jpeg)))\)"
matches = re.finditer(regex, self.raw_content, re.MULTILINE | re.IGNORECASE) matches = re.finditer(regex, self.raw_content, re.MULTILINE | re.IGNORECASE)
for match in matches: for match in matches:

Wyświetl plik

@ -3,6 +3,8 @@ import importlib
import warnings import warnings
from typing import List, Set from typing import List, Set
from commonmark import commonmark
from federation.entities.activitypub.enums import ActivityType from federation.entities.activitypub.enums import ActivityType
@ -175,12 +177,23 @@ class CreatedAtMixin(BaseEntity):
class RawContentMixin(BaseEntity): class RawContentMixin(BaseEntity):
raw_content = "" _media_type: str = "text/markdown"
_rendered_content: str = ""
raw_content: str = ""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._required += ["raw_content"] self._required += ["raw_content"]
@property
def rendered_content(self) -> str:
"""Returns the rendered version of raw_content, or just raw_content."""
if self._rendered_content:
return self._rendered_content
elif self._media_type == "text/markdown" and self.raw_content:
return commonmark(self.raw_content).strip()
return self.raw_content
@property @property
def tags(self): def tags(self):
"""Returns a `set` of unique tags contained in `raw_content`.""" """Returns a `set` of unique tags contained in `raw_content`."""

Wyświetl plik

@ -10,7 +10,7 @@ from federation.entities.base import Accept, Follow, Profile, Post, Comment, Ima
from federation.tests.fixtures.payloads import ( from federation.tests.fixtures.payloads import (
ACTIVITYPUB_FOLLOW, ACTIVITYPUB_PROFILE, ACTIVITYPUB_PROFILE_INVALID, ACTIVITYPUB_UNDO_FOLLOW, ACTIVITYPUB_POST, 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) ACTIVITYPUB_POST_IMAGES, ACTIVITYPUB_POST_WITH_SOURCE)
from federation.types import UserType, ReceiverVariant from federation.types import UserType, ReceiverVariant
@ -67,9 +67,26 @@ class TestActivitypubEntityMappersReceive:
assert isinstance(post, Post) assert isinstance(post, Post)
assert post.raw_content == '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" ' \ assert post.raw_content == '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" ' \
'class="u-url mention">@<span>jaywink</span></a></span> boom</p>' 'class="u-url mention">@<span>jaywink</span></a></span> boom</p>'
assert post.rendered_content == post.raw_content
assert post.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237" assert post.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237"
assert post.actor_id == "https://diaspodon.fr/users/jaywink" assert post.actor_id == "https://diaspodon.fr/users/jaywink"
assert post.public is True assert post.public is True
assert post._media_type == "text/html"
assert getattr(post, "target_id", None) is None
def test_message_to_objects_simple_post__with_source(self):
entities = message_to_objects(ACTIVITYPUB_POST_WITH_SOURCE, "https://diaspodon.fr/users/jaywink")
assert len(entities) == 1
post = entities[0]
assert isinstance(post, ActivitypubPost)
assert isinstance(post, Post)
assert post.rendered_content == '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" ' \
'class="u-url mention">@<span>jaywink</span></a></span> boom</p>'
assert post.raw_content == "@jaywink boom"
assert post.id == "https://diaspodon.fr/users/jaywink/statuses/102356911717767237"
assert post.actor_id == "https://diaspodon.fr/users/jaywink"
assert post.public is True
assert post._media_type == "text/markdown"
assert getattr(post, "target_id", None) is None assert getattr(post, "target_id", None) is None
def test_message_to_objects_post_with_photos(self): def test_message_to_objects_post_with_photos(self):

Wyświetl plik

@ -292,6 +292,60 @@ ACTIVITYPUB_POST = {
'signatureValue': 'SjDACS7Z/Cb1SEC3AtxEokID5SHAYl7kpys/hhmaRbpXuFKCxfj2P9BmH8QhLnuam3sENZlrnBOcB5NlcBhIfwo/Xh242RZBmPQf+edTVYVCe1j19dihcftNCHtnqAcKwp/51dNM/OlKu2730FrwvOUXVIPtB7iVqkseO9TRzDYIDj+zBTksnR/NAYtq6SUpmefXfON0uW3N3Uq6PGfExJaS+aeqRf8cPGkZFSIUQZwOLXbIpb7BFjJ1+y1OMOAJueqvikUprAit3v6BiNWurAvSQpC7WWMFUKyA79/xtkO9kIPA/Q4C9ryqdzxZJ0jDhXiaIIQj2JZfIADdjLZHJA=='} 'signatureValue': 'SjDACS7Z/Cb1SEC3AtxEokID5SHAYl7kpys/hhmaRbpXuFKCxfj2P9BmH8QhLnuam3sENZlrnBOcB5NlcBhIfwo/Xh242RZBmPQf+edTVYVCe1j19dihcftNCHtnqAcKwp/51dNM/OlKu2730FrwvOUXVIPtB7iVqkseO9TRzDYIDj+zBTksnR/NAYtq6SUpmefXfON0uW3N3Uq6PGfExJaS+aeqRf8cPGkZFSIUQZwOLXbIpb7BFjJ1+y1OMOAJueqvikUprAit3v6BiNWurAvSQpC7WWMFUKyA79/xtkO9kIPA/Q4C9ryqdzxZJ0jDhXiaIIQj2JZfIADdjLZHJA=='}
} }
ACTIVITYPUB_POST_WITH_SOURCE = {
'@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://diaspodon.fr/users/jaywink/statuses/102356911717767237/activity',
'type': 'Create',
'actor': 'https://diaspodon.fr/users/jaywink',
'published': '2019-06-29T21:08:45Z',
'to': 'https://www.w3.org/ns/activitystreams#Public',
'cc': ['https://diaspodon.fr/users/jaywink/followers',
'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/'],
'object': {'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237',
'type': 'Note',
'summary': None,
'inReplyTo': None,
'published': '2019-06-29T21:08:45Z',
'url': 'https://diaspodon.fr/@jaywink/102356911717767237',
'attributedTo': 'https://diaspodon.fr/users/jaywink',
'to': 'https://www.w3.org/ns/activitystreams#Public',
'cc': ['https://diaspodon.fr/users/jaywink/followers',
'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/'],
'sensitive': False,
'atomUri': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237',
'inReplyToAtomUri': None,
'conversation': 'tag:diaspodon.fr,2019-06-28:objectId=2347687:objectType=Conversation',
'content': '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" class="u-url mention">@<span>jaywink</span></a></span> boom</p>',
'source': {
'content': "@jaywink boom",
'mediaType': "text/markdown",
},
'contentMap': {'en': '<p><span class="h-card"><a href="https://dev.jasonrobinson.me/u/jaywink/" class="u-url mention">@<span>jaywink</span></a></span> boom</p>'},
'attachment': [],
'tag': [{'type': 'Mention',
'href': 'https://dev.jasonrobinson.me/p/d4574854-a5d7-42be-bfac-f70c16fcaa97/',
'name': '@jaywink@dev.jasonrobinson.me'}],
'replies': {'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237/replies',
'type': 'Collection',
'first': {'type': 'CollectionPage',
'partOf': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237/replies',
'items': []}}},
'signature': {'type': 'RsaSignature2017',
'creator': 'https://diaspodon.fr/users/jaywink#main-key',
'created': '2019-06-29T21:08:45Z',
'signatureValue': 'SjDACS7Z/Cb1SEC3AtxEokID5SHAYl7kpys/hhmaRbpXuFKCxfj2P9BmH8QhLnuam3sENZlrnBOcB5NlcBhIfwo/Xh242RZBmPQf+edTVYVCe1j19dihcftNCHtnqAcKwp/51dNM/OlKu2730FrwvOUXVIPtB7iVqkseO9TRzDYIDj+zBTksnR/NAYtq6SUpmefXfON0uW3N3Uq6PGfExJaS+aeqRf8cPGkZFSIUQZwOLXbIpb7BFjJ1+y1OMOAJueqvikUprAit3v6BiNWurAvSQpC7WWMFUKyA79/xtkO9kIPA/Q4C9ryqdzxZJ0jDhXiaIIQj2JZfIADdjLZHJA=='}
}
ACTIVITYPUB_POST_OBJECT = { ACTIVITYPUB_POST_OBJECT = {
'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237', 'id': 'https://diaspodon.fr/users/jaywink/statuses/102356911717767237',
'type': 'Note', 'type': 'Note',