AP users: rename [is_]homepage => [is_]web_url, move into Protocol subclasses

for #512
circle-datastore-transactions
Ryan Barrett 2023-05-31 18:34:33 -07:00
rodzic 28eabd07a3
commit 958f81ddd1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
10 zmienionych plików z 151 dodań i 103 usunięć

Wyświetl plik

@ -54,6 +54,24 @@ class ActivityPub(User, Protocol):
"""
LABEL = 'activitypub'
def web_url(self):
"""Returns this user's web URL aka web_url, eg 'https://foo.com/'."""
return util.get_url(self.actor_as2) or self.ap_actor()
def ap_address(self):
"""Returns this user's ActivityPub address, eg '@foo.com@foo.com'."""
if self.direct:
return f'@{self.username()}@{self.key.id()}'
else:
return f'@{self.key.id()}@{request.host}'
def ap_actor(self, rest=None):
"""Returns this user's ActivityPub/AS2 actor id URL.
Eg 'https://fed.brid.gy/foo.com'
"""
return self.key.id()
@classmethod
def send(cls, obj, url, log_data=True):
"""Delivers an activity to an inbox URL."""
@ -373,13 +391,13 @@ def postprocess_as2(activity, target=None, wrap=True):
activity['object'] = target_id
elif not id:
obj['id'] = util.get_first(obj, 'url') or target_id
elif g.user and g.user.is_homepage(id):
elif g.user and g.user.is_web_url(id):
obj['id'] = g.user.ap_actor()
elif g.external_user:
obj['id'] = redirect_wrap(g.external_user)
# for Accepts
if g.user and g.user.is_homepage(obj.get('object')):
if g.user and g.user.is_web_url(obj.get('object')):
obj['object'] = g.user.ap_actor()
elif g.external_user and g.external_user == obj.get('object'):
obj['object'] = redirect_wrap(g.external_user)
@ -469,11 +487,11 @@ def postprocess_as2_actor(actor, wrap=True):
if not actor:
return actor
elif isinstance(actor, str):
if g.user and g.user.is_homepage(actor):
if g.user and g.user.is_web_url(actor):
return g.user.ap_actor()
return redirect_wrap(actor)
url = g.user.homepage if g.user else None
url = g.user.web_url() if g.user else None
urls = util.get_list(actor, 'url')
if not urls and url:
urls = [url]
@ -483,7 +501,7 @@ def postprocess_as2_actor(actor, wrap=True):
urls[0] = redirect_wrap(urls[0])
id = actor.get('id')
if g.user and (not id or g.user.is_homepage(id)):
if g.user and (not id or g.user.is_web_url(id)):
actor['id'] = g.user.ap_actor()
elif g.external_user and (not id or id == g.external_user):
actor['id'] = redirect_wrap(g.external_user)

Wyświetl plik

@ -95,7 +95,7 @@ def pretty_link(url, text=None, **kwargs):
text: str
kwargs: passed through to :func:`webutil.util.pretty_link`
"""
if g.user and g.user.is_homepage(url):
if g.user and g.user.is_web_url(url):
return g.user.user_page_link()
if text is None:

Wyświetl plik

@ -103,11 +103,6 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
"""Try to prevent instantiation. Use subclasses instead."""
raise NotImplementedError()
# TODO(#512): move this and is_homepage to web.py?
@property
def homepage(self):
return f'https://{self.key.id()}/'
def _post_put_hook(self, future):
logger.info(f'Wrote {self.key}')
@ -200,16 +195,48 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
logger.info(f'Defaulting username to key id {id}')
return id
def web_url(self):
"""Returns this user's web URL aka web_url, eg 'https://foo.com/'.
To be implemented by subclasses.
Returns:
str
"""
raise NotImplementedError()
def is_web_url(self, url):
"""Returns True if the given URL is this user's web URL (web_url).
Args:
url: str
Returns:
boolean
"""
if not url:
return False
url = url.strip().rstrip('/')
parsed_url = urllib.parse.urlparse(url)
if parsed_url.scheme not in ('http', 'https', ''):
return False
this = self.web_url().rstrip('/')
parsed_this = urllib.parse.urlparse(this)
return (url == this or url == parsed_this.netloc or
parsed_url[1:] == parsed_this[1:]) # ignore http vs https
def ap_address(self):
"""Returns this user's ActivityPub address, eg '@me@foo.com'.
To be implemented by subclasses.
Returns:
str
"""
raise NotImplementedError()
if self.direct:
return f'@{self.username()}@{self.key.id()}'
else:
return f'@{self.key.id()}@{request.host}'
def ap_actor(self, rest=None):
"""Returns this user's ActivityPub/AS2 actor id.
@ -217,33 +244,14 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
Eg 'https://fed.brid.gy/ap/bluesky/foo.com'
To be implemented by subclasses.
Args:
rest: str, optional, appended to URL path
Returns:
str
"""
raise NotImplementedError()
if self.direct or rest:
# special case Web users to skip /ap/web/ prefix, for backward compatibility
url = common.host_url(self.key.id() if self.LABEL == 'web'
else f'/ap{self.user_page_path()}')
if rest:
url += f'/{rest}'
return url
# TODO(#512): drop once we fetch site if web user doesn't already exist
else:
return redirect_wrap(self.homepage)
def is_homepage(self, url):
"""Returns True if the given URL points to this user's home page."""
if not url:
return False
url = url.strip().rstrip('/')
if url == self.key.id():
return True
parsed = urllib.parse.urlparse(url)
return (parsed.netloc == self.key.id()
and parsed.scheme in ('', 'http', 'https')
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."""

Wyświetl plik

@ -131,7 +131,7 @@ def feed(protocol, domain):
actor = {
'displayName': domain,
'url': g.user.homepage,
'url': g.user.web_url(),
}
title = f'Bridgy Fed feed for {domain}'
@ -277,7 +277,7 @@ def nodeinfo():
'name': 'bridgy-fed',
'version': os.getenv('GAE_VERSION'),
'repository': 'https://github.com/snarfed/bridgy-fed',
'homepage': 'https://fed.brid.gy/',
'web_url': 'https://fed.brid.gy/',
},
'protocols': [
'activitypub',

Wyświetl plik

@ -278,11 +278,11 @@ class ActivityPubTest(TestCase):
self.assertTrue(type.startswith(as2.CONTENT_TYPE), type)
self.assertEqual({
'preferredUsername': 'fake.com',
'id': 'http://bf/fake/ap',
'inbox': 'http://bf/fake/ap/inbox',
'outbox': 'http://bf/fake/ap/outbox',
'following': 'http://bf/fake/ap/following',
'followers': 'http://bf/fake/ap/followers',
'id': 'http://bf/fake.com/ap',
'inbox': 'http://bf/fake.com/ap/inbox',
'outbox': 'http://bf/fake.com/ap/outbox',
'following': 'http://bf/fake.com/ap/following',
'followers': 'http://bf/fake.com/ap/followers',
'endpoints': {
'sharedInbox': 'http://localhost/ap/sharedInbox',
},
@ -1259,8 +1259,8 @@ class ActivityPubUtilsTest(TestCase):
def setUp(self):
super().setUp()
self.request_context.push()
g.user = self.make_user('user.com', has_hcard=True,
actor_as2=ACTOR)
g.user = self.make_user('user.com', has_hcard=True, actor_as2=ACTOR)
def tearDown(self):
self.request_context.pop()
super().tearDown()
@ -1302,16 +1302,16 @@ class ActivityPubUtilsTest(TestCase):
'actor': {
'id': 'baj',
'preferredUsername': 'site',
'url': 'http://localhost/r/https://site/',
'url': 'http://localhost/r/https://site',
},
'attributedTo': [{
'id': 'bar',
'preferredUsername': 'site',
'url': 'http://localhost/r/https://site/',
'url': 'http://localhost/r/https://site',
}, {
'id': 'baz',
'preferredUsername': 'site',
'url': 'http://localhost/r/https://site/',
'url': 'http://localhost/r/https://site',
}],
'to': [as2.PUBLIC_AUDIENCE],
}, activitypub.postprocess_as2({
@ -1542,3 +1542,21 @@ class ActivityPubUtilsTest(TestCase):
activitypub.postprocess_as2(obj),
activitypub.postprocess_as2(activitypub.postprocess_as2(obj)),
ignore=['to'])
def test_ap_actor(self):
user = self.make_user('http://foo/actor', cls=ActivityPub)
self.assertEqual('http://foo/actor', user.ap_actor())
def test_ap_address(self):
user = self.make_user('http://foo/actor', cls=ActivityPub)
self.assertEqual('http://foo/actor', user.ap_actor())
def test_web_url(self):
user = self.make_user('http://foo/actor', cls=ActivityPub)
self.assertEqual('http://foo/actor', user.web_url())
user.actor_as2 = copy.deepcopy(ACTOR) # no url
self.assertEqual('http://foo/actor', user.web_url())
user.actor_as2['url'] = ['http://my/url']
self.assertEqual('http://my/url', user.web_url())

Wyświetl plik

@ -86,6 +86,14 @@ class UserTest(TestCase):
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_is_web_url(self):
for url in 'y.z', '//y.z', 'http://y.z', 'https://y.z':
self.assertTrue(g.user.is_web_url(url), url)
for url in (None, '', 'user', 'com', 'com.user', 'ftp://y.z',
'https://user', '://y.z'):
self.assertFalse(g.user.is_web_url(url), url)
class ObjectTest(TestCase):
def setUp(self):

Wyświetl plik

@ -1501,16 +1501,31 @@ http://this/404s
'preferredUsername': 'user.com',
})
def test_homepage(self, _, __):
self.assertEqual('https://user.com/', self.user.homepage)
def test_web_url(self, _, __):
self.assertEqual('https://user.com/', self.user.web_url())
def test_is_homepage(self, _, __):
for url in 'user.com', '//user.com', 'http://user.com', 'https://user.com':
self.assertTrue(self.user.is_homepage(url), url)
def test_ap_address(self, *_):
self.assertEqual('@user.com@user.com', g.user.ap_address())
for url in (None, '', 'user', 'com', 'com.user', 'ftp://user.com',
'https://user', '://user.com'):
self.assertFalse(self.user.is_homepage(url), url)
g.user.actor_as2 = {'type': 'Person'}
self.assertEqual('@user.com@user.com', g.user.ap_address())
g.user.actor_as2 = {'url': 'http://foo'}
self.assertEqual('@user.com@user.com', g.user.ap_address())
g.user.actor_as2 = {'url': ['http://foo', 'acct:bar@foo', 'acct:baz@user.com']}
self.assertEqual('@baz@user.com', g.user.ap_address())
g.user.direct = False
self.assertEqual('@user.com@localhost', g.user.ap_address())
def test_ap_actor(self, *_):
self.assertEqual('http://localhost/user.com', g.user.ap_actor())
g.user.direct = False
self.assertEqual('http://localhost/r/https://user.com/', g.user.ap_actor())
self.assertEqual('http://localhost/user.com/inbox', g.user.ap_actor('inbox'))
def test_check_web_site(self, mock_get, _):
redir = 'http://localhost/.well-known/webfinger?resource=acct:user.com@user.com'
@ -1547,29 +1562,6 @@ http://this/404s
self.assertTrue(get_flashed_messages()[0].startswith(
"Couldn't connect to https://orig/: "))
def test_ap_address(self, *_):
self.assertEqual('@user.com@user.com', g.user.ap_address())
g.user.actor_as2 = {'type': 'Person'}
self.assertEqual('@user.com@user.com', g.user.ap_address())
g.user.actor_as2 = {'url': 'http://foo'}
self.assertEqual('@user.com@user.com', g.user.ap_address())
g.user.actor_as2 = {'url': ['http://foo', 'acct:bar@foo', 'acct:baz@user.com']}
self.assertEqual('@baz@user.com', g.user.ap_address())
g.user.direct = False
self.assertEqual('@user.com@localhost', g.user.ap_address())
def test_ap_actor(self, *_):
self.assertEqual('http://localhost/user.com', g.user.ap_actor())
g.user.direct = False
self.assertEqual('http://localhost/r/https://user.com/', g.user.ap_actor())
self.assertEqual('http://localhost/user.com/inbox', g.user.ap_actor('inbox'))
@patch('requests.post')
@patch('requests.get')

Wyświetl plik

@ -44,11 +44,14 @@ class Fake(User, protocol.Protocol):
# in-order list of ids
fetched = []
def web_url(self):
return f'https://{self.key.id()}'
def ap_address(self):
return '@fake@fake'
return f'@{self.key.id()}@fake'
def ap_actor(self, rest=None):
return 'http://bf/fake/ap' + (f'/{rest}' if rest else '')
return f'http://bf/{self.key.id()}/ap' + (f'/{rest}' if rest else '')
@classmethod
def send(cls, obj, url, log_data=True):

30
web.py
Wyświetl plik

@ -49,6 +49,10 @@ class Web(User, Protocol):
def _get_kind(cls):
return 'MagicKey'
def web_url(self):
"""Returns this user's web URL aka web_url, eg 'https://foo.com/'."""
return f'https://{self.key.id()}/'
def ap_address(self):
"""Returns this user's ActivityPub address, eg '@foo.com@foo.com'.
@ -74,7 +78,7 @@ class Web(User, Protocol):
return url
# TODO(#512): drop once we fetch site if web user doesn't already exist
return common.redirect_wrap(self.homepage)
return common.redirect_wrap(self.web_url())
def verify(self):
"""Fetches site a couple ways to check for redirects and h-card.
@ -94,7 +98,7 @@ class Web(User, Protocol):
root_site = f'https://{root}/'
try:
resp = util.requests_get(root_site, gateway=False)
if resp.ok and self.is_homepage(resp.url):
if resp.ok and self.is_web_url(resp.url):
logger.info(f'{root_site} redirects to {resp.url} ; using {root} instead')
root_user = Web.get_or_create(root)
self.use_instead = root_user.key
@ -108,7 +112,7 @@ class Web(User, Protocol):
self.has_redirects = False
self.redirects_error = None
try:
url = urljoin(self.homepage, path)
url = urljoin(self.web_url(), path)
resp = util.requests_get(url, gateway=False)
domain_urls = ([f'https://{domain}/' for domain in common.DOMAINS] +
[common.host_url()])
@ -129,7 +133,7 @@ class Web(User, Protocol):
# check home page
try:
obj = Web.load(self.homepage, gateway=True)
obj = Web.load(self.web_url(), gateway=True)
self.actor_as2 = activitypub.postprocess_as2(as2.from_as1(obj.as1))
self.has_hcard = True
except (BadRequest, NotFound):
@ -168,12 +172,12 @@ class Web(User, Protocol):
check_backlink: bool, optional, whether to require a link to Bridgy Fed
"""
url = obj.key.id()
is_homepage = ((g.user and g.user.is_homepage(url)) or
is_web_url = ((g.user and g.user.is_web_url(url)) or
(g.external_user and g.external_user == url))
require_backlink = None
if check_backlink or (check_backlink is None and not is_homepage):
if check_backlink or (check_backlink is None and not is_web_url):
require_backlink = common.host_url().rstrip('/')
try:
@ -186,8 +190,8 @@ class Web(User, Protocol):
error(f'id {urlparse(url).fragment} not found in {url}')
# find mf2 item
if is_homepage:
logger.info(f"{url} is user's homepage")
if is_web_url:
logger.info(f"{url} is user's web url")
entry = mf2util.representative_hcard(parsed, parsed['url'])
logger.info(f'Representative h-card: {json_dumps(entry, indent=2)}')
if not entry:
@ -200,7 +204,7 @@ class Web(User, Protocol):
# store final URL in mf2 object, and also default url property to it,
# since that's the fallback for AS1/AS2 id
entry['url'] = parsed['url']
if is_homepage:
if is_web_url:
entry.setdefault('rel-urls', {}).update(parsed.get('rel-urls', {}))
props = entry.setdefault('properties', {})
props.setdefault('url', [parsed['url']])
@ -209,7 +213,7 @@ class Web(User, Protocol):
# run full authorship algorithm if necessary: https://indieweb.org/authorship
# duplicated in microformats2.json_to_object
author = util.get_first(props, 'author')
if not isinstance(author, dict) and not is_homepage:
if not isinstance(author, dict) and not is_web_url:
logger.info(f'Fetching full authorship for author {author}')
author = mf2util.find_author({'items': [entry]}, hentry=entry,
fetch_mf2_func=util.fetch_mf2)
@ -329,7 +333,7 @@ def webmention_interactive():
"""
try:
webmention_external()
flash(f'Updating fediverse profile from <a href="{g.user.homepage}">{g.user.key.id()}</a>...')
flash(f'Updating fediverse profile from <a href="{g.user.web_url()}">{g.user.key.id()}</a>...')
except HTTPException as e:
flash(util.linkify(str(e.description), pretty=True))
@ -379,14 +383,14 @@ def webmention_task():
# set actor to user
props = obj.mf2['properties']
author_urls = microformats2.get_string_urls(props.get('author', []))
if author_urls and not g.user.is_homepage(author_urls[0]):
if author_urls and not g.user.is_web_url(author_urls[0]):
logger.info(f'Overriding author {author_urls[0]} with {g.user.ap_actor()}')
props['author'] = [g.user.ap_actor()]
logger.info(f'Converted to AS1: {obj.type}: {json_dumps(obj.as1, indent=2)}')
# if source is home page, send an actor Update to followers' instances
if g.user.is_homepage(obj.key.id()):
if g.user.is_web_url(obj.key.id()):
obj.put()
actor_as1 = {
**obj.as1,

Wyświetl plik

@ -54,20 +54,17 @@ class Actor(flask_util.XrdOrJrd):
error(f'No user or web site found for {domain}', status=404)
actor = g.user.to_as1() or {}
homepage = g.user.homepage
handle = g.user.ap_address()
logger.info(f'Generating WebFinger data for {domain}')
logger.info(f'AS1 actor: {actor}')
urls = util.dedupe_urls(util.get_list(actor, 'urls') +
util.get_list(actor, 'url') +
[homepage])
[g.user.web_url()])
logger.info(f'URLs: {urls}')
canonical_url = urls[0]
# generate webfinger content
data = util.trim_nulls({
'subject': 'acct:' + handle.lstrip('@'),
'subject': 'acct:' + g.user.ap_address().lstrip('@'),
'aliases': urls,
'links':
[{