diff --git a/atproto.py b/atproto.py index eaa5c87..9e47520 100644 --- a/atproto.py +++ b/atproto.py @@ -653,42 +653,27 @@ def poll_notifications(): # URL route is registered in hub.py def poll_posts(): - """Fetches and enqueueus new posts from the AppView for our users. + """Fetches and enqueueus bridged Bluesky users' new posts from the AppView. - Uses the ``getTimeline`` endpoint, which is intended for end users. 🤷 + Uses the ``getAuthorFeed`` endpoint, which is intended for clients. 🤷 TODO: unify with poll_notifications """ - repos = {r.key.id(): r for r in AtpRepo.query()} - logger.info(f'Got {len(repos)} repos') - if not repos: - return 'Nothing to do ¯\_(ツ)_/¯', 204 - - users = itertools.chain(*(cls.query(cls.copies.uri.IN(list(repos))) - for cls in set(PROTOCOLS.values()) - if cls and cls != ATProto)) - # this client needs to be request-local because we set its service token # below per user that we're polling client = Client(f'https://{os.environ["APPVIEW_HOST"]}', headers={'User-Agent': USER_AGENT}) - for user in users: - if not user.is_enabled(ATProto): - logger.info(f'Skipping {user.key.id()}') - continue - - logger.debug(f'Fetching posts for {user.key.id()}') + for user in ATProto.query(ATProto.enabled_protocols != None): + did = user.key.id() + logger.debug(f'Fetching posts for {did} {user.handle}') # TODO: store and use cursor # seenAt would be easier, but they don't support it yet # https://github.com/bluesky-social/atproto/issues/1636 - did = user.get_copy(ATProto) - repo = repos[did] - client.session['accessJwt'] = service_jwt(os.environ['APPVIEW_HOST'], - repo_did=did, - privkey=repo.signing_key) - resp = client.app.bsky.feed.getTimeline(limit=10) + resp = appview.app.bsky.feed.getAuthorFeed( + actor=did, filter='posts_no_replies', limit=10) + for item in resp['feed']: # TODO: handle reposts once we have a URI for them # https://github.com/bluesky-social/atproto/issues/1811 @@ -700,10 +685,9 @@ def poll_posts(): # TODO: verify sig. skipping this for now because we're getting # these from the AppView, which is trusted, specifically we expect # the BGS and/or the AppView already checked sigs. - author_did = post['author']['did'] + assert did == post['author']['did'] obj = Object.get_or_create(id=post['uri'], bsky=post['record'], - source_protocol=ATProto.ABBREV, - actor=author_did) + source_protocol=ATProto.ABBREV, actor=did) if obj.status in ('complete', 'ignored'): continue @@ -713,7 +697,6 @@ def poll_posts(): obj.add('feed', user.key) obj.put() - common.create_task(queue='receive', obj=obj.key.urlsafe(), - authed_as=author_did) + common.create_task(queue='receive', obj=obj.key.urlsafe(), authed_as=did) return 'OK' diff --git a/tests/test_atproto.py b/tests/test_atproto.py index c011f94..5dea6b3 100644 --- a/tests/test_atproto.py +++ b/tests/test_atproto.py @@ -1266,15 +1266,23 @@ class ATProtoTest(TestCase): @patch.object(tasks_client, 'create_task', return_value=Task(name='my task')) @patch('requests.get') def test_poll_posts(self, mock_get, mock_create_task): - user_a = self.make_user(id='fake:user-a', cls=Fake, - copies=[Target(uri='did:plc:a', protocol='atproto')]) - user_b = self.make_user(id='fake:user-b', cls=Fake, - copies=[Target(uri='did:plc:b', protocol='atproto')]) - user_c = self.make_user(id='fake:user-c', cls=Fake, - copies=[Target(uri='did:plc:c', protocol='atproto')]) - Repo.create(self.storage, 'did:plc:a', signing_key=ATPROTO_KEY) - Repo.create(self.storage, 'did:plc:b', signing_key=ATPROTO_KEY) - Repo.create(self.storage, 'did:plc:c', signing_key=ATPROTO_KEY) + for i in ['a', 'b', 'c', 'd']: + did = f'did:plc:{i}' + self.store_object(id=did, raw={ + **DID_DOC, + 'id': did, + }) + + user_a = self.make_user( + id='did:plc:a', cls=ATProto, enabled_protocols=['fake'], + copies=[Target(uri='fake:user-a', protocol='fake')]) + user_b = self.make_user(id='did:plc:b', cls=ATProto) # no enabled protocols + user_c = self.make_user( + id='did:plc:c', cls=ATProto, enabled_protocols=['fake'], + copies=[Target(uri='fake:user-c', protocol='fake')]) + user_d = self.make_user( + id='did:plc:d', cls=ATProto, enabled_protocols=['fake'], + copies=[Target(uri='fake:user-d', protocol='fake')]) post = { '$type': 'app.bsky.feed.post', @@ -1288,7 +1296,7 @@ class ATProtoTest(TestCase): 'record': post, 'author': { '$type': 'app.bsky.actor.defs#profileViewBasic', - 'did': 'did:web:alice.com', + 'did': 'did:plc:a', 'handle': 'alice.com', }, } @@ -1330,25 +1338,24 @@ class ATProtoTest(TestCase): resp = self.post('/queue/atproto-poll-posts', client=hub.app.test_client()) self.assertEqual(200, resp.status_code) - get_timeline = call( - 'https://appview.local/xrpc/app.bsky.feed.getTimeline?limit=10', + get = [call( + f'https://api.bsky-sandbox.dev/xrpc/app.bsky.feed.getAuthorFeed?actor=did%3Aplc%3A{i}&filter=posts_no_replies&limit=10', json=None, data=None, headers={ 'Content-Type': 'application/json', 'User-Agent': common.USER_AGENT, - 'Authorization': ANY, - }) + }) for i in ('a', 'c', 'd')] self.assertEqual([ - get_timeline, + get[0], self.req('https://alice.com/.well-known/did.json'), - get_timeline, - get_timeline, + get[1], + get[2], ], mock_get.call_args_list) post_obj = Object.get_by_id('at://did:web:alice.com/app.bsky.feed.post/123') self.assertEqual(post, post_obj.bsky) self.assert_task(mock_create_task, 'receive', '/queue/receive', - obj=post_obj.key.urlsafe(), authed_as='did:web:alice.com') + obj=post_obj.key.urlsafe(), authed_as='did:plc:a') # TODO: https://github.com/snarfed/bridgy-fed/issues/728 # repost_obj = Object.get_by_id('at://did:plc:d/app.bsky.feed.post/456')