From 898b8545ac49c45cf89c4c1b2b78fa9f1f15e947 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Fri, 13 Jan 2023 11:40:52 -0800 Subject: [PATCH] bluesky: implement app.bsky.graph.getFollowers --- models.py | 13 +++++ tests/test_xrpc_graph.py | 114 ++++++++++++++++++++++++--------------- xrpc_graph.py | 31 +++++++++-- 3 files changed, 111 insertions(+), 47 deletions(-) diff --git a/models.py b/models.py index 13a4a3a..5d2ac65 100644 --- a/models.py +++ b/models.py @@ -112,6 +112,11 @@ class User(StringIdModel): base64_to_long(str(self.private_exponent)))) return rsa.exportKey(format='PEM') + def to_as1(self): + """Returns this user as an AS1 actor dict, if possible.""" + if self.actor_as2: + return as2.to_as1(json_loads(self.actor_as2)) + def username(self): """Returns the user's preferred username from an acct: url, if available. @@ -357,3 +362,11 @@ class Follower(StringIdModel): setattr(follower, prop, val) follower.put() return follower + + def to_as1(self): + """Returns this follower as an AS1 actor dict, if possible.""" + if self.last_follow: + last_follow = json_loads(self.last_follow) + actor = last_follow.get('actor') + if actor: + return as2.to_as1(actor) diff --git a/tests/test_xrpc_graph.py b/tests/test_xrpc_graph.py index 1f01ebd..8351cf5 100644 --- a/tests/test_xrpc_graph.py +++ b/tests/test_xrpc_graph.py @@ -1,60 +1,86 @@ """Unit tests for graph.py.""" +import copy from unittest.mock import patch -from oauth_dropins.webutil import util +from granary import bluesky from oauth_dropins.webutil.testutil import requests_response +from oauth_dropins.webutil.util import json_dumps, json_loads import requests +from .test_activitypub import FOLLOW_WITH_ACTOR from . import testutil +from models import Follower + +ACTOR_DECLARATION = { + '$type': 'app.bsky.system.declRef', + 'actorType': 'app.bsky.system.actorUser', + 'cid': 'TODO', +} +SUBJECT = { + '$type': 'app.bsky.actor.ref#withInfo', + 'did': 'did:web:foo.com', + 'handle': 'foo.com', + 'declaration': ACTOR_DECLARATION, +} +FOLLOWERS_BSKY = [{ + '$type': 'app.bsky.graph.getFollowers#follower', + 'did': 'did:web:other', + 'handle': 'other-username', + 'declaration': ACTOR_DECLARATION, + 'indexedAt': '2022-01-02T03:04:05+00:00', +}, { + '$type': 'app.bsky.graph.getFollowers#follower', + 'did': 'did:web:mastodon.social:users:swentel', + 'handle': 'mastodon.social/users/swentel', + 'declaration': ACTOR_DECLARATION, + 'indexedAt': '2022-01-02T03:04:05+00:00', +}] -# @patch('requests.get') -# class XrpcGraphTest(testutil.TestCase): +@patch('requests.get') +class XrpcGraphTest(testutil.TestCase): -# def test_getAuthorFeed(self, mock_get): -# mock_get.return_value = requests_response(""" -# -# -# """, url='https://foo.com/') + def test_getFollowers_not_domain(self, mock_get): + resp = self.client.get('/xrpc/app.bsky.graph.getFollowers', + query_string={'user': 'not a domain'}) + self.assertEqual(400, resp.status_code) -# got = self.client.get('/xrpc/app.bsky.actor.getProfile', -# query_string={'actor': 'foo.com'}, -# ).json -# self.assertEqual({ -# }, got) + def test_getFollowers_empty(self, mock_get): + resp = self.client.get('/xrpc/app.bsky.graph.getFollowers', + query_string={'user': 'foo.com'}) + self.assertEqual(200, resp.status_code) + self.assert_equals({ + 'subject': SUBJECT, + 'cursor': '', + 'followers': [], + }, resp.json) -# def test_getPostThread(self, mock_get): -# mock_get.return_value = requests_response(""" -# -# -# """, url='https://foo.com/') + def test_getFollowers(self, mock_get): + Follower.get_or_create('foo.com', 'https://no/stored/follow') + Follower.get_or_create('foo.com', 'https://masto/user', + last_follow=json_dumps(FOLLOW_WITH_ACTOR)) -# got = self.client.get('/xrpc/app.bsky.actor.getProfile', -# query_string={'actor': 'foo.com'}, -# ).json -# self.assertEqual({ -# }, got) + other = copy.deepcopy(FOLLOW_WITH_ACTOR) + other['actor'].update({ + 'url': 'http://other', + 'preferredUsername': 'other-username', + }) + Follower.get_or_create('foo.com', 'http://other', last_follow=json_dumps(other)) -# def test_getRepostedBy(self, mock_get): -# mock_get.return_value = requests_response(""" -# -# -# """, url='https://foo.com/') + other['actor']['url'] = 'http://nope' + Follower.get_or_create('nope.com', 'http://nope' , last_follow=json_dumps(other)) -# got = self.client.get('/xrpc/app.bsky.actor.getProfile', -# query_string={'actor': 'foo.com'}, -# ).json -# self.assertEqual({ -# }, got) + resp = self.client.get('/xrpc/app.bsky.graph.getFollowers', + query_string={'user': 'foo.com'}) + self.assertEqual(200, resp.status_code) + self.assert_equals({ + 'subject': SUBJECT, + 'cursor': '', + 'followers': FOLLOWERS_BSKY, + }, resp.json) -# def test_getTimeline(self, mock_get): -# mock_get.return_value = requests_response(""" -# -# -# """, url='https://foo.com/') - -# got = self.client.get('/xrpc/app.bsky.actor.getProfile', -# query_string={'actor': 'foo.com'}, -# ).json -# self.assertEqual({ -# }, got) + # def test_getFollows(self, mock_get): + # resp = self.client.get('/xrpc/app.bsky.graph.getFollows', + # query_string={'user': 'foo.com'}) + # self.assertEqual({ + # }, resp.json) diff --git a/xrpc_graph.py b/xrpc_graph.py index 0652cdf..c3e75fc 100644 --- a/xrpc_graph.py +++ b/xrpc_graph.py @@ -1,20 +1,45 @@ """app.bsky.graph.* XRPC methods.""" import logging +import re + +from granary import bluesky +from oauth_dropins.webutil import util from app import xrpc_server +from models import Follower logger = logging.getLogger(__name__) -# get these from datastore @xrpc_server.method('app.bsky.graph.getFollowers') -def getFollowers(input): +def getFollowers(input, user=None, limit=50, before=None): """ lexicons/app/bsky/graph/getFollowers.json """ + # TODO: what is user? + if not re.match(util.DOMAIN_RE, user): + raise ValueError(f'{user} is not a domain') + + followers = [] + for follower in Follower.query(Follower.dest == user).fetch(limit): + actor = follower.to_as1() + print('@', actor) + if actor: + followers.append({ + **bluesky.actor_to_ref(actor), + '$type': 'app.bsky.graph.getFollowers#follower', + 'indexedAt': util.now().isoformat(), + }) + + return { + 'subject': bluesky.actor_to_ref({'url': f'https://{user}/'}), + 'followers': followers, + 'cursor': '', + } + @xrpc_server.method('app.bsky.graph.getFollows') -def getFollows(input): +def getFollows(input, user=None, limit=None, before=None): """ lexicons/app/bsky/graph/getFollows.json """