show fediverse address on user page UI, including custom username

for #281
pull/299/head
Ryan Barrett 2022-11-20 09:38:46 -08:00
rodzic 754d694823
commit 65f3ef3cc7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
8 zmienionych plików z 92 dodań i 39 usunięć

Wyświetl plik

@ -16,6 +16,7 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
import requests
from werkzeug.exceptions import BadGateway
import common
from models import Activity, User
logger = logging.getLogger(__name__)
@ -476,9 +477,11 @@ def actor(domain, user=None):
actor = postprocess_as2(
as2.from_as1(microformats2.json_to_object(hcard)), user=user)
urls = util.dedupe_urls(microformats2.get_string_urls([hcard]))
username = common.get_username(domain, urls)
actor.update({
'id': f'{request.host_url}{domain}',
'preferredUsername': domain,
'preferredUsername': username,
'inbox': f'{request.host_url}{domain}/inbox',
'outbox': f'{request.host_url}{domain}/outbox',
'following': f'{request.host_url}{domain}/following',
@ -490,3 +493,28 @@ def actor(domain, user=None):
logger.info(f'Generated AS2 actor: {json_dumps(actor, indent=2)}')
return actor
def get_username(domain, urls):
"""Returns a user's preferred username from an acct: url, if available.
If there's no acct: URL, returns domain.
Args:
domain: str
urls: sequence of str
Returns: str
"""
assert domain
assert urls
for url in urls:
if url.startswith('acct:'):
urluser, urldomain = util.parse_acct_uri(url)
if urldomain == domain:
logger.info(f'Found custom username: urluser')
return urluser
logger.info(f'Defaulting username to domain {domain}')
return domain

Wyświetl plik

@ -10,6 +10,7 @@ from django_salmon import magicsigs
from flask import request
from google.cloud import ndb
from oauth_dropins.webutil.models import StringIdModel
from oauth_dropins.webutil.util import json_dumps, json_loads
import common
@ -34,6 +35,7 @@ class User(StringIdModel):
private_exponent = ndb.StringProperty(required=True)
has_redirects = ndb.BooleanProperty()
has_hcard = ndb.BooleanProperty()
actor_as2 = ndb.TextProperty()
@classmethod
def _get_kind(cls):
@ -72,6 +74,16 @@ class User(StringIdModel):
magicsigs.base64_to_long(str(self.private_exponent))))
return rsa.exportKey(format='PEM')
def address(self):
"""Returns this user's ActivityPub address, eg '@me@foo.com'."""
domain = self.key.id()
username = None
if self.actor_as2 is not None:
username = json_loads(self.actor_as2).get('preferredUsername')
return f'@{username or domain}@{domain}'
def verify(self):
"""Fetches site a couple ways to check for redirects and h-card."""
domain = self.key.id()
@ -91,9 +103,10 @@ class User(StringIdModel):
# check home page
try:
common.actor(self.key.id(), user=self)
self.actor_as2 = json_dumps(common.actor(self.key.id(), user=self))
self.has_hcard = True
except (BadRequest, NotFound):
self.actor_as2 = None
self.has_hcard = False

Wyświetl plik

@ -320,7 +320,7 @@ button[disabled]:hover {
border-radius: 1em;
}
.btn-home img {
.btn-home img, .logo {
height: 1em;
}

Wyświetl plik

@ -69,11 +69,7 @@ We're aware of the sites below, and we've made progress on some, but they're not
<li class="answer">
<p>
Federated social network identities take the form <code>@username@example.com</code>, like an email address with a leading <code>@</code>. Your site's identity via Bridgy Fed will be <code>@yourdomain.com@yourdomain.com</code>. Once you've <a href="#setup">set up Atom on your site</a>, people can follow you at that address.
</p>
<p>
Most fedsocnets also publish Atom themselves, so you can add profile URLs like <a href="https://mastodon.technology/@snarfed">mastodon.technology/@snarfed</a> to your <a href="https://indieweb.org/reader">reader</a> and see their posts there too.
Federated social network identities take the form <code>@username@example.com</code>, like an email address with a leading <code>@</code>. Your site's identity via Bridgy Fed will be <code>@yourdomain.com@yourdomain.com</code>.
</p>
<p>
@ -159,10 +155,6 @@ https://en.support.wordpress.com/site-redirect/
</ul>
<p>
If you want people on OStatus sites like Hubzilla to see your posts, your web site will also need to support <a href="https://indieweb.org/WebSub">WebSub</a> (née PubSubHubbub). Specifically, <a href="https://blog.superfeedr.com/howto-pubsubhubbub/#discovery">your Atom feed needs to advertise it</a>. <a href="https://github.com/tootsuite/mastodon/issues/1441#issuecomment-302969948">Example details for Mastodon.</a> If you're on a CMS, it may already have a plugin! <a href="https://wordpress.org/">WordPress</a> has <a href="https://indieweb.org/WebSub#WordPress_Plugins_for_PuSH">a couple</a>, and <a href="http://withknown.com/">Known</a> has it <a href="https://indieweb.org/WebSub#1000s_of_Known_Sites">built in</a>. Or you can use <a href="https://superfeedr.com/publisher">Superfeedr</a> or <a href="https://switchboard.p3k.io/">Switchboard</a>.
</p>
</ol>
</li>

Wyświetl plik

@ -11,7 +11,7 @@
<div class="col-lg-6 col-md-12">
<a class="btn btn-default btn-home" href="https://brid.gy/mastodon/start">
<p class="bigger">Cross-post to a Mastodon account:<br>
@user@mastodon.server</p>
@you@mastodon.server</p>
<pre class="bigger"> <img title="Pleroma" style="height: 1.5em; margin-bottom: -.2em" src="/static/pleroma_logo.svg">
/
<span title="Your web site">🌐</span><span class="highlight"><img title="Mastodon" src="/oauth_dropins_static/mastodon_2x.png"></span><img title="Friendica" src="/static/friendica_logo.svg">
@ -25,7 +25,7 @@
<div class="col-lg-6 col-md-12">
<a class="btn btn-default btn-home" href="/web-site">
<p class="bigger">Connect directly to the fediverse:<br>
@website.com</p>
@yoursite.com</p>
<pre class="bigger"><img title="Fediverse" src="/static/fediverse_logo.svg"> <img title="PeerTube" src="/static/peertube_logo.svg">
\ /
<img title="Misskey" src="/static/misskey_logo.png"><span class="highlight" title="Your web site">🌐</span><img title="Mastodon" src="/oauth_dropins_static/mastodon_2x.png">

Wyświetl plik

@ -21,14 +21,16 @@
</div>
{% endif %}
<br>
<div class="row">
<h2 style="display: inline">
<a href="https://{{ domain }}/">🌐 {{ domain }}</a>
<span title="Fediverse address">
<img class="logo" src="/static/fediverse_logo.svg"> {{ user.address() }}</span> |
<a title="Web site" href="https://{{ domain }}/">🌐 {{ domain }}</a>
</h2>
</div>
<div class="row">
<h3 style="display: inline">
| <a href="/user/{{ domain }}/followers">{{ followers }} follower{% if followers != '1' %}s{% endif %}</a>
<a href="/user/{{ domain }}/followers">{{ followers }} follower{% if followers != '1' %}s{% endif %}</a>
| <a href="/user/{{ domain }}/following">following {{ following }}</a>
| <a href="/user/{{ domain }}/feed">HTML</a>,
<a href="/user/{{ domain }}/feed?format=atom">Atom</a>,

Wyświetl plik

@ -3,6 +3,7 @@
from unittest import mock
from oauth_dropins.webutil.testutil import requests_response
from oauth_dropins.webutil.util import json_dumps, json_loads
from app import app
from models import User, Activity
@ -15,7 +16,7 @@ class UserTest(testutil.TestCase):
super(UserTest, self).setUp()
self.user = User.get_or_create('y.z')
def test_magic_key_get_or_create(self):
def test_get_or_create(self):
assert self.user.mod
assert self.user.public_exponent
assert self.user.private_exponent
@ -39,52 +40,75 @@ class UserTest(testutil.TestCase):
self.assertTrue(pem.decode().startswith('-----BEGIN RSA PRIVATE KEY-----\n'), pem)
self.assertTrue(pem.decode().endswith('-----END RSA PRIVATE KEY-----'), pem)
def test_address(self):
self.assertEqual('@y.z@y.z', self.user.address())
self.user.actor_as2 = '{"type": "Person"}'
self.assertEqual('@y.z@y.z', self.user.address())
self.user.actor_as2 = '{"preferredUsername": "foo"}'
self.assertEqual('@foo@y.z', self.user.address())
@mock.patch('requests.get')
def test_verify(self, mock_get):
self.assertFalse(self.user.has_redirects)
self.assertFalse(self.user.has_hcard)
def check(redirects, hcard):
def check(redirects, hcard, actor):
with app.test_request_context('/'):
self.user.verify()
with self.subTest(redirects=redirects, hcard=hcard):
self.assertEqual(redirects, bool(self.user.has_redirects))
self.assertEqual(hcard, bool(self.user.has_hcard))
with self.subTest(redirects=redirects, hcard=hcard, actor=actor):
self.assert_equals(redirects, bool(self.user.has_redirects))
self.assert_equals(hcard, bool(self.user.has_hcard))
if actor is None:
self.assertIsNone(self.user.actor_as2)
else:
got = {k: v for k, v in json_loads(self.user.actor_as2).items()
if k in actor}
self.assert_equals(actor, got)
# both fail
empty = requests_response('')
mock_get.side_effect = [empty, empty]
check(False, False)
check(False, False, None)
# redirect works but strips query params, no h-card
half_redir = requests_response(
status=302, redirected_url='http://localhost/.well-known/webfinger')
no_hcard = requests_response('<html><body></body></html>')
mock_get.side_effect = [half_redir, no_hcard]
check(False, False)
check(False, False, None)
# redirect works, non-representative h-card
full_redir = requests_response(
status=302, allow_redirects=False,
redirected_url='http://localhost/.well-known/webfinger?resource=acct:y.z@y.z')
bad_hcard = requests_response(
'<html><body><a class="h-card u-url" href="https://a.b/">me</a></body></html>',
'<html><body><a class="h-card u-url" href="https://a.b/">acct:me@y.z</a></body></html>',
url='https://y.z/',
)
mock_get.side_effect = [full_redir, bad_hcard]
check(True, False)
check(True, False, None)
# both work
hcard = requests_response(
'<html><body><a class="h-card u-url" href="/">me</a></body></html>',
hcard = requests_response("""
<html><body class="h-card">
<a class="u-url p-name" href="/">me</a>
<a class="u-url" href="acct:myself@y.z">Masto</a>
</body></html>""",
url='https://y.z/',
)
mock_get.side_effect = [full_redir, hcard]
check(True, True)
check(True, True, {
'type': 'Person',
'name': 'me',
'url': 'http://localhost/r/https://y.z/',
'preferredUsername': 'myself',
})
# reset
mock_get.side_effect = [empty, empty]
check(False, False)
check(False, False, None)
class ActivityTest(testutil.TestCase):

Wyświetl plik

@ -64,14 +64,8 @@ class Actor(flask_util.XrdOrJrd):
urls = util.dedupe_urls(props.get('url', []) + [resp.url])
canonical_url = urls[0]
acct = f'{domain}@{domain}'
for url in urls:
if url.startswith('acct:'):
urluser, urldomain = util.parse_acct_uri(url)
if urldomain == domain:
acct = f'{urluser}@{domain}'
logger.info(f'Found custom username: acct:{acct}')
break
username = common.get_username(domain, urls)
acct = f'{username}@{domain}'
# discover atom feed, if any
atom = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM)