kopia lustrzana https://github.com/snarfed/bridgy-fed
serve AS2 /[domain] actors from datastore instead of refetching h-card
for #392. not pretty, but gets the job done. more code cleanup needed eventually.pull/424/head
rodzic
d64e5e875e
commit
c12bb6db6d
|
@ -16,7 +16,7 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
|
|||
|
||||
from app import app, cache
|
||||
import common
|
||||
from common import CACHE_TIME, redirect_unwrap, redirect_wrap, TLD_BLOCKLIST
|
||||
from common import CACHE_TIME, host_url, redirect_unwrap, redirect_wrap, TLD_BLOCKLIST
|
||||
from models import Follower, Object, Target, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -46,9 +46,9 @@ seen_ids_lock = threading.Lock()
|
|||
|
||||
|
||||
@app.get(f'/<regex("{common.DOMAIN_RE}"):domain>')
|
||||
@flask_util.cached(cache, CACHE_TIME, http_5xx=True)
|
||||
@flask_util.cached(cache, CACHE_TIME)
|
||||
def actor(domain):
|
||||
"""Fetches a domain's h-card and converts to AS2 actor."""
|
||||
"""Serves a user's AS2 actor from the datastore."""
|
||||
tld = domain.split('.')[-1]
|
||||
if tld in TLD_BLOCKLIST:
|
||||
error('', status=404)
|
||||
|
@ -56,12 +56,33 @@ def actor(domain):
|
|||
user = User.get_by_id(domain)
|
||||
if not user:
|
||||
return f'User {domain} not found', 404
|
||||
elif not user.actor_as2:
|
||||
return f'User {domain} not fully set up', 404
|
||||
|
||||
_, _, actor = common.actor(user)
|
||||
return (actor, {
|
||||
# TODO: unify with common.actor()
|
||||
actor = {
|
||||
**common.postprocess_as2(json_loads(user.actor_as2), user=user),
|
||||
'id': host_url(domain),
|
||||
# This has to be the domain for Mastodon etc interop! It seems like it
|
||||
# should be the custom username from the acct: u-url in their h-card,
|
||||
# but that breaks Mastodon's Webfinger discovery. Background:
|
||||
# https://github.com/snarfed/bridgy-fed/issues/302#issuecomment-1324305460
|
||||
# https://github.com/snarfed/bridgy-fed/issues/77
|
||||
'preferredUsername': domain,
|
||||
'inbox': host_url(f'{domain}/inbox'),
|
||||
'outbox': host_url(f'{domain}/outbox'),
|
||||
'following': host_url(f'{domain}/following'),
|
||||
'followers': host_url(f'{domain}/followers'),
|
||||
'endpoints': {
|
||||
'sharedInbox': host_url('inbox'),
|
||||
},
|
||||
}
|
||||
|
||||
logger.info(f'Returning: {json_dumps(actor, indent=2)}')
|
||||
return actor, {
|
||||
'Content-Type': as2.CONTENT_TYPE,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@app.post('/inbox')
|
||||
|
|
|
@ -596,10 +596,6 @@ def redirect_unwrap(val):
|
|||
def actor(user):
|
||||
"""Fetches a home page, converts its representative h-card to AS2 actor.
|
||||
|
||||
Creates a User for the given domain if one doesn't already exist.
|
||||
|
||||
TODO: unify with webfinger.Actor
|
||||
|
||||
Args:
|
||||
user: :class:`User`
|
||||
|
||||
|
@ -620,6 +616,7 @@ def actor(user):
|
|||
|
||||
actor_as1 = microformats2.json_to_object(hcard, rel_urls=mf2.get('rel-urls'))
|
||||
actor_as2 = postprocess_as2(as2.from_as1(actor_as1), user=user)
|
||||
# TODO: unify with activitypub.actor()
|
||||
actor_as2.update({
|
||||
'id': host_url(domain),
|
||||
# This has to be the domain for Mastodon etc interop! It seems like it
|
||||
|
|
|
@ -21,6 +21,8 @@ ACTOR = {
|
|||
'id': 'https://mastodon.social/users/swentel',
|
||||
'type': 'Person',
|
||||
'inbox': 'http://follower/inbox',
|
||||
'name': 'Mrs. ☕ Foo',
|
||||
'icon': {'type': 'Image', 'url': 'https://foo.com/me.jpg'},
|
||||
}
|
||||
REPLY_OBJECT = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
|
@ -190,18 +192,12 @@ class ActivityPubTest(testutil.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.get_or_create('foo.com')
|
||||
self.user = User.get_or_create('foo.com', has_hcard=True,
|
||||
actor_as2=json_dumps(ACTOR))
|
||||
activitypub.seen_ids.clear()
|
||||
|
||||
def test_actor(self, _, mock_get, __):
|
||||
mock_get.return_value = requests_response("""
|
||||
<body>
|
||||
<a class="h-card u-url" rel="me" href="/about-me">Mrs. ☕ Foo</a>
|
||||
</body>
|
||||
""", url='https://foo.com/', content_type=common.CONTENT_TYPE_HTML)
|
||||
|
||||
def test_actor(self, *_):
|
||||
got = self.client.get('/foo.com')
|
||||
self.assert_req(mock_get, 'https://foo.com/')
|
||||
self.assertEqual(200, got.status_code)
|
||||
type = got.headers['Content-Type']
|
||||
self.assertTrue(type.startswith(as2.CONTENT_TYPE), type)
|
||||
|
@ -215,12 +211,8 @@ class ActivityPubTest(testutil.TestCase):
|
|||
'summary': '',
|
||||
'preferredUsername': 'foo.com',
|
||||
'id': 'http://localhost/foo.com',
|
||||
'url': 'http://localhost/r/https://foo.com/about-me',
|
||||
'attachment': [{
|
||||
'type': 'PropertyValue',
|
||||
'name': 'Mrs. ☕ Foo',
|
||||
'value': '<a rel=\"me\" href="https://foo.com/about-me">foo.com/about-me</a>',
|
||||
}],
|
||||
'url': 'http://localhost/r/https://foo.com/',
|
||||
'icon': {'type': 'Image', 'url': 'https://foo.com/me.jpg'},
|
||||
'inbox': 'http://localhost/foo.com/inbox',
|
||||
'outbox': 'http://localhost/foo.com/outbox',
|
||||
'following': 'http://localhost/foo.com/following',
|
||||
|
@ -235,79 +227,10 @@ class ActivityPubTest(testutil.TestCase):
|
|||
},
|
||||
}, got.json)
|
||||
|
||||
def test_actor_rel_me_links(self, _, mock_get, __):
|
||||
mock_get.return_value = requests_response("""
|
||||
<body>
|
||||
<div class="h-card">
|
||||
<a class="u-url" rel="me" href="/about-me">Mrs. ☕ Foo</a>
|
||||
<a class="u-url" rel="me" href="/">should be ignored</a>
|
||||
<a class="u-url" rel="me" href="http://one" title="one title">
|
||||
one text
|
||||
</a>
|
||||
<a class="u-url" rel="me" href="https://two" title=" two title "> </a>
|
||||
</div>
|
||||
</body>
|
||||
""", url='https://foo.com/', content_type=common.CONTENT_TYPE_HTML)
|
||||
|
||||
got = self.client.get('/foo.com')
|
||||
self.assertEqual(200, got.status_code)
|
||||
self.assertEqual([{
|
||||
'type': 'PropertyValue',
|
||||
'name': 'Mrs. ☕ Foo',
|
||||
'value': '<a rel="me" href="https://foo.com/about-me">foo.com/about-me</a>',
|
||||
}, {
|
||||
'type': 'PropertyValue',
|
||||
'name': 'Web site',
|
||||
'value': '<a rel="me" href="https://foo.com/">foo.com</a>',
|
||||
}, {
|
||||
'type': 'PropertyValue',
|
||||
'name': 'one text',
|
||||
'value': '<a rel="me" href="http://one">one</a>',
|
||||
}, {
|
||||
'type': 'PropertyValue',
|
||||
'name': 'two title',
|
||||
'value': '<a rel="me" href="https://two">two</a>',
|
||||
}], got.json['attachment'])
|
||||
|
||||
def test_actor_no_hcard(self, _, mock_get, __):
|
||||
mock_get.return_value = requests_response("""
|
||||
<body>
|
||||
<div class="h-entry">
|
||||
<p class="e-content">foo bar</p>
|
||||
</div>
|
||||
</body>
|
||||
""")
|
||||
|
||||
got = self.client.get('/foo.com')
|
||||
self.assert_req(mock_get, 'https://foo.com/')
|
||||
self.assertEqual(400, got.status_code)
|
||||
self.assertIn('representative h-card', got.get_data(as_text=True))
|
||||
|
||||
def test_actor_override_preferredUsername(self, _, mock_get, __):
|
||||
mock_get.return_value = requests_response("""
|
||||
<body>
|
||||
<a class="h-card u-url" rel="me" href="/about-me">
|
||||
<span class="p-nickname">Nick</span>
|
||||
</a>
|
||||
</body>
|
||||
""", url='https://foo.com/', content_type=common.CONTENT_TYPE_HTML)
|
||||
|
||||
got = self.client.get('/foo.com')
|
||||
self.assertEqual(200, got.status_code)
|
||||
self.assertEqual('foo.com', got.json['preferredUsername'])
|
||||
|
||||
def test_actor_blocked_tld(self, _, __, ___):
|
||||
got = self.client.get('/foo.json')
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
||||
def test_actor_bad_domain(self, _, mock_get, ___):
|
||||
# https://console.cloud.google.com/errors/detail/CKGv-b6impW3Jg;time=P30D?project=bridgy-federated
|
||||
mock_get.side_effect = [
|
||||
ValueError('Invalid IPv6 URL'),
|
||||
]
|
||||
got = self.client.get('/foo.com')
|
||||
self.assertEqual(400, got.status_code)
|
||||
|
||||
def test_actor_no_user(self, *mocks):
|
||||
got = self.client.get('/nope.com')
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"""Unit tests for models.py."""
|
||||
from unittest import mock
|
||||
|
||||
from flask import get_flashed_messages
|
||||
from granary import as2
|
||||
from oauth_dropins.webutil.testutil import requests_response
|
||||
from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
|
@ -18,6 +19,10 @@ class UserTest(testutil.TestCase):
|
|||
super(UserTest, self).setUp()
|
||||
self.user = User.get_or_create('y.z')
|
||||
|
||||
self.full_redir = requests_response(
|
||||
status=302,
|
||||
redirected_url='http://localhost/.well-known/webfinger?resource=acct:y.z@y.z')
|
||||
|
||||
def test_get_or_create(self):
|
||||
assert self.user.mod
|
||||
assert self.user.public_exponent
|
||||
|
@ -116,23 +121,31 @@ Current vs expected:<pre>- http://localhost/.well-known/webfinger
|
|||
http://this/404s
|
||||
returned HTTP 404</pre>""")
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_verify_no_hcard(self, mock_get):
|
||||
mock_get.side_effect = [
|
||||
self.full_redir,
|
||||
requests_response("""
|
||||
<body>
|
||||
<div class="h-entry">
|
||||
<p class="e-content">foo bar</p>
|
||||
</div>
|
||||
</body>
|
||||
"""),
|
||||
]
|
||||
self._test_verify(True, False, None)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_verify_non_representative_hcard(self, mock_get):
|
||||
full_redir = requests_response(
|
||||
status=302,
|
||||
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/">acct:me@y.z</a></body></html>',
|
||||
url='https://y.z/',
|
||||
)
|
||||
mock_get.side_effect = [full_redir, bad_hcard]
|
||||
mock_get.side_effect = [self.full_redir, bad_hcard]
|
||||
self._test_verify(True, False, None)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_verify_both_work(self, mock_get):
|
||||
full_redir = requests_response(
|
||||
status=302,
|
||||
redirected_url='http://localhost/.well-known/webfinger?resource=acct:y.z@y.z')
|
||||
hcard = requests_response("""
|
||||
<html><body class="h-card">
|
||||
<a class="u-url p-name" href="/">me</a>
|
||||
|
@ -140,7 +153,7 @@ http://this/404s
|
|||
</body></html>""",
|
||||
url='https://y.z/',
|
||||
)
|
||||
mock_get.side_effect = [full_redir, hcard]
|
||||
mock_get.side_effect = [self.full_redir, hcard]
|
||||
self._test_verify(True, True, {
|
||||
'type': 'Person',
|
||||
'name': 'me',
|
||||
|
@ -166,6 +179,61 @@ http://this/404s
|
|||
self.assertEqual(root_user.key, www_user.key.get().use_instead)
|
||||
self.assertEqual(root_user.key, User.get_or_create('www.y.z').key)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_verify_actor_rel_me_links(self, mock_get):
|
||||
mock_get.side_effect = [
|
||||
self.full_redir,
|
||||
requests_response("""
|
||||
<body>
|
||||
<div class="h-card">
|
||||
<a class="u-url" rel="me" href="/about-me">Mrs. ☕ Foo</a>
|
||||
<a class="u-url" rel="me" href="/">should be ignored</a>
|
||||
<a class="u-url" rel="me" href="http://one" title="one title">
|
||||
one text
|
||||
</a>
|
||||
<a class="u-url" rel="me" href="https://two" title=" two title "> </a>
|
||||
</div>
|
||||
</body>
|
||||
""", url='https://y.z/'),
|
||||
]
|
||||
self._test_verify(True, True, {
|
||||
'attachment': [{
|
||||
'type': 'PropertyValue',
|
||||
'name': 'Mrs. ☕ Foo',
|
||||
'value': '<a rel="me" href="https://y.z/about-me">y.z/about-me</a>',
|
||||
}, {
|
||||
'type': 'PropertyValue',
|
||||
'name': 'Web site',
|
||||
'value': '<a rel="me" href="https://y.z/">y.z</a>',
|
||||
}, {
|
||||
'type': 'PropertyValue',
|
||||
'name': 'one text',
|
||||
'value': '<a rel="me" href="http://one">one</a>',
|
||||
}, {
|
||||
'type': 'PropertyValue',
|
||||
'name': 'two title',
|
||||
'value': '<a rel="me" href="https://two">two</a>',
|
||||
}]})
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_verify_override_preferredUsername(self, mock_get):
|
||||
mock_get.side_effect = [
|
||||
self.full_redir,
|
||||
requests_response("""
|
||||
<body>
|
||||
<a class="h-card u-url" rel="me" href="/about-me">
|
||||
<span class="p-nickname">Nick</span>
|
||||
</a>
|
||||
</body>
|
||||
""", url='https://y.z/'),
|
||||
]
|
||||
self._test_verify(True, True, {
|
||||
# stays y.z despite user's username. since Mastodon queries Webfinger
|
||||
# for preferredUsername@fed.brid.gy
|
||||
# https://github.com/snarfed/bridgy-fed/issues/77#issuecomment-949955109
|
||||
'preferredUsername': 'y.z',
|
||||
})
|
||||
|
||||
def test_homepage(self):
|
||||
self.assertEqual('https://y.z/', self.user.homepage)
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ FOLLOWERS_BSKY = [{
|
|||
'$type': 'app.bsky.graph.getFollowers#follower',
|
||||
'did': 'did:web:mastodon.social:users:swentel',
|
||||
'handle': 'mastodon.social/users/swentel',
|
||||
'displayName': 'Mrs. ☕ Foo',
|
||||
'avatar': 'https://foo.com/me.jpg',
|
||||
'declaration': ACTOR_DECLARATION,
|
||||
'indexedAt': '2022-01-02T03:04:05+00:00',
|
||||
}]
|
||||
|
|
Ładowanie…
Reference in New Issue