diff --git a/activitypub.py b/activitypub.py index 14907a8..26c3778 100644 --- a/activitypub.py +++ b/activitypub.py @@ -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', diff --git a/common.py b/common.py index 888636b..0e1e04e 100644 --- a/common.py +++ b/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 diff --git a/models.py b/models.py index d97da82..a3b86f3 100644 --- a/models.py +++ b/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' % ( diff --git a/pages.py b/pages.py index 347c1de..450e246 100644 --- a/pages.py +++ b/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/') 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//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, ) diff --git a/redirect.py b/redirect.py index 2fdae69..c4dffe5 100644 --- a/redirect.py +++ b/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']) diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 3628ff3..bdc03f4 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -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) diff --git a/tests/test_common.py b/tests/test_common.py index 72f047d..72ecf77 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -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('', 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'))) diff --git a/tests/test_models.py b/tests/test_models.py index e8b9f7e..9ecf62f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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) diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 0965643..9926109 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -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') diff --git a/tests/test_salmon.py b/tests/test_salmon.py index 70a7eff..6360f17 100644 --- a/tests/test_salmon.py +++ b/tests/test_salmon.py @@ -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 diff --git a/tests/test_webfinger.py b/tests/test_webfinger.py index e8b0574..24073d9 100644 --- a/tests/test_webfinger.py +++ b/tests/test_webfinger.py @@ -32,7 +32,7 @@ class WebfingerTest(testutil.TestCase): """ - 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': [ diff --git a/tests/test_webmention.py b/tests/test_webmention.py index 8953c85..8739511 100644 --- a/tests/test_webmention.py +++ b/tests/test_webmention.py @@ -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("""\ diff --git a/webfinger.py b/webfinger.py index 83180af..61583bb 100644 --- a/webfinger.py +++ b/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:', - 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')) diff --git a/webmention.py b/webmention.py index 9693e8f..744bb9b 100644 --- a/webmention.py +++ b/webmention.py @@ -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(