diff --git a/follow.py b/follow.py
index 08cd38f..34ace4e 100644
--- a/follow.py
+++ b/follow.py
@@ -89,7 +89,7 @@ def remote_follow():
addr = request.values['address']
webfinger = fetch_webfinger(addr)
if webfinger is None:
- return redirect(f'/user/{domain}')
+ return redirect(g.user.user_page_path())
for link in webfinger.get('links', []):
if link.get('rel') == SUBSCRIBE_LINK_REL:
@@ -98,7 +98,7 @@ def remote_follow():
return redirect(template.replace('{uri}', g.user.address()))
flash(f"Couldn't find remote follow link for {addr}")
- return redirect(f'/user/{domain}')
+ return redirect(g.user.user_page_path())
class FollowStart(indieauth.Start):
@@ -120,7 +120,7 @@ class FollowStart(indieauth.Start):
if util.is_connection_failure(e) or util.interpret_http_exception(e)[0]:
flash(f"Couldn't fetch your web site: {e}")
domain = util.domain_from_link(me)
- return redirect(f'/user/{domain}/following?address={address}')
+ return redirect(f'/web/{domain}/following?address={address}')
raise
@@ -149,7 +149,7 @@ class FollowCallback(indieauth.Callback):
else:
webfinger = fetch_webfinger(addr)
if webfinger is None:
- return redirect(f'/user/{domain}/following')
+ return redirect(g.user.user_page_path('following'))
as2_url = None
for link in webfinger.get('links', []):
@@ -159,7 +159,7 @@ class FollowCallback(indieauth.Callback):
if not as2_url:
flash(f"Couldn't find ActivityPub profile link for {addr}")
- return redirect(f'/user/{domain}/following')
+ return redirect(g.user.user_page_path('following'))
# TODO: make this generic across protocols
followee = ActivityPub.load(as2_url).as2
@@ -167,10 +167,10 @@ class FollowCallback(indieauth.Callback):
inbox = followee.get('inbox')
if not id or not inbox:
flash(f"AS2 profile {as2_url} missing id or inbox")
- return redirect(f'/user/{domain}/following')
+ return redirect(g.user.user_page_path('following'))
timestamp = NOW.replace(microsecond=0, tzinfo=None).isoformat()
- follow_id = common.host_url(f'/user/{domain}/following#{timestamp}-{addr}')
+ follow_id = common.host_url(g.user.user_page_path(f'following#{timestamp}-{addr}'))
follow_as2 = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Follow',
@@ -189,7 +189,7 @@ class FollowCallback(indieauth.Callback):
link = common.pretty_link(util.get_url(followee) or id, text=addr)
flash(f'Followed {link}.')
- return redirect(f'/user/{domain}/following')
+ return redirect(g.user.user_page_path('following'))
class UnfollowStart(indieauth.Start):
@@ -209,7 +209,7 @@ class UnfollowStart(indieauth.Start):
except Exception as e:
if util.is_connection_failure(e) or util.interpret_http_exception(e)[0]:
flash(f"Couldn't fetch your web site: {e}")
- return redirect(f'/user/{domain}/following')
+ return redirect(g.user.user_page_path('following'))
raise
@@ -246,10 +246,10 @@ class UnfollowCallback(indieauth.Callback):
inbox = followee.get('inbox')
if not inbox:
flash(f"AS2 profile {followee_id} missing inbox")
- return redirect(f'/user/{domain}/following')
+ return redirect(g.user.user_page_path('following'))
timestamp = NOW.replace(microsecond=0, tzinfo=None).isoformat()
- unfollow_id = common.host_url(f'/user/{domain}/following#undo-{timestamp}-{followee_id}')
+ unfollow_id = common.host_url(g.user.user_page_path(f'following#undo-{timestamp}-{followee_id}'))
unfollow_as2 = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Undo',
@@ -268,7 +268,7 @@ class UnfollowCallback(indieauth.Callback):
link = common.pretty_link(util.get_url(followee) or followee_id)
flash(f'Unfollowed {link}.')
- return redirect(f'/user/{domain}/following')
+ return redirect(g.user.user_page_path('following'))
app.add_url_rule('/follow/start',
diff --git a/models.py b/models.py
index 9a87395..f3ba02c 100644
--- a/models.py
+++ b/models.py
@@ -230,6 +230,13 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
and not parsed.path and not parsed.query
and not parsed.params and not parsed.fragment)
+ def user_page_path(self, rest=None):
+ """Returns the user's Bridgy Fed user page path."""
+ path = f'/{self.LABEL}/{self.key.id()}'
+ if rest:
+ path += f'/{rest}'
+ return path
+
def user_page_link(self):
"""Returns a pretty user page link with the user's name and profile picture."""
domain = self.key.id()
@@ -239,7 +246,7 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
util.domain_from_link(self.username()))
img = util.get_url(actor, 'icon') or ''
- return f' {name}'
+ return f' {name}'
class Target(ndb.Model):
diff --git a/pages.py b/pages.py
index 9b3b033..88dccbd 100644
--- a/pages.py
+++ b/pages.py
@@ -17,7 +17,7 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
from flask_app import app, cache
import common
from common import DOMAIN_RE
-from models import fetch_page, Follower, Object, PAGE_SIZE, User
+from models import fetch_page, Follower, Object, PAGE_SIZE, PROTOCOLS, User
from web import Web
FOLLOWERS_UI_LIMIT = 999
@@ -70,16 +70,24 @@ def check_web_site():
raise
g.user.put()
- return redirect(f'/user/{g.user.key.id()}')
+ return redirect(f'/web/{g.user.key.id()}')
@app.get(f'/user/')
-def user(domain):
+@app.get(f'/user//feed')
+@app.get(f'/user//')
+def web_user_redirects(**kwargs):
+ path = request.url.removeprefix(request.root_url).removeprefix('user/')
+ return redirect(f'/web/{path}', code=301)
+
+
+@app.get(f'//')
+def user(protocol, domain):
g.user = Web.get_by_id(domain)
if not g.user or not g.user.direct:
return USER_NOT_FOUND_HTML, 404
elif g.user.key.id() != domain:
- return redirect(f'/user/{g.user.key.id()}', code=301)
+ return redirect(f'/{protocol}/{g.user.key.id()}', code=301)
assert not g.user.use_instead
@@ -108,7 +116,7 @@ def user(domain):
)
-@app.get(f'/user//')
+@app.get(f'/web//')
def followers_or_following(domain, collection):
g.user = Web.get_by_id(domain) # g.user is used in template
if not g.user:
@@ -133,7 +141,7 @@ def followers_or_following(domain, collection):
)
-@app.get(f'/user//feed')
+@app.get(f'/web//feed')
def feed(domain):
format = request.args.get('format', 'html')
if format not in ('html', 'atom', 'rss'):
diff --git a/tests/test_follow.py b/tests/test_follow.py
index 1bf5955..c1b0c1a 100644
--- a/tests/test_follow.py
+++ b/tests/test_follow.py
@@ -41,17 +41,17 @@ FOLLOWEE = {
FOLLOW_ADDRESS = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Follow',
- 'id': f'http://localhost/user/alice.com/following#2022-01-02T03:04:05-@foo@bar',
+ 'id': f'http://localhost/web/alice.com/following#2022-01-02T03:04:05-@foo@bar',
'actor': 'http://localhost/alice.com',
'object': FOLLOWEE,
'to': [as2.PUBLIC_AUDIENCE],
}
FOLLOW_URL = copy.deepcopy(FOLLOW_ADDRESS)
-FOLLOW_URL['id'] = f'http://localhost/user/alice.com/following#2022-01-02T03:04:05-https://bar/actor'
+FOLLOW_URL['id'] = f'http://localhost/web/alice.com/following#2022-01-02T03:04:05-https://bar/actor'
UNDO_FOLLOW = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Undo',
- 'id': f'http://localhost/user/alice.com/following#undo-2022-01-02T03:04:05-https://bar/id',
+ 'id': f'http://localhost/web/alice.com/following#undo-2022-01-02T03:04:05-https://bar/id',
'actor': 'http://localhost/alice.com',
'object': FOLLOW_ADDRESS,
}
@@ -105,21 +105,21 @@ class RemoteFollowTest(testutil.TestCase):
got = self.client.post('/remote-follow?address=https://bar/foo&domain=me')
self.assertEqual(302, got.status_code)
- self.assertEqual('/user/me', got.headers['Location'])
+ self.assertEqual('/web/me', got.headers['Location'])
def test_follow_no_webfinger_subscribe_link(self, mock_get):
mock_get.return_value = requests_response(status_code=500)
got = self.client.post('/remote-follow?address=https://bar/foo&domain=me')
self.assertEqual(302, got.status_code)
- self.assertEqual('/user/me', got.headers['Location'])
+ self.assertEqual('/web/me', got.headers['Location'])
def test_follow_no_webfinger_subscribe_link(self, mock_get):
mock_get.return_value = requests_response('not json')
got = self.client.post('/remote-follow?address=https://bar/foo&domain=me')
self.assertEqual(302, got.status_code)
- self.assertEqual('/user/me', got.headers['Location'])
+ self.assertEqual('/web/me', got.headers['Location'])
@patch('requests.post')
@@ -182,7 +182,7 @@ class FollowTest(testutil.TestCase):
def check(self, input, resp, expected_follow, mock_get, mock_post):
self.assertEqual(302, resp.status_code)
- self.assertEqual('/user/alice.com/following',resp.headers['Location'])
+ self.assertEqual('/web/alice.com/following',resp.headers['Location'])
self.assertEqual([f'Followed {input}.'],
get_flashed_messages())
@@ -205,7 +205,7 @@ class FollowTest(testutil.TestCase):
followers,
ignore=['created', 'updated'])
- id = f'http://localhost/user/alice.com/following#2022-01-02T03:04:05-{input}'
+ id = f'http://localhost/web/alice.com/following#2022-01-02T03:04:05-{input}'
self.assert_object(id, domains=['alice.com'], status='complete',
labels=['user', 'activity'], source_protocol='ui',
as2=expected_follow, as1=as2.to_as1(expected_follow))
@@ -237,9 +237,9 @@ class FollowTest(testutil.TestCase):
state = util.encode_oauth_state(self.state)
resp = self.client.get(f'/follow/callback?code=my_code&state={state}')
self.assertEqual(302, resp.status_code)
- self.assertEqual('/user/www.alice.com/following', resp.headers['Location'])
+ self.assertEqual('/web/www.alice.com/following', resp.headers['Location'])
- id = 'http://localhost/user/www.alice.com/following#2022-01-02T03:04:05-https://bar/actor'
+ id = 'http://localhost/web/www.alice.com/following#2022-01-02T03:04:05-https://bar/actor'
expected_follow = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Follow',
@@ -360,7 +360,7 @@ class UnfollowTest(testutil.TestCase):
def check(self, resp, expected_undo, mock_get, mock_post):
self.assertEqual(302, resp.status_code)
- self.assertEqual('/user/alice.com/following', resp.headers['Location'])
+ self.assertEqual('/web/alice.com/following', resp.headers['Location'])
self.assertEqual([f'Unfollowed bar/url.'],
get_flashed_messages())
@@ -377,7 +377,7 @@ class UnfollowTest(testutil.TestCase):
self.assertEqual('inactive', follower.status)
self.assert_object(
- 'http://localhost/user/alice.com/following#undo-2022-01-02T03:04:05-https://bar/id',
+ 'http://localhost/web/alice.com/following#undo-2022-01-02T03:04:05-https://bar/id',
domains=['alice.com'], status='complete',
source_protocol='ui', labels=['user', 'activity'],
as2=expected_undo,
@@ -411,9 +411,9 @@ class UnfollowTest(testutil.TestCase):
})
resp = self.client.get(f'/unfollow/callback?code=my_code&state={state}')
self.assertEqual(302, resp.status_code)
- self.assertEqual('/user/www.alice.com/following', resp.headers['Location'])
+ self.assertEqual('/web/www.alice.com/following', resp.headers['Location'])
- id = 'http://localhost/user/www.alice.com/following#undo-2022-01-02T03:04:05-https://bar/id'
+ id = 'http://localhost/web/www.alice.com/following#undo-2022-01-02T03:04:05-https://bar/id'
expected_undo = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Undo',
diff --git a/tests/test_models.py b/tests/test_models.py
index 0fdab56..3eca549 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -75,6 +75,16 @@ class UserTest(TestCase):
self.assertTrue(pem.decode().startswith('-----BEGIN RSA PRIVATE KEY-----\n'), pem)
self.assertTrue(pem.decode().endswith('-----END RSA PRIVATE KEY-----'), pem)
+ def test_user_page_path(self):
+ self.assertEqual('/web/y.z', g.user.user_page_path())
+ self.assertEqual('/web/y.z/followers', g.user.user_page_path('followers'))
+ self.assertEqual('/fake/foo', self.make_user('foo', cls=Fake).user_page_path())
+
+ def test_user_page_link(self):
+ self.assertEqual(' y.z', g.user.user_page_link())
+ g.user.actor_as2 = ACTOR
+ self.assertEqual(' Mrs. ☕ Foo', g.user.user_page_link())
+
def test_address(self):
self.assertEqual('@y.z@y.z', g.user.address())
@@ -156,7 +166,7 @@ class ObjectTest(TestCase):
g.user = Fake(id='user.com', actor_as2={"name": "Alice"})
obj = Object(id='x', source_protocol='ui', domains=['user.com'])
self.assertIn(
- 'href="/user/user.com"> Alice',
+ 'href="/fake/user.com"> Alice',
obj.actor_link())
def test_put_updates_load_cache(self):
diff --git a/tests/test_pages.py b/tests/test_pages.py
index c58d611..fd156b5 100644
--- a/tests/test_pages.py
+++ b/tests/test_pages.py
@@ -36,39 +36,44 @@ class PagesTest(TestCase):
self.user = self.make_user('user.com')
def test_user(self):
- got = self.client.get('/user/user.com')
+ got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_objects(self):
self.add_objects()
- got = self.client.get('/user/user.com')
+ got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_not_found(self):
- got = self.client.get('/user/bar.com')
+ got = self.client.get('/web/bar.com')
self.assert_equals(404, got.status_code)
def test_user_not_direct(self):
self.user.direct = False
self.user.put()
- got = self.client.get('/user/user.com')
+ got = self.client.get('/web/user.com')
self.assert_equals(404, got.status_code)
+ def test_user_redirect(self):
+ got = self.client.get('/user/user.com')
+ self.assert_equals(301, got.status_code)
+ self.assert_equals('/web/user.com', got.headers['Location'])
+
def test_user_use_instead(self):
bar = self.make_user('bar.com')
bar.use_instead = self.user.key
bar.put()
- got = self.client.get('/user/bar.com')
+ got = self.client.get('/web/bar.com')
self.assert_equals(301, got.status_code)
- self.assert_equals('/user/user.com', got.headers['Location'])
+ self.assert_equals('/web/user.com', got.headers['Location'])
def test_user_object_bare_string_id(self):
with self.request_context:
Object(id='a', domains=['user.com'], labels=['notification'],
as2=REPOST_AS2).put()
- got = self.client.get('/user/user.com')
+ got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_object_url_object(self):
@@ -81,27 +86,27 @@ class PagesTest(TestCase):
},
}).put()
- got = self.client.get('/user/user.com')
+ got = self.client.get('/web/user.com')
self.assert_equals(200, got.status_code)
def test_user_before(self):
self.add_objects()
- got = self.client.get(f'/user/user.com?before={util.now().isoformat()}')
+ got = self.client.get(f'/web/user.com?before={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_user_after(self):
self.add_objects()
- got = self.client.get(f'/user/user.com?after={util.now().isoformat()}')
+ got = self.client.get(f'/web/user.com?after={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_user_before_bad(self):
self.add_objects()
- got = self.client.get('/user/user.com?before=nope')
+ got = self.client.get('/web/user.com?before=nope')
self.assert_equals(400, got.status_code)
def test_user_before_and_after(self):
self.add_objects()
- got = self.client.get('/user/user.com?before=2024-01-01+01:01:01&after=2023-01-01+01:01:01')
+ got = self.client.get('/web/user.com?before=2024-01-01+01:01:01&after=2023-01-01+01:01:01')
self.assert_equals(400, got.status_code)
@patch('requests.get')
@@ -115,12 +120,12 @@ class PagesTest(TestCase):
got = self.client.post('/web-site', data={'url': 'https://user.com/'})
self.assert_equals(302, got.status_code)
- self.assert_equals('/user/user.com', got.headers['Location'])
+ self.assert_equals('/web/user.com', got.headers['Location'])
user = Web.get_by_id('user.com')
self.assertTrue(user.has_hcard)
self.assertEqual('Person', user.actor_as2['type'])
- self.assertEqual('http://localhost/user.com', user.actor_as2['id'])
+ self.assertEqual('http://localhost/web/user.com', user.actor_as2['id'])
def test_check_web_site_bad_url(self):
got = self.client.post('/web-site', data={'url': '!!!'})
@@ -146,7 +151,7 @@ class PagesTest(TestCase):
Follower.get_or_create('bar.com', 'https://no/stored/follow')
Follower.get_or_create('bar.com', 'https://masto/user',
last_follow=FOLLOW_WITH_ACTOR)
- got = self.client.get('/user/bar.com/followers')
+ got = self.client.get('/web/bar.com/followers')
self.assert_equals(200, got.status_code)
body = got.get_data(as_text=True)
@@ -155,20 +160,25 @@ class PagesTest(TestCase):
def test_followers_empty(self):
self.make_user('bar.com')
- got = self.client.get('/user/bar.com/followers')
+ got = self.client.get('/web/bar.com/followers')
self.assert_equals(200, got.status_code)
self.assertNotIn('class="follower', got.get_data(as_text=True))
def test_followers_user_not_found(self):
- got = self.client.get('/user/bar.com/followers')
+ got = self.client.get('/web/bar.com/followers')
self.assert_equals(404, got.status_code)
+ def test_followers_redirect(self):
+ got = self.client.get('/user/user.com/followers')
+ self.assert_equals(301, got.status_code)
+ self.assert_equals('/web/user.com/followers', got.headers['Location'])
+
def test_following(self):
Follower.get_or_create('https://no/stored/follow', 'bar.com')
Follower.get_or_create('https://masto/user', 'bar.com',
last_follow=FOLLOW_WITH_OBJECT)
self.make_user('bar.com')
- got = self.client.get('/user/bar.com/following')
+ got = self.client.get('/web/bar.com/following')
self.assert_equals(200, got.status_code)
body = got.get_data(as_text=True)
@@ -177,59 +187,69 @@ class PagesTest(TestCase):
def test_following_empty(self):
self.make_user('bar.com')
- got = self.client.get('/user/bar.com/following')
+ got = self.client.get('/web/bar.com/following')
self.assert_equals(200, got.status_code)
self.assertNotIn('class="follower', got.get_data(as_text=True))
def test_following_user_not_found(self):
- got = self.client.get('/user/bar.com/following')
+ got = self.client.get('/web/bar.com/following')
self.assert_equals(404, got.status_code)
+ def test_following_redirect(self):
+ got = self.client.get('/user/user.com/following')
+ self.assert_equals(301, got.status_code)
+ self.assert_equals('/web/user.com/following', got.headers['Location'])
+
def test_following_before_empty(self):
self.make_user('bar.com')
- got = self.client.get(f'/user/bar.com/following?before={util.now().isoformat()}')
+ got = self.client.get(f'/web/bar.com/following?before={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_following_after_empty(self):
self.make_user('bar.com')
- got = self.client.get(f'/user/bar.com/following?after={util.now().isoformat()}')
+ got = self.client.get(f'/web/bar.com/following?after={util.now().isoformat()}')
self.assert_equals(200, got.status_code)
def test_feed_user_not_found(self):
- got = self.client.get('/user/bar.com/feed')
+ got = self.client.get('/web/bar.com/feed')
self.assert_equals(404, got.status_code)
- def test_feed_html_empty(self):
+ def test_feed_redirect(self):
got = self.client.get('/user/user.com/feed')
+ self.assert_equals(301, got.status_code)
+ self.assert_equals('/web/user.com/feed', got.headers['Location'])
+
+ def test_feed_html_empty(self):
+ got = self.client.get('/web/user.com/feed')
self.assert_equals(200, got.status_code)
self.assert_equals([], microformats2.html_to_activities(got.text))
def test_feed_html(self):
self.add_objects()
- got = self.client.get('/user/user.com/feed')
+ got = self.client.get('/web/user.com/feed')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED,
contents(microformats2.html_to_activities(got.text)))
def test_feed_atom_empty(self):
- got = self.client.get('/user/user.com/feed?format=atom')
+ got = self.client.get('/web/user.com/feed?format=atom')
self.assert_equals(200, got.status_code)
self.assert_equals([], atom.atom_to_activities(got.text))
def test_feed_atom(self):
self.add_objects()
- got = self.client.get('/user/user.com/feed?format=atom')
+ got = self.client.get('/web/user.com/feed?format=atom')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED, contents(atom.atom_to_activities(got.text)))
def test_feed_rss_empty(self):
- got = self.client.get('/user/user.com/feed?format=rss')
+ got = self.client.get('/web/user.com/feed?format=rss')
self.assert_equals(200, got.status_code)
self.assert_equals([], rss.to_activities(got.text))
def test_feed_rss(self):
self.add_objects()
- got = self.client.get('/user/user.com/feed?format=rss')
+ got = self.client.get('/web/user.com/feed?format=rss')
self.assert_equals(200, got.status_code)
self.assert_equals(self.EXPECTED, contents(rss.to_activities(got.text)))