diff --git a/common.py b/common.py index 5bf0af9..888636b 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 +from models import Activity, Domain logger = logging.getLogger(__name__) @@ -242,22 +242,23 @@ def send_webmentions(activity_wrapped, proxy=None, **activity_props): error(msg, status=int(errors[0][0] or 502)) -def postprocess_as2(activity, target=None, domain=None): +def postprocess_as2(activity, domain=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 + publicKey fields if needed. target: dict, AS2 object, optional. The target of activity's inReplyTo or Like/Announce/etc object, if any. - domain: :class:`models.Domain`, optional. populated into publicKey field - if provided. """ + assert domain type = activity.get('type') # actor objects if type == 'Person': - postprocess_as2_actor(activity) - if not activity.get('publicKey') and domain: + postprocess_as2_actor(activity, domain) + if not activity.get('publicKey'): # underspecified, inferred from this issue and Mastodon's implementation: # https://github.com/w3c/activitypub/issues/203#issuecomment-297553229 # https://github.com/tootsuite/mastodon/blob/bc2c263504e584e154384ecc2d804aeb1afb1ba3/app/services/activitypub/process_account_service.rb#L77 @@ -275,7 +276,7 @@ def postprocess_as2(activity, target=None, domain=None): for actor in (util.get_list(activity, 'attributedTo') + util.get_list(activity, 'actor')): - postprocess_as2_actor(actor) + postprocess_as2_actor(actor, domain) # inReplyTo: singly valued, prefer id over url target_id = target.get('id') if target else None @@ -344,7 +345,9 @@ def postprocess_as2(activity, target=None, domain=None): # to public, since Mastodon interprets to public as public, cc public as unlisted: # https://socialhub.activitypub.rocks/t/visibility-to-cc-mapping/284 # https://wordsmith.social/falkreon/securing-activitypub - activity.setdefault('to', []).append(AS2_PUBLIC_AUDIENCE) + to = activity.setdefault('to', []) + if AS2_PUBLIC_AUDIENCE not in to: + to.append(AS2_PUBLIC_AUDIENCE) # wrap articles and notes in a Create activity if type in ('Article', 'Note'): @@ -352,29 +355,37 @@ def postprocess_as2(activity, target=None, domain=None): '@context': as2.CONTEXT, 'type': 'Create', 'id': f'{activity["id"]}#bridgy-fed-create', + 'actor': postprocess_as2_actor({}, domain), 'object': activity, } return util.trim_nulls(activity) -def postprocess_as2_actor(actor): +def postprocess_as2_actor(actor, domain=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` + + Returns: + actor dict """ - url = actor.get('url') - if url: - domain = urllib.parse.urlparse(url).netloc - actor.update({ - 'id': request.host_url + domain, - 'url': redirect_wrap(url), - 'preferredUsername': domain, - }) + url = actor.get('url') or f'https://{domain.key.id()}/' + domain_str = urllib.parse.urlparse(url).netloc + + actor.setdefault('id', request.host_url + domain_str) + actor.update({ + 'url': redirect_wrap(url), + 'preferredUsername': domain_str, + }) # required by pixelfed. https://github.com/snarfed/bridgy-fed/issues/39 actor.setdefault('summary', '') + return actor def redirect_wrap(url): diff --git a/redirect.py b/redirect.py index 7bfd4c5..2fdae69 100644 --- a/redirect.py +++ b/redirect.py @@ -51,9 +51,11 @@ def redir(to): util.domain_from_link(to, minimize=False), urllib.parse.urlparse(to).hostname)) for domain in domains: - if domain and Domain.get_by_id(domain): - logger.info(f'Found Domain for domain {domain}') - break + if domain: + entity = Domain.get_by_id(domain) + if entity: + logger.info(f'Found Domain for domain {domain}') + break else: logger.info(f'No user found for any of {domains}; returning 404') abort(404) @@ -62,24 +64,29 @@ def redir(to): # priorities. if request.headers.get('Accept') in (common.CONTENT_TYPE_AS2, common.CONTENT_TYPE_AS2_LD): - return convert_to_as2(to) + return convert_to_as2(to, entity) # redirect logger.info(f'redirecting to {to}') return redirect(to, code=301) -def convert_to_as2(url): +def convert_to_as2(url, domain): """Fetch a URL as HTML, convert it to AS2, and return it. Currently mainly for Pixelfed. https://github.com/snarfed/bridgy-fed/issues/39 + + Args: + url: str + domain: :class:`Domain` """ mf2 = util.fetch_mf2(url) entry = mf2util.find_first_entry(mf2, ['h-entry']) logger.info(f"Parsed mf2 for {mf2['url']}: {json_dumps(entry, indent=2)}") - obj = common.postprocess_as2(as2.from_as1(microformats2.json_to_object(entry))) + obj = common.postprocess_as2(as2.from_as1(microformats2.json_to_object(entry)), + domain) logger.info(f'Returning: {json_dumps(obj, indent=2)}') return obj, { diff --git a/tests/test_common.py b/tests/test_common.py index 7c5ce39..72f047d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -9,6 +9,7 @@ from werkzeug.exceptions import BadGateway from app import app import common +from models import Domain from . import testutil HTML = requests_response('', headers={ @@ -67,11 +68,56 @@ class CommonTest(testutil.TestCase): def test_postprocess_as2_multiple_in_reply_tos(self): with app.test_request_context('/'): - self.assertEqual({ + self.assert_equals({ 'id': 'http://localhost/r/xyz', 'inReplyTo': 'foo', 'to': [common.AS2_PUBLIC_AUDIENCE], }, common.postprocess_as2({ 'id': 'xyz', 'inReplyTo': ['foo', 'bar'], - })) + }, domain=Domain(id='foo.com'))) + + def test_postprocess_as2_actor_attributedTo(self): + with app.test_request_context('/'): + self.assert_equals({ + 'actor': { + 'id': 'baj', + 'preferredUsername': 'foo.com', + 'url': 'http://localhost/r/https://foo.com/', + }, + 'attributedTo': [{ + 'id': 'bar', + 'preferredUsername': 'foo.com', + 'url': 'http://localhost/r/https://foo.com/', + }, { + 'id': 'baz', + 'preferredUsername': 'foo.com', + 'url': 'http://localhost/r/https://foo.com/', + }], + 'to': [common.AS2_PUBLIC_AUDIENCE], + }, common.postprocess_as2({ + 'attributedTo': [{'id': 'bar'}, {'id': 'baz'}], + 'actor': {'id': 'baj'}, + }, domain=Domain(id='foo.com'))) + + def test_postprocess_as2_note(self): + with app.test_request_context('/'): + self.assert_equals({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': 'http://localhost/r/xyz#bridgy-fed-create', + 'type': 'Create', + 'actor': { + 'id': 'http://localhost/foo.com', + 'url': 'http://localhost/r/https://foo.com/', + 'preferredUsername': 'foo.com' + }, + 'object': { + 'id': 'http://localhost/r/xyz', + 'type': 'Note', + 'to': [common.AS2_PUBLIC_AUDIENCE], + }, + }, common.postprocess_as2({ + 'id': 'xyz', + 'type': 'Note', + }, domain=Domain(id='foo.com'))) + diff --git a/tests/test_webmention.py b/tests/test_webmention.py index bc6da60..8953c85 100644 --- a/tests/test_webmention.py +++ b/tests/test_webmention.py @@ -158,6 +158,11 @@ class WebmentionTest(testutil.TestCase): '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Create', 'id': 'http://localhost/r/http://a/reply#bridgy-fed-create', + 'actor': { + 'id': 'http://localhost/a', + 'url': 'http://localhost/r/https://a/', + 'preferredUsername': 'a', + }, 'object': { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Note', @@ -240,6 +245,11 @@ class WebmentionTest(testutil.TestCase): '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Create', 'id': 'http://localhost/r/http://orig/post#bridgy-fed-create', + 'actor': { + 'id': 'http://localhost/orig', + 'url': 'http://localhost/r/https://orig/', + 'preferredUsername': 'orig', + }, 'object': { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Note',