kopia lustrzana https://github.com/snarfed/bridgy-fed
switch actor_id() and address90 back to User methods, to be implemented by subclasses
partially reverts 9e906f18e4
circle-datastore-transactions
rodzic
9e906f18e4
commit
35060c172a
|
@ -27,7 +27,7 @@ from common import (
|
|||
TLD_BLOCKLIST,
|
||||
)
|
||||
from models import Follower, Object, PROTOCOLS, Target, User
|
||||
import protocol
|
||||
from protocol import Protocol
|
||||
import web
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -47,7 +47,7 @@ def default_signature_user():
|
|||
return _DEFAULT_SIGNATURE_USER
|
||||
|
||||
|
||||
class ActivityPub(User, protocol.Protocol):
|
||||
class ActivityPub(User, Protocol):
|
||||
"""ActivityPub protocol class."""
|
||||
LABEL = 'activitypub'
|
||||
|
||||
|
@ -58,7 +58,7 @@ class ActivityPub(User, protocol.Protocol):
|
|||
target = getattr(obj, 'target_as2', None)
|
||||
|
||||
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)
|
||||
# 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)
|
||||
|
||||
|
||||
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):
|
||||
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.
|
||||
# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.3
|
||||
# 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',
|
||||
headers=HTTP_SIG_HEADERS)
|
||||
|
||||
|
@ -410,13 +371,13 @@ def postprocess_as2(activity, target=None, wrap=True):
|
|||
elif not id:
|
||||
obj['id'] = util.get_first(obj, 'url') or target_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:
|
||||
obj['id'] = redirect_wrap(g.external_user)
|
||||
|
||||
# for Accepts
|
||||
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'):
|
||||
obj['object'] = redirect_wrap(g.external_user)
|
||||
|
||||
|
@ -506,7 +467,7 @@ def postprocess_as2_actor(actor, wrap=True):
|
|||
return actor
|
||||
elif isinstance(actor, str):
|
||||
if g.user and g.user.is_homepage(actor):
|
||||
return actor_id(g.user)
|
||||
return g.user.ap_actor()
|
||||
return redirect_wrap(actor)
|
||||
|
||||
url = g.user.homepage if g.user else None
|
||||
|
@ -520,7 +481,7 @@ def postprocess_as2_actor(actor, wrap=True):
|
|||
|
||||
id = actor.get('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):
|
||||
actor['id'] = redirect_wrap(g.external_user)
|
||||
|
||||
|
@ -562,7 +523,7 @@ def actor(protocol, domain):
|
|||
# TODO: unify with common.actor()
|
||||
actor = postprocess_as2(g.user.actor_as2 or {})
|
||||
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
|
||||
# should be the custom username from the acct: u-url in their h-card,
|
||||
# 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/77
|
||||
'preferredUsername': domain,
|
||||
'inbox': actor_id(g.user, 'inbox'),
|
||||
'outbox': actor_id(g.user, 'outbox'),
|
||||
'following': actor_id(g.user, 'following'),
|
||||
'followers': actor_id(g.user, 'followers'),
|
||||
'inbox': g.user.ap_actor('inbox'),
|
||||
'outbox': g.user.ap_actor('outbox'),
|
||||
'following': g.user.ap_actor('following'),
|
||||
'followers': g.user.ap_actor('followers'),
|
||||
'endpoints': {
|
||||
'sharedInbox': host_url('/ap/sharedInbox'),
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ from oauth_dropins.webutil import util
|
|||
from oauth_dropins.webutil.testutil import NOW
|
||||
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
|
||||
import common
|
||||
from models import Follower, Object, PROTOCOLS
|
||||
|
@ -98,7 +98,7 @@ def remote_follow():
|
|||
if link.get('rel') == SUBSCRIBE_LINK_REL:
|
||||
template = link.get('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}")
|
||||
return redirect(g.user.user_page_path())
|
||||
|
@ -179,7 +179,7 @@ class FollowCallback(indieauth.Callback):
|
|||
'type': 'Follow',
|
||||
'id': follow_id,
|
||||
'object': followee,
|
||||
'actor': actor_id(g.user),
|
||||
'actor': g.user.ap_actor(),
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}
|
||||
obj = Object(id=follow_id, domains=[domain], labels=['user'],
|
||||
|
@ -257,7 +257,7 @@ class UnfollowCallback(indieauth.Callback):
|
|||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Undo',
|
||||
'id': unfollow_id,
|
||||
'actor': actor_id(g.user),
|
||||
'actor': g.user.ap_actor(),
|
||||
'object': follower.last_follow,
|
||||
}
|
||||
|
||||
|
|
23
models.py
23
models.py
|
@ -199,6 +199,29 @@ class User(StringIdModel, metaclass=ProtocolUserMeta):
|
|||
logger.info(f'Defaulting username to domain {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):
|
||||
"""Returns True if the given URL points to this user's home page."""
|
||||
if not url:
|
||||
|
|
6
pages.py
6
pages.py
|
@ -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.util import json_dumps, json_loads
|
||||
|
||||
import activitypub
|
||||
import common
|
||||
from common import DOMAIN_RE
|
||||
from flask_app import app, cache
|
||||
|
@ -85,7 +84,6 @@ def user(protocol, domain):
|
|||
util=util,
|
||||
address=request.args.get('address'),
|
||||
g=g,
|
||||
activitypub=activitypub,
|
||||
**locals(),
|
||||
)
|
||||
|
||||
|
@ -111,7 +109,6 @@ def followers_or_following(protocol, domain, collection):
|
|||
util=util,
|
||||
address=request.args.get('address'),
|
||||
g=g,
|
||||
activitypub=activitypub,
|
||||
**locals()
|
||||
)
|
||||
|
||||
|
@ -143,8 +140,7 @@ def feed(protocol, domain):
|
|||
# syntax. maybe a fediverse kwarg down through the call chain?
|
||||
if format == 'html':
|
||||
entries = [microformats2.object_to_html(a) for a in activities]
|
||||
return render_template('feed.html', util=util, g=g,
|
||||
activitypub=activitypub, **locals())
|
||||
return render_template('feed.html', util=util, g=g, **locals())
|
||||
elif format == 'atom':
|
||||
body = atom.activities_to_atom(activities, actor=actor, title=title,
|
||||
request_url=request.url)
|
||||
|
|
|
@ -278,10 +278,7 @@ class Protocol:
|
|||
follower_obj.put()
|
||||
|
||||
# send AP Accept
|
||||
# TODO: switch back to activitypub.actor_id() once this is moved into
|
||||
# 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()}')
|
||||
followee_actor_url = g.user.ap_actor()
|
||||
accept = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': common.host_url(f'/user/{g.user.key.id()}/followers#accept-{obj.key.id()}'),
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<span title="Fediverse address">
|
||||
<nobr>
|
||||
<img class="logo" src="/static/fediverse_logo.svg">
|
||||
{{ activitypub.address(g.user) }}
|
||||
{{ g.user.ap_address() }}
|
||||
</nobr>
|
||||
</span>
|
||||
·
|
||||
|
@ -19,6 +19,5 @@
|
|||
class="btn btn-default glyphicon glyphicon-refresh"></button>
|
||||
</form>
|
||||
</nobr>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,7 @@ from werkzeug.exceptions import BadGateway
|
|||
from .testutil import Fake, TestCase
|
||||
|
||||
import activitypub
|
||||
from activitypub import ActivityPub, actor_id, address
|
||||
from activitypub import ActivityPub
|
||||
import common
|
||||
import models
|
||||
from models import Follower, Object
|
||||
|
@ -1510,29 +1510,3 @@ class ActivityPubUtilsTest(TestCase):
|
|||
activitypub.postprocess_as2(obj),
|
||||
activitypub.postprocess_as2(activitypub.postprocess_as2(obj)),
|
||||
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'))
|
||||
|
|
|
@ -86,6 +86,32 @@ 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_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):
|
||||
def setUp(self):
|
||||
|
|
14
web.py
14
web.py
|
@ -344,7 +344,7 @@ def webmention_task():
|
|||
'id': id,
|
||||
'objectType': 'activity',
|
||||
'verb': 'delete',
|
||||
'actor': activitypub.actor_id(g.user),
|
||||
'actor': g.user.ap_actor(),
|
||||
'object': source,
|
||||
})
|
||||
|
||||
|
@ -353,8 +353,8 @@ def webmention_task():
|
|||
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]):
|
||||
logger.info(f'Overriding author {author_urls[0]} with {activitypub.actor_id(g.user)}')
|
||||
props['author'] = [activitypub.actor_id(g.user)]
|
||||
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)}')
|
||||
|
||||
|
@ -363,7 +363,7 @@ def webmention_task():
|
|||
obj.put()
|
||||
actor_as1 = {
|
||||
**obj.as1,
|
||||
'id': activitypub.actor_id(g.user),
|
||||
'id': g.user.ap_actor(),
|
||||
'updated': util.now().isoformat(),
|
||||
}
|
||||
id = common.host_url(f'{obj.key.id()}#update-{util.now().isoformat()}')
|
||||
|
@ -371,7 +371,7 @@ def webmention_task():
|
|||
'objectType': 'activity',
|
||||
'verb': 'update',
|
||||
'id': id,
|
||||
'actor': activitypub.actor_id(g.user),
|
||||
'actor': g.user.ap_actor(),
|
||||
'object': actor_as1,
|
||||
})
|
||||
|
||||
|
@ -403,7 +403,7 @@ def webmention_task():
|
|||
'objectType': 'activity',
|
||||
'verb': 'update',
|
||||
'id': id,
|
||||
'actor': activitypub.actor_id(g.user),
|
||||
'actor': g.user.ap_actor(),
|
||||
'object': {
|
||||
# Mastodon requires the updated field for Updates, so
|
||||
# add a default value.
|
||||
|
@ -426,7 +426,7 @@ def webmention_task():
|
|||
'objectType': 'activity',
|
||||
'verb': 'post',
|
||||
'id': id,
|
||||
'actor': activitypub.actor_id(g.user),
|
||||
'actor': g.user.ap_actor(),
|
||||
'object': obj.as1,
|
||||
}
|
||||
obj = Object(id=id, mf2=obj.mf2, our_as1=create_as1,
|
||||
|
|
|
@ -14,7 +14,6 @@ from oauth_dropins.webutil import flask_util, util
|
|||
from oauth_dropins.webutil.flask_util import error
|
||||
from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
|
||||
from activitypub import actor_id, address
|
||||
import common
|
||||
from flask_app import app, cache
|
||||
from models import User
|
||||
|
@ -56,7 +55,7 @@ class Actor(flask_util.XrdOrJrd):
|
|||
|
||||
actor = g.user.to_as1() or {}
|
||||
homepage = g.user.homepage
|
||||
handle = address(g.user)
|
||||
handle = g.user.ap_address()
|
||||
|
||||
logger.info(f'Generating WebFinger data for {domain}')
|
||||
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,
|
||||
# http://localhost:8080 would become just http://localhost. no
|
||||
# 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
|
||||
# webfinger, so strictly speaking, it's probably not needed here.
|
||||
'rel': 'inbox',
|
||||
'type': as2.CONTENT_TYPE,
|
||||
'href': actor_id(g.user, 'inbox'),
|
||||
'href': g.user.ap_actor('inbox'),
|
||||
}, {
|
||||
# https://www.w3.org/TR/activitypub/#sharedInbox
|
||||
'rel': 'sharedInbox',
|
||||
|
|
Ładowanie…
Reference in New Issue