kopia lustrzana https://github.com/snarfed/bridgy-fed
noop, rename Domain => User
rodzic
46f039af63
commit
50956c02b0
|
@ -17,7 +17,7 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
|
|||
from app import app, cache
|
||||
import common
|
||||
from common import redirect_unwrap, redirect_wrap
|
||||
from models import Follower, Domain
|
||||
from models import Follower, User
|
||||
from httpsig.requests_auth import HTTPSignatureAuth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -58,8 +58,8 @@ def send(activity, inbox_url, user_domain):
|
|||
# https://tools.ietf.org/html/draft-cavage-http-signatures-07
|
||||
# https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846
|
||||
key_id = request.host_url + user_domain
|
||||
domain = Domain.get_or_create(user_domain)
|
||||
auth = HTTPSignatureAuth(secret=domain.private_pem(), key_id=key_id,
|
||||
user = User.get_or_create(user_domain)
|
||||
auth = HTTPSignatureAuth(secret=user.private_pem(), key_id=key_id,
|
||||
algorithm='rsa-sha256', sign_header='signature',
|
||||
headers=('Date', 'Digest', 'Host'))
|
||||
|
||||
|
@ -94,9 +94,9 @@ def actor(domain):
|
|||
if not hcard:
|
||||
error(f"Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {mf2['url']}")
|
||||
|
||||
entity = Domain.get_or_create(domain)
|
||||
user = User.get_or_create(domain)
|
||||
obj = common.postprocess_as2(
|
||||
as2.from_as1(microformats2.json_to_object(hcard)), domain=entity)
|
||||
as2.from_as1(microformats2.json_to_object(hcard)), user=user)
|
||||
obj.update({
|
||||
'preferredUsername': domain,
|
||||
'inbox': f'{request.host_url}{domain}/inbox',
|
||||
|
|
28
common.py
28
common.py
|
@ -14,7 +14,7 @@ from oauth_dropins.webutil.flask_util import error
|
|||
import requests
|
||||
from werkzeug.exceptions import BadGateway
|
||||
|
||||
from models import Activity, Domain
|
||||
from models import Activity, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -242,22 +242,22 @@ def send_webmentions(activity_wrapped, proxy=None, **activity_props):
|
|||
error(msg, status=int(errors[0][0] or 502))
|
||||
|
||||
|
||||
def postprocess_as2(activity, domain=None, target=None):
|
||||
def postprocess_as2(activity, user=None, target=None):
|
||||
"""Prepare an AS2 object to be served or sent via ActivityPub.
|
||||
|
||||
Args:
|
||||
activity: dict, AS2 object or activity
|
||||
domain: :class:`Domain`, required. populated into actor.id and
|
||||
user: :class:`User`, required. populated into actor.id and
|
||||
publicKey fields if needed.
|
||||
target: dict, AS2 object, optional. The target of activity's inReplyTo or
|
||||
Like/Announce/etc object, if any.
|
||||
"""
|
||||
assert domain
|
||||
assert user
|
||||
type = activity.get('type')
|
||||
|
||||
# actor objects
|
||||
if type == 'Person':
|
||||
postprocess_as2_actor(activity, domain)
|
||||
postprocess_as2_actor(activity, user)
|
||||
if not activity.get('publicKey'):
|
||||
# underspecified, inferred from this issue and Mastodon's implementation:
|
||||
# https://github.com/w3c/activitypub/issues/203#issuecomment-297553229
|
||||
|
@ -267,7 +267,7 @@ def postprocess_as2(activity, domain=None, target=None):
|
|||
'publicKey': {
|
||||
'id': actor_url,
|
||||
'owner': actor_url,
|
||||
'publicKeyPem': domain.public_pem().decode(),
|
||||
'publicKeyPem': user.public_pem().decode(),
|
||||
},
|
||||
'@context': (util.get_list(activity, '@context') +
|
||||
['https://w3id.org/security/v1']),
|
||||
|
@ -276,7 +276,7 @@ def postprocess_as2(activity, domain=None, target=None):
|
|||
|
||||
for actor in (util.get_list(activity, 'attributedTo') +
|
||||
util.get_list(activity, 'actor')):
|
||||
postprocess_as2_actor(actor, domain)
|
||||
postprocess_as2_actor(actor, user)
|
||||
|
||||
# inReplyTo: singly valued, prefer id over url
|
||||
target_id = target.get('id') if target else None
|
||||
|
@ -355,32 +355,32 @@ def postprocess_as2(activity, domain=None, target=None):
|
|||
'@context': as2.CONTEXT,
|
||||
'type': 'Create',
|
||||
'id': f'{activity["id"]}#bridgy-fed-create',
|
||||
'actor': postprocess_as2_actor({}, domain),
|
||||
'actor': postprocess_as2_actor({}, user),
|
||||
'object': activity,
|
||||
}
|
||||
|
||||
return util.trim_nulls(activity)
|
||||
|
||||
|
||||
def postprocess_as2_actor(actor, domain=None):
|
||||
def postprocess_as2_actor(actor, user=None):
|
||||
"""Prepare an AS2 actor object to be served or sent via ActivityPub.
|
||||
|
||||
Modifies actor in place.
|
||||
|
||||
Args:
|
||||
actor: dict, AS2 actor object
|
||||
domain: :class:`Domain`
|
||||
user: :class:`User`
|
||||
|
||||
Returns:
|
||||
actor dict
|
||||
"""
|
||||
url = actor.get('url') or f'https://{domain.key.id()}/'
|
||||
domain_str = urllib.parse.urlparse(url).netloc
|
||||
url = actor.get('url') or f'https://{user.key.id()}/'
|
||||
domain = urllib.parse.urlparse(url).netloc
|
||||
|
||||
actor.setdefault('id', request.host_url + domain_str)
|
||||
actor.setdefault('id', request.host_url + domain)
|
||||
actor.update({
|
||||
'url': redirect_wrap(url),
|
||||
'preferredUsername': domain_str,
|
||||
'preferredUsername': domain,
|
||||
})
|
||||
|
||||
# required by pixelfed. https://github.com/snarfed/bridgy-fed/issues/39
|
||||
|
|
14
models.py
14
models.py
|
@ -11,7 +11,7 @@ from oauth_dropins.webutil.models import StringIdModel
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Domain(StringIdModel):
|
||||
class User(StringIdModel):
|
||||
"""Stores a user's public/private key pair used for Magic Signatures.
|
||||
|
||||
The key name is the domain.
|
||||
|
@ -35,18 +35,18 @@ class Domain(StringIdModel):
|
|||
@staticmethod
|
||||
@ndb.transactional()
|
||||
def get_or_create(domain):
|
||||
"""Loads and returns a Domain. Creates it if necessary."""
|
||||
entity = Domain.get_by_id(domain)
|
||||
"""Loads and returns a User. Creates it if necessary."""
|
||||
user = User.get_by_id(domain)
|
||||
|
||||
if not entity:
|
||||
if not user:
|
||||
# this uses urandom(), and does nontrivial math, so it can take a
|
||||
# while depending on the amount of randomness available.
|
||||
pubexp, mod, privexp = magicsigs.generate()
|
||||
entity = Domain(id=domain, mod=mod, public_exponent=pubexp,
|
||||
user = User(id=domain, mod=mod, public_exponent=pubexp,
|
||||
private_exponent=privexp)
|
||||
entity.put()
|
||||
user.put()
|
||||
|
||||
return entity
|
||||
return user
|
||||
|
||||
def href(self):
|
||||
return 'data:application/magic-public-key,RSA.%s.%s' % (
|
||||
|
|
10
pages.py
10
pages.py
|
@ -13,7 +13,7 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
|
|||
|
||||
from app import app, cache
|
||||
import common
|
||||
from models import Follower, Domain, Activity
|
||||
from models import Follower, User, Activity
|
||||
|
||||
PAGE_SIZE = 20
|
||||
FOLLOWERS_UI_LIMIT = 999
|
||||
|
@ -33,7 +33,7 @@ def user_deprecated(domain):
|
|||
|
||||
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>')
|
||||
def user(domain):
|
||||
if not Domain.get_by_id(domain):
|
||||
if not User.get_by_id(domain):
|
||||
return render_template('user_not_found.html', domain=domain), 404
|
||||
|
||||
query = Activity.query(
|
||||
|
@ -63,7 +63,7 @@ def followers(domain):
|
|||
# TODO:
|
||||
# pull more info from last_follow, eg name, profile picture, url
|
||||
# unify with following
|
||||
if not Domain.get_by_id(domain):
|
||||
if not User.get_by_id(domain):
|
||||
return render_template('user_not_found.html', domain=domain), 404
|
||||
|
||||
query = Follower.query(
|
||||
|
@ -88,7 +88,7 @@ def followers(domain):
|
|||
|
||||
@app.get(f'/user/<regex("{common.DOMAIN_RE}"):domain>/following')
|
||||
def following(domain):
|
||||
if not Domain.get_by_id(domain):
|
||||
if not User.get_by_id(domain):
|
||||
return render_template('user_not_found.html', domain=domain), 404
|
||||
|
||||
query = Follower.query(
|
||||
|
@ -192,7 +192,7 @@ def fetch_page(query, model_class):
|
|||
def stats():
|
||||
return render_template(
|
||||
'stats.html',
|
||||
users=KindStat.query(KindStat.kind_name == 'Domain').get().count,
|
||||
users=KindStat.query(KindStat.kind_name == 'User').get().count,
|
||||
activities=KindStat.query(KindStat.kind_name == 'Activity').get().count,
|
||||
followers=KindStat.query(KindStat.kind_name == 'Follower').get().count,
|
||||
)
|
||||
|
|
12
redirect.py
12
redirect.py
|
@ -23,7 +23,7 @@ from werkzeug.exceptions import abort
|
|||
|
||||
from app import app, cache
|
||||
import common
|
||||
from models import Domain
|
||||
from models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -52,9 +52,9 @@ def redir(to):
|
|||
urllib.parse.urlparse(to).hostname))
|
||||
for domain in domains:
|
||||
if domain:
|
||||
entity = Domain.get_by_id(domain)
|
||||
if entity:
|
||||
logger.info(f'Found Domain for domain {domain}')
|
||||
user = User.get_by_id(domain)
|
||||
if user:
|
||||
logger.info(f'Found User for domain {domain}')
|
||||
break
|
||||
else:
|
||||
logger.info(f'No user found for any of {domains}; returning 404')
|
||||
|
@ -64,7 +64,7 @@ def redir(to):
|
|||
# priorities.
|
||||
if request.headers.get('Accept') in (common.CONTENT_TYPE_AS2,
|
||||
common.CONTENT_TYPE_AS2_LD):
|
||||
return convert_to_as2(to, entity)
|
||||
return convert_to_as2(to, user)
|
||||
|
||||
# redirect
|
||||
logger.info(f'redirecting to {to}')
|
||||
|
@ -79,7 +79,7 @@ def convert_to_as2(url, domain):
|
|||
|
||||
Args:
|
||||
url: str
|
||||
domain: :class:`Domain`
|
||||
domain: :class:`User`
|
||||
"""
|
||||
mf2 = util.fetch_mf2(url)
|
||||
entry = mf2util.find_first_entry(mf2, ['h-entry'])
|
||||
|
|
|
@ -14,7 +14,7 @@ from urllib3.exceptions import ReadTimeoutError
|
|||
|
||||
import activitypub
|
||||
import common
|
||||
from models import Follower, Domain, Activity
|
||||
from models import Follower, User, Activity
|
||||
from . import testutil
|
||||
|
||||
REPLY_OBJECT = {
|
||||
|
@ -180,7 +180,7 @@ class ActivityPubTest(testutil.TestCase):
|
|||
'publicKey': {
|
||||
'id': 'http://localhost/foo.com',
|
||||
'owner': 'http://localhost/foo.com',
|
||||
'publicKeyPem': Domain.get_by_id('foo.com').public_pem().decode(),
|
||||
'publicKeyPem': User.get_by_id('foo.com').public_pem().decode(),
|
||||
},
|
||||
}, got.json)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from werkzeug.exceptions import BadGateway
|
|||
|
||||
from app import app
|
||||
import common
|
||||
from models import Domain
|
||||
from models import User
|
||||
from . import testutil
|
||||
|
||||
HTML = requests_response('<html></html>', headers={
|
||||
|
@ -75,7 +75,7 @@ class CommonTest(testutil.TestCase):
|
|||
}, common.postprocess_as2({
|
||||
'id': 'xyz',
|
||||
'inReplyTo': ['foo', 'bar'],
|
||||
}, domain=Domain(id='foo.com')))
|
||||
}, user=User(id='foo.com')))
|
||||
|
||||
def test_postprocess_as2_actor_attributedTo(self):
|
||||
with app.test_request_context('/'):
|
||||
|
@ -98,7 +98,7 @@ class CommonTest(testutil.TestCase):
|
|||
}, common.postprocess_as2({
|
||||
'attributedTo': [{'id': 'bar'}, {'id': 'baz'}],
|
||||
'actor': {'id': 'baj'},
|
||||
}, domain=Domain(id='foo.com')))
|
||||
}, user=User(id='foo.com')))
|
||||
|
||||
def test_postprocess_as2_note(self):
|
||||
with app.test_request_context('/'):
|
||||
|
@ -119,5 +119,5 @@ class CommonTest(testutil.TestCase):
|
|||
}, common.postprocess_as2({
|
||||
'id': 'xyz',
|
||||
'type': 'Note',
|
||||
}, domain=Domain(id='foo.com')))
|
||||
}, user=User(id='foo.com')))
|
||||
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
# coding=utf-8
|
||||
"""Unit tests for models.py."""
|
||||
from app import app
|
||||
from models import Domain, Activity
|
||||
from models import User, Activity
|
||||
from . import testutil
|
||||
|
||||
|
||||
class DomainTest(testutil.TestCase):
|
||||
class UserTest(testutil.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DomainTest, self).setUp()
|
||||
self.domain = Domain.get_or_create('y.z')
|
||||
super(UserTest, self).setUp()
|
||||
self.user = User.get_or_create('y.z')
|
||||
|
||||
def test_magic_key_get_or_create(self):
|
||||
assert self.domain.mod
|
||||
assert self.domain.public_exponent
|
||||
assert self.domain.private_exponent
|
||||
assert self.user.mod
|
||||
assert self.user.public_exponent
|
||||
assert self.user.private_exponent
|
||||
|
||||
same = Domain.get_or_create('y.z')
|
||||
self.assertEqual(same, self.domain)
|
||||
same = User.get_or_create('y.z')
|
||||
self.assertEqual(same, self.user)
|
||||
|
||||
def test_href(self):
|
||||
href = self.domain.href()
|
||||
href = self.user.href()
|
||||
self.assertTrue(href.startswith('data:application/magic-public-key,RSA.'), href)
|
||||
self.assertIn(self.domain.mod, href)
|
||||
self.assertIn(self.domain.public_exponent, href)
|
||||
self.assertIn(self.user.mod, href)
|
||||
self.assertIn(self.user.public_exponent, href)
|
||||
|
||||
def test_public_pem(self):
|
||||
pem = self.domain.public_pem()
|
||||
pem = self.user.public_pem()
|
||||
self.assertTrue(pem.decode().startswith('-----BEGIN PUBLIC KEY-----\n'), pem)
|
||||
self.assertTrue(pem.decode().endswith('-----END PUBLIC KEY-----'), pem)
|
||||
|
||||
def test_private_pem(self):
|
||||
pem = self.domain.private_pem()
|
||||
pem = self.user.private_pem()
|
||||
self.assertTrue(pem.decode().startswith('-----BEGIN RSA PRIVATE KEY-----\n'), pem)
|
||||
self.assertTrue(pem.decode().endswith('-----END RSA PRIVATE KEY-----'), pem)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from unittest.mock import patch
|
|||
from oauth_dropins.webutil.testutil import requests_response
|
||||
|
||||
import common
|
||||
from models import Domain
|
||||
from models import User
|
||||
from .test_webmention import REPOST_HTML, REPOST_AS2
|
||||
from . import testutil
|
||||
|
||||
|
@ -15,7 +15,7 @@ class RedirectTest(testutil.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
Domain.get_or_create('foo.com')
|
||||
User.get_or_create('foo.com')
|
||||
|
||||
def test_redirect(self):
|
||||
got = self.client.get('/r/https://foo.com/bar?baz=baj&biff')
|
||||
|
|
|
@ -12,7 +12,7 @@ from oauth_dropins.webutil.testutil import requests_response, UrlopenResult
|
|||
import requests
|
||||
|
||||
import common
|
||||
from models import Domain, Activity
|
||||
from models import User, Activity
|
||||
from . import testutil
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ class SalmonTest(testutil.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.key = Domain.get_or_create('alice')
|
||||
self.key = User.get_or_create('alice')
|
||||
|
||||
def send_slap(self, mock_urlopen, mock_head, mock_get, mock_post, atom_slap):
|
||||
# salmon magic key discovery. first host-meta, then webfinger
|
||||
|
|
|
@ -32,7 +32,7 @@ class WebfingerTest(testutil.TestCase):
|
|||
</a>
|
||||
</body>
|
||||
"""
|
||||
self.key = models.Domain.get_or_create('foo.com')
|
||||
self.key = models.User.get_or_create('foo.com')
|
||||
self.expected_webfinger = {
|
||||
'subject': 'acct:foo.com@foo.com',
|
||||
'aliases': [
|
||||
|
|
|
@ -26,7 +26,7 @@ from common import (
|
|||
CONTENT_TYPE_HTML,
|
||||
CONTENT_TYPE_MAGIC_ENVELOPE,
|
||||
)
|
||||
from models import Follower, Domain, Activity
|
||||
from models import Follower, User, Activity
|
||||
import webmention
|
||||
from . import testutil
|
||||
|
||||
|
@ -69,7 +69,7 @@ REPOST_AS2 = {
|
|||
class WebmentionTest(testutil.TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.key = Domain.get_or_create('a')
|
||||
self.key = User.get_or_create('a')
|
||||
|
||||
self.orig_html_as2 = requests_response("""\
|
||||
<html>
|
||||
|
|
14
webfinger.py
14
webfinger.py
|
@ -17,7 +17,7 @@ from oauth_dropins.webutil.util import json_dumps
|
|||
|
||||
from app import app, cache
|
||||
import common
|
||||
import models
|
||||
from models import User
|
||||
|
||||
CACHE_TIME = datetime.timedelta(seconds=15)
|
||||
NON_TLDS = frozenset(('html', 'json', 'php', 'xml'))
|
||||
|
@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
|||
# CACHE_TIME.total_seconds(),
|
||||
# make_cache_key=lambda domain: f'{request.path} {request.headers.get("Accept")}')
|
||||
|
||||
class User(flask_util.XrdOrJrd):
|
||||
class Actor(flask_util.XrdOrJrd):
|
||||
"""Fetches a site's home page, converts its mf2 to WebFinger, and serves."""
|
||||
def template_prefix(self):
|
||||
return 'webfinger_user'
|
||||
|
@ -59,7 +59,7 @@ class User(flask_util.XrdOrJrd):
|
|||
error(f"didn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {resp.url}")
|
||||
|
||||
logger.info(f'Generating WebFinger data for {domain}')
|
||||
entity = models.Domain.get_or_create(domain)
|
||||
user = User.get_or_create(domain)
|
||||
props = hcard.get('properties', {})
|
||||
urls = util.dedupe_urls(props.get('url', []) + [resp.url])
|
||||
canonical_url = urls[0]
|
||||
|
@ -97,7 +97,7 @@ class User(flask_util.XrdOrJrd):
|
|||
data = util.trim_nulls({
|
||||
'subject': 'acct:' + acct,
|
||||
'aliases': urls,
|
||||
'magic_keys': [{'value': entity.href()}],
|
||||
'magic_keys': [{'value': user.href()}],
|
||||
'links': sum(([{
|
||||
'rel': 'http://webfinger.net/rel/profile-page',
|
||||
'type': 'text/html',
|
||||
|
@ -135,7 +135,7 @@ class User(flask_util.XrdOrJrd):
|
|||
'href': hub,
|
||||
}, {
|
||||
'rel': 'magic-public-key',
|
||||
'href': entity.href(),
|
||||
'href': user.href(),
|
||||
}, {
|
||||
'rel': 'salmon',
|
||||
'href': f'{request.host_url}{domain}/salmon',
|
||||
|
@ -145,7 +145,7 @@ class User(flask_util.XrdOrJrd):
|
|||
return data
|
||||
|
||||
|
||||
class Webfinger(User):
|
||||
class Webfinger(Actor):
|
||||
"""Handles Webfinger requests.
|
||||
|
||||
https://webfinger.net/
|
||||
|
@ -192,7 +192,7 @@ def host_meta_xrds():
|
|||
|
||||
|
||||
app.add_url_rule(f'/acct:<regex("{common.DOMAIN_RE}"):domain>',
|
||||
view_func=User.as_view('actor_acct'))
|
||||
view_func=Actor.as_view('actor_acct'))
|
||||
app.add_url_rule('/.well-known/webfinger', view_func=Webfinger.as_view('webfinger'))
|
||||
app.add_url_rule('/.well-known/host-meta', view_func=HostMeta.as_view('hostmeta'))
|
||||
app.add_url_rule('/.well-known/host-meta.json', view_func=HostMeta.as_view('hostmeta-json'))
|
||||
|
|
|
@ -25,7 +25,7 @@ from werkzeug.exceptions import BadGateway
|
|||
import activitypub
|
||||
from app import app
|
||||
import common
|
||||
from models import Follower, Domain, Activity
|
||||
from models import Follower, User, Activity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -91,7 +91,7 @@ class Webmention(View):
|
|||
if not targets:
|
||||
return None
|
||||
|
||||
domain = Domain.get_or_create(self.source_domain)
|
||||
user = User.get_or_create(self.source_domain)
|
||||
error = None
|
||||
last_success = None
|
||||
|
||||
|
@ -106,7 +106,7 @@ class Webmention(View):
|
|||
for resp, inbox in targets:
|
||||
target_obj = json_loads(resp.target_as2) if resp.target_as2 else None
|
||||
source_activity = common.postprocess_as2(
|
||||
as2.from_as1(self.source_obj), target=target_obj, domain=domain)
|
||||
as2.from_as1(self.source_obj), target=target_obj, user=user)
|
||||
|
||||
if resp.status == 'complete':
|
||||
if resp.source_mf2:
|
||||
|
@ -361,10 +361,10 @@ class Webmention(View):
|
|||
|
||||
# sign reply and wrap in magic envelope
|
||||
domain = urllib.parse.urlparse(self.source_url).netloc
|
||||
entity = Domain.get_or_create(domain)
|
||||
logger.info(f'Using key for {domain}: {entity}')
|
||||
user = User.get_or_create(domain)
|
||||
logger.info(f'Using key for {domain}: {user}')
|
||||
magic_envelope = magicsigs.magic_envelope(
|
||||
entry, common.CONTENT_TYPE_ATOM, entity).decode()
|
||||
entry, common.CONTENT_TYPE_ATOM, user).decode()
|
||||
|
||||
logger.info(f'Sending Salmon slap to {endpoint}')
|
||||
common.requests_post(
|
||||
|
|
Ładowanie…
Reference in New Issue