diff --git a/activitypub.py b/activitypub.py index 81d43dc..5037c16 100644 --- a/activitypub.py +++ b/activitypub.py @@ -61,6 +61,12 @@ class ActivityPub(User, Protocol): def address(self): 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): """Returns this user's human-readable unique id, eg '@me@snarfed.org'.""" return self.address or self.key.id() diff --git a/pages.py b/pages.py index 8a9fb87..5b6377a 100644 --- a/pages.py +++ b/pages.py @@ -53,27 +53,27 @@ def web_user_redirects(**kwargs): return redirect(f'/web/{path}', code=301) -@app.get(f'//') -def user(protocol, domain): - g.user = PROTOCOLS[protocol].get_by_id(domain) +@app.get(f'//') +def user(protocol, id): + g.user = PROTOCOLS[protocol].get_by_id(id) if not g.user or not g.user.direct: return USER_NOT_FOUND_HTML, 404 - elif g.user.key.id() != domain: - return redirect(f'/{protocol}/{g.user.key.id()}', code=301) + elif id != g.user.label_id(): + return redirect(g.user.user_page_path(), code=301) assert not g.user.use_instead query = Object.query( - Object.domains == domain, + Object.domains == id, Object.labels.IN(('notification', 'user')), ) 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) 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) following = f'{following}{"+" if following == FOLLOWERS_UI_LIMIT else ""}' @@ -88,13 +88,13 @@ def user(protocol, domain): ) -@app.get(f'///') -def followers_or_following(protocol, domain, collection): - g.user = PROTOCOLS[protocol].get_by_id(domain) # g.user is used in template +@app.get(f'///') +def followers_or_following(protocol, id, collection): + g.user = PROTOCOLS[protocol].get_by_id(id) # g.user is used in template if not g.user: 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: f.url = f.src if collection == 'followers' else f.dest @@ -113,27 +113,27 @@ def followers_or_following(protocol, domain, collection): ) -@app.get(f'///feed') -def feed(protocol, domain): +@app.get(f'///feed') +def feed(protocol, id): format = request.args.get('format', 'html') if format not in ('html', 'atom', '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: - return render_template('user_not_found.html', domain=domain), 404 + return render_template('user_not_found.html', domain=id), 404 objects, _, _ = Object.query( - Object.domains == domain, Object.labels == 'feed') \ + Object.domains == id, Object.labels == 'feed') \ .order(-Object.created) \ .fetch_page(PAGE_SIZE) activities = [obj.as1 for obj in objects if not obj.deleted] actor = { - 'displayName': domain, + 'displayName': id, '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 # (specifically into hcard_to_html) somehow to convert Mastodon URLs to @-@ diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 2e8c3f7..3d0f34f 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -1576,3 +1576,13 @@ class ActivityPubUtilsTest(TestCase): user.actor_as2 = ACTOR 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')) diff --git a/tests/test_pages.py b/tests/test_pages.py index 2c4162f..0a8965d 100644 --- a/tests/test_pages.py +++ b/tests/test_pages.py @@ -15,12 +15,17 @@ from oauth_dropins.webutil.testutil import requests_response # import first so that Fake is defined before URL routes are registered from .testutil import Fake, TestCase +from activitypub import ActivityPub import common from models import Object, Follower, User from web import Web from .test_web import ACTOR_AS2, ACTOR_HTML, ACTOR_MF2, REPOST_AS2 +ACTOR_WITH_PREFERRED_USERNAME = { + **ACTOR, + 'preferredUsername': 'me', +} def contents(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') 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): self.add_objects() 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://masto/user', last_follow={ **FOLLOW_WITH_ACTOR, - 'actor': { - **ACTOR, - 'preferredUsername': 'me', - }, + 'actor': ACTOR_WITH_PREFERRED_USERNAME, }) got = self.client.get('/web/bar.com/followers') self.assert_equals(200, got.status_code)