process hashtags in tab objects with Mastodon-specific logic

lots of background in #45. also https://docs.joinmastodon.org/spec/activitypub/#properties-used
pull/451/head
Ryan Barrett 2023-03-14 14:59:28 -07:00
rodzic d22cc3bdbd
commit ce37dd8637
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
3 zmienionych plików z 53 dodań i 13 usunięć

Wyświetl plik

@ -4,6 +4,7 @@ from base64 import b64encode
from hashlib import sha256
import itertools
import logging
from urllib.parse import quote_plus
from flask import abort, request
from granary import as1, as2
@ -273,7 +274,7 @@ def signed_request(fn, url, *, user=None, data=None, log_data=True,
return resp
def postprocess_as2(activity, user=None, target=None, create=True):
def postprocess_as2(activity, *, user=None, target=None, create=True):
"""Prepare an AS2 object to be served or sent via ActivityPub.
Args:
@ -290,7 +291,7 @@ def postprocess_as2(activity, user=None, target=None, create=True):
# actor objects
if type == 'Person':
postprocess_as2_actor(activity, user)
postprocess_as2_actor(activity, user=user)
if not activity.get('publicKey'):
# underspecified, inferred from this issue and Mastodon's implementation:
# https://github.com/w3c/activitypub/issues/203#issuecomment-297553229
@ -309,7 +310,7 @@ def postprocess_as2(activity, user=None, target=None, create=True):
for actor in (util.get_list(activity, 'attributedTo') +
util.get_list(activity, 'actor')):
postprocess_as2_actor(actor, user)
postprocess_as2_actor(actor, user=user)
# inReplyTo: singly valued, prefer id over url
target_id = target.get('id') if target else None
@ -340,12 +341,12 @@ def postprocess_as2(activity, user=None, target=None, create=True):
})
# activity objects (for Like, Announce, etc): prefer id over url
obj = activity.get('object')
if obj:
if isinstance(obj, dict) and not obj.get('id'):
obj['id'] = target_id or util.get_first(obj, 'url')
elif target_id and obj != target_id:
activity['object'] = target_id
obj = as1.get_object(activity)
id = obj.get('id')
if not id:
obj['id'] = target_id or util.get_first(obj, 'url')
elif target_id and id != target_id:
activity['object'] = target_id
# id is required for most things. default to url if it's not set.
if not activity.get('id'):
@ -363,7 +364,7 @@ def postprocess_as2(activity, user=None, target=None, create=True):
# copy image(s) into attachment(s). may be Mastodon-specific.
# https://github.com/snarfed/bridgy-fed/issues/33#issuecomment-440965618
obj_or_activity = obj if isinstance(obj, dict) else activity
obj_or_activity = obj if obj.keys() > set(['id']) else activity
img = util.get_list(obj_or_activity, 'image')
if img:
obj_or_activity.setdefault('attachment', []).extend(img)
@ -384,20 +385,41 @@ def postprocess_as2(activity, user=None, target=None, create=True):
if as2.PUBLIC_AUDIENCE not in to:
to.append(as2.PUBLIC_AUDIENCE)
# hashtags. Mastodon requires:
# * type: Hashtag
# * name starts with #
# * href is set to a valid URL (doesn't matter which)
#
# If content has an <a> tag with the hashtag name as its text, Mastodon will
# rewrite its href to the local instance's search for that hashtag. If
# content doesn't have a link for a hashtag, Mastodon won't add one, but the
# hashtag will still be indexed in search.
#
# https://docs.joinmastodon.org/spec/activitypub/#properties-used
# https://github.com/snarfed/bridgy-fed/issues/45
for tag in util.get_list(activity, 'tag') + util.get_list(obj, 'tag'):
name = tag.get('name')
if name and tag.get('type', 'Tag') == 'Tag':
tag['type'] = 'Hashtag'
tag.setdefault('href', common.host_url(
f'hashtag/{quote_plus(name.removeprefix("#"))}'))
if not name.startswith('#'):
tag['name'] = f'#{name}'
# wrap articles and notes in a Create activity
if create and type in ('Article', 'Note'):
activity = {
'@context': as2.CONTEXT,
'type': 'Create',
'id': f'{activity["id"]}#bridgy-fed-create',
'actor': postprocess_as2_actor({}, user),
'actor': postprocess_as2_actor({}, user=user),
'object': activity,
}
return util.trim_nulls(activity)
def postprocess_as2_actor(actor, user=None):
def postprocess_as2_actor(actor, *, user=None):
"""Prepare an AS2 actor object to be served or sent via ActivityPub.
Modifies actor in place.

Wyświetl plik

@ -81,7 +81,7 @@ def redir(to):
if not obj or obj.deleted:
return f'Object not found: {to}', 404
ret = activitypub.postprocess_as2(as2.from_as1(obj.as1),
user, create=False)
user=user, create=False)
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
return ret, {
'Content-Type': type,

Wyświetl plik

@ -1233,6 +1233,24 @@ class ActivityPubUtilsTest(testutil.TestCase):
'type': 'Note',
}, user=User(id='site')))
def test_postprocess_as2_hashtag(self):
"""https://github.com/snarfed/bridgy-fed/issues/45"""
self.assert_equals({
'tag': [
{'type': 'Hashtag', 'name': '#bar', 'href': 'bar'},
{'type': 'Hashtag', 'name': '#baz', 'href': 'http://localhost/hashtag/baz'},
{'type': 'Mention', 'href': 'foo'},
],
'to': ['https://www.w3.org/ns/activitystreams#Public'],
}, activitypub.postprocess_as2({
'tag': [
{'name': 'bar', 'href': 'bar'},
{'type': 'Tag','name': '#baz'},
# should leave alone
{'type': 'Mention', 'href': 'foo'},
],
}, user=User(id='site')))
# TODO: make these generic and use FakeProtocol
@patch('requests.get')
def test_get_object_http(self, mock_get):