From 2279db46fc3c2dfd2f2471f63fc9b062c58c1771 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Mon, 21 Nov 2022 18:46:10 -0800 Subject: [PATCH] start on AP followers/following collections for #264 --- activitypub.py | 59 ++++++++++++++++++++++++++++++ pages.py | 3 -- tests/test_activitypub.py | 75 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) diff --git a/activitypub.py b/activitypub.py index 95e4222..14dd649 100644 --- a/activitypub.py +++ b/activitypub.py @@ -259,3 +259,62 @@ def undo_follow(undo_unwrapped): logger.warning(f'No Follower found for {user_domain} {follower}') # TODO send webmention with 410 of u-follow + + +# TODO: unify with following_collection +@app.get(f'//followers') +@flask_util.cached(cache, CACHE_TIME) +def followers_collection(domain): + """ActivityPub Followers collection. + + https://www.w3.org/TR/activitypub/#followers + https://www.w3.org/TR/activitypub/#collections + https://www.w3.org/TR/activitystreams-core/#paging + """ + if not User.get_by_id(domain): + return f'User {domain} not found', 404 + + logger.info(f"Counting {domain}'s followers") + count = Follower.query( + Follower.status == 'active', + Follower.dest == domain, + ).count() + + ret = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'summary': f"{domain}'s followers", + 'type': 'Collection', + 'totalItems': count, + 'items': [], # TODO + } + logger.info(f'Returning {json_dumps(ret, indent=2)}') + return ret + + +@app.get(f'//following') +@flask_util.cached(cache, CACHE_TIME) +def following_collection(domain): + """ActivityPub Following collection. + + https://www.w3.org/TR/activitypub/#following + https://www.w3.org/TR/activitypub/#collections + https://www.w3.org/TR/activitystreams-core/#paging + """ + if not User.get_by_id(domain): + return f'User {domain} not found', 404 + + logger.info(f"Counting {domain}'s following") + count = Follower.query( + Follower.status == 'active', + Follower.src == domain, + ).count() + + ret = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'summary': f"{domain}'s following", + 'type': 'Collection', + 'totalItems': count, + 'items': [], # TODO + } + logger.info(f'Returning {json_dumps(ret, indent=2)}') + return ret diff --git a/pages.py b/pages.py index 0ff4a91..cdf9beb 100644 --- a/pages.py +++ b/pages.py @@ -99,8 +99,6 @@ def user(domain): @app.get(f'/user//followers') def followers(domain): - # TODO: - # pull more info from last_follow, eg name, profile picture, url # unify with following if not User.get_by_id(domain): return render_template('user_not_found.html', domain=domain), 404 @@ -116,7 +114,6 @@ def followers(domain): f.handle = re.sub(r'^https?://(.+)/(users/|@)(.+)$', r'@\3@\1', f.src) if f.last_follow: last_follow = json_loads(f.last_follow) - print('@', last_follow) actor = last_follow.get('actor', {}) f.name = actor.get('name') or '' f.picture = actor.get('icon', {}).get('url') diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 103ff2b..f5bc2d0 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -520,3 +520,78 @@ class ActivityPubTest(testutil.TestCase): self.assertEqual('in', activity.direction) self.assertEqual('activitypub', activity.protocol) self.assertEqual('ignored', activity.status) + + def test_followers_collection_unknown_user(self, *args): + resp = self.client.get('/foo.com/followers') + self.assertEqual(404, resp.status_code) + + def test_followers_collection(self, *args): + User.get_or_create('foo.com') + + resp = self.client.get('/foo.com/followers') + self.assertEqual(200, resp.status_code) + self.assertEqual({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'summary': "foo.com's followers", + 'type': 'Collection', + 'totalItems': 0, + 'items': [], + }, resp.json) + + Follower.get_or_create('foo.com', 'bar.com') + Follower.get_or_create('http://other/actor', 'foo.com') + Follower.get_or_create('foo.com', 'baz.com') + Follower.get_or_create('foo.com', 'baj.com', status='inactive') + + resp = self.client.get('/foo.com/followers') + self.assertEqual(200, resp.status_code) + self.assertEqual({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'summary': "foo.com's followers", + 'type': 'Collection', + 'totalItems': 2, + 'items': [], +# TODO +# { +# 'type': 'Create', +# 'actor': 'http://www.test.example/sally', +# 'object': 'http://example.org/foo', +# }, +# { +# 'type': 'Like', +# 'actor': 'http://www.test.example/joe', +# 'object': 'http://example.org/foo', +# }], + }, resp.json) + + def test_following_collection_unknown_user(self, *args): + resp = self.client.get('/foo.com/following') + self.assertEqual(404, resp.status_code) + + def test_following_collection(self, *args): + User.get_or_create('foo.com') + + resp = self.client.get('/foo.com/following') + self.assertEqual(200, resp.status_code) + self.assertEqual({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'summary': "foo.com's following", + 'type': 'Collection', + 'totalItems': 0, + 'items': [], + }, resp.json) + + Follower.get_or_create('bar.com', 'foo.com') + Follower.get_or_create('foo.com', 'http://other/actor') + Follower.get_or_create('baz.com', 'foo.com') + Follower.get_or_create('baj.com', 'foo.com', status='inactive') + + resp = self.client.get('/foo.com/following') + self.assertEqual(200, resp.status_code) + self.assertEqual({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'summary': "foo.com's following", + 'type': 'Collection', + 'totalItems': 2, + 'items': [], + }, resp.json)