merging receive: delivery bug fix, stop suppressing webmention send errors

for #529
pull/582/head
Ryan Barrett 2023-07-06 21:16:04 -07:00
rodzic 5f4d6757e7
commit 9c62786f06
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
5 zmienionych plików z 104 dodań i 82 usunięć

Wyświetl plik

@ -474,6 +474,7 @@ class Protocol:
# fetch actor if necessary so we have name, profile photo, etc
if actor and actor.keys() == set(['id']):
logger.info('Fetching actor so we have name, profile photo, etc')
actor_obj = cls.load(actor['id'])
if actor_obj.as1:
obj.our_as1 = {**obj.as1, 'actor': actor_obj.as1}
@ -481,6 +482,7 @@ class Protocol:
# fetch object if necessary so we can render it in feeds
if obj.type == 'share' and inner_obj_as1.keys() == set(['id']):
if not inner_obj and cls.owns_id(inner_obj_id):
logger.info('Fetching object so we can render it in feeds')
inner_obj = cls.load(inner_obj_id)
if inner_obj and inner_obj.as1:
obj.our_as1 = {
@ -673,7 +675,7 @@ class Protocol:
sent = protocol.send(obj, target.uri, log_data=log_data)
if sent:
add(obj.delivered, target)
obj.undelivered.remove(target)
obj.undelivered.remove(target)
except BaseException as e:
code, body = util.interpret_http_exception(e)
if not code and not body:

Wyświetl plik

@ -276,6 +276,8 @@ class ActivityPubTest(TestCase):
self.user = self.make_user('user.com', has_hcard=True, has_redirects=True,
obj_as2={**ACTOR, 'id': 'https://user.com/'})
self.swentel_key = ndb.Key(ActivityPub, 'https://mas.to/users/swentel')
self.user_actor_key = ndb.Key(ActivityPub, 'https://user.com/actor')
ACTOR_BASE['publicKey']['publicKeyPem'] = self.user.public_pem().decode()
@ -396,7 +398,8 @@ class ActivityPubTest(TestCase):
self._test_inbox_reply(reply, {
'as2': reply,
'type': 'post',
'labels': ['activity', 'notification'],
'labels': ['activity', 'user', 'notification'],
'users': [self.user.key, self.user_actor_key],
}, mock_head, mock_get, mock_post)
self.assert_user(ActivityPub, 'https://user.com/actor',
@ -427,7 +430,7 @@ class ActivityPubTest(TestCase):
{'as2': REPLY,
'type': 'post',
'object_ids': [REPLY_OBJECT['id']],
'labels': ['notification', 'activity'],
'labels': ['notification', 'activity', 'user'],
},
mock_head, mock_get, mock_post)
self.assert_object(REPLY_OBJECT['id'],
@ -435,7 +438,8 @@ class ActivityPubTest(TestCase):
our_as1=as2.to_as1(REPLY_OBJECT),
type='comment')
def _test_inbox_reply(self, reply, expected_props, mock_head, mock_get, mock_post):
def _test_inbox_reply(self, reply, expected_props, mock_head, mock_get,
mock_post):
mock_head.return_value = requests_response(url='https://user.com/post')
mock_get.side_effect = (
(list(mock_get.side_effect) if mock_get.side_effect else [])
@ -461,12 +465,14 @@ class ActivityPubTest(TestCase):
},
)
self.assert_object(reply['id'],
users=[self.user.key],
source_protocol='activitypub',
status='complete',
delivered=['https://user.com/post'],
**expected_props)
expected_props = {
'users': [self.user.key, self.swentel_key],
'source_protocol': 'activitypub',
'status': 'complete',
'delivered': ['https://user.com/post'],
**expected_props,
}
self.assert_object(reply['id'], **expected_props)
def test_inbox_reply_to_self_domain(self, *mocks):
self._test_inbox_ignore_reply_to('http://localhost/mas.to', *mocks)
@ -480,14 +486,15 @@ class ActivityPubTest(TestCase):
mock_head.return_value = requests_response(url='http://mas.to/')
mock_get.side_effect = [
# actor fetch
self.as2_resp(ACTOR),
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
]
got = self.post('/user.com/inbox', json=reply)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertEqual(204, got.status_code, got.get_data(as_text=True))
mock_post.assert_not_called()
def test_individual_inbox_create_obj(self, *mocks):
@ -566,24 +573,25 @@ class ActivityPubTest(TestCase):
},
)
repost['object'] = note
del repost['object']['to']
del repost['object']['cc']
self.assert_object(REPOST_FULL['id'],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(repost),
users=[self.user.key],
as2={
**REPOST,
'actor': ACTOR,
'object': orig_url,
},
users=[self.swentel_key],
delivered=['https://user.com/orig'],
type='share',
labels=['activity', 'feed', 'notification'],
labels=['activity', 'user', 'feed'],
object_ids=['https://user.com/orig'])
def test_shared_inbox_repost_of_fediverse(self, mock_head, mock_get, mock_post):
Follower.get_or_create(to=ActivityPub.get_or_create(ACTOR['id']),
from_=self.user)
Follower.get_or_create(to=ActivityPub.get_or_create(ACTOR['id']),
from_=Fake.get_or_create('http://baz'))
baz = Fake.get_or_create('http://baz')
Follower.get_or_create(to=ActivityPub.get_or_create(ACTOR['id']), from_=baz)
Follower.get_or_create(to=ActivityPub.get_or_create(ACTOR['id']),
from_=Fake.get_or_create('http://baj'),
status='inactive')
@ -599,7 +607,7 @@ class ActivityPubTest(TestCase):
]
got = self.post('/ap/sharedInbox', json=REPOST)
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
self.assertEqual(204, got.status_code, got.get_data(as_text=True))
mock_post.assert_not_called() # no webmention
@ -607,7 +615,7 @@ class ActivityPubTest(TestCase):
source_protocol='activitypub',
status='ignored',
our_as1=as2.to_as1(REPOST_FULL),
users=[self.user.key, Fake(id='http://baz').key],
users=[self.user.key, baz, self.swentel_key],
type='share',
labels=['activity', 'feed'],
object_ids=[REPOST['object']])
@ -623,8 +631,11 @@ class ActivityPubTest(TestCase):
HTML,
]
got = self.post('/ap/sharedInbox', json={**LIKE, 'object': 'http://nope.com/post'})
self.assertEqual(200, got.status_code)
got = self.post('/ap/sharedInbox', json={
**LIKE,
'object': 'http://nope.com/post',
})
self.assertEqual(204, got.status_code)
self.assert_object('http://mas.to/like#ok',
# no nope.com Web user key since it didn't exist
@ -744,7 +755,7 @@ class ActivityPubTest(TestCase):
}, kwargs['data'])
self.assert_object('http://mas.to/like#ok',
users=[self.user.key],
users=[self.user.key, self.user_actor_key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(LIKE_WITH_ACTOR),
@ -764,14 +775,14 @@ class ActivityPubTest(TestCase):
obj_as2=LIKE_ACTOR, direct=True)
def test_inbox_follow_accept_with_id(self, *mocks):
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, *mocks)
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, 200, *mocks)
follow = {
**FOLLOW_WITH_ACTOR,
'url': 'https://mas.to/users/swentel#followed-https://user.com/',
}
self.assert_object('https://mas.to/6d1a',
users=[self.user.key],
users=[self.user.key, self.swentel_key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(follow),
@ -788,14 +799,14 @@ class ActivityPubTest(TestCase):
'url': FOLLOW['object'],
},
}
self._test_inbox_follow_accept(follow, ACCEPT, *mocks)
self._test_inbox_follow_accept(follow, ACCEPT, 200, *mocks)
follow.update({
'actor': ACTOR,
'url': 'https://mas.to/users/swentel#followed-https://user.com/',
})
self.assert_object('https://mas.to/6d1a',
users=[self.user.key],
users=[self.user.key, self.swentel_key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(follow),
@ -804,29 +815,28 @@ class ActivityPubTest(TestCase):
labels=['notification', 'activity'],
object_ids=[FOLLOW['object']])
def test_inbox_follow_accept_webmention_fails(self, mock_head, mock_get, mock_post):
def test_inbox_follow_accept_webmention_fails(self, mock_head, mock_get,
mock_post):
mock_post.side_effect = [
requests_response(), # AP Accept
requests.ConnectionError(), # webmention
]
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT,
self._test_inbox_follow_accept(FOLLOW_WRAPPED, ACCEPT, 304,
mock_head, mock_get, mock_post)
follow = {
**FOLLOW_WITH_ACTOR,
'url': 'https://mas.to/users/swentel#followed-https://user.com/',
}
url = 'https://mas.to/users/swentel#followed-https://user.com/'
self.assert_object('https://mas.to/6d1a',
users=[self.user.key],
users=[self.user.key, self.swentel_key],
source_protocol='activitypub',
status='complete',
our_as1=as2.to_as1(follow),
status='failed',
our_as1=as2.to_as1({**FOLLOW_WITH_ACTOR, 'url': url}),
delivered=[],
failed=['https://user.com/'],
type='follow',
labels=['notification', 'activity'],
labels=['notification', 'activity', 'user'],
object_ids=[FOLLOW['object']])
def _test_inbox_follow_accept(self, follow_as2, accept_as2,
def _test_inbox_follow_accept(self, follow_as2, accept_as2, expected_status,
mock_head, mock_get, mock_post):
# this should makes us make the follower ActivityPub as direct=True
self.user.direct = False
@ -842,7 +852,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=follow_as2)
self.assertEqual(200, got.status_code)
self.assertEqual(expected_status, got.status_code)
mock_get.assert_has_calls((
self.as2_req(FOLLOW['actor']),
@ -889,7 +899,7 @@ class ActivityPubTest(TestCase):
mock_post.return_value = requests_response()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
self.assertEqual(204, got.status_code)
follower = Follower.query().get()
self.assert_entities_equal(
@ -994,23 +1004,29 @@ class ActivityPubTest(TestCase):
id = 'https://mas.to/users/tmichellemoore#likes/56486252'
bad_url = 'http://localhost/r/Testing \u2013 Brid.gy \u2013 Post to Mastodon 3'
got = self.post('/user.com/inbox', json={
bad = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': id,
'type': 'Like',
'actor': ACTOR['id'],
'object': bad_url,
})
}
got = self.post('/user.com/inbox', json=bad)
# bad object, should ignore activity
self.assertEqual(200, got.status_code)
self.assertEqual(204, got.status_code)
mock_post.assert_not_called()
obj = Object.get_by_id(id)
self.assertEqual(['activity'], obj.labels)
self.assertEqual([], obj.users)
self.assertEqual([], obj.domains)
self.assert_object(id,
our_as1={
**as2.to_as1(bad),
'actor': as2.to_as1(ACTOR),
},
labels=['activity', 'user'],
users=[self.swentel_key],
source_protocol='activitypub',
status='ignored',
)
self.assertIsNone(Object.get_by_id(bad_url))
@patch('activitypub.logger.info', side_effect=logging.info)
@ -1034,7 +1050,7 @@ class ActivityPubTest(TestCase):
body = json_dumps(NOTE)
headers = self.sign('/ap/sharedInbox', json_dumps(NOTE))
resp = self.client.post('/ap/sharedInbox', data=body, headers=headers)
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
self.assertEqual(204, resp.status_code, resp.get_data(as_text=True))
mock_get.assert_has_calls((
self.as2_req('http://my/key/id'),
))
@ -1133,7 +1149,6 @@ class ActivityPubTest(TestCase):
self.assertEqual(204, resp.status_code)
self.assertTrue(obj.key.get().deleted)
self.assert_object(delete['id'],
as2=delete,
our_as1={
**as2.to_as1(delete),
'actor': as2.to_as1(ACTOR),
@ -1162,21 +1177,29 @@ class ActivityPubTest(TestCase):
]
resp = self.post('/ap/sharedInbox', json=UPDATE_NOTE)
self.assertEqual(200, resp.status_code)
self.assertEqual(204, resp.status_code)
note_as1 = as2.to_as1({
**UPDATE_NOTE['object'],
'author': {'id': 'https://mas.to/users/swentel'},
})
self.assert_object('https://a/note',
type='note',
our_as1=as2.to_as1({
**UPDATE_NOTE['object'],
'author': {'id': 'https://mas.to/users/swentel'},
}),
our_as1=note_as1,
source_protocol='activitypub')
update_as1 = {
**as2.to_as1(UPDATE_NOTE),
'object': note_as1,
'actor': as2.to_as1(ACTOR),
}
self.assert_object(UPDATE_NOTE['id'],
source_protocol='activitypub',
type='update',
status='complete',
as2=UPDATE_NOTE,
labels=['activity'])
status='ignored',
our_as1=update_as1,
labels=['activity', 'user'],
users=[self.swentel_key])
self.assert_entities_equal(Object.get_by_id('https://a/note'),
protocol.objects_cache['https://a/note'])
@ -1208,22 +1231,22 @@ class ActivityPubTest(TestCase):
]
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(200, got.status_code)
self.assertEqual(204, got.status_code)
self.assert_object('http://mas.to/like#ok',
users=[self.user.key],
users=[self.user.key, self.user_actor_key],
source_protocol='activitypub',
status='complete',
status='ignored',
our_as1=as2.to_as1(LIKE_WITH_ACTOR),
type='like',
labels=['activity', 'notification'],
labels=['activity', 'user', 'notification'],
object_ids=[LIKE['object']])
def test_inbox_id_already_seen(self, *mocks):
obj_key = Object(id=FOLLOW_WRAPPED['id'], as2={}).put()
got = self.post('/user.com/inbox', json=FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
self.assertEqual(204, got.status_code)
self.assertEqual(0, Follower.query().count())
# second time should use in memory cache

Wyświetl plik

@ -279,13 +279,7 @@ class ProtocolReceiveTest(TestCase):
def assert_object(self, id, **props):
props.setdefault('source_protocol', 'fake')
props.setdefault('delivered_protocol', 'fake')
ignore = []
for field in 'as2', 'bsky', 'mf2':
if 'our_as1' in props and field not in props:
ignore.append(field)
return super().assert_object(id, ignore=ignore, **props)
return super().assert_object(id, **props)
def make_followers(self):
Follower.get_or_create(to=self.user, from_=self.alice)

Wyświetl plik

@ -300,6 +300,7 @@ class TestCase(unittest.TestCase, testutil.Asserts):
mock.assert_any_call(url, **kwargs)
def assert_object(self, id, delivered_protocol=None, **props):
ignore = props.pop('ignore', [])
got = Object.get_by_id(id)
assert got, id
@ -308,6 +309,12 @@ class TestCase(unittest.TestCase, testutil.Asserts):
props[field] = [Target(uri=uri, protocol=delivered_protocol)
for uri in props.get(field, [])]
if 'our_as1' in props:
assert 'as2' not in props
assert 'bsky' not in props
assert 'mf2' not in props
ignore.extend(['as2', 'bsky', 'mf2'])
mf2 = props.get('mf2')
if mf2 and 'items' in mf2:
props['mf2'] = mf2['items'][0]
@ -329,7 +336,6 @@ class TestCase(unittest.TestCase, testutil.Asserts):
for target in got.delivered:
del target.key
ignore = props.pop('ignore', [])
self.assert_entities_equal(Object(id=id, **props), got,
ignore=['as1', 'created', 'expire',
'object_ids', 'type', 'updated'

15
web.py
Wyświetl plik

@ -270,23 +270,20 @@ class Web(User, Protocol):
See :meth:`Protocol.send` for details.
*Does not* propagate HTTP errors, DNS or connection failures, or other
exceptions, since webmention support is optional for web recipients.
Returns true if the target URL doesn't advertise a webmention endpoint,
since webmention support itself is optional for web recipients.
https://fed.brid.gy/docs#error-handling
"""
source_url = obj.proxy_url()
logger.info(f'Sending webmention from {source_url} to {url}')
endpoint = common.webmention_discover(url).endpoint
try:
if endpoint:
webmention.send(endpoint, source_url, url)
return True
except RequestException as e:
# log exception, then ignore it
util.interpret_http_exception(e)
if not endpoint:
return False
webmention.send(endpoint, source_url, url)
return True
@classmethod
def fetch(cls, obj, gateway=False, check_backlink=False, **kwargs):
"""Fetches a URL over HTTP and extracts its microformats2.