abstract redirect.py to be multi-protocol

...mostly. creating the underlying user opportunistically is still Web-only.
pull/962/head
Ryan Barrett 2024-04-14 18:26:34 -07:00
rodzic 5b5ed4173a
commit 2ec22de09f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
4 zmienionych plików z 70 dodań i 63 usunięć

Wyświetl plik

@ -940,8 +940,7 @@ class Object(StringIdModel):
:meth:`protocol.Protocol.translate_ids` is partly the inverse of this.
Much of the same logic is duplicated there!
TODO: unify with :meth:`normalize_ids`,
:meth:`protocol.Protocol.normalize_ids`.
TODO: unify with :meth:`normalize_ids`, :meth:`Object.normalize_ids`.
"""
if not self.as1:
return

Wyświetl plik

@ -515,7 +515,7 @@ class Protocol:
same logic is duplicated there!
TODO: unify with :meth:`Object.resolve_ids`,
:meth:`protocol.Protocol.normalize_ids`.
:meth:`models.Object.normalize_ids`.
Args:
to_proto (Protocol subclass)

Wyświetl plik

@ -26,8 +26,9 @@ from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_dumps, json_loads
from activitypub import ActivityPub
from flask_app import app, cache
from common import CACHE_TIME, CONTENT_TYPE_HTML
from flask_app import app, cache
from protocol import Protocol
from web import Web
logger = logging.getLogger(__name__)
@ -85,51 +86,59 @@ def redir(to):
domains = set((util.domain_from_link(to, minimize=True),
util.domain_from_link(to, minimize=False),
to_domain))
web_user = None
for domain in domains:
if domain:
if domain in DOMAIN_ALLOWLIST:
break
if Web.get_by_id(domain):
if web_user := Web.get_by_id(domain):
logger.info(f'Found web user for domain {domain}')
break
else:
if not accept_as2:
return f'No web user found for any of {domains}', 404, VARY_HEADER
if accept_as2:
# AS2 requested, fetch and convert and serve
obj = Web.load(to, check_backlink=False)
if not obj or obj.deleted:
if not accept_as2:
# redirect. include rel-alternate link to make posts discoverable by entering
# https://fed.brid.gy/r/[URL] in a fediverse instance's search.
logger.info(f'redirecting to {to}')
return f"""\
<!doctype html>
<html>
<head>
<link href="{request.url}" rel="alternate" type="application/activity+json">
</head>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="{to}">{to}</a>. If not, click the link.
</html>
""", 301, {
'Location': to,
**VARY_HEADER,
}
# AS2 requested, fetch and convert and serve
proto = Protocol.for_id(to)
if not proto:
return f"Couldn't determine protocol for {to}", 404, VARY_HEADER
obj = proto.load(to)
if not obj or obj.deleted:
return f'Object not found: {to}', 404, VARY_HEADER
# TODO: do this for other protocols too?
if proto == Web and not web_user:
web_user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
if not web_user:
return f'Object not found: {to}', 404, VARY_HEADER
user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
if not user:
return f'Object not found: {to}', 404, VARY_HEADER
ret = ActivityPub.convert(obj, from_user=web_user)
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
return ret, {
'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE
if accept_type == as2.CONTENT_TYPE_LD
else accept_type),
'Access-Control-Allow-Origin': '*',
**VARY_HEADER,
}
ret = ActivityPub.convert(obj, from_user=user)
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
return ret, {
'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE
if accept_type == as2.CONTENT_TYPE_LD
else accept_type),
'Access-Control-Allow-Origin': '*',
**VARY_HEADER,
}
# redirect. include rel-alternate link to make posts discoverable by entering
# https://fed.brid.gy/r/[URL] in a fediverse instance's search.
logger.info(f'redirecting to {to}')
return f"""\
<!doctype html>
<html>
<head>
<link href="{request.url}" rel="alternate" type="application/activity+json">
</head>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="{to}">{to}</a>. If not, click the link.
</html>
""", 301, {
'Location': to,
**VARY_HEADER,
}

Wyświetl plik

@ -21,6 +21,7 @@ from .test_web import (
REPOST_AS2,
REPOST_HTML,
TOOT_AS2,
TOOT_AS2_DATA,
)
REPOST_AS2 = {
@ -82,7 +83,8 @@ class RedirectTest(testutil.TestCase):
self._test_as2(as2.CONTENT_TYPE_LD_PROFILE)
def test_as2_creates_user(self):
Object(id='https://user.com/repost', as2=REPOST_AS2).put()
Object(id='https://user.com/repost', source_protocol='web',
as2=REPOST_AS2).put()
self.user.key.delete()
@ -96,34 +98,19 @@ class RedirectTest(testutil.TestCase):
@patch('requests.get')
def test_as2_fetch_post(self, mock_get):
mock_get.side_effect = [
requests_response(REPOST_HTML),
TOOT_AS2,
]
mock_get.return_value = TOOT_AS2 # from Protocol.for_id
resp = self.client.get('/r/https://user.com/repost',
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
self.assert_equals(REPOST_AS2, resp.json)
self.assert_equals(TOOT_AS2_DATA, resp.json)
self.assertEqual('Accept', resp.headers['Vary'])
@patch('requests.get')
def test_as2_fetch_post_no_backlink(self, mock_get):
mock_get.side_effect = [
requests_response(
REPOST_HTML.replace('<a href="http://localhost/"></a>', '')),
TOOT_AS2,
]
resp = self.client.get('/r/https://user.com/repost',
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
self.assert_equals(REPOST_AS2, resp.json)
self.assertEqual('Accept', resp.headers['Vary'])
@patch('requests.get')
@patch('requests.get', side_effect=[
requests_response(ACTOR_HTML), # AS2 fetch
requests_response(ACTOR_HTML), # web fetch
])
def test_as2_no_user_fetch_homepage(self, mock_get):
mock_get.return_value = requests_response(ACTOR_HTML)
self.user.key.delete()
self.user.obj_key.delete()
protocol.objects_cache.clear()
@ -174,7 +161,8 @@ class RedirectTest(testutil.TestCase):
self.assertEqual('https://user.com/bar', resp.headers['Location'])
def _test_as2(self, content_type):
self.obj = Object(id='https://user.com/', as2=REPOST_AS2).put()
self.obj = Object(id='https://user.com/', source_protocol='web',
as2=REPOST_AS2).put()
resp = self.client.get('/r/https://user.com/', headers={'Accept': content_type})
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
@ -183,7 +171,8 @@ class RedirectTest(testutil.TestCase):
self.assertEqual('Accept', resp.headers['Vary'])
def test_as2_deleted(self):
Object(id='https://user.com/bar', as2={}, deleted=True).put()
Object(id='https://user.com/bar', as2={}, source_protocol='web',
deleted=True).put()
resp = self.client.get('/r/https://user.com/bar',
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
@ -196,3 +185,13 @@ class RedirectTest(testutil.TestCase):
resp = self.client.get('/r/https://user.com/',
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
self.assertEqual(404, resp.status_code, resp.get_data(as_text=True))
def test_as2_atproto_normalize_id(self):
self.obj = Object(id='at://did:plc:foo/app.bsky.feed.post/123',
source_protocol='atproto', as2=REPOST_AS2).put()
resp = self.client.get('/r/https://bsky.app/profile/did:plc:foo/post/123',
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
self.assertEqual(as2.CONTENT_TYPE_LD_PROFILE, resp.content_type)
self.assert_equals(REPOST_AS2, resp.json)