From 39cb90e533060e9f4f35c3404976caef830ba5d6 Mon Sep 17 00:00:00 2001 From: Jamie Tanna Date: Fri, 18 Nov 2022 10:18:35 +0000 Subject: [PATCH] Allow providing a URL fragment for posts A number of folks use a single page for multiple posts, using a URL Fragment parameter to denote which of the posts on the given page should be used, and are used to this experience with Bridgy. To allow use of this on Bridgy Fed, too, we can add support for discovering the ID of the `h-entry` to be Webmention'd in the same way that we do with [Bridgy]. This largely copies the structure of the existing tests, copying `test_activitypub_follow`, and adding in multiple posts on the page. For debugging purposes, we can make sure to log out the `fragment`. [Bridgy]: https://github.com/snarfed/bridgy/commit/f760c0d10e4d28a84585dbbff259eff42466e119 --- templates/index.html | 11 +++++ tests/test_webmention.py | 86 ++++++++++++++++++++++++++++++++++++++++ webmention.py | 5 ++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/templates/index.html b/templates/index.html index f4f63f5..2d2669e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,6 +9,7 @@
  • How do I use it?
  • How do I set it up?
  • How do I include an image in a post?
  • +
  • Can I publish just one part of a page?
  • How can people on the fediverse find me?
  • I tried it, and it didn't work!
  • How much does it cost?
  • @@ -188,6 +189,16 @@ I love scotch. Scotchy scotchy scotch.

    +
  • Can I publish just one part of a page?
  • +
  • +

    If that HTML element has its own id, then sure! Just put the id in the fragment of the URL that you publish. For example, to publish the bar post here:

    +
    <div id="a" class="h-entry">foo</div>
    +<div id="b" class="h-entry">bar</div>
    +<div id="c" class="h-entry">baz</div>
    +
    +

    ...just add the id to your page's URL in a fragment, e.g. http://site/post#b here.

    +
  • +
  • How can people on the fediverse find me?
  • In general, all you have to do is use Bridgy Fed to interact with the fediverse once. Send an original post from your site, like or repost something, follow someone, etc. Then, when other users search for @yourdomain.com@yourdomain.com, they should find your profile! diff --git a/tests/test_webmention.py b/tests/test_webmention.py index 6ab7889..fc22555 100644 --- a/tests/test_webmention.py +++ b/tests/test_webmention.py @@ -774,6 +774,92 @@ class WebmentionTest(testutil.TestCase): self.assertEqual('a', followers[0].src) self.assertEqual('http://followee', followers[0].dest) + def test_activitypub_follow_fragment(self, mock_get, mock_post): + self.follow_html = """\ + + +

    +

    Ignored

    +
    + + + +""" + self.follow = requests_response( + self.follow_html, content_type=CONTENT_TYPE_HTML) + self.follow_mf2 = util.parse_mf2(self.follow_html, url='http://a/follow') + self.follow_as2 = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'type': 'Follow', + 'id': 'http://localhost/r/http://a/follow#2', + 'url': 'http://localhost/r/http://a/follow#2', + 'object': 'http://followee', + 'actor': { + 'id': 'http://localhost/orig', + 'name': 'Ms. ☕ Baz', + 'preferredUsername': 'orig', + 'type': 'Person', + 'url': 'http://localhost/r/https://orig', + }, + 'to': [as2.PUBLIC_AUDIENCE], + } + + mock_get.side_effect = [self.follow, self.actor] + mock_post.return_value = requests_response('abc xyz') + + got = self.client.post('/webmention', data={ + 'source': 'http://a/follow#2', + 'target': 'https://fed.brid.gy/', + }) + self.assertEqual(200, got.status_code) + + mock_get.assert_has_calls(( + self.req('http://a/follow#2'), + self.req('http://followee/', headers=CONNEG_HEADERS_AS2_HTML), + )) + + args, kwargs = mock_post.call_args + self.assertEqual(('https://foo.com/inbox',), args) + self.assertEqual(self.follow_as2, json_loads(kwargs['data'])) + + headers = kwargs['headers'] + self.assertEqual(CONTENT_TYPE_AS2, headers['Content-Type']) + + rsa_key = kwargs['auth'].header_signer._rsa._key + self.assertEqual(self.key.private_pem(), rsa_key.exportKey()) + + activity = Activity.get_by_id('http://a/follow__2 http://followee/') + self.assertEqual(['a'], activity.domain) + self.assertEqual('out', activity.direction) + self.assertEqual('activitypub', activity.protocol) + self.assertEqual('complete', activity.status) + follow_html_for_fragement = """ + + + + + """ + follow_mf2_for_fragment = util.parse_mf2(follow_html_for_fragement, url='http://a/follow') + # for some reason, this doesn't get picked up correctly when it's a fragment + follow_mf2_for_fragment['debug']['markup parser'] = 'unknown' + self.assertEqual(follow_mf2_for_fragment, json_loads(activity.source_mf2)) + + followers = Follower.query().fetch() + self.assertEqual(1, len(followers)) + self.assertEqual('http://followee a', followers[0].key.id()) + self.assertEqual('a', followers[0].src) + self.assertEqual('http://followee', followers[0].dest) + def test_activitypub_error_no_salmon_fallback(self, mock_get, mock_post): mock_get.side_effect = [self.follow, self.actor] mock_post.return_value = requests_response( diff --git a/webmention.py b/webmention.py index 8e0aba6..f12ed36 100644 --- a/webmention.py +++ b/webmention.py @@ -48,7 +48,8 @@ class Webmention(View): source_resp = common.requests_get(source) self.source_url = source_resp.url or source self.source_domain = urllib.parse.urlparse(self.source_url).netloc.split(':')[0] - self.source_mf2 = util.parse_mf2(source_resp) + fragment = urllib.parse.urlparse(self.source_url).fragment + self.source_mf2 = util.parse_mf2(source_resp, id=fragment) # logger.debug(f'Parsed mf2 for {source_resp.url} : {json_dumps(self.source_mf2 indent=2)}') @@ -64,7 +65,7 @@ class Webmention(View): if not entry: error(f'No microformats2 found on {self.source_url}') - logger.info(f'First entry: {json_dumps(entry, indent=2)}') + logger.info(f'First entry (id={fragment}: {json_dumps(entry, indent=2)}') # make sure it has url, since we use that for AS2 id, which is required # for ActivityPub. props = entry.setdefault('properties', {})