kopia lustrzana https://github.com/snarfed/bridgy-fed
refactoring, move Object creation out of common.send_webmentions
ugh this was painfulpull/434/head
rodzic
6500f71d3f
commit
74b3b3b689
|
@ -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()
|
||||
|
|
42
common.py
42
common.py
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
Ładowanie…
Reference in New Issue