add conneg + AS2 support to /r/... redirect URLs

for pixelfed, #39. specifically, if the client asks for application/activity+json or application/ld+json (which pixelfed does), fetch and convert to AS2 instead of returning a 302 redirect.
pull/59/head
Ryan Barrett 2019-01-04 07:17:45 -08:00
rodzic b237e52d6a
commit b7e8cd7d42
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
3 zmienionych plików z 106 dodań i 32 usunięć

Wyświetl plik

@ -1,5 +1,7 @@
"""Simple endpoint that redirects to the embedded fully qualified URL.
May also instead fetch and convert to AS2, depending on conneg.
Used to wrap ActivityPub ids with the fed.brid.gy domain so that Mastodon
accepts them. Background:
@ -7,7 +9,11 @@ https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
"""
import logging
import json
from granary import as2, microformats2
import mf2py
import mf2util
import webapp2
import appengine_config
@ -25,9 +31,37 @@ class RedirectHandler(webapp2.RequestHandler):
to = self.request.path_qs[3:]
if not to.startswith('http://') and not to.startswith('https://'):
common.error(self, 'Expected fully qualified URL; got %s' % to)
# poor man's conneg, only handle single Accept values, not multiple with
# priorities.
if self.request.headers.get('Accept') in (common.CONTENT_TYPE_AS2,
common.CONTENT_TYPE_AS2_LD):
return self.convert_to_as2(to)
# redirect
logging.info('redirecting to %s', to)
self.redirect(to)
def convert_to_as2(self, url):
"""Fetch a URL as HTML, convert it to AS2, and return it.
Currently mainly for Pixelfed.
https://github.com/snarfed/bridgy-fed/issues/39
"""
resp = common.requests_get(url)
mf2 = mf2py.parse(resp.text, url=resp.url, img_with_alt=True)
entry = mf2util.find_first_entry(mf2, ['h-entry'])
logging.info('Parsed mf2 for %s: %s', resp.url, json.dumps(entry, indent=2))
obj = common.postprocess_as2(as2.from_as1(microformats2.json_to_object(entry)))
logging.info('Returning: %s', json.dumps(obj, indent=2))
self.response.headers.update({
'Content-Type': common.CONTENT_TYPE_AS2,
'Access-Control-Allow-Origin': '*',
})
self.response.write(json.dumps(obj, indent=2))
app = webapp2.WSGIApplication([
(r'/r/.+', RedirectHandler),

Wyświetl plik

@ -1,10 +1,17 @@
"""Unit tests for redirect.py.
"""
import copy
from mock import patch
from oauth_dropins.webutil.testutil import requests_response
import common
from redirect import app
from test_webmention import REPOST_HTML, REPOST_AS2
import testutil
class ActivityPubTest(testutil.TestCase):
class RedirectTest(testutil.TestCase):
def test_redirect(self):
got = app.get_response('/r/https://foo.com/bar?baz=baj&biff')
@ -18,3 +25,34 @@ class ActivityPubTest(testutil.TestCase):
def test_redirect_url_missing(self):
got = app.get_response('/r/')
self.assertEqual(404, got.status_int)
def test_as2(self):
self._test_as2(common.CONTENT_TYPE_AS2)
def test_as2_ld(self):
self._test_as2(common.CONTENT_TYPE_AS2_LD)
@patch('requests.get')
def _test_as2(self, content_type, mock_get):
"""Currently mainly for Pixelfed.
https://github.com/snarfed/bridgy-fed/issues/39
"""
as2 = copy.deepcopy(REPOST_AS2)
as2.update({
'cc': [common.AS2_PUBLIC_AUDIENCE],
'object': 'http://orig/post',
})
mock_get.return_value = requests_response(
REPOST_HTML, content_type=content_type)
got = app.get_response('/r/https://foo.com/bar', headers={
'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
})
args, kwargs = mock_get.call_args
self.assertEqual(('https://foo.com/bar',), args)
self.assertEqual(200, got.status_int)
self.assertEqual(as2, got.json)

Wyświetl plik

@ -35,6 +35,37 @@ import testutil
import webmention
from webmention import app
REPOST_HTML = """\
<html>
<body class="h-entry">
<a class="u-url" href="http://a/repost"></a>
<a class="u-repost-of p-name" href="http://orig/post">reposted!</a>
<a class="p-author h-card" href="http://orig">Ms. Baz</a>
<a href="http://localhost/"></a>
</body>
</html>
"""
REPOST_AS2 = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Announce',
'id': 'http://localhost/r/http://a/repost',
'url': 'http://localhost/r/http://a/repost',
'name': 'reposted!',
'object': 'tag:orig,2017:as2',
'cc': [
AS2_PUBLIC_AUDIENCE,
'http://orig/author',
'http://orig/recipient',
'http://orig/bystander',
],
'actor': {
'type': 'Person',
'id': 'http://localhost/orig',
'url': 'http://localhost/r/http://orig',
'name': 'Ms. ☕ Baz',
'preferredUsername': 'orig',
},
}
@mock.patch('requests.post')
@mock.patch('requests.get')
@ -98,40 +129,11 @@ class WebmentionTest(testutil.TestCase):
self.reply_html, content_type=CONTENT_TYPE_HTML)
self.reply_mf2 = mf2py.parse(self.reply_html, url='http://a/reply')
self.repost_html = """\
<html>
<body class="h-entry">
<a class="u-url" href="http://a/repost"></a>
<a class="u-repost-of p-name" href="http://orig/post">reposted!</a>
<a class="p-author h-card" href="http://orig">Ms. Baz</a>
<a href="http://localhost/"></a>
</body>
</html>
"""
self.repost_html = REPOST_HTML
self.repost = requests_response(
self.repost_html, content_type=CONTENT_TYPE_HTML)
self.repost_mf2 = mf2py.parse(self.repost_html, url='http://a/repost')
self.repost_as2 = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Announce',
'id': 'http://localhost/r/http://a/repost',
'url': 'http://localhost/r/http://a/repost',
'name': 'reposted!',
'object': 'tag:orig,2017:as2',
'cc': [
AS2_PUBLIC_AUDIENCE,
'http://orig/author',
'http://orig/recipient',
'http://orig/bystander',
],
'actor': {
'type': 'Person',
'id': 'http://localhost/orig',
'url': 'http://localhost/r/http://orig',
'name': 'Ms. ☕ Baz',
'preferredUsername': 'orig',
},
}
self.repost_as2 = REPOST_AS2
self.like_html = """\
<html>