AP users: serve ActivityPub user page with address (handle) in URL

eg /activitypub/@me@instance.com

for #512
circle-datastore-transactions
Ryan Barrett 2023-06-01 22:00:47 -07:00
rodzic ca64793fff
commit e05ddb0a45
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
4 zmienionych plików z 53 dodań i 23 usunięć

Wyświetl plik

@ -61,6 +61,12 @@ class ActivityPub(User, Protocol):
def address(self): def address(self):
return self.ap_address() return self.ap_address()
@classmethod
def get_by_id(cls, id):
"""Override User.get_by_id to fall back to querying by address."""
return (super(User, ActivityPub).get_by_id(id)
or ActivityPub.query(ActivityPub.address == id).get())
def label_id(self): def label_id(self):
"""Returns this user's human-readable unique id, eg '@me@snarfed.org'.""" """Returns this user's human-readable unique id, eg '@me@snarfed.org'."""
return self.address or self.key.id() return self.address or self.key.id()

Wyświetl plik

@ -53,27 +53,27 @@ def web_user_redirects(**kwargs):
return redirect(f'/web/{path}', code=301) return redirect(f'/web/{path}', code=301)
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<regex("{DOMAIN_RE}"):domain>') @app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>')
def user(protocol, domain): def user(protocol, id):
g.user = PROTOCOLS[protocol].get_by_id(domain) g.user = PROTOCOLS[protocol].get_by_id(id)
if not g.user or not g.user.direct: if not g.user or not g.user.direct:
return USER_NOT_FOUND_HTML, 404 return USER_NOT_FOUND_HTML, 404
elif g.user.key.id() != domain: elif id != g.user.label_id():
return redirect(f'/{protocol}/{g.user.key.id()}', code=301) return redirect(g.user.user_page_path(), code=301)
assert not g.user.use_instead assert not g.user.use_instead
query = Object.query( query = Object.query(
Object.domains == domain, Object.domains == id,
Object.labels.IN(('notification', 'user')), Object.labels.IN(('notification', 'user')),
) )
objects, before, after = fetch_objects(query) objects, before, after = fetch_objects(query)
followers = Follower.query(Follower.dest == domain, Follower.status == 'active')\ followers = Follower.query(Follower.dest == id, Follower.status == 'active')\
.count(limit=FOLLOWERS_UI_LIMIT) .count(limit=FOLLOWERS_UI_LIMIT)
followers = f'{followers}{"+" if followers == FOLLOWERS_UI_LIMIT else ""}' followers = f'{followers}{"+" if followers == FOLLOWERS_UI_LIMIT else ""}'
following = Follower.query(Follower.src == domain, Follower.status == 'active')\ following = Follower.query(Follower.src == id, Follower.status == 'active')\
.count(limit=FOLLOWERS_UI_LIMIT) .count(limit=FOLLOWERS_UI_LIMIT)
following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}' following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}'
@ -88,13 +88,13 @@ def user(protocol, domain):
) )
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<regex("{DOMAIN_RE}"):domain>/<any(followers,following):collection>') @app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/<any(followers,following):collection>')
def followers_or_following(protocol, domain, collection): def followers_or_following(protocol, id, collection):
g.user = PROTOCOLS[protocol].get_by_id(domain) # g.user is used in template g.user = PROTOCOLS[protocol].get_by_id(id) # g.user is used in template
if not g.user: if not g.user:
return USER_NOT_FOUND_HTML, 404 return USER_NOT_FOUND_HTML, 404
followers, before, after = Follower.fetch_page(domain, collection) followers, before, after = Follower.fetch_page(id, collection)
for f in followers: for f in followers:
f.url = f.src if collection == 'followers' else f.dest f.url = f.src if collection == 'followers' else f.dest
@ -113,27 +113,27 @@ def followers_or_following(protocol, domain, collection):
) )
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<regex("{DOMAIN_RE}"):domain>/feed') @app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<id>/feed')
def feed(protocol, domain): def feed(protocol, id):
format = request.args.get('format', 'html') format = request.args.get('format', 'html')
if format not in ('html', 'atom', 'rss'): if format not in ('html', 'atom', 'rss'):
error(f'format {format} not supported; expected html, atom, or rss') error(f'format {format} not supported; expected html, atom, or rss')
g.user = PROTOCOLS[protocol].get_by_id(domain) g.user = PROTOCOLS[protocol].get_by_id(id)
if not g.user: if not g.user:
return render_template('user_not_found.html', domain=domain), 404 return render_template('user_not_found.html', domain=id), 404
objects, _, _ = Object.query( objects, _, _ = Object.query(
Object.domains == domain, Object.labels == 'feed') \ Object.domains == id, Object.labels == 'feed') \
.order(-Object.created) \ .order(-Object.created) \
.fetch_page(PAGE_SIZE) .fetch_page(PAGE_SIZE)
activities = [obj.as1 for obj in objects if not obj.deleted] activities = [obj.as1 for obj in objects if not obj.deleted]
actor = { actor = {
'displayName': domain, 'displayName': id,
'url': g.user.web_url(), 'url': g.user.web_url(),
} }
title = f'Bridgy Fed feed for {domain}' title = f'Bridgy Fed feed for {id}'
# TODO: inject/merge common.pretty_link into microformats2.render_content # TODO: inject/merge common.pretty_link into microformats2.render_content
# (specifically into hcard_to_html) somehow to convert Mastodon URLs to @-@ # (specifically into hcard_to_html) somehow to convert Mastodon URLs to @-@

Wyświetl plik

@ -1576,3 +1576,13 @@ class ActivityPubUtilsTest(TestCase):
user.actor_as2 = ACTOR user.actor_as2 = ACTOR
self.assertEqual('@swentel@mas.to', user.label_id()) self.assertEqual('@swentel@mas.to', user.label_id())
def test_get_by_id(self):
user = self.make_user('http://foo', cls=ActivityPub)
self.assert_entities_equal(user, ActivityPub.get_by_id('http://foo'))
self.assertIsNone(ActivityPub.get_by_id('@swentel@mas.to'))
user.actor_as2 = ACTOR
user.put()
self.assert_entities_equal(user, ActivityPub.get_by_id('http://foo'))
self.assert_entities_equal(user, ActivityPub.get_by_id('@swentel@mas.to'))

Wyświetl plik

@ -15,12 +15,17 @@ from oauth_dropins.webutil.testutil import requests_response
# import first so that Fake is defined before URL routes are registered # import first so that Fake is defined before URL routes are registered
from .testutil import Fake, TestCase from .testutil import Fake, TestCase
from activitypub import ActivityPub
import common import common
from models import Object, Follower, User from models import Object, Follower, User
from web import Web from web import Web
from .test_web import ACTOR_AS2, ACTOR_HTML, ACTOR_MF2, REPOST_AS2 from .test_web import ACTOR_AS2, ACTOR_HTML, ACTOR_MF2, REPOST_AS2
ACTOR_WITH_PREFERRED_USERNAME = {
**ACTOR,
'preferredUsername': 'me',
}
def contents(activities): def contents(activities):
return [(a.get('object') or a)['content'] for a in activities] return [(a.get('object') or a)['content'] for a in activities]
@ -42,6 +47,18 @@ class PagesTest(TestCase):
got = self.client.get('/fake/foo.com') got = self.client.get('/fake/foo.com')
self.assert_equals(200, got.status_code) self.assert_equals(200, got.status_code)
def test_user_activitypub_address(self):
user = self.make_user('foo', cls=ActivityPub,
actor_as2=ACTOR_WITH_PREFERRED_USERNAME)
self.assertEqual('@me@plus.google.com', user.address)
got = self.client.get('/activitypub/@me@plus.google.com')
self.assert_equals(200, got.status_code)
got = self.client.get('/activitypub/foo')
self.assert_equals(301, got.status_code)
self.assert_equals('/activitypub/@me@plus.google.com', got.headers['Location'])
def test_user_objects(self): def test_user_objects(self):
self.add_objects() self.add_objects()
got = self.client.get('/web/user.com') got = self.client.get('/web/user.com')
@ -117,10 +134,7 @@ class PagesTest(TestCase):
Follower.get_or_create('bar.com', 'https://no.stored/users/follow') Follower.get_or_create('bar.com', 'https://no.stored/users/follow')
Follower.get_or_create('bar.com', 'https://masto/user', last_follow={ Follower.get_or_create('bar.com', 'https://masto/user', last_follow={
**FOLLOW_WITH_ACTOR, **FOLLOW_WITH_ACTOR,
'actor': { 'actor': ACTOR_WITH_PREFERRED_USERNAME,
**ACTOR,
'preferredUsername': 'me',
},
}) })
got = self.client.get('/web/bar.com/followers') got = self.client.get('/web/bar.com/followers')
self.assert_equals(200, got.status_code) self.assert_equals(200, got.status_code)