switch actor_id() and address90 back to User methods, to be implemented by subclasses

partially reverts 9e906f18e4
circle-datastore-transactions
Ryan Barrett 2023-05-31 10:10:14 -07:00
rodzic 9e906f18e4
commit 35060c172a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
10 zmienionych plików z 80 dodań i 105 usunięć

Wyświetl plik

@ -27,7 +27,7 @@ from common import (
TLD_BLOCKLIST, TLD_BLOCKLIST,
) )
from models import Follower, Object, PROTOCOLS, Target, User from models import Follower, Object, PROTOCOLS, Target, User
import protocol from protocol import Protocol
import web import web
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -47,7 +47,7 @@ def default_signature_user():
return _DEFAULT_SIGNATURE_USER return _DEFAULT_SIGNATURE_USER
class ActivityPub(User, protocol.Protocol): class ActivityPub(User, Protocol):
"""ActivityPub protocol class.""" """ActivityPub protocol class."""
LABEL = 'activitypub' LABEL = 'activitypub'
@ -58,7 +58,7 @@ class ActivityPub(User, protocol.Protocol):
target = getattr(obj, 'target_as2', None) target = getattr(obj, 'target_as2', None)
activity = obj.as2 or postprocess_as2(as2.from_as1(obj.as1), target=target) activity = obj.as2 or postprocess_as2(as2.from_as1(obj.as1), target=target)
activity['actor'] = actor_id(g.user) activity['actor'] = g.user.ap_actor()
return signed_post(url, log_data=True, data=activity) return signed_post(url, log_data=True, data=activity)
# TODO: return bool or otherwise unify return value with others # TODO: return bool or otherwise unify return value with others
@ -214,45 +214,6 @@ class ActivityPub(User, protocol.Protocol):
error('HTTP Signature verification failed', status=401) error('HTTP Signature verification failed', status=401)
def address(user):
"""Returns a user's ActivityPub address, eg '@me@foo.com'.
Args:
user: :class:`User`
Returns:
str
"""
if user.direct:
return f'@{user.username()}@{user.key.id()}'
else:
return f'@{user.key.id()}@{request.host}'
def actor_id(user, rest=None):
"""Returns a user's AS2 actor id.
Example: 'https://fed.brid.gy/ap/bluesky/foo.com'
Args:
user: :class:`User`
rest: str, optional, added as path suffix
Returns:
str
"""
if user.direct or rest:
# special case Web users to skip /ap/web/ prefix, for backward compatibility
url = common.host_url(user.key.id() if user.LABEL == 'web'
else f'/ap{user.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(user.homepage)
def signed_get(url, **kwargs): def signed_get(url, **kwargs):
return signed_request(util.requests_get, url, **kwargs) return signed_request(util.requests_get, url, **kwargs)
@ -306,7 +267,7 @@ def signed_request(fn, url, data=None, log_data=True, headers=None, **kwargs):
# implementations require, eg Peertube. # implementations require, eg Peertube.
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.3 # https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.3
# https://github.com/snarfed/bridgy-fed/issues/40 # https://github.com/snarfed/bridgy-fed/issues/40
auth = HTTPSignatureAuth(secret=user.private_pem(), key_id=actor_id(user), auth = HTTPSignatureAuth(secret=user.private_pem(), key_id=user.ap_actor(),
algorithm='rsa-sha256', sign_header='signature', algorithm='rsa-sha256', sign_header='signature',
headers=HTTP_SIG_HEADERS) headers=HTTP_SIG_HEADERS)
@ -410,13 +371,13 @@ def postprocess_as2(activity, target=None, wrap=True):
elif not id: elif not id:
obj['id'] = util.get_first(obj, 'url') or target_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_homepage(id):
obj['id'] = actor_id(g.user) obj['id'] = g.user.ap_actor()
elif g.external_user: elif g.external_user:
obj['id'] = redirect_wrap(g.external_user) obj['id'] = redirect_wrap(g.external_user)
# for Accepts # for Accepts
if g.user and g.user.is_homepage(obj.get('object')): if g.user and g.user.is_homepage(obj.get('object')):
obj['object'] = actor_id(g.user) obj['object'] = g.user.ap_actor()
elif g.external_user and g.external_user == obj.get('object'): elif g.external_user and g.external_user == obj.get('object'):
obj['object'] = redirect_wrap(g.external_user) obj['object'] = redirect_wrap(g.external_user)
@ -506,7 +467,7 @@ def postprocess_as2_actor(actor, wrap=True):
return actor return actor
elif isinstance(actor, str): elif isinstance(actor, str):
if g.user and g.user.is_homepage(actor): if g.user and g.user.is_homepage(actor):
return actor_id(g.user) return g.user.ap_actor()
return redirect_wrap(actor) return redirect_wrap(actor)
url = g.user.homepage if g.user else None url = g.user.homepage if g.user else None
@ -520,7 +481,7 @@ def postprocess_as2_actor(actor, wrap=True):
id = actor.get('id') 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_homepage(id)):
actor['id'] = actor_id(g.user) actor['id'] = g.user.ap_actor()
elif g.external_user and (not id or id == g.external_user): elif g.external_user and (not id or id == g.external_user):
actor['id'] = redirect_wrap(g.external_user) actor['id'] = redirect_wrap(g.external_user)
@ -562,7 +523,7 @@ def actor(protocol, domain):
# TODO: unify with common.actor() # TODO: unify with common.actor()
actor = postprocess_as2(g.user.actor_as2 or {}) actor = postprocess_as2(g.user.actor_as2 or {})
actor.update({ actor.update({
'id': actor_id(g.user), 'id': g.user.ap_actor(),
# This has to be the domain for Mastodon etc interop! It seems like it # 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, # should be the custom username from the acct: u-url in their h-card,
# but that breaks Mastodon's Webfinger discovery. Background: # but that breaks Mastodon's Webfinger discovery. Background:
@ -571,10 +532,10 @@ def actor(protocol, domain):
# https://github.com/snarfed/bridgy-fed/issues/302#issuecomment-1324305460 # https://github.com/snarfed/bridgy-fed/issues/302#issuecomment-1324305460
# https://github.com/snarfed/bridgy-fed/issues/77 # https://github.com/snarfed/bridgy-fed/issues/77
'preferredUsername': domain, 'preferredUsername': domain,
'inbox': actor_id(g.user, 'inbox'), 'inbox': g.user.ap_actor('inbox'),
'outbox': actor_id(g.user, 'outbox'), 'outbox': g.user.ap_actor('outbox'),
'following': actor_id(g.user, 'following'), 'following': g.user.ap_actor('following'),
'followers': actor_id(g.user, 'followers'), 'followers': g.user.ap_actor('followers'),
'endpoints': { 'endpoints': {
'sharedInbox': host_url('/ap/sharedInbox'), 'sharedInbox': host_url('/ap/sharedInbox'),
}, },

Wyświetl plik

@ -16,7 +16,7 @@ from oauth_dropins.webutil import util
from oauth_dropins.webutil.testutil import NOW from oauth_dropins.webutil.testutil import NOW
from oauth_dropins.webutil.util import json_dumps, json_loads from oauth_dropins.webutil.util import json_dumps, json_loads
from activitypub import ActivityPub, actor_id, address from activitypub import ActivityPub
from flask_app import app from flask_app import app
import common import common
from models import Follower, Object, PROTOCOLS from models import Follower, Object, PROTOCOLS
@ -98,7 +98,7 @@ def remote_follow():
if link.get('rel') == SUBSCRIBE_LINK_REL: if link.get('rel') == SUBSCRIBE_LINK_REL:
template = link.get('template') template = link.get('template')
if template and '{uri}' in template: if template and '{uri}' in template:
return redirect(template.replace('{uri}', address(g.user))) return redirect(template.replace('{uri}', g.user.ap_address()))
flash(f"Couldn't find remote follow link for {addr}") flash(f"Couldn't find remote follow link for {addr}")
return redirect(g.user.user_page_path()) return redirect(g.user.user_page_path())
@ -179,7 +179,7 @@ class FollowCallback(indieauth.Callback):
'type': 'Follow', 'type': 'Follow',
'id': follow_id, 'id': follow_id,
'object': followee, 'object': followee,
'actor': actor_id(g.user), 'actor': g.user.ap_actor(),
'to': [as2.PUBLIC_AUDIENCE], 'to': [as2.PUBLIC_AUDIENCE],
} }
obj = Object(id=follow_id, domains=[domain], labels=['user'], obj = Object(id=follow_id, domains=[domain], labels=['user'],
@ -257,7 +257,7 @@ class UnfollowCallback(indieauth.Callback):
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Undo', 'type': 'Undo',
'id': unfollow_id, 'id': unfollow_id,
'actor': actor_id(g.user), 'actor': g.user.ap_actor(),
'object': follower.last_follow, 'object': follower.last_follow,
} }

Wyświetl plik

@ -199,6 +199,29 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
logger.info(f'Defaulting username to domain {domain}') logger.info(f'Defaulting username to domain {domain}')
return domain return domain
def ap_address(self):
"""Returns this user's ActivityPub address, eg '@me@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 AS2 actor id.
Example: 'https://fed.brid.gy/ap/bluesky/foo.com'
"""
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): def is_homepage(self, url):
"""Returns True if the given URL points to this user's home page.""" """Returns True if the given URL points to this user's home page."""
if not url: if not url:

Wyświetl plik

@ -14,7 +14,6 @@ from oauth_dropins.webutil import flask_util, logs, util
from oauth_dropins.webutil.flask_util import error, flash, redirect from oauth_dropins.webutil.flask_util import error, flash, redirect
from oauth_dropins.webutil.util import json_dumps, json_loads from oauth_dropins.webutil.util import json_dumps, json_loads
import activitypub
import common import common
from common import DOMAIN_RE from common import DOMAIN_RE
from flask_app import app, cache from flask_app import app, cache
@ -85,7 +84,6 @@ def user(protocol, domain):
util=util, util=util,
address=request.args.get('address'), address=request.args.get('address'),
g=g, g=g,
activitypub=activitypub,
**locals(), **locals(),
) )
@ -111,7 +109,6 @@ def followers_or_following(protocol, domain, collection):
util=util, util=util,
address=request.args.get('address'), address=request.args.get('address'),
g=g, g=g,
activitypub=activitypub,
**locals() **locals()
) )
@ -143,8 +140,7 @@ def feed(protocol, domain):
# syntax. maybe a fediverse kwarg down through the call chain? # syntax. maybe a fediverse kwarg down through the call chain?
if format == 'html': if format == 'html':
entries = [microformats2.object_to_html(a) for a in activities] entries = [microformats2.object_to_html(a) for a in activities]
return render_template('feed.html', util=util, g=g, return render_template('feed.html', util=util, g=g, **locals())
activitypub=activitypub, **locals())
elif format == 'atom': elif format == 'atom':
body = atom.activities_to_atom(activities, actor=actor, title=title, body = atom.activities_to_atom(activities, actor=actor, title=title,
request_url=request.url) request_url=request.url)

Wyświetl plik

@ -278,10 +278,7 @@ class Protocol:
follower_obj.put() follower_obj.put()
# send AP Accept # send AP Accept
# TODO: switch back to activitypub.actor_id() once this is moved into followee_actor_url = g.user.ap_actor()
# activitypub.py
followee_actor_url = common.host_url(g.user.key.id() if g.user.LABEL == 'web'
else f'/ap{g.user.user_page_path()}')
accept = { accept = {
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
'id': common.host_url(f'/user/{g.user.key.id()}/followers#accept-{obj.key.id()}'), 'id': common.host_url(f'/user/{g.user.key.id()}/followers#accept-{obj.key.id()}'),

Wyświetl plik

@ -5,7 +5,7 @@
<span title="Fediverse address"> <span title="Fediverse address">
<nobr> <nobr>
<img class="logo" src="/static/fediverse_logo.svg"> <img class="logo" src="/static/fediverse_logo.svg">
{{ activitypub.address(g.user) }} {{ g.user.ap_address() }}
</nobr> </nobr>
</span> </span>
&middot; &middot;
@ -19,6 +19,5 @@
class="btn btn-default glyphicon glyphicon-refresh"></button> class="btn btn-default glyphicon glyphicon-refresh"></button>
</form> </form>
</nobr> </nobr>
</div> </div>
</div> </div>

Wyświetl plik

@ -24,7 +24,7 @@ from werkzeug.exceptions import BadGateway
from .testutil import Fake, TestCase from .testutil import Fake, TestCase
import activitypub import activitypub
from activitypub import ActivityPub, actor_id, address from activitypub import ActivityPub
import common import common
import models import models
from models import Follower, Object from models import Follower, Object
@ -1510,29 +1510,3 @@ class ActivityPubUtilsTest(TestCase):
activitypub.postprocess_as2(obj), activitypub.postprocess_as2(obj),
activitypub.postprocess_as2(activitypub.postprocess_as2(obj)), activitypub.postprocess_as2(activitypub.postprocess_as2(obj)),
ignore=['to']) ignore=['to'])
def test_address(self):
self.assertEqual('@user.com@user.com', address(g.user))
g.user.actor_as2 = {'type': 'Person'}
self.assertEqual('@user.com@user.com', address(g.user))
g.user.actor_as2 = {'url': 'http://foo'}
self.assertEqual('@user.com@user.com', address(g.user))
g.user.actor_as2 = {'url': ['http://foo', 'acct:bar@foo', 'acct:baz@user.com']}
self.assertEqual('@baz@user.com', address(g.user))
g.user.direct = False
self.assertEqual('@user.com@localhost', address(g.user))
def test_actor_id(self):
self.assertEqual('http://localhost/ap/fake/foo',
actor_id(self.make_user('foo', cls=Fake)))
self.assertEqual('http://localhost/user.com', actor_id(g.user))
g.user.direct = False
self.assertEqual('http://localhost/r/https://user.com/', actor_id(g.user))
self.assertEqual('http://localhost/user.com/inbox', actor_id(g.user, 'inbox'))

Wyświetl plik

@ -86,6 +86,32 @@ class UserTest(TestCase):
g.user.actor_as2 = ACTOR 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()) 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.ap_address())
g.user.actor_as2 = {'type': 'Person'}
self.assertEqual('@y.z@y.z', g.user.ap_address())
g.user.actor_as2 = {'url': 'http://foo'}
self.assertEqual('@y.z@y.z', g.user.ap_address())
g.user.actor_as2 = {'url': ['http://foo', 'acct:bar@foo', 'acct:baz@y.z']}
self.assertEqual('@baz@y.z', g.user.ap_address())
g.user.direct = False
self.assertEqual('@y.z@localhost', g.user.ap_address())
def test_ap_actor(self):
self.assertEqual('http://localhost/ap/fake/foo',
self.make_user('foo', cls=Fake).ap_actor())
self.assertEqual('http://localhost/y.z', g.user.ap_actor())
g.user.direct = False
self.assertEqual('http://localhost/r/https://y.z/', g.user.ap_actor())
self.assertEqual('http://localhost/y.z/inbox', g.user.ap_actor('inbox'))
class ObjectTest(TestCase): class ObjectTest(TestCase):
def setUp(self): def setUp(self):

14
web.py
Wyświetl plik

@ -344,7 +344,7 @@ def webmention_task():
'id': id, 'id': id,
'objectType': 'activity', 'objectType': 'activity',
'verb': 'delete', 'verb': 'delete',
'actor': activitypub.actor_id(g.user), 'actor': g.user.ap_actor(),
'object': source, 'object': source,
}) })
@ -353,8 +353,8 @@ def webmention_task():
props = obj.mf2['properties'] props = obj.mf2['properties']
author_urls = microformats2.get_string_urls(props.get('author', [])) 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_homepage(author_urls[0]):
logger.info(f'Overriding author {author_urls[0]} with {activitypub.actor_id(g.user)}') logger.info(f'Overriding author {author_urls[0]} with {g.user.ap_actor()}')
props['author'] = [activitypub.actor_id(g.user)] props['author'] = [g.user.ap_actor()]
logger.info(f'Converted to AS1: {obj.type}: {json_dumps(obj.as1, indent=2)}') logger.info(f'Converted to AS1: {obj.type}: {json_dumps(obj.as1, indent=2)}')
@ -363,7 +363,7 @@ def webmention_task():
obj.put() obj.put()
actor_as1 = { actor_as1 = {
**obj.as1, **obj.as1,
'id': activitypub.actor_id(g.user), 'id': g.user.ap_actor(),
'updated': util.now().isoformat(), 'updated': util.now().isoformat(),
} }
id = common.host_url(f'{obj.key.id()}#update-{util.now().isoformat()}') id = common.host_url(f'{obj.key.id()}#update-{util.now().isoformat()}')
@ -371,7 +371,7 @@ def webmention_task():
'objectType': 'activity', 'objectType': 'activity',
'verb': 'update', 'verb': 'update',
'id': id, 'id': id,
'actor': activitypub.actor_id(g.user), 'actor': g.user.ap_actor(),
'object': actor_as1, 'object': actor_as1,
}) })
@ -403,7 +403,7 @@ def webmention_task():
'objectType': 'activity', 'objectType': 'activity',
'verb': 'update', 'verb': 'update',
'id': id, 'id': id,
'actor': activitypub.actor_id(g.user), 'actor': g.user.ap_actor(),
'object': { 'object': {
# Mastodon requires the updated field for Updates, so # Mastodon requires the updated field for Updates, so
# add a default value. # add a default value.
@ -426,7 +426,7 @@ def webmention_task():
'objectType': 'activity', 'objectType': 'activity',
'verb': 'post', 'verb': 'post',
'id': id, 'id': id,
'actor': activitypub.actor_id(g.user), 'actor': g.user.ap_actor(),
'object': obj.as1, 'object': obj.as1,
} }
obj = Object(id=id, mf2=obj.mf2, our_as1=create_as1, obj = Object(id=id, mf2=obj.mf2, our_as1=create_as1,

Wyświetl plik

@ -14,7 +14,6 @@ from oauth_dropins.webutil import flask_util, util
from oauth_dropins.webutil.flask_util import error from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_dumps, json_loads from oauth_dropins.webutil.util import json_dumps, json_loads
from activitypub import actor_id, address
import common import common
from flask_app import app, cache from flask_app import app, cache
from models import User from models import User
@ -56,7 +55,7 @@ class Actor(flask_util.XrdOrJrd):
actor = g.user.to_as1() or {} actor = g.user.to_as1() or {}
homepage = g.user.homepage homepage = g.user.homepage
handle = address(g.user) handle = g.user.ap_address()
logger.info(f'Generating WebFinger data for {domain}') logger.info(f'Generating WebFinger data for {domain}')
logger.info(f'AS1 actor: {actor}') logger.info(f'AS1 actor: {actor}')
@ -95,13 +94,13 @@ class Actor(flask_util.XrdOrJrd):
# WARNING: in python 2 sometimes request.host_url lost port, # WARNING: in python 2 sometimes request.host_url lost port,
# http://localhost:8080 would become just http://localhost. no # http://localhost:8080 would become just http://localhost. no
# clue how or why. pay attention here if that happens again. # clue how or why. pay attention here if that happens again.
'href': actor_id(g.user), 'href': g.user.ap_actor(),
}, { }, {
# AP reads this and sharedInbox from the AS2 actor, not # AP reads this and sharedInbox from the AS2 actor, not
# webfinger, so strictly speaking, it's probably not needed here. # webfinger, so strictly speaking, it's probably not needed here.
'rel': 'inbox', 'rel': 'inbox',
'type': as2.CONTENT_TYPE, 'type': as2.CONTENT_TYPE,
'href': actor_id(g.user, 'inbox'), 'href': g.user.ap_actor('inbox'),
}, { }, {
# https://www.w3.org/TR/activitypub/#sharedInbox # https://www.w3.org/TR/activitypub/#sharedInbox
'rel': 'sharedInbox', 'rel': 'sharedInbox',