kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
974fa71443
commit
ed734f3532
34
protocol.py
34
protocol.py
|
@ -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()
|
||||
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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
22
web.py
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue