From 579f55d965f76dd43c99fcc91eb5b1261bdd6e01 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Thu, 23 Nov 2023 22:35:38 -0800 Subject: [PATCH] AP: add paging to outbox finishes / fixes #383 --- activitypub.py | 40 ++++++++++++++++++++++++++++++--------- tests/test_activitypub.py | 37 +++++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/activitypub.py b/activitypub.py index 94fab6a..59fd6b3 100644 --- a/activitypub.py +++ b/activitypub.py @@ -883,6 +883,8 @@ def follower_collection(id, collection): * https://www.w3.org/TR/activitypub/#followers * https://www.w3.org/TR/activitypub/#collections * https://www.w3.org/TR/activitystreams-core/#paging + + TODO: unify page generation with outbox() """ protocol = Protocol.for_request(fed='web') assert protocol @@ -932,6 +934,10 @@ def follower_collection(id, collection): @app.get(f'//outbox') @flask_util.cached(cache, CACHE_TIME) def outbox(id): + """Serves a user's AP outbox. + + TODO: unify page generation with follower_collection() + """ protocol = Protocol.for_request(fed='web') if not protocol: error(f"Couldn't determine protocol", status=404) @@ -941,18 +947,34 @@ def outbox(id): error(f'User {id} not found', status=404) query = Object.query(Object.users == g.user.key) - objects, before, after = fetch_objects(query, by=Object.updated, user=g.user) + objects, new_before, new_after = fetch_objects(query, by=Object.updated, + user=g.user) + # page + page = { + 'type': 'CollectionPage', + 'partOf': request.base_url, + 'items': util.trim_nulls([ActivityPub.convert(obj) for obj in objects]), + } + if new_before: + page['next'] = f'{request.base_url}?before={new_before}' + if new_after: + page['prev'] = f'{request.base_url}?after={new_after}' + + if 'before' in request.args or 'after' in request.args: + page.update({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': request.url, + }) + logger.info(f'Returning {json_dumps(page, indent=2)}') + return page, {'Content-Type': as2.CONTENT_TYPE} + + # collection return { '@context': 'https://www.w3.org/ns/activitystreams', 'id': request.url, - 'summary': f"{id}'s outbox", 'type': 'OrderedCollection', - # TODO. needs to handle deleted - # 'totalItems': query.count(), - 'first': { - 'type': 'CollectionPage', - 'partOf': request.base_url, - 'items': [ActivityPub.convert(obj) for obj in objects], - }, + 'summary': f"{id}'s outbox", + 'totalItems': query.count(), + 'first': page, }, {'Content-Type': as2.CONTENT_TYPE} diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 5ebe765..79cac91 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -1568,6 +1568,7 @@ class ActivityPubTest(TestCase): 'id': 'https://fa.brid.gy/ap/fake:foo/outbox', 'summary': "fake:foo's outbox", 'type': 'OrderedCollection', + 'totalItems': 0, 'first': { 'type': 'CollectionPage', 'partOf': 'https://fa.brid.gy/ap/fake:foo/outbox', @@ -1575,26 +1576,55 @@ class ActivityPubTest(TestCase): }, }, 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) + + @patch('models.PAGE_SIZE', 2) def test_outbox_fake_objects(self, *_): user = self.make_user('fake:foo', cls=Fake) - for i, obj in enumerate([REPLY, MENTION, LIKE, DELETE]): - self.store_object(id=str(i), users=[user.key], as2=obj) + self.store_outbox_objects(user) resp = self.client.get(f'/ap/fake:foo/outbox', base_url='https://fa.brid.gy') self.assertEqual(200, resp.status_code) + + after = Object.get_by_id(LIKE['id']).updated.isoformat() self.assertEqual({ '@context': 'https://www.w3.org/ns/activitystreams', 'id': 'https://fa.brid.gy/ap/fake:foo/outbox', 'summary': "fake:foo's outbox", 'type': 'OrderedCollection', + 'totalItems': 4, 'first': { 'type': 'CollectionPage', 'partOf': 'https://fa.brid.gy/ap/fake:foo/outbox', - 'items': [DELETE, LIKE, MENTION, REPLY], + 'items': [DELETE, LIKE], + 'next': f'https://fa.brid.gy/ap/fake:foo/outbox?before={after}', }, }, resp.json) + @patch('models.PAGE_SIZE', 2) + def test_outbox_fake_objects_page(self, *_): + user = self.make_user('fake:foo', cls=Fake) + self.store_outbox_objects(user) + + after = datetime(1900, 1, 1).isoformat() + resp = self.client.get(f'/ap/fake:foo/outbox?after={after}', + base_url='https://fa.brid.gy') + self.assertEqual(200, resp.status_code) + + prev = Object.get_by_id(MENTION['id']).updated.isoformat() + self.assertEqual({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': f'https://fa.brid.gy/ap/fake:foo/outbox?after={after}', + 'type': 'CollectionPage', + 'partOf': 'https://fa.brid.gy/ap/fake:foo/outbox', + 'prev': f'https://fa.brid.gy/ap/fake:foo/outbox?after={prev}', + 'next': f'https://fa.brid.gy/ap/fake:foo/outbox?before={after}', + 'items': [MENTION, REPLY], + }, resp.json) + def test_outbox_web_empty(self, *_): resp = self.client.get(f'/user.com/outbox') self.assertEqual(200, resp.status_code) @@ -1603,6 +1633,7 @@ class ActivityPubTest(TestCase): 'id': 'http://localhost/user.com/outbox', 'summary': "user.com's outbox", 'type': 'OrderedCollection', + 'totalItems': 0, 'first': { 'type': 'CollectionPage', 'partOf': 'http://localhost/user.com/outbox',