add new common.fetch_followers() fn, use in UI, AP collections, and XRPCs

for #264
pull/380/head
Ryan Barrett 2023-01-19 15:29:52 -08:00
rodzic ea02b82f39
commit 74ebbc428b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
5 zmienionych plików z 56 dodań i 19 usunięć

Wyświetl plik

@ -250,16 +250,14 @@ def follower_collection(domain, collection):
if not User.get_by_id(domain):
return f'User {domain} not found', 404
# this query is duplicated in pages.followers_or_following()
logger.info(f"Counting {domain}'s {collection}")
followers, before, after = common.fetch_followers(domain, collection)
domain_prop = Follower.dest if collection == 'followers' else Follower.src
query = Follower.query(
Follower.status == 'active',
domain_prop == domain,
)
count = query.count()
followers, before, after = common.fetch_page(query, Follower)
ret = {
'@context': 'https://www.w3.org/ns/activitystreams',
'summary': f"{domain}'s {collection}",

Wyświetl plik

@ -22,7 +22,7 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
import requests
from werkzeug.exceptions import BadGateway
from models import Activity, User
from models import Activity, Follower, User
logger = logging.getLogger(__name__)
@ -589,6 +589,32 @@ def actor(domain, user=None):
return actor
def fetch_followers(domain, collection):
"""Fetches a page of Follower entities.
Wraps :func:`common.fetch_page`. Paging uses the `before` and `after` query
parameters, if available in the request.
Args:
domain: str, user to fetch entities for
collection, str, 'followers' or 'following'
Returns:
(results, new_before, new_after) tuple with:
results: list of Follower entities
new_before, new_after: str query param values for `before` and `after`
to fetch the previous and next pages, respectively
"""
assert collection in ('followers', 'following'), collection
domain_prop = Follower.dest if collection == 'followers' else Follower.src
query = Follower.query(
Follower.status == 'active',
domain_prop == domain,
).order(-Follower.updated)
return fetch_page(query, Follower)
def fetch_page(query, model_class):
"""Fetches a page of results from a datastore query.

Wyświetl plik

@ -103,16 +103,10 @@ def user(domain):
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>/<any(followers,following):collection>')
def followers_or_following(domain, collection):
if not (user := User.get_by_id(domain)):
if not (user := User.get_by_id(domain)): # user var is used in template
return render_template('user_not_found.html', domain=domain), 404
# this query is duplicated in activitypub.followers_collection()
domain_prop = Follower.dest if collection == 'followers' else Follower.src
query = Follower.query(
Follower.status == 'active',
domain_prop == domain,
).order(-Follower.updated)
followers, before, after = common.fetch_page(query, Follower)
followers, before, after = common.fetch_followers(domain, collection)
for f in followers:
f.url = f.src if collection == 'followers' else f.dest

Wyświetl plik

@ -9,7 +9,7 @@ import requests
from .test_activitypub import ACTOR, FOLLOW, FOLLOW_WITH_ACTOR, FOLLOW_WITH_OBJECT
from . import testutil
from models import Follower
from models import Follower, User
ACTOR_DECLARATION = {
'$type': 'app.bsky.system.declRef',
@ -49,7 +49,14 @@ class XrpcGraphTest(testutil.TestCase):
query_string={'user': 'not a domain'})
self.assertEqual(400, resp.status_code)
def test_getFollowers_no_user(self, mock_get):
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, mock_get):
User.get_or_create('foo.com')
resp = self.client.get('/xrpc/app.bsky.graph.getFollowers',
query_string={'user': 'foo.com'})
self.assertEqual(200, resp.status_code)
@ -60,6 +67,8 @@ class XrpcGraphTest(testutil.TestCase):
}, resp.json)
def test_getFollowers(self, mock_get):
User.get_or_create('foo.com')
other_follow = copy.deepcopy(FOLLOW)
other_follow['actor'] = {
'url': 'http://other',
@ -89,6 +98,8 @@ class XrpcGraphTest(testutil.TestCase):
self.assertEqual(400, resp.status_code)
def test_getFollows_empty(self, mock_get):
User.get_or_create('foo.com')
resp = self.client.get('/xrpc/app.bsky.graph.getFollows',
query_string={'user': 'foo.com'})
self.assertEqual(200, resp.status_code)
@ -99,6 +110,8 @@ class XrpcGraphTest(testutil.TestCase):
}, resp.json)
def test_getFollows(self, mock_get):
User.get_or_create('foo.com')
other_follow = copy.deepcopy(FOLLOW)
other_follow['object'] = {
'url': 'http://other',

Wyświetl plik

@ -6,7 +6,8 @@ from granary import bluesky
from oauth_dropins.webutil import util
from app import xrpc_server
from models import Follower
import common
from models import Follower, User
logger = logging.getLogger(__name__)
@ -24,12 +25,17 @@ def get_followers(query_prop, output_field, user=None, limit=50, before=None):
# 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 User.get_by_id(user):
raise ValueError(f'Unknown user {user}')
followers = []
for follower in Follower.query(query_prop == user).fetch(limit):
collection = 'followers' if output_field == 'followers' else 'following'
followers, before, after = common.fetch_followers(user, collection)
actors = []
for follower in followers:
actor = follower.to_as1()
if actor:
followers.append({
actors.append({
**bluesky.actor_to_ref(actor),
'$type': 'app.bsky.graph.getFollowers#follower',
'indexedAt': util.now().isoformat(),
@ -37,7 +43,7 @@ def get_followers(query_prop, output_field, user=None, limit=50, before=None):
return {
'subject': bluesky.actor_to_ref({'url': f'https://{user}/'}),
output_field: followers,
output_field: actors,
'cursor': '',
}