kopia lustrzana https://github.com/snarfed/bridgy-fed
abstract redirect.py to be multi-protocol
...mostly. creating the underlying user opportunistically is still Web-only.pull/962/head
rodzic
5b5ed4173a
commit
2ec22de09f
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
81
redirect.py
81
redirect.py
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue