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.
|
:meth:`protocol.Protocol.translate_ids` is partly the inverse of this.
|
||||||
Much of the same logic is duplicated there!
|
Much of the same logic is duplicated there!
|
||||||
|
|
||||||
TODO: unify with :meth:`normalize_ids`,
|
TODO: unify with :meth:`normalize_ids`, :meth:`Object.normalize_ids`.
|
||||||
:meth:`protocol.Protocol.normalize_ids`.
|
|
||||||
"""
|
"""
|
||||||
if not self.as1:
|
if not self.as1:
|
||||||
return
|
return
|
||||||
|
|
|
@ -515,7 +515,7 @@ class Protocol:
|
||||||
same logic is duplicated there!
|
same logic is duplicated there!
|
||||||
|
|
||||||
TODO: unify with :meth:`Object.resolve_ids`,
|
TODO: unify with :meth:`Object.resolve_ids`,
|
||||||
:meth:`protocol.Protocol.normalize_ids`.
|
:meth:`models.Object.normalize_ids`.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
to_proto (Protocol subclass)
|
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 oauth_dropins.webutil.util import json_dumps, json_loads
|
||||||
|
|
||||||
from activitypub import ActivityPub
|
from activitypub import ActivityPub
|
||||||
from flask_app import app, cache
|
|
||||||
from common import CACHE_TIME, CONTENT_TYPE_HTML
|
from common import CACHE_TIME, CONTENT_TYPE_HTML
|
||||||
|
from flask_app import app, cache
|
||||||
|
from protocol import Protocol
|
||||||
from web import Web
|
from web import Web
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -85,51 +86,59 @@ def redir(to):
|
||||||
domains = set((util.domain_from_link(to, minimize=True),
|
domains = set((util.domain_from_link(to, minimize=True),
|
||||||
util.domain_from_link(to, minimize=False),
|
util.domain_from_link(to, minimize=False),
|
||||||
to_domain))
|
to_domain))
|
||||||
|
web_user = None
|
||||||
for domain in domains:
|
for domain in domains:
|
||||||
if domain:
|
if domain:
|
||||||
if domain in DOMAIN_ALLOWLIST:
|
if domain in DOMAIN_ALLOWLIST:
|
||||||
break
|
break
|
||||||
if Web.get_by_id(domain):
|
if web_user := Web.get_by_id(domain):
|
||||||
logger.info(f'Found web user for domain {domain}')
|
logger.info(f'Found web user for domain {domain}')
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if not accept_as2:
|
if not accept_as2:
|
||||||
return f'No web user found for any of {domains}', 404, VARY_HEADER
|
return f'No web user found for any of {domains}', 404, VARY_HEADER
|
||||||
|
|
||||||
if accept_as2:
|
if not accept_as2:
|
||||||
# AS2 requested, fetch and convert and serve
|
# redirect. include rel-alternate link to make posts discoverable by entering
|
||||||
obj = Web.load(to, check_backlink=False)
|
# https://fed.brid.gy/r/[URL] in a fediverse instance's search.
|
||||||
if not obj or obj.deleted:
|
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
|
return f'Object not found: {to}', 404, VARY_HEADER
|
||||||
|
|
||||||
user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
|
ret = ActivityPub.convert(obj, from_user=web_user)
|
||||||
if not user:
|
logger.info(f'Returning: {json_dumps(ret, indent=2)}')
|
||||||
return f'Object not found: {to}', 404, VARY_HEADER
|
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_AS2,
|
||||||
REPOST_HTML,
|
REPOST_HTML,
|
||||||
TOOT_AS2,
|
TOOT_AS2,
|
||||||
|
TOOT_AS2_DATA,
|
||||||
)
|
)
|
||||||
|
|
||||||
REPOST_AS2 = {
|
REPOST_AS2 = {
|
||||||
|
@ -82,7 +83,8 @@ class RedirectTest(testutil.TestCase):
|
||||||
self._test_as2(as2.CONTENT_TYPE_LD_PROFILE)
|
self._test_as2(as2.CONTENT_TYPE_LD_PROFILE)
|
||||||
|
|
||||||
def test_as2_creates_user(self):
|
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()
|
self.user.key.delete()
|
||||||
|
|
||||||
|
@ -96,34 +98,19 @@ class RedirectTest(testutil.TestCase):
|
||||||
|
|
||||||
@patch('requests.get')
|
@patch('requests.get')
|
||||||
def test_as2_fetch_post(self, mock_get):
|
def test_as2_fetch_post(self, mock_get):
|
||||||
mock_get.side_effect = [
|
mock_get.return_value = TOOT_AS2 # from Protocol.for_id
|
||||||
requests_response(REPOST_HTML),
|
|
||||||
TOOT_AS2,
|
|
||||||
]
|
|
||||||
|
|
||||||
resp = self.client.get('/r/https://user.com/repost',
|
resp = self.client.get('/r/https://user.com/repost',
|
||||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
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'])
|
self.assertEqual('Accept', resp.headers['Vary'])
|
||||||
|
|
||||||
@patch('requests.get')
|
@patch('requests.get', side_effect=[
|
||||||
def test_as2_fetch_post_no_backlink(self, mock_get):
|
requests_response(ACTOR_HTML), # AS2 fetch
|
||||||
mock_get.side_effect = [
|
requests_response(ACTOR_HTML), # web fetch
|
||||||
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')
|
|
||||||
def test_as2_no_user_fetch_homepage(self, mock_get):
|
def test_as2_no_user_fetch_homepage(self, mock_get):
|
||||||
mock_get.return_value = requests_response(ACTOR_HTML)
|
|
||||||
self.user.key.delete()
|
self.user.key.delete()
|
||||||
self.user.obj_key.delete()
|
self.user.obj_key.delete()
|
||||||
protocol.objects_cache.clear()
|
protocol.objects_cache.clear()
|
||||||
|
@ -174,7 +161,8 @@ class RedirectTest(testutil.TestCase):
|
||||||
self.assertEqual('https://user.com/bar', resp.headers['Location'])
|
self.assertEqual('https://user.com/bar', resp.headers['Location'])
|
||||||
|
|
||||||
def _test_as2(self, content_type):
|
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})
|
resp = self.client.get('/r/https://user.com/', headers={'Accept': content_type})
|
||||||
self.assertEqual(200, resp.status_code, resp.get_data(as_text=True))
|
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'])
|
self.assertEqual('Accept', resp.headers['Vary'])
|
||||||
|
|
||||||
def test_as2_deleted(self):
|
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',
|
resp = self.client.get('/r/https://user.com/bar',
|
||||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||||
|
@ -196,3 +185,13 @@ class RedirectTest(testutil.TestCase):
|
||||||
resp = self.client.get('/r/https://user.com/',
|
resp = self.client.get('/r/https://user.com/',
|
||||||
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE})
|
||||||
self.assertEqual(404, resp.status_code, resp.get_data(as_text=True))
|
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