refactoring, move Object creation out of common.send_webmentions

ugh this was painful
pull/434/head
Ryan Barrett 2023-02-18 17:53:27 -08:00
rodzic 6500f71d3f
commit 74b3b3b689
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
3 zmienionych plików z 92 dodań i 81 usunięć

Wyświetl plik

@ -102,6 +102,7 @@ def inbox(domain=None):
except (TypeError, ValueError, AssertionError):
error(f"Couldn't parse body as JSON: {body}", exc_info=True)
type = activity.get('type')
actor = activity.get('actor')
actor_id = actor.get('id') if isinstance(actor, dict) else actor
@ -124,11 +125,12 @@ def inbox(domain=None):
logger.info(msg)
return msg, 200
activity_as1 = as2.to_as1(activity)
as1_type = as1.object_type(activity_as1)
activity_unwrapped = redirect_unwrap(activity)
activity_obj = Object(
id=id, as2=json_dumps(activity), as1=json_dumps(activity_as1),
source_protocol='activitypub', status='complete')
id=id,
as2=json_dumps(activity_unwrapped),
as1=json_dumps(as2.to_as1(activity_unwrapped)),
source_protocol='activitypub')
activity_obj.put()
if type == 'Accept': # eg in response to a Follow
@ -178,8 +180,10 @@ def inbox(domain=None):
# handle activity!
if type == 'Undo' and obj_as2.get('type') == 'Follow':
# skip actor fetch below; we don't need it to undo a follow
undo_follow(redirect_unwrap(activity))
return ''
undo_follow(activity_unwrapped)
activity_obj.status = 'complete'
activity_obj.put()
return 'OK'
elif type == 'Update':
obj_id = obj_as2.get('id')
@ -193,6 +197,9 @@ def inbox(domain=None):
source_protocol='activitypub',
)
obj.put()
activity_obj.status = 'complete'
activity_obj.put()
return 'OK'
elif type == 'Delete':
@ -214,32 +221,28 @@ def inbox(domain=None):
).fetch()
for f in followers:
f.status = 'inactive'
ndb.put_multi(followers)
activity_obj.status = 'complete'
ndb.put_multi(followers + [activity_obj])
return 'OK'
# fetch actor if necessary so we have name, profile photo, etc
if actor and isinstance(actor, str):
actor = activity['actor'] = \
actor = activity['actor'] = activity_unwrapped['actor'] = \
json_loads(common.get_object(actor, user=user).as2)
# fetch object if necessary so we can render it in feeds
activity_unwrapped = redirect_unwrap(activity)
inner_obj = activity_unwrapped.get('object')
if type in FETCH_OBJECT_TYPES and isinstance(inner_obj, str):
obj = Object.get_by_id(inner_obj) or common.get_object(inner_obj, user=user)
if type in FETCH_OBJECT_TYPES and isinstance(activity.get('object'), str):
obj_as2 = activity['object'] = activity_unwrapped['object'] = \
json_loads(obj.as2) if obj.as2 else as2.from_as1(json_loads(obj.as1))
json_loads(common.get_object(activity['object'], user=user).as2)
if type == 'Follow':
return accept_follow(activity, activity_unwrapped, user)
resp = accept_follow(activity, activity_unwrapped, user)
# send webmentions to each target
activity_as2_str = json_dumps(activity_unwrapped)
activity_as1 = as2.to_as1(activity_unwrapped)
activity_as1_str = json_dumps(activity_as1)
sent = common.send_webmentions(as2.to_as1(activity), proxy=True,
source_protocol='activitypub',
as2=activity_as2_str, as1=activity_as1_str)
activity_obj.populate(as2=json_dumps(activity_unwrapped),
as1=json_dumps(activity_as1))
common.send_webmentions(as2.to_as1(activity), activity_obj, proxy=True)
# deliver original posts and reposts to followers
if ((type == 'Create' and not activity.get('inReplyTo') and not obj_as2.get('inReplyTo'))
@ -260,16 +263,18 @@ def inbox(domain=None):
actor_id = actor.get('id')
if actor_id:
logger.info(f'Finding followers of {actor_id}')
followers = Follower.query(Follower.dest == actor_id,
projection=[Follower.src]).fetch()
if followers:
activity_obj.domains = (set(activity_obj.domains) |
set(f.src for f in followers))
if 'feed' not in activity_obj.labels:
activity_obj.labels.append('feed')
for f in Follower.query(Follower.dest == actor_id,
projection=[Follower.src]):
if f.src not in activity_obj.domains:
activity_obj.domains.append(f.src)
if activity_obj.domains and 'feed' not in activity_obj.labels:
activity_obj.labels.append('feed')
activity_obj.put()
if (activity_as1.get('objectType') == 'activity'
and 'activity' not in activity_obj.labels):
activity_obj.labels.append('activity')
activity_obj.put()
return 'OK'
@ -324,14 +329,7 @@ def accept_follow(follow, follow_unwrapped, user):
'object': followee,
}
}
resp = common.signed_post(inbox, data=accept, user=user)
# send webmention
common.send_webmentions(as2.to_as1(follow), proxy=True, source_protocol='activitypub',
as2=json_dumps(follow_unwrapped),
as1=json_dumps(as2.to_as1(follow_unwrapped)))
return resp.text, resp.status_code
return common.signed_post(inbox, data=accept, user=user)
@ndb.transactional()

Wyświetl plik

@ -311,30 +311,31 @@ def remove_blocklisted(urls):
util.domain_from_link(u), DOMAIN_BLOCKLIST)]
def send_webmentions(activity_wrapped, proxy=None, **object_props):
def send_webmentions(activity_wrapped, obj, proxy=None):
"""Sends webmentions for an incoming ActivityPub inbox delivery.
Args:
activity_wrapped: dict, AS1 activity
object_props: passed through to the newly created Object entities
obj: :class:`Object`
proxy: boolean, whether to use our proxy URL as the webmention source
Returns: boolean, True if any webmentions were sent, False otherwise
"""
activity = redirect_unwrap(activity_wrapped)
activity_unwrapped = json_loads(obj.as1)
verb = activity.get('verb')
verb = activity_unwrapped.get('verb')
if verb and verb not in SUPPORTED_VERBS:
error(f'{verb} activities are not supported yet.', status=501)
# extract source and targets
source = activity.get('url') or activity.get('id')
obj = activity.get('object')
obj_url = util.get_url(obj)
source = activity_unwrapped.get('url') or activity_unwrapped.get('id')
inner_obj = activity_unwrapped.get('object')
obj_url = util.get_url(inner_obj)
targets = util.get_list(activity, 'inReplyTo')
if isinstance(obj, dict):
targets = util.get_list(activity_unwrapped, 'inReplyTo')
if isinstance(inner_obj, dict):
if not source or verb in ('create', 'post', 'update'):
source = obj_url or obj.get('id')
targets.extend(util.get_list(obj, 'inReplyTo'))
source = obj_url or inner_obj.get('id')
targets.extend(util.get_list(inner_obj, 'inReplyTo'))
if not source:
error("Couldn't find original post URL")
@ -360,16 +361,16 @@ def send_webmentions(activity_wrapped, proxy=None, **object_props):
logger.info(f'targets: {targets}')
# send webmentions and store Objects
# send webmentions and update Object
errors = [] # stores (code, body) tuples
domains = []
targets = [Target(uri=uri, protocol='activitypub') for uri in targets]
obj = Object(id=source, labels=['notification'], undelivered=targets,
status='in progress', **object_props)
if activity.get('objectType') == 'activity' and 'activity' not in obj.labels:
obj.labels.append('activity')
obj.put()
obj.populate(
undelivered=targets,
status='in progress',
)
if obj.undelivered and 'notification' not in obj.labels:
obj.labels.append('notification')
while obj.undelivered:
target = obj.undelivered.pop()
@ -402,8 +403,9 @@ def send_webmentions(activity_wrapped, proxy=None, **object_props):
obj.put()
obj.status = 'complete' if obj.delivered else 'failed' if obj.failed else 'ignored'
obj.put()
obj.status = ('complete' if obj.delivered or obj.domains
else 'failed' if obj.failed
else 'ignored')
if errors:
msg = 'Errors: ' + ', '.join(f'{code} {body}' for code, body in errors)

Wyświetl plik

@ -113,7 +113,7 @@ LIKE_WITH_ACTOR['actor'] = {
# repost of fediverse post, should be delivered to followers
REPOST = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://th.is/users/alice/statuses/654/activity',
'id': 'https://mas.to/users/alice/statuses/654/activity',
'type': 'Announce',
'actor': ACTOR['id'],
'object': NOTE_OBJECT['id'],
@ -190,6 +190,9 @@ UPDATE_NOTE = {
'id': 'https://a/note',
},
}
WEBMENTION_DISCOVERY = requests_response(
'<html><head><link rel="webmention" href="/webmention"></html>')
@patch('requests.post')
@patch('requests.get')
@ -282,18 +285,19 @@ class ActivityPubTest(testutil.TestCase):
got = self.client.post('/foo.com/inbox', json=reply)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assert_req(mock_get, 'http://or.ig/post')
expected_id = urllib.parse.quote_plus(reply['id'])
self.assert_req(
mock_post,
'http://or.ig/webmention',
headers={'Accept': '*/*'},
allow_redirects=False,
data={
'source': 'http://localhost/render?id=http%3A%2F%2Fth.is%2Freply',
'source': f'http://localhost/render?id={expected_id}',
'target': 'http://or.ig/post',
},
)
self.assert_object('http://th.is/reply',
self.assert_object(reply['id'],
domains=['or.ig'],
source_protocol='activitypub',
status='complete',
@ -345,7 +349,6 @@ class ActivityPubTest(testutil.TestCase):
self.assert_object('http://th.is/note/as2',
source_protocol='activitypub',
status='complete',
as2=expected_as2,
as1=as2.to_as1(expected_as2),
domains=['foo.com', 'baz.com'],
@ -409,21 +412,36 @@ class ActivityPubTest(testutil.TestCase):
mock_get.side_effect = [
self.as2_resp(ACTOR), # source actor
self.as2_resp(NOTE_OBJECT), # object of repost
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response() # webmention
got = self.client.post('/inbox', json=REPOST)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
# webmention
expected_id = urllib.parse.quote_plus(REPOST['id'])
self.assert_req(
mock_post,
'http://th.is/webmention',
headers={'Accept': '*/*'},
allow_redirects=False,
data={
'source': f'http://localhost/render?id={expected_id}',
'target': NOTE_OBJECT['url'],
},
)
self.assert_object(REPOST['id'],
source_protocol='activitypub',
status='ignored',
as2=REPOST_FULL,
as1=as2.to_as1(REPOST_FULL),
domains=['foo.com', 'baz.com'],
domains=['foo.com', 'baz.com', 'th.is'],
type='share',
labels=['activity', 'feed', 'notification'],
object_ids=[REPOST['object']])
object_ids=[REPOST['object']],
delivered=[NOTE_OBJECT['url']])
def test_inbox_not_public(self, mock_head, mock_get, mock_post):
Follower.get_or_create(ACTOR['id'], 'foo.com')
@ -473,19 +491,20 @@ class ActivityPubTest(testutil.TestCase):
got = self.client.post('/foo.com/inbox', json=mention)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assert_req(mock_get, 'http://tar.get/')
expected_id = urllib.parse.quote_plus(mention['id'])
self.assert_req(
mock_post,
'http://tar.get/webmention',
headers={'Accept': '*/*'},
allow_redirects=False,
data={
'source': 'http://localhost/render?id=http%3A%2F%2Fth.is%2Fmention',
'source': f'http://localhost/render?id={expected_id}',
'target': 'http://tar.get/',
},
)
expected_as2 = common.redirect_unwrap(mention)
self.assert_object('http://th.is/mention',
self.assert_object(mention['id'],
domains=['tar.get'],
source_protocol='activitypub',
status='complete',
@ -499,9 +518,7 @@ class ActivityPubTest(testutil.TestCase):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
# target post webmention discovery
requests_response(
'<html><head><link rel="webmention" href="/webmention"></html>'),
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
@ -561,8 +578,10 @@ class ActivityPubTest(testutil.TestCase):
'url': FOLLOW['object'],
}
follow = copy.deepcopy(FOLLOW_WRAPPED)
follow['object'] = wrapped_user
follow = {
**FOLLOW_WRAPPED,
'object': wrapped_user,
}
accept = copy.deepcopy(ACCEPT)
accept['actor'] = accept['object']['object'] = wrapped_user
@ -570,14 +589,10 @@ class ActivityPubTest(testutil.TestCase):
self._test_inbox_follow_accept(follow, accept, *mocks)
follower = Follower.query().get()
follow.update({
'actor': ACTOR,
'object': wrapped_user,
})
follow['actor'] = ACTOR
self.assertEqual(follow, json_loads(follower.last_follow))
follow.update({
'actor': FOLLOW_WITH_ACTOR['actor'],
'object': unwrapped_user,
'url': 'https://mastodon.social/users/swentel#followed-https://foo.com/',
})
@ -598,9 +613,7 @@ class ActivityPubTest(testutil.TestCase):
mock_get.side_effect = [
# source actor
self.as2_resp(FOLLOW_WITH_ACTOR['actor']),
# target post webmention discovery
requests_response(
'<html><head><link rel="webmention" href="/webmention"></html>'),
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
@ -670,9 +683,7 @@ class ActivityPubTest(testutil.TestCase):
mock_get.side_effect = [
# source actor
self.as2_resp(FOLLOW_WITH_ACTOR['actor']),
# target post webmention discovery
requests_response(
'<html><head><link rel="webmention" href="/webmention"></html>'),
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
@ -745,7 +756,7 @@ class ActivityPubTest(testutil.TestCase):
mock_post.assert_not_called()
obj = Object.get_by_id(id)
self.assertEqual([], obj.labels)
self.assertEqual(['activity'], obj.labels)
self.assertEqual([], obj.domains)
self.assertIsNone(Object.get_by_id(bad_url))
@ -919,7 +930,7 @@ class ActivityPubTest(testutil.TestCase):
self.assert_object('http://th.is/like#ok',
domains=['or.ig'],
source_protocol='activitypub',
status='ignored',
status='complete',
as2=LIKE_WITH_ACTOR,
as1=as2.to_as1(LIKE_WITH_ACTOR),
type='like',