Protocol.receive: finish full protocol inference based on target ids

for #548, #512
deploy
Ryan Barrett 2023-06-14 13:46:13 -07:00
rodzic 974fa71443
commit ed734f3532
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
4 zmienionych plików z 99 dodań i 42 usunięć

Wyświetl plik

@ -130,10 +130,17 @@ class Protocol:
@classmethod
def key_for(cls, id):
"""Returns the :class:`ndb.Key` for this protocol for a given id.
"""Returns the :class:`ndb.Key` for a given id's :class:`User`.
Canonicalizes the id if necessary.
If called via `Protocol.key_for`, infers the appropriate protocol with
:meth:`for_id`. If called with a concrete subclass, uses that subclass
as is.
"""
if cls == Protocol:
return Protocol.for_id(id).key_for(id)
return cls(id=id).key
@staticmethod
@ -153,15 +160,19 @@ class Protocol:
if not id:
return None
# check for our per-protocol subdomains
if util.is_web(id):
by_domain = Protocol.for_domain(id)
if by_domain:
return by_domain
candidates = []
for protocol in set(PROTOCOLS.values()):
if not protocol:
continue
# sort to be deterministic
protocols = sorted(set(p for p in PROTOCOLS.values() if p),
key=lambda p: p.__name__)
candidates = []
for protocol in protocols:
owns = protocol.owns_id(id)
if owns:
return protocol
@ -312,20 +323,19 @@ class Protocol:
error(f'Undo of Follow requires actor id and object id. Got: {actor_id} {inner_obj_id} {obj.as1}')
# deactivate Follower
followee_domain = util.domain_from_link(inner_obj_id, minimize=False)
# TODO: avoid import?
from web import Web
to_cls = Protocol.for_domain(followee_domain) or Protocol.for_request() or Web
follower = Follower.query(
Follower.to == to_cls(id=followee_domain).key,
Follower.from_ == from_cls(id=actor_id).key,
Follower.status == 'active').get()
from_ = from_cls.key_for(actor_id)
to = (Protocol.for_id(inner_obj_id) or Web).key_for(inner_obj_id)
follower = Follower.query(Follower.to == to,
Follower.from_ == from_,
Follower.status == 'active').get()
if follower:
logger.info(f'Marking {follower} inactive')
follower.status = 'inactive'
follower.put()
else:
logger.warning(f'No Follower found for {followee_domain} {actor_id}')
logger.warning(f'No Follower found for {from_} => {to}')
# TODO send webmention with 410 of u-follow
@ -479,7 +489,7 @@ class Protocol:
# TODO: avoid import?
from web import Web
targets = [Target(uri=uri, protocol=(Protocol.for_domain(uri) or Web).LABEL)
targets = [Target(uri=uri, protocol=(Protocol.for_id(uri) or Web).LABEL)
for uri in targets]
no_user_domains = set()

Wyświetl plik

@ -32,6 +32,7 @@ import protocol
from protocol import Protocol
from web import Web
# have to import module, not attrs, to avoid circular import
from . import test_web
ACTOR = {
@ -270,7 +271,8 @@ class ActivityPubTest(TestCase):
def setUp(self):
super().setUp()
self.user = self.make_user('user.com', has_hcard=True, actor_as2=ACTOR)
self.user = self.make_user('user.com', has_hcard=True, actor_as2=ACTOR,
has_redirects=True)
ACTOR_BASE['publicKey']['publicKeyPem'] = self.user.public_pem().decode()
with self.request_context:
@ -410,14 +412,14 @@ class ActivityPubTest(TestCase):
'labels': ['notification']},
*mocks)
def test_inbox_reply_create_activity(self, *mocks):
def test_inbox_reply_create_activity(self, mock_head, mock_get, mock_post):
self._test_inbox_reply(REPLY,
{'as2': REPLY,
'type': 'post',
'object_ids': [REPLY_OBJECT['id']],
'labels': ['notification', 'activity'],
},
*mocks)
mock_head, mock_get, mock_post)
self.assert_object(REPLY_OBJECT['id'],
source_protocol='activitypub',
as2=REPLY_OBJECT,
@ -427,7 +429,11 @@ class ActivityPubTest(TestCase):
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 [])
+ [WEBMENTION_DISCOVERY])
+ [
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
WEBMENTION_DISCOVERY,
])
mock_post.return_value = requests_response()
got = self.post('/ap/web/user.com/inbox', json=reply)
@ -452,9 +458,8 @@ class ActivityPubTest(TestCase):
delivered=['https://user.com/post'],
**expected_props)
def test_inbox_reply_to_self_domain(self, mock_head, mock_get, mock_post):
self._test_inbox_ignore_reply_to('http://localhost/mas.to',
mock_head, mock_get, mock_post)
def test_inbox_reply_to_self_domain(self, *mocks):
self._test_inbox_ignore_reply_to('http://localhost/mas.to', *mocks)
def test_inbox_reply_to_in_blocklist(self, *mocks):
self._test_inbox_ignore_reply_to('https://twitter.com/foo', *mocks)
@ -464,11 +469,15 @@ class ActivityPubTest(TestCase):
reply['inReplyTo'] = reply_to
mock_head.return_value = requests_response(url='http://mas.to/')
mock_get.side_effect = [
# 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))
mock_get.assert_not_called()
mock_post.assert_not_called()
def test_individual_inbox_create_obj(self, *mocks):
@ -523,7 +532,8 @@ class ActivityPubTest(TestCase):
}
del note['url']
with self.request_context:
Object(id=orig_url, mf2=microformats2.object_to_json(as2.to_as1(note))).put()
Object(id=orig_url, mf2=microformats2.object_to_json(as2.to_as1(note)),
source_protocol='web').put()
repost = copy.deepcopy(REPOST_FULL)
repost['object'] = f'http://localhost/r/{orig_url}'
@ -568,6 +578,9 @@ class ActivityPubTest(TestCase):
mock_get.side_effect = [
self.as2_resp(ACTOR), # source actor
self.as2_resp(NOTE_OBJECT), # object of repost
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
HTML, # no webmention endpoint
]
@ -589,6 +602,9 @@ class ActivityPubTest(TestCase):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
# target post webmention discovery
HTML,
]
@ -655,8 +671,10 @@ class ActivityPubTest(TestCase):
self.make_user('tar.get')
mock_get.side_effect = [
self.as2_resp(ACTOR),
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
WEBMENTION_DISCOVERY,
HTML,
]
mock_post.return_value = requests_response()
@ -689,6 +707,8 @@ class ActivityPubTest(TestCase):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
WEBMENTION_DISCOVERY,
]
mock_post.return_value = requests_response()
@ -696,10 +716,8 @@ class ActivityPubTest(TestCase):
got = self.post('/user.com/inbox', json=LIKE)
self.assertEqual(200, got.status_code)
mock_get.assert_has_calls((
self.as2_req('https://user.com/actor'),
self.req('https://user.com/post'),
)),
self.assertIn(self.as2_req('https://user.com/actor'), mock_get.mock_calls)
self.assertIn(self.req('https://user.com/post'), mock_get.mock_calls)
args, kwargs = mock_post.call_args
self.assertEqual(('https://user.com/webmention',), args)
@ -883,13 +901,11 @@ class ActivityPubTest(TestCase):
def test_inbox_undo_follow(self, mock_head, mock_get, mock_post):
mock_head.return_value = requests_response(url='https://user.com/')
mock_get.side_effect = [
self.as2_resp(ACTOR),
]
follower = Follower(to=self.user.key,
from_=ActivityPub.get_or_create(ACTOR['id']).key,
status='active')
follower.put()
follower = Follower.get_or_create(to=self.user,
from_=ActivityPub.get_or_create(ACTOR['id']))
got = self.post('/user.com/inbox', json=UNDO_FOLLOW_WRAPPED)
self.assertEqual(200, got.status_code)
@ -1145,6 +1161,9 @@ class ActivityPubTest(TestCase):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
# target post webmention discovery
ReadTimeoutError(None, None, None),
]
@ -1156,6 +1175,9 @@ class ActivityPubTest(TestCase):
mock_get.side_effect = [
# source actor
self.as2_resp(LIKE_WITH_ACTOR['actor']),
# protocol inference
requests_response(test_web.NOTE_HTML),
requests_response(test_web.NOTE_HTML),
# target post webmention discovery
HTML,
]

Wyświetl plik

@ -396,7 +396,7 @@ ACTIVITYPUB_GETS = [REPLY, NOT_FEDIVERSE, TOOT_AS2, ACTOR]
class WebTest(TestCase):
def setUp(self):
super().setUp()
g.user = self.make_user('user.com')
g.user = self.make_user('user.com', has_redirects=True)
self.request_context.push()
def assert_deliveries(self, mock_post, inboxes, data, ignore=()):
@ -1398,7 +1398,8 @@ class WebTest(TestCase):
expected_as2)
# updated Web user
self.assert_user(Web, 'user.com', actor_as2=ACTOR_AS2_USER, direct=True)
self.assert_user(Web, 'user.com', actor_as2=ACTOR_AS2_USER, direct=True,
has_redirects=True)
# homepage object
self.assert_object('https://user.com/',
@ -1436,6 +1437,9 @@ class WebTest(TestCase):
)
def _test_verify(self, redirects, hcard, actor, redirects_error=None):
g.user.has_redirects = False
g.user.put()
got = g.user.verify()
self.assertEqual(g.user.key, got.key)
@ -1703,16 +1707,25 @@ class WebProtocolTest(TestCase):
for id in 'user.com', 'http://user.com', 'https://user.com/':
self.assertEqual(Web(id='user.com').key, Web.key_for(id))
for bad in None, '', 'foo bar':
for bad in None, '', 'foo', 'https://foo/', 'foo bar':
with self.assertRaises(AssertionError):
Web.key_for(bad)
def test_owns_id(self, *_):
self.assertIsNone(Web.owns_id('http://foo'))
self.assertIsNone(Web.owns_id('https://bar/baz'))
self.assertIsNone(Web.owns_id('http://foo.com'))
self.assertIsNone(Web.owns_id('https://bar.com/'))
self.assertIsNone(Web.owns_id('https://bar.com/baz'))
self.assertIsNone(Web.owns_id('https://bar/'))
self.assertFalse(Web.owns_id('at://did:plc:foo/bar/123'))
self.assertFalse(Web.owns_id('e45fab982'))
self.assertFalse(Web.owns_id('user.com'))
g.user.has_redirects = True
g.user.put()
self.assertTrue(Web.owns_id('user.com'))
g.user.key.delete()
self.assertIsNone(Web.owns_id('user.com'))
def test_fetch(self, mock_get, __):
mock_get.return_value = REPOST

22
web.py
Wyświetl plik

@ -209,14 +209,15 @@ class Web(User, Protocol):
"""
assert id
if re.match(common.DOMAIN_RE, id):
return cls(id=id).key
elif util.is_web(id):
if util.is_web(id):
parsed = urlparse(id)
if parsed.path in ('', '/'):
return cls(id=parsed.netloc).key
id = parsed.netloc
assert False, f'{id} is not domain or usable home page URL'
if re.match(common.DOMAIN_RE, id):
return cls(id=id).key
assert False, f'{id} is not a domain or usable home page URL'
@classmethod
def owns_id(cls, id):
@ -224,6 +225,17 @@ class Web(User, Protocol):
All web pages are http(s) URLs, but not all http(s) URLs are web pages.
"""
if not id:
return False
try:
key = cls.key_for(id)
if key:
user = key.get()
return True if user and user.has_redirects else None
except AssertionError:
pass
return None if util.is_web(id) else False
@classmethod