From f03b97e44a452ed653396768ce9c74823bd7590f Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Mon, 4 Sep 2023 08:11:19 -0700 Subject: [PATCH] delete XRPC method handlers, they're unused --- .circleci/config.yml | 1 - app.py | 2 +- atproto.py | 9 +- docs/source/modules.rst | 29 +++---- flask_app.py | 9 -- tests/test_xrpc_actor.py | 69 --------------- tests/test_xrpc_feed.py | 179 --------------------------------------- tests/test_xrpc_graph.py | 134 ----------------------------- xrpc_actor.py | 64 -------------- xrpc_feed.py | 120 -------------------------- xrpc_graph.py | 67 --------------- 11 files changed, 15 insertions(+), 668 deletions(-) delete mode 100644 tests/test_xrpc_actor.py delete mode 100644 tests/test_xrpc_feed.py delete mode 100644 tests/test_xrpc_graph.py delete mode 100644 xrpc_actor.py delete mode 100644 xrpc_feed.py delete mode 100644 xrpc_graph.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 70351fd..cf32898 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,6 @@ jobs: curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates gnupg google-cloud-sdk google-cloud-sdk-datastore-emulator default-jre - git clone --depth=1 https://github.com/bluesky-social/atproto.git ../atproto - run: name: Python dependencies diff --git a/app.py b/app.py index d958aec..177cacb 100644 --- a/app.py +++ b/app.py @@ -6,7 +6,7 @@ registered. from flask_app import app # import all modules to register their Flask handlers -import activitypub, convert, follow, pages, redirect, superfeedr, ui, webfinger, web, xrpc_actor, xrpc_feed, xrpc_graph +import activitypub, convert, follow, pages, redirect, superfeedr, ui, webfinger, web import models models.reset_protocol_properties() diff --git a/atproto.py b/atproto.py index ef1c38d..1619fb9 100644 --- a/atproto.py +++ b/atproto.py @@ -9,14 +9,13 @@ TODO """ import json import logging -from pathlib import Path import re from arroba import did from arroba.datastore_storage import DatastoreStorage from arroba.repo import Repo, Write from arroba.storage import Action -from arroba.util import next_tid, new_key, parse_at_uri +from arroba.util import lexicons, next_tid, new_key, parse_at_uri from flask import abort, g, request from google.cloud import ndb from granary import as1, bluesky @@ -38,13 +37,9 @@ from protocol import Protocol logger = logging.getLogger(__name__) -lexicons = [] -for filename in (Path(__file__).parent / 'lexicons').glob('**/*.json'): - with open(filename) as f: - lexicons.append(json.load(f)) - storage = DatastoreStorage() + class ATProto(User, Protocol): """AT Protocol class. diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 438f1bb..d1ccdb5 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -9,10 +9,18 @@ activitypub ----------- .. automodule:: activitypub +atproto +------- +.. automodule:: atproto + common ------ .. automodule:: common +convert +------- +.. automodule:: convert + follow ------ .. automodule:: follow @@ -41,23 +49,10 @@ superfeedr ---------- .. automodule:: superfeedr +web +--- +.. automodule:: web + webfinger --------- .. automodule:: webfinger - -webmention ----------- -.. automodule:: webmention - -xrpc_actor ----------- -.. automodule:: xrpc - -xrpc_feed ---------- -.. automodule:: xrpc - -xrpc_graph ----------- -.. automodule:: xrpc - diff --git a/flask_app.py b/flask_app.py index 32ccc40..a0ff74f 100644 --- a/flask_app.py +++ b/flask_app.py @@ -59,12 +59,3 @@ app.wsgi_app = flask_util.ndb_context_middleware( cache = Cache(app) util.set_user_agent(USER_AGENT) - -# XRPC server -lexicons = [] -for filename in (app_dir / 'lexicons').glob('**/*.json'): - with open(filename) as f: - lexicons.append(json.load(f)) - -xrpc_server = Server(lexicons, validate=False) -init_flask(xrpc_server, app) diff --git a/tests/test_xrpc_actor.py b/tests/test_xrpc_actor.py deleted file mode 100644 index dcaf2ab..0000000 --- a/tests/test_xrpc_actor.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Unit tests for actor.py.""" -from unittest import skip - -# import first so that Fake is defined before URL routes are registered -from . import testutil - -from .test_activitypub import ACTOR - - -@skip -class XrpcActorTest(testutil.TestCase): - - def test_getProfile(self): - actor = { - **ACTOR, - 'summary': "I'm a person", - 'image': [{'type': 'Image', 'url': 'http://user.com/header.png'}], - } - self.make_user('user.com', has_hcard=True, actor_as2=actor) - - resp = self.client.get('/xrpc/app.bsky.actor.getProfile', - query_string={'actor': 'user.com'}) - self.assertEqual(200, resp.status_code) - self.assertEqual({ - '$type': 'app.bsky.actor.defs#profileView', - 'handle': 'mas.to/users/swentel', - 'did': 'did:web:mas.to:users:swentel', - 'displayName': 'Mrs. ☕ Foo', - 'description': "I'm a person", - 'avatar': 'https://user.com/me.jpg', - 'banner': 'http://user.com/header.png', - }, resp.json) - - def test_getProfile_unset(self): - resp = self.client.get('/xrpc/app.bsky.actor.getProfile') - self.assertEqual(400, resp.status_code) - - def test_getProfile_not_domain(self): - resp = self.client.get('/xrpc/app.bsky.actor.getProfile', - query_string={'actor': 'not a domain'}) - self.assertEqual(400, resp.status_code) - - def test_getProfile_no_user(self): - resp = self.client.get('/xrpc/app.bsky.actor.getProfile', - query_string={'actor': 'user.com'}) - self.assertEqual(400, resp.status_code) - - def test_getSuggestions(self): - resp = self.client.get('/xrpc/app.bsky.actor.getSuggestions') - self.assertEqual(200, resp.status_code) - self.assertEqual({ - 'actors': [], - }, resp.json) - - def test_search(self): - resp = self.client.get('/xrpc/app.bsky.actor.searchActors', - query_string={'term': 'foo'}) - self.assertEqual(200, resp.status_code) - self.assertEqual({ - 'actors': [], - }, resp.json) - - def test_searchTypeahead(self): - resp = self.client.get('/xrpc/app.bsky.actor.searchActorsTypeahead', - query_string={'term': 'foo'}) - self.assertEqual(200, resp.status_code) - self.assertEqual({ - 'actors': [], - }, resp.json) diff --git a/tests/test_xrpc_feed.py b/tests/test_xrpc_feed.py deleted file mode 100644 index 4c14355..0000000 --- a/tests/test_xrpc_feed.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Unit tests for feed.py.""" -from unittest import skip - -from granary import as2, bluesky -from granary.tests.test_as1 import COMMENT, NOTE -from granary.tests.test_bluesky import ( - POST_BSKY, - POST_AS, - REPLY_BSKY, - REPLY_AS, - REPOST_AS, -) - -# import first so that Fake is defined before URL routes are registered -from . import testutil - -from models import Object -from .test_activitypub import ACTOR - -POST_THREAD_AS = { - **POST_AS, - 'replies': { - 'items': [{ - 'objectType': 'comment', - 'id': 'http://bob.org/reply', - 'content': 'Uh huh', - 'author': { - 'objectType': 'person', - 'displayName': 'Bob', - 'url': 'http://bob.org/', - }, - }], - }, -} -POST_THREAD_BSKY = { - 'thread': { - '$type': 'app.bsky.feed.defs#threadViewPost', - 'post': POST_BSKY['post'], - 'replies': [{ - '$type': 'app.bsky.feed.defs#threadViewPost', - 'post': { - '$type': 'app.bsky.feed.defs#postView', - 'uri': 'http://bob.org/reply', - 'cid': 'TODO', - 'record': { - '$type': 'app.bsky.feed.post', - 'text': 'Uh huh', - 'createdAt': '', - }, - 'author': { - '$type': 'app.bsky.actor.defs#profileViewBasic', - 'did': 'did:web:bob.org', - 'displayName': 'Bob', - 'handle': 'bob.org', - 'description': None, - }, - 'replyCount': 0, - 'repostCount': 0, - 'upvoteCount': 0, - 'downvoteCount': 0, - 'indexedAt': '2022-01-02T03:04:05+00:00', - }, - }], - }, -} - - -@skip -class XrpcFeedTest(testutil.TestCase): - - def setUp(self): - super().setUp() - self.make_user('user.com', has_hcard=True, actor_as2=ACTOR) - - def test_getAuthorFeed(self): - post_as2 = as2.from_as1(POST_AS) - - Object(id='a', domains=['user.com'], labels=['user'], as2=post_as2).put() - Object(id='b', domains=['user.com'], labels=['user'], - as2=as2.from_as1(REPLY_AS)).put() - # not outbound from user - Object(id='d', domains=['user.com'], labels=['feed'], as2=post_as2).put() - # deleted - Object(id='e', domains=['user.com'], labels=['user'], as2=post_as2, - deleted=True).put() - # other user's - Object(id='f', domains=['bar.org'], labels=['user'], as2=post_as2).put() - - resp = self.client.get('/xrpc/app.bsky.feed.getAuthorFeed', - query_string={'author': 'user.com'}) - self.assertEqual(200, resp.status_code, resp.get_data(as_text=True)) - self.assert_equals({ - 'feed': [REPLY_BSKY, POST_BSKY], - }, resp.json, ignore=['did']) - - def test_getAuthorFeed_no_author_param(self): - resp = self.client.get('/xrpc/app.bsky.feed.getAuthorFeed') - self.assertEqual(400, resp.status_code) - - def test_getAuthorFeed_not_domain(self): - resp = self.client.get('/xrpc/app.bsky.feed.getAuthorFeed', - query_string={'author': 'not a domain'}) - self.assertEqual(400, resp.status_code) - - def test_getAuthorFeed_no_user(self): - resp = self.client.get('/xrpc/app.bsky.feed.getAuthorFeed', - query_string={'author': 'no.com'}) - self.assertEqual(400, resp.status_code) - - def test_getAuthorFeed_no_objects(self): - resp = self.client.get('/xrpc/app.bsky.feed.getAuthorFeed', - query_string={'author': 'user.com'}) - self.assertEqual(200, resp.status_code) - self.assert_equals({'feed': []}, resp.json) - - def test_getPostThread(self): - Object(id='http://a/post', domains=['user.com'], labels=['user'], - as2=as2.from_as1(POST_THREAD_AS)).put() - - resp = self.client.get('/xrpc/app.bsky.feed.getPostThread', - query_string={'uri': 'http://a/post'}) - self.assertEqual(200, resp.status_code, resp.get_data(as_text=True)) - self.assertEqual(POST_THREAD_BSKY, resp.json) - - def test_getPostThread_no_uri_param(self): - resp = self.client.get('/xrpc/app.bsky.feed.getPostThread') - self.assertEqual(400, resp.status_code) - - def test_getPostThread_no_post(self): - resp = self.client.get('/xrpc/app.bsky.feed.getPostThread', - query_string={'uri': 'http://no/post'}) - self.assertEqual(400, resp.status_code, resp.get_data(as_text=True)) - - def test_getRepostedBy(self): - Object(id='repost/1', domains=['user.com'], as2=as2.from_as1({ - **REPOST_AS, - 'object': 'http://a/post', - })).put() - Object(id='repost/2', domains=['user.com'], as2=as2.from_as1({ - **REPOST_AS, - 'object': 'http://a/post', - 'actor': as2.to_as1(ACTOR), - })).put() - - got = self.client.get('/xrpc/app.bsky.feed.getRepostedBy', - query_string={'uri': 'http://a/post'}) - self.assertEqual({ - 'uri': 'http://orig/post', - 'repostBy': [{ - '$type': 'app.bsky.feed.getRepostedBy#repostedBy', - 'description': None, - 'did': 'did:web:mas.to:users:swentel', - 'handle': 'mas.to/users/swentel', - 'displayName': 'Mrs. ☕ Foo', - 'avatar': 'https://user.com/me.jpg', - }, { - '$type': 'app.bsky.feed.getRepostedBy#repostedBy', - 'description': None, - 'did': 'did:web:bsky.app:profile:bob.com', - 'handle': 'bsky.app/profile/bob.com', - 'displayName': 'Bob', - }], - }, got.json) - - def test_getTimeline(self): - self.add_objects() - - got = self.client.get('/xrpc/app.bsky.feed.getTimeline') - self.assertEqual({ - 'feed': [bluesky.from_as1(COMMENT), bluesky.from_as1(NOTE)], - }, got.json) - - def test_getLikes(self): - resp = self.client.get('/xrpc/app.bsky.feed.getLikes', - query_string={'uri': 'http://a/post'}) - self.assertEqual({ - 'uri': 'http://a/post', - 'likes': [], - }, resp.json) diff --git a/tests/test_xrpc_graph.py b/tests/test_xrpc_graph.py deleted file mode 100644 index 766c31a..0000000 --- a/tests/test_xrpc_graph.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Unit tests for graph.py.""" -# import first so that Fake is defined before URL routes are registered -from . import testutil - -from .test_activitypub import FOLLOW, FOLLOW_WITH_ACTOR, FOLLOW_WITH_OBJECT -from models import Follower -from unittest import skip - -SUBJECT = { - '$type': 'app.bsky.actor.defs#profileView', - 'did': 'did:web:user.com', - 'handle': 'user.com', - 'description': None, -} -FOLLOWERS_BSKY = [{ - '$type': 'app.bsky.graph.getFollowers#follower', - 'did': 'did:web:other', - 'handle': 'yoozer@other', - 'indexedAt': '2022-01-02T03:04:05+00:00', - 'description': None, -}, { - '$type': 'app.bsky.graph.getFollowers#follower', - 'did': 'did:web:mas.to:users:swentel', - 'handle': 'mas.to/users/swentel', - 'displayName': 'Mrs. ☕ Foo', - 'avatar': 'https://user.com/me.jpg', - 'indexedAt': '2022-01-02T03:04:05+00:00', - 'description': None, -}] - - -@skip -class XrpcGraphTest(testutil.TestCase): - - def test_getProfile_no_user(self): - resp = self.client.get('/xrpc/app.bsky.graph.getFollowers') - self.assertEqual(400, resp.status_code) - - def test_getFollowers_not_domain(self): - resp = self.client.get('/xrpc/app.bsky.graph.getFollowers', - query_string={'user': 'not a domain'}) - self.assertEqual(400, resp.status_code) - - def test_getFollowers_no_user(self): - resp = self.client.get('/xrpc/app.bsky.graph.getFollowers', - query_string={'user': 'no.com'}) - self.assertEqual(400, resp.status_code) - - def test_getFollowers_empty(self): - self.make_user('user.com') - - resp = self.client.get('/xrpc/app.bsky.graph.getFollowers', - query_string={'user': 'user.com'}) - self.assertEqual(200, resp.status_code) - self.assertEqual({ - 'subject': SUBJECT, - 'cursor': '', - 'followers': [], - }, resp.json) - - def test_getFollowers(self): - self.make_user('user.com') - - other_follow = { - **FOLLOW, - 'actor': { - 'type': 'Person', - 'url': 'http://other', - 'preferredUsername': 'yoozer', - }, - } - - Follower.get_or_create('user.com', 'https://no/stored/follow') - Follower.get_or_create('user.com', 'https://masto/user', - last_follow=FOLLOW_WITH_ACTOR) - Follower.get_or_create('user.com', 'http://other', - last_follow=other_follow) - Follower.get_or_create('nope.com', 'http://nope', - last_follow=other_follow) - - resp = self.client.get('/xrpc/app.bsky.graph.getFollowers', - query_string={'user': 'user.com'}) - self.assertEqual(200, resp.status_code) - self.assertEqual({ - 'subject': SUBJECT, - 'cursor': '', - 'followers': FOLLOWERS_BSKY, - }, resp.json) - - def test_getFollows_not_domain(self): - resp = self.client.get('/xrpc/app.bsky.graph.getFollows', - query_string={'user': 'not a domain'}) - self.assertEqual(400, resp.status_code) - - def test_getFollows_empty(self): - self.make_user('user.com') - - resp = self.client.get('/xrpc/app.bsky.graph.getFollows', - query_string={'user': 'user.com'}) - self.assertEqual(200, resp.status_code) - self.assertEqual({ - 'subject': SUBJECT, - 'cursor': '', - 'follows': [], - }, resp.json) - - def test_getFollows(self): - self.make_user('user.com') - - other_follow = { - **FOLLOW, - 'object': { - 'type': 'Person', - 'url': 'http://other', - 'preferredUsername': 'yoozer', - }, - } - - Follower.get_or_create('https://no/stored/follow', 'user.com') - Follower.get_or_create('https://masto/user', 'user.com', - last_follow=FOLLOW_WITH_OBJECT) - Follower.get_or_create('http://other', 'user.com', - last_follow=other_follow) - Follower.get_or_create('http://nope', 'nope.com', - last_follow=other_follow) - - resp = self.client.get('/xrpc/app.bsky.graph.getFollows', - query_string={'user': 'user.com'}) - self.assertEqual(200, resp.status_code) - self.assertEqual({ - 'subject': SUBJECT, - 'cursor': '', - 'follows': FOLLOWERS_BSKY, - }, resp.json) diff --git a/xrpc_actor.py b/xrpc_actor.py deleted file mode 100644 index de63bc9..0000000 --- a/xrpc_actor.py +++ /dev/null @@ -1,64 +0,0 @@ -"""app.bsky.actor.* XRPC methods.""" -import logging -import json -import re - -from flask import g -from granary import bluesky -from oauth_dropins.webutil import util - -from flask_app import xrpc_server -from web import Web - -logger = logging.getLogger(__name__) - - -@xrpc_server.method('app.bsky.actor.getProfile') -def getProfile(input, actor=None): - """ - lexicons/app/bsky/actor/getProfile.json - """ - # TODO: actor is either handle or DID - # see actorWhereClause in atproto/packages/pds/src/db/util.ts - if not actor or not re.match(util.DOMAIN_RE, actor): - raise ValueError(f'{actor} is not a domain') - - g.user = Web.get_by_id(actor) - if not g.user: - raise ValueError(f'User {actor} not found') - elif not g.user.obj.as1: - return ValueError(f'User {actor} not fully set up') - - actor_as1 = g.user.obj.as1 - logger.info(f'AS1 actor: {json.dumps(actor_as1, indent=2)}') - - profile = bluesky.from_as1(actor_as1) - logger.info(f'Bluesky profile: {json.dumps(profile, indent=2)}') - return profile - - -@xrpc_server.method('app.bsky.actor.getSuggestions') -def getSuggestions(input): - """ - lexicons/app/bsky/actor/getSuggestions.json - """ - # TODO based on stored users - return {'actors': []} - - -@xrpc_server.method('app.bsky.actor.searchActors') -def searchActors(input, term=None, limit=None, before=None): - """ - lexicons/app/bsky/actor/searchActors.json - """ - # TODO based on stored users - return {'actors': []} - - -@xrpc_server.method('app.bsky.actor.searchActorsTypeahead') -def searchActorsTypeahead(input, term=None, limit=None): - """ - lexicons/app/bsky/actor/searchActorsTypeahead.json - """ - # TODO based on stored users - return {'actors': []} diff --git a/xrpc_feed.py b/xrpc_feed.py deleted file mode 100644 index c391710..0000000 --- a/xrpc_feed.py +++ /dev/null @@ -1,120 +0,0 @@ -"""app.bsky.feed.* XRPC methods.""" -import json -import logging -import re - -from flask import g -from granary import bluesky -from oauth_dropins.webutil import util - -from flask_app import xrpc_server -from models import Object, PAGE_SIZE -from web import Web - -logger = logging.getLogger(__name__) - - -@xrpc_server.method('app.bsky.feed.getAuthorFeed') -def getAuthorFeed(input, author=None, limit=None, before=None): - """ - lexicons/app/bsky/feed/getAuthorFeed.json, feedViewPost.json - """ - if not author or not re.match(util.DOMAIN_RE, author): - raise ValueError(f'{author} is not a domain') - - g.user = Web.get_by_id(author) - if not g.user: - raise ValueError(f'User {author} not found') - elif not g.user.obj.as1: - return ValueError(f'User {author} not fully set up') - - # TODO: unify with pages.feed? - limit = min(limit or PAGE_SIZE, PAGE_SIZE) - objects, _, _ = Object.query(Object.domains == author, Object.labels == 'user') \ - .order(-Object.created) \ - .fetch_page(limit) - activities = [obj.as1 for obj in objects if not obj.deleted] - logger.info(f'AS1 activities: {json.dumps(activities, indent=2)}') - - return {'feed': [bluesky.from_as1(a) for a in activities]} - - -@xrpc_server.method('app.bsky.feed.getPostThread') -def getPostThread(input, uri=None, depth=None): - """ - lexicons/app/bsky/feed/getPostThread.json - """ - if not uri: - raise ValueError('Missing uri') - - obj = Object.get_by_id(uri) - if not obj: - raise ValueError(f'{uri} not found') - - logger.info(f'AS1: {json.dumps(obj.as1, indent=2)}') - - return { - 'thread': { - '$type': 'app.bsky.feed.defs#threadViewPost', - 'post': bluesky.from_as1(obj.as1)['post'], - 'replies': [{ - '$type': 'app.bsky.feed.defs#threadViewPost', - 'post': bluesky.from_as1(reply)['post'], - } for reply in obj.as1.get('replies', {}).get('items', [])], - }, - } - - -@xrpc_server.method('app.bsky.feed.getRepostedBy') -def getRepostedBy(input, uri=None, cid=None, limit=None, before=None): - """ - TODO: implement before, as query filter. what's input type? str or datetime? - lexicons/app/bsky/feed/getRepostedBy.json - """ - if not uri: - raise ValueError('Missing uri') - - limit = min(limit or PAGE_SIZE, PAGE_SIZE) - objects, _, _ = Object.query(Object.object_ids == uri) \ - .order(-Object.created) \ - .fetch_page(limit) - activities = [obj.as1 for obj in objects if not obj.deleted] - logger.info(f'AS1 activities: {json.dumps(activities, indent=2)}') - - return { - 'uri': 'http://orig/post', - 'repostBy': [{ - **bluesky.from_as1(a['actor']), - '$type': 'app.bsky.feed.getRepostedBy#repostedBy', - } for a in activities if a.get('actor')], - } - - -# TODO: cursor -@xrpc_server.method('app.bsky.feed.getTimeline') -def getTimeline(input, algorithm=None, limit=50, before=None): - """ - lexicons/app/bsky/feed/getTimeline.json - """ - # TODO: how to get authed user? - domain = 'user.com' - - # TODO: de-dupe with pages.feed() - logger.info(f'Fetching {limit} objects for {domain}') - objects, _, _ = Object.query(Object.domains == domain, Object.labels == 'feed') \ - .order(-Object.created) \ - .fetch_page(limit) - - return {'feed': [bluesky.from_as1(obj.as1) for obj in objects if not obj.deleted]} - - -# TODO -@xrpc_server.method('app.bsky.feed.getLikes') -def getLikes(input, uri=None, direction=None, cid=None, limit=None, before=None): - """ - lexicons/app/bsky/feed/getLikes.json - """ - return { - 'uri': uri, - 'likes': [], - } diff --git a/xrpc_graph.py b/xrpc_graph.py deleted file mode 100644 index 1c18f42..0000000 --- a/xrpc_graph.py +++ /dev/null @@ -1,67 +0,0 @@ -"""app.bsky.graph.* XRPC methods.""" -import logging -import re - -from granary import bluesky -from oauth_dropins.webutil import util - -from flask_app import xrpc_server -from models import Follower -from web import Web - -logger = logging.getLogger(__name__) - - -def get_followers(query_prop, output_field, user=None, limit=50, before=None): - """Runs the getFollowers or getFollows method. (They're almost identical.) - - Args: - query_prop: str, property of Follower class to query - output_field: str, field in output to populate followers into - - Returns: - dict, XRPC method output - """ - # TODO: what is user? - if not user or not re.match(util.DOMAIN_RE, user): - raise ValueError(f'{user} is not a domain') - elif not Web.get_by_id(user): - raise ValueError(f'Unknown user {user}') - - collection = 'followers' if output_field == 'followers' else 'following' - followers, before, after = Follower.fetch_page(user, collection) - - actors = [] - for follower in followers: - actor = follower.to_as1() - if actor: - actors.append({ - **bluesky.from_as1(actor), - '$type': 'app.bsky.graph.getFollowers#follower', - 'indexedAt': util.now().isoformat(), - }) - - return { - 'subject': bluesky.from_as1({ - 'objectType': 'person', - 'url': f'https://{user}/', - }), - output_field: actors, - 'cursor': '', - } - - -@xrpc_server.method('app.bsky.graph.getFollowers') -def getFollowers(input, **kwargs): - """ - lexicons/app/bsky/graph/getFollowers.json - """ - return get_followers(Follower.dest, 'followers', **kwargs) - - -@xrpc_server.method('app.bsky.graph.getFollows') -def getFollows(input, **kwargs): - """ - lexicons/app/bsky/graph/getFollows.json - """ - return get_followers(Follower.src, 'follows', **kwargs)