kopia lustrzana https://github.com/snarfed/bridgy-fed
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
rodzic
b237e52d6a
commit
b7e8cd7d42
34
redirect.py
34
redirect.py
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue