AP users: change web UI user page paths from /user/... to /[protocol]/...

for #512
circle-datastore-transactions
Ryan Barrett 2023-05-30 14:08:13 -07:00
rodzic 47b04f5574
commit 8d4228b811
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
6 zmienionych plików z 108 dodań i 63 usunięć

Wyświetl plik

@ -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',

Wyświetl plik

@ -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'<a class="h-card u-author" href="/user/{domain}"><img src="{img}" class="profile"> {name}</a>'
return f'<a class="h-card u-author" href="{self.user_page_path()}"><img src="{img}" class="profile"> {name}</a>'
class Target(ndb.Model):

Wyświetl plik

@ -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/<regex("{DOMAIN_RE}"):domain>')
def user(domain):
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>/feed')
@app.get(f'/user/<regex("{DOMAIN_RE}"):domain>/<any(followers,following):collection>')
def web_user_redirects(**kwargs):
path = request.url.removeprefix(request.root_url).removeprefix('user/')
return redirect(f'/web/{path}', code=301)
@app.get(f'/<any({",".join(PROTOCOLS)}):protocol>/<regex("{DOMAIN_RE}"):domain>')
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/<regex("{DOMAIN_RE}"):domain>/<any(followers,following):collection>')
@app.get(f'/web/<regex("{DOMAIN_RE}"):domain>/<any(followers,following):collection>')
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/<regex("{DOMAIN_RE}"):domain>/feed')
@app.get(f'/web/<regex("{DOMAIN_RE}"):domain>/feed')
def feed(domain):
format = request.args.get('format', 'html')
if format not in ('html', 'atom', 'rss'):

Wyświetl plik

@ -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('<html>not json</html>')
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 <a href="https://bar/url">{input}</a>.'],
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 <a href="https://bar/url">bar/url</a>.'],
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',

Wyświetl plik

@ -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('<a class="h-card u-author" href="/web/y.z"><img src="" class="profile"> y.z</a>', g.user.user_page_link())
g.user.actor_as2 = ACTOR
self.assertEqual('<a class="h-card u-author" href="/web/y.z"><img src="https://user.com/me.jpg" class="profile"> Mrs. ☕ Foo</a>', 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"><img src="" class="profile"> Alice</a>',
'href="/fake/user.com"><img src="" class="profile"> Alice</a>',
obj.actor_link())
def test_put_updates_load_cache(self):

Wyświetl plik

@ -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)))