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
Ryan Barrett 2023-02-14 08:25:41 -08:00
rodzic d64e5e875e
commit c12bb6db6d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
5 zmienionych plików z 113 dodań i 102 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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