drop User.as2, Object.as_as2, switch callers to Protocol.convert

pull/698/head
Ryan Barrett 2023-10-25 13:23:11 -07:00
rodzic 3471476092
commit ca8b7484c0
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
11 zmienionych plików z 97 dodań i 126 usunięć

Wyświetl plik

@ -93,7 +93,7 @@ class ActivityPub(User, Protocol):
def handle(self):
"""Returns this user's ActivityPub address, eg ``@user@foo.com``."""
if self.obj and self.obj.as1:
addr = as2.address(self.as2())
addr = as2.address(self.convert(self.obj))
if addr:
return addr
@ -179,7 +179,7 @@ class ActivityPub(User, Protocol):
logger.info(f'{obj.key} type {obj.type} is not an actor and has no author or actor with inbox')
actor = obj.as_as2()
actor = ActivityPub.convert(obj)
if shared:
shared_inbox = actor.get('endpoints', {}).get('sharedInbox')
@ -199,9 +199,7 @@ class ActivityPub(User, Protocol):
logger.info(f'Skipping sending to blocklisted {url}')
return False
orig_as2 = orig_obj.as_as2() if orig_obj else None
activity = obj.as2 or postprocess_as2(obj.as_as2(), orig_obj=orig_as2)
activity = to_cls.convert(obj, orig_obj=to_cls.convert(orig_obj))
if not activity.get('actor'):
logger.warning('Outgoing AP activity has no actor!')
@ -318,7 +316,7 @@ class ActivityPub(User, Protocol):
return False
@classmethod
def convert(cls, obj):
def convert(cls, obj, **kwargs):
"""Convert a :class:`models.Object` to AS2.
Args:
@ -326,8 +324,17 @@ class ActivityPub(User, Protocol):
Returns:
dict: AS2 JSON
kwargs: passed through to :func:`postprocess_as2`
"""
return postprocess_as2(as2.from_as1(obj.as1))
if not obj:
return {}
if obj.as2:
return obj.as2
elif obj.source_protocol in ('ap', 'activitypub'):
return as2.from_as1(obj.as1)
return postprocess_as2(as2.from_as1(obj.as1), **kwargs)
@classmethod
def verify_signature(cls, activity):
@ -399,7 +406,8 @@ class ActivityPub(User, Protocol):
elif not key_actor or not key_actor.as1:
error(f"Couldn't load {keyId} to verify signature", status=401)
key = key_actor.as_as2().get('publicKey', {}).get('publicKeyPem')
# don't ActivityPub.convert since we don't want to postprocess_as2
key = as2.from_as1(key_actor.as1).get('publicKey', {}).get('publicKeyPem')
if not key:
error(f'No public key for {keyId}', status=401)
@ -524,8 +532,9 @@ def postprocess_as2(activity, orig_obj=None, wrap=True):
"""
if not activity or isinstance(activity, str):
return activity
elif activity.keys() == {'id'}:
return activity['id']
assert g.user
type = activity.get('type')
# actor objects
@ -681,10 +690,12 @@ def postprocess_as2(activity, orig_obj=None, wrap=True):
if content := obj_or_activity.get('content'):
obj_or_activity.setdefault('contentMap', {'en': content})
activity['object'] = postprocess_as2(
activity.get('object'),
orig_obj=orig_obj,
wrap=wrap and type in ('Create', 'Update', 'Delete'))
activity['object'] = [
postprocess_as2(o, orig_obj=orig_obj,
wrap=wrap and type in ('Create', 'Update', 'Delete'))
for o in as1.get_objects(activity)]
if len(activity['object']) == 1:
activity['object'] = activity['object'][0]
return util.trim_nulls(activity)
@ -712,28 +723,26 @@ def postprocess_as2_actor(actor, wrap=True):
urls = util.get_list(actor, 'url')
if not urls and url:
urls = [url]
domain = util.domain_from_link(urls[0], minimize=False)
if wrap:
if urls and wrap:
urls[0] = redirect_wrap(urls[0])
id = actor.get('id')
if g.user and (not id or g.user.is_web_url(id)):
actor['id'] = g.user.ap_actor()
actor.update({
'url': urls if len(urls) > 1 else urls[0],
# required by ActivityPub
# https://www.w3.org/TR/activitypub/#actor-objects
'inbox': g.user.ap_actor('inbox'),
'outbox': g.user.ap_actor('outbox'),
})
actor['url'] = urls[0] if len(urls) == 1 else urls
# required by ActivityPub
# https://www.w3.org/TR/activitypub/#actor-objects
actor.setdefault('inbox', g.user.ap_actor('inbox'))
actor.setdefault('outbox', g.user.ap_actor('outbox'))
# TODO: genericize (see line 752 in actor())
if g.user.LABEL != 'atproto':
# This has to be the domain for Mastodon interop/Webfinger discovery!
# See related comment in actor() below.
actor['preferredUsername'] = domain
assert urls
actor['preferredUsername'] = util.domain_from_link(
unwrap(urls[0]), minimize=False)
# Override the label for their home page to be "Web site"
for att in util.get_list(actor, 'attachment'):
@ -780,7 +789,7 @@ def actor(handle_or_id):
if not g.user.obj or not g.user.obj.as1:
g.user.obj = cls.load(g.user.profile_id(), gateway=True)
actor = g.user.as2() or {
actor = ActivityPub.convert(g.user.obj) or {
'@context': [as2.CONTEXT],
'type': 'Person',
}
@ -893,7 +902,7 @@ def follower_collection(id, collection):
page = {
'type': 'CollectionPage',
'partOf': request.base_url,
'items': util.trim_nulls([f.user.as2() for f in followers]),
'items': util.trim_nulls([ActivityPub.convert(f.user.obj) for f in followers]),
}
if new_before:
page['next'] = f'{request.base_url}?before={new_before}'

Wyświetl plik

@ -110,7 +110,7 @@ class FollowCallback(indieauth.Callback):
return redirect(g.user.user_page_path('following'))
followee_id = followee.as1.get('id')
followee_as2 = followee.as_as2()
followee_as2 = ActivityPub.convert(followee)
inbox = followee_as2.get('inbox')
if not followee_id or not inbox:
flash(f"AS2 profile {as2_url} missing id or inbox")
@ -122,7 +122,7 @@ class FollowCallback(indieauth.Callback):
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Follow',
'id': follow_id,
'object': followee_as2,
'object': followee_id,
'actor': g.user.ap_actor(),
'to': [as2.PUBLIC_AUDIENCE],
}
@ -197,7 +197,7 @@ class UnfollowCallback(indieauth.Callback):
followee.put()
# TODO(#529): generalize
inbox = followee.as2().get('inbox')
inbox = ActivityPub.convert(followee.obj).get('inbox')
if not inbox:
flash(f"AS2 profile {followee_id} missing inbox")
return redirect(g.user.user_page_path('following'))

Wyświetl plik

@ -306,10 +306,6 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
for u in users:
u._obj = keys_to_objs.get(u.obj_key)
def as2(self):
"""Returns this user as an AS2 actor."""
return self.obj.as_as2() if self.obj else {}
@ndb.ComputedProperty
def handle(self):
"""This user's unique, human-chosen handle, eg ``@me@snarfed.org``.
@ -540,6 +536,7 @@ class Object(StringIdModel):
# choices is populated in app, after all User subclasses are created,
# so that PROTOCOLS is fully populated
# TODO: remove? is this redundant with the protocol-specific data fields below?
# TODO: otherwise, nail down whether this is ABBREV or LABEL
source_protocol = ndb.StringProperty(choices=[])
labels = ndb.StringProperty(repeated=True, choices=LABELS)
@ -805,10 +802,6 @@ class Object(StringIdModel):
with self.lock:
setattr(self, prop, None)
def as_as2(self):
"""Returns this object as an AS2 dict."""
return self.as2 or as2.from_as1(self.as1) or {}
def as_bsky(self, fetch_blobs=False):
"""Returns this object as a Bluesky record.
@ -938,7 +931,7 @@ class Object(StringIdModel):
* ``object.inReplyTo``
* ``tags.[objectType=mention].url``
"""
if not self.as1 or not self.source_protocol:
if not self.as1:
return
# extract ids, strip Bridgy Fed subdomain URLs
@ -946,6 +939,9 @@ class Object(StringIdModel):
if outer_obj != self.as1:
self.our_as1 = util.trim_nulls(outer_obj)
if not self.source_protocol:
return
inner_obj = outer_obj['object'] = as1.get_object(outer_obj)
fields = ['actor', 'author', 'inReplyTo']
mention_tags = [t for t in (as1.get_objects(outer_obj, 'tags')

Wyświetl plik

@ -20,6 +20,7 @@ from oauth_dropins.webutil.flask_util import (
redirect,
)
from activitypub import ActivityPub
import common
from common import DOMAIN_RE
from flask_app import app, cache
@ -165,6 +166,7 @@ def followers_or_following(protocol, id, collection):
f'{collection}.html',
address=request.args.get('address'),
follow_url=request.values.get('url'),
ActivityPub=ActivityPub,
**TEMPLATE_VARS,
**locals(),
)

Wyświetl plik

@ -11,7 +11,7 @@
{% endif %}
{% endwith %}
{{ user_as1.get('displayName') or '' }}
{{ as2.address(f.user.as2() or url) or url }}
{{ as2.address(ActivityPub.convert(f.user.obj) or url) or url }}
</a>
{% endwith %}

Wyświetl plik

@ -1974,10 +1974,31 @@ class ActivityPubUtilsTest(TestCase):
self.assertFalse(ActivityPub.fetch(obj))
self.assertIsNone(obj.as1)
@skip
def test_convert(self):
obj = Object(id='http://orig', as2=LIKE)
self.assertEqual(LIKE_WRAPPED, ActivityPub.convert(obj))
obj = Object()
self.assertEqual({}, ActivityPub.convert(obj))
obj.our_as1 = {}
self.assertEqual({}, ActivityPub.convert(obj))
obj = Object(id='http://orig', our_as1={
'id': 'http://user.com/like',
'objectType': 'activity',
'verb': 'like',
'actor': 'https://user.com/',
'object': 'https://mas.to/post',
})
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://localhost/r/http://user.com/like',
'type': 'Like',
'actor': 'http://localhost/user.com',
'object': 'https://mas.to/post',
'to': ['https://www.w3.org/ns/activitystreams#Public'],
}, ActivityPub.convert(obj))
obj.as2 = {'baz': 'biff'}
self.assertEqual({'baz': 'biff'}, ActivityPub.convert(obj))
def test_postprocess_as2_idempotent(self):
g.user = self.make_user('foo.com')

Wyświetl plik

@ -35,13 +35,14 @@ FOLLOWEE = {
'id': 'https://bar/id',
'url': 'https://bar/url',
'inbox': 'http://bar/inbox',
'outbox': 'http://bar/outbox',
}
FOLLOW_ADDRESS = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Follow',
'id': f'http://localhost/web/alice.com/following#2022-01-02T03:04:05-@foo@bar',
'actor': 'http://localhost/alice.com',
'object': FOLLOWEE,
'object': FOLLOWEE['id'],
'to': [as2.PUBLIC_AUDIENCE],
}
FOLLOW_URL = copy.deepcopy(FOLLOW_ADDRESS)
@ -188,10 +189,7 @@ class FollowTest(TestCase):
self.check('https://bar/actor', resp, FOLLOW_URL, mock_get, mock_post)
def test_callback_stored_followee_with_our_as1(self, mock_get, mock_post):
self.store_object(id='https://bar/id', our_as1=as2.to_as1({
**FOLLOWEE,
# 'id': 'https://bar/actor',
}))
self.store_object(id='https://bar/id', our_as1=as2.to_as1(FOLLOWEE))
mock_get.side_effect = (
requests_response(''),
@ -208,14 +206,7 @@ class FollowTest(TestCase):
follow_with_profile_link = {
**FOLLOW_URL,
'id': f'http://localhost/web/alice.com/following#2022-01-02T03:04:05-https://bar/id',
'object': {
**FOLLOWEE,
'attachment': [{
'type': 'PropertyValue',
'name': 'Link',
'value': '<a rel="me" href="https://bar/url"><span class="invisible">https://</span>bar/url</a>',
}],
},
'object': 'https://bar/id',
}
self.check('https://bar/id', resp, follow_with_profile_link, mock_get,
mock_post, fetched_followee=False)
@ -246,9 +237,7 @@ class FollowTest(TestCase):
state = util.encode_oauth_state(self.state)
resp = self.client.get(f'/follow/callback?code=my_code&state={state}')
expected_follow = copy.deepcopy(FOLLOW_URL)
expected_follow['object'] = followee
self.check('https://bar/actor', resp, expected_follow, mock_get, mock_post)
self.check('https://bar/actor', resp, FOLLOW_URL, mock_get, mock_post)
def check(self, input, resp, expected_follow, mock_get, mock_post,
fetched_followee=True):
@ -321,18 +310,14 @@ class FollowTest(TestCase):
id = 'http://localhost/web/www.alice.com/following#2022-01-02T03:04:05-https://bar/actor'
expected_follow = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Follow',
**FOLLOW_URL,
'id': id,
'actor': 'http://localhost/www.alice.com',
'object': FOLLOWEE,
'to': [as2.PUBLIC_AUDIENCE],
}
followee = ActivityPub(id='https://bar/id').key
follow_obj = self.assert_object(
id, users=[user.key, followee], status='complete',
labels=['user', 'activity'], source_protocol='ui', as2=expected_follow,
as1=as2.to_as1(expected_follow))
labels=['user', 'activity'], source_protocol='ui', as2=expected_follow)
followers = Follower.query().fetch()
self.assert_entities_equal(
@ -363,11 +348,7 @@ class FollowTest(TestCase):
state = util.encode_oauth_state(self.state)
resp = self.client.get(f'/follow/callback?code=my_code&state={state}')
expected_follow = {
**FOLLOW_URL,
'object': followee,
}
self.check('https://bar/actor', resp, expected_follow, mock_get, mock_post)
self.check('https://bar/actor', resp, FOLLOW_URL, mock_get, mock_post)
self.assertEqual(
[f'Followed <a href="https://bar/url">https://bar/actor</a>.'],
get_flashed_messages())

Wyświetl plik

@ -160,18 +160,6 @@ class UserTest(TestCase):
def test_handle(self):
self.assertEqual('y.z', g.user.handle)
def test_as2(self):
self.assertEqual({}, g.user.as2())
obj = Object(id='foo')
g.user.obj_key = obj.key # doesn't exist
self.assertEqual({}, g.user.as2())
del g.user._obj
obj.as2 = {'foo': 'bar'}
obj.put()
self.assertEqual({'foo': 'bar'}, g.user.as2())
def test_id_as(self):
user = self.make_user('fake:user', cls=Fake)
self.assertEqual('fake:user', user.id_as(Fake))
@ -532,26 +520,6 @@ class ObjectTest(TestCase):
obj.put()
self.assertEqual(['user'], obj.labels)
def test_as_as2(self):
obj = Object()
self.assertEqual({}, obj.as_as2())
obj.our_as1 = {}
self.assertEqual({}, obj.as_as2())
obj.our_as1 = {
'objectType': 'person',
'foo': 'bar',
}
self.assertEqual({
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Person',
'foo': 'bar',
}, obj.as_as2())
obj.as2 = {'baz': 'biff'}
self.assertEqual({'baz': 'biff'}, obj.as_as2())
def test_as1_from_as2(self):
self.assert_equals({
'objectType': 'person',

Wyświetl plik

@ -1432,7 +1432,7 @@ class ProtocolReceiveTest(TestCase):
Fake.receive(obj)
self.assert_equals({
**follow,
'actor': 'fake:alice',
'actor': {'id': 'fake:alice'},
'object': 'other:bob',
}, Object.get_by_id('fake:follow').our_as1)

Wyświetl plik

@ -62,17 +62,6 @@ ACTOR_AS2 = {
'inbox': 'http://localhost/user.com/inbox',
'outbox': 'http://localhost/user.com/outbox',
}
ACTOR_AS2_USER = {
'type': 'Person',
'id': 'https://user.com/',
'url': 'https://user.com/',
'name': 'Ms. ☕ Baz',
'attachment': [{
'name': 'Ms. ☕ Baz',
'type': 'PropertyValue',
'value': '<a rel="me" href="https://user.com"><span class="invisible">https://</span>user.com</a>',
}],
}
ACTOR_AS2_FULL = {
**ACTOR_AS2,
'@context': [
@ -931,8 +920,8 @@ class WebTest(TestCase):
self.assertEqual(('https://mas.to/inbox',), args)
self.assert_equals(AS2_CREATE, json_loads(kwargs['data']))
def test_like_stored_object_without_as2(self, mock_get, mock_post):
Object(id='https://mas.to/toot', mf2=NOTE_MF2, source_protocol='ap').put()
def test_like_stored_object(self, mock_get, mock_post):
Object(id='https://mas.to/toot', source_protocol='ap').put()
Object(id='https://user.com/', mf2=ACTOR_MF2).put()
mock_get.side_effect = [
LIKE,
@ -1608,15 +1597,20 @@ class WebTest(TestCase):
expected_as2)
# updated Web user
self.assert_user(Web, 'user.com',
obj_as2={
**ACTOR_AS2_USER,
'updated': '2022-01-02T03:04:05+00:00',
},
direct=True,
has_redirects=True,
)
expected_actor_as2 = {
'type': 'Person',
'id': 'https://user.com/',
'url': 'https://user.com/',
'name': 'Ms. ☕ Baz',
'attachment': [{
'name': 'Ms. ☕ Baz',
'type': 'PropertyValue',
'value': '<a rel="me" href="https://user.com"><span class="invisible">https://</span>user.com</a>',
}],
'updated': '2022-01-02T03:04:05+00:00',
}
self.assert_user(Web, 'user.com', obj_as2=expected_actor_as2, direct=True,
has_redirects=True)
# homepage object
actor = {
@ -1841,7 +1835,7 @@ http://this/404s
# preferredUsername stays y.z despite user's username. since Mastodon
# queries Webfinger for preferredUsername@fed.brid.gy
# https://github.com/snarfed/bridgy-fed/issues/77#issuecomment-949955109
postprocessed = postprocess_as2(g.user.as2())
postprocessed = ActivityPub.convert(g.user.obj)
self.assertEqual('user.com', postprocessed['preferredUsername'])
def test_web_url(self, _, __):

Wyświetl plik

@ -456,7 +456,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
obj_as2 = props.pop('obj_as2', None)
if obj_as2:
self.assert_equals(obj_as2, got.as2())
self.assert_equals(obj_as2, as2.from_as1(got.obj.as1))
# generated, computed, etc
ignore = ['created', 'mod', 'handle', 'obj_key', 'private_exponent',