kopia lustrzana https://gitlab.com/jaywink/federation
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
rodzic
b0c6be6cd7
commit
61a0fc442b
|
@ -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.
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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`."""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Ładowanie…
Reference in New Issue