kopia lustrzana https://github.com/snarfed/bridgy-fed
AP: fix followers and outbox for protocol bot users
eg https://bsky.brid.gy/bsky.brid.gy/followers , wasn't working beforepull/1263/head
rodzic
ea6e195835
commit
34cb4fbe59
|
@ -7,7 +7,7 @@ import re
|
|||
from urllib.parse import quote_plus, urljoin, urlparse
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from flask import abort, g, redirect, request
|
||||
from flask import abort, g, request
|
||||
from google.cloud import ndb
|
||||
from google.cloud.ndb.query import FilterNode, OR, Query
|
||||
from granary import as1, as2
|
||||
|
@ -15,6 +15,7 @@ from httpsig import HeaderVerifier
|
|||
from httpsig.requests_auth import HTTPSignatureAuth
|
||||
from httpsig.utils import parse_signature_header
|
||||
from oauth_dropins.webutil import appengine_info, flask_util, util
|
||||
from oauth_dropins.webutil.flask_util import MovedPermanently
|
||||
from oauth_dropins.webutil.util import fragmentless, json_dumps, json_loads
|
||||
import requests
|
||||
from requests import TooManyRedirects
|
||||
|
@ -928,6 +929,36 @@ def postprocess_as2_actor(actor, user):
|
|||
|
||||
return actor
|
||||
|
||||
|
||||
def _load_user(handle_or_id, create=False):
|
||||
if handle_or_id == PRIMARY_DOMAIN or handle_or_id in PROTOCOL_DOMAINS:
|
||||
from web import Web
|
||||
proto = Web
|
||||
else:
|
||||
proto = Protocol.for_request(fed='web')
|
||||
|
||||
if not proto:
|
||||
error(f"Couldn't determine protocol", status=404)
|
||||
|
||||
if proto.owns_id(handle_or_id) is False:
|
||||
if proto.owns_handle(handle_or_id) is False:
|
||||
error(f"{handle_or_id} doesn't look like a {proto.LABEL} id or handle",
|
||||
status=404)
|
||||
id = proto.handle_to_id(handle_or_id)
|
||||
if not id:
|
||||
error(f"Couldn't resolve {handle_or_id} as a {proto.LABEL} handle",
|
||||
status=404)
|
||||
else:
|
||||
id = handle_or_id
|
||||
|
||||
assert id
|
||||
user = proto.get_or_create(id) if create else proto.get_by_id(id)
|
||||
if not user or not user.is_enabled(ActivityPub):
|
||||
error(f'{proto.LABEL} user {id} not found', status=404)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
# source protocol in subdomain.
|
||||
# WARNING: the user page handler in pages.py overrides this for fediverse
|
||||
# addresses with leading @ character. be careful when changing this route!
|
||||
|
@ -939,42 +970,21 @@ def postprocess_as2_actor(actor, user):
|
|||
@flask_util.headers(CACHE_CONTROL)
|
||||
def actor(handle_or_id):
|
||||
"""Serves a user's AS2 actor from the datastore."""
|
||||
if handle_or_id == PRIMARY_DOMAIN or handle_or_id in PROTOCOL_DOMAINS:
|
||||
from web import Web
|
||||
cls = Web
|
||||
else:
|
||||
cls = Protocol.for_request(fed='web')
|
||||
user = _load_user(handle_or_id, create=True)
|
||||
proto = user
|
||||
|
||||
if not cls:
|
||||
error(f"Couldn't determine protocol", status=404)
|
||||
elif cls.LABEL == 'web' and request.path.startswith('/ap/'):
|
||||
if proto.LABEL == 'web' and request.path.startswith('/ap/'):
|
||||
# we started out with web users' AP ids as fed.brid.gy/[domain], so we
|
||||
# need to preserve those for backward compatibility
|
||||
return redirect(subdomain_wrap(None, f'/{handle_or_id}'), code=301)
|
||||
|
||||
if cls.owns_id(handle_or_id) is False:
|
||||
if cls.owns_handle(handle_or_id) is False:
|
||||
error(f"{handle_or_id} doesn't look like a {cls.LABEL} id or handle",
|
||||
status=404)
|
||||
id = cls.handle_to_id(handle_or_id)
|
||||
if not id:
|
||||
error(f"Couldn't resolve {handle_or_id} as a {cls.LABEL} handle",
|
||||
status=404)
|
||||
else:
|
||||
id = handle_or_id
|
||||
|
||||
assert id
|
||||
user = cls.get_or_create(id)
|
||||
if not user or not user.is_enabled(ActivityPub):
|
||||
error(f'{cls.LABEL} user {id} not found', status=404)
|
||||
raise MovedPermanently(location=subdomain_wrap(None, f'/{handle_or_id}'))
|
||||
|
||||
id = user.id_as(ActivityPub)
|
||||
# check that we're serving from the right subdomain
|
||||
if request.host != urlparse(id).netloc:
|
||||
return redirect(id)
|
||||
raise MovedPermanently(location=id)
|
||||
|
||||
if not user.obj or not user.obj.as1:
|
||||
user.obj = cls.load(user.profile_id(), gateway=True)
|
||||
user.obj = proto.load(user.profile_id(), gateway=True)
|
||||
if user.obj:
|
||||
user.obj.put()
|
||||
|
||||
|
@ -990,7 +1000,7 @@ def actor(handle_or_id):
|
|||
'following': id + '/following',
|
||||
'followers': id + '/followers',
|
||||
'endpoints': {
|
||||
'sharedInbox': subdomain_wrap(cls, '/ap/sharedInbox'),
|
||||
'sharedInbox': subdomain_wrap(proto, '/ap/sharedInbox'),
|
||||
},
|
||||
# add this if we ever change the Web actor ids to be /web/[id]
|
||||
# 'alsoKnownAs': [host_url(id)],
|
||||
|
@ -1139,11 +1149,7 @@ def follower_collection(id, collection):
|
|||
import pages
|
||||
return pages.followers_or_following('ap', id, collection)
|
||||
|
||||
protocol = Protocol.for_request(fed='web')
|
||||
assert protocol
|
||||
user = protocol.get_by_id(id)
|
||||
if not user or not user.is_enabled(ActivityPub):
|
||||
return f'{protocol} user {id} not found', 404
|
||||
user = _load_user(id)
|
||||
|
||||
if request.method == 'HEAD':
|
||||
return '', {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE}
|
||||
|
@ -1203,13 +1209,7 @@ def outbox(id):
|
|||
|
||||
TODO: unify page generation with follower_collection()
|
||||
"""
|
||||
protocol = Protocol.for_request(fed='web')
|
||||
if not protocol:
|
||||
error(f"Couldn't determine protocol", status=404)
|
||||
|
||||
user = protocol.get_by_id(id)
|
||||
if not user or not user.is_enabled(ActivityPub):
|
||||
error(f'User {id} not found', status=404)
|
||||
user = _load_user(id)
|
||||
|
||||
if request.method == 'HEAD':
|
||||
return '', {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE}
|
||||
|
|
|
@ -476,13 +476,13 @@ class ActivityPubTest(TestCase):
|
|||
self.user.ap_subdomain = 'web'
|
||||
self.user.put()
|
||||
resp = self.client.get('/user.com', base_url='https://fed.brid.gy/')
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertEqual(301, resp.status_code)
|
||||
self.assertEqual('https://web.brid.gy/user.com', resp.headers['Location'])
|
||||
|
||||
self.user.ap_subdomain = 'fed'
|
||||
self.user.put()
|
||||
got = self.client.get('/user.com', base_url='https://web.brid.gy/')
|
||||
self.assertEqual(302, got.status_code)
|
||||
self.assertEqual(301, got.status_code)
|
||||
self.assertEqual('https://fed.brid.gy/user.com', got.headers['Location'])
|
||||
|
||||
def test_actor_opted_out(self, *_):
|
||||
|
@ -493,8 +493,6 @@ class ActivityPubTest(TestCase):
|
|||
got = self.client.get('/user.com')
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
||||
# skip _pre_put_hook since it doesn't allow internal domains
|
||||
@patch.object(Web, '_pre_put_hook', new=lambda self: None)
|
||||
def test_actor_protocol_bot_user(self, *_):
|
||||
"""Web users are special cased to drop the /web/ prefix."""
|
||||
actor_as2 = json_loads(util.read('bsky.brid.gy.as2.json'))
|
||||
|
@ -1844,6 +1842,26 @@ class ActivityPubTest(TestCase):
|
|||
},
|
||||
}, resp.json)
|
||||
|
||||
def test_followers_collection_protocol_bot_user(self, *_):
|
||||
self.user = self.make_user('bsky.brid.gy', cls=Web, ap_subdomain='bsky')
|
||||
self.store_followers()
|
||||
|
||||
resp = self.client.get('/bsky.brid.gy/followers',
|
||||
base_url='https://bsky.brid.gy')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assert_equals({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': 'https://bsky.brid.gy/bsky.brid.gy/followers',
|
||||
'type': 'Collection',
|
||||
'summary': "bsky.brid.gy's followers",
|
||||
'totalItems': 2,
|
||||
'first': {
|
||||
'type': 'CollectionPage',
|
||||
'partOf': 'https://bsky.brid.gy/bsky.brid.gy/followers',
|
||||
'items': [ACTOR, ACTOR],
|
||||
},
|
||||
}, resp.json)
|
||||
|
||||
@patch('models.PAGE_SIZE', 1)
|
||||
def test_followers_collection_page(self, *_):
|
||||
self.store_followers()
|
||||
|
@ -1864,6 +1882,15 @@ class ActivityPubTest(TestCase):
|
|||
'items': [ACTOR],
|
||||
}, resp.json)
|
||||
|
||||
def test_followers_collection_page_protocol_bot_user(self, *_):
|
||||
self.user = self.make_user('bsky.brid.gy', cls=Web, ap_subdomain='bsky')
|
||||
self.store_followers()
|
||||
|
||||
before = (datetime.utcnow() + timedelta(seconds=1)).isoformat()
|
||||
resp = self.client.get(f'/bsky.brid.gy/followers?before={before}',
|
||||
base_url='https://bsky.brid.gy')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_following_collection_unknown_user(self, *_):
|
||||
resp = self.client.get('/nope.com/following')
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
@ -1920,6 +1947,26 @@ class ActivityPubTest(TestCase):
|
|||
},
|
||||
}, resp.json)
|
||||
|
||||
def test_following_collection_protocol_bot_user(self, *_):
|
||||
self.user = self.make_user('bsky.brid.gy', cls=Web, ap_subdomain='bsky')
|
||||
self.store_following()
|
||||
|
||||
resp = self.client.get('/bsky.brid.gy/following',
|
||||
base_url='https://bsky.brid.gy')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assert_equals({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': 'https://bsky.brid.gy/bsky.brid.gy/following',
|
||||
'type': 'Collection',
|
||||
'summary': "bsky.brid.gy's following",
|
||||
'totalItems': 2,
|
||||
'first': {
|
||||
'type': 'CollectionPage',
|
||||
'partOf': 'https://bsky.brid.gy/bsky.brid.gy/following',
|
||||
'items': [ACTOR, ACTOR],
|
||||
},
|
||||
}, resp.json)
|
||||
|
||||
@patch('models.PAGE_SIZE', 1)
|
||||
def test_following_collection_page(self, *_):
|
||||
self.store_following()
|
||||
|
@ -1940,6 +1987,15 @@ class ActivityPubTest(TestCase):
|
|||
'items': [ACTOR],
|
||||
}, resp.json)
|
||||
|
||||
def test_following_collection_page_protocol_bot_user(self, *_):
|
||||
self.user = self.make_user('bsky.brid.gy', cls=Web, ap_subdomain='bsky')
|
||||
self.store_following()
|
||||
|
||||
before = (datetime.utcnow() + timedelta(seconds=1)).isoformat()
|
||||
resp = self.client.get(f'/bsky.brid.gy/following?before={before}',
|
||||
base_url='https://bsky.brid.gy')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_following_collection_head(self, *_):
|
||||
resp = self.client.head(f'/user.com/following')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
@ -1974,6 +2030,24 @@ class ActivityPubTest(TestCase):
|
|||
},
|
||||
}, resp.json)
|
||||
|
||||
def test_outbox_protocol_bot_user_empty(self, *_):
|
||||
self.make_user('bsky.brid.gy', cls=Web, ap_subdomain='bsky')
|
||||
resp = self.client.get(f'/bsky.brid.gy/outbox',
|
||||
base_url='https://bsky.brid.gy')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': 'https://bsky.brid.gy/bsky.brid.gy/outbox',
|
||||
'summary': "bsky.brid.gy's outbox",
|
||||
'type': 'OrderedCollection',
|
||||
'totalItems': 0,
|
||||
'first': {
|
||||
'type': 'CollectionPage',
|
||||
'partOf': 'https://bsky.brid.gy/bsky.brid.gy/outbox',
|
||||
'items': [],
|
||||
},
|
||||
}, resp.json)
|
||||
|
||||
def store_outbox_objects(self, user):
|
||||
for i, obj in enumerate([REPLY, MENTION, LIKE, DELETE]):
|
||||
self.store_object(id=obj['id'], users=[user.key], as2=obj)
|
||||
|
|
6
web.py
6
web.py
|
@ -68,7 +68,7 @@ def is_valid_domain(domain, allow_internal=True):
|
|||
Valid means TLD is ok, not blacklisted, etc.
|
||||
"""
|
||||
if not domain or not re.match(DOMAIN_RE, domain):
|
||||
# logger.debug(f"{domain} doesn't look like a domain")
|
||||
logger.debug(f"{domain} doesn't look like a domain")
|
||||
return False
|
||||
|
||||
if Web.is_blocklisted(domain, allow_internal=allow_internal):
|
||||
|
@ -404,7 +404,7 @@ class Web(User, Protocol):
|
|||
# homepage, check domain too
|
||||
(urlparse(url).path.strip('/') == ''
|
||||
and util.domain_from_link(url) in targets)):
|
||||
# logger.info(f'Skipping sending to {url} , not a target in the object')
|
||||
logger.debug(f'Skipping sending to {url} , not a target in the object')
|
||||
return False
|
||||
|
||||
if to_cls.is_blocklisted(url):
|
||||
|
@ -739,7 +739,7 @@ def poll_feed_task():
|
|||
if url := elem.get('url'):
|
||||
elem['id'] = elem['url']
|
||||
|
||||
# logger.info(f'Converted to AS1: {json_dumps(activity, indent=2)}')
|
||||
logger.debug(f'Converted to AS1: {json_dumps(activity, indent=2)}')
|
||||
|
||||
id = Object(our_as1=activity).as1.get('id')
|
||||
if not id:
|
||||
|
|
Ładowanie…
Reference in New Issue