diff --git a/models.py b/models.py index 52fbdb3..a95302d 100644 --- a/models.py +++ b/models.py @@ -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 diff --git a/protocol.py b/protocol.py index d1e8968..47a0604 100644 --- a/protocol.py +++ b/protocol.py @@ -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) diff --git a/redirect.py b/redirect.py index e8e8bc6..ecba7c5 100644 --- a/redirect.py +++ b/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"""\ + + + + + + Redirecting... +

Redirecting...

+

You should be redirected automatically to the target URL: {to}. If not, click the link. + + """, 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"""\ - - - - - -Redirecting... -

Redirecting...

-

You should be redirected automatically to the target URL: {to}. If not, click the link. - -""", 301, { - 'Location': to, - **VARY_HEADER, -} diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 3eefaec..e74520a 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -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('', '')), - 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)