kopia lustrzana https://github.com/snarfed/bridgy-fed
add /render logic to /convert/...
rodzic
df35ce16cc
commit
6d976854dc
36
convert.py
36
convert.py
|
@ -9,13 +9,15 @@ import logging
|
|||
import re
|
||||
import urllib.parse
|
||||
|
||||
from flask import request
|
||||
from flask import redirect, request
|
||||
from granary import as1
|
||||
from oauth_dropins.webutil import flask_util, util
|
||||
from oauth_dropins.webutil.flask_util import error
|
||||
|
||||
from activitypub import ActivityPub
|
||||
from common import CACHE_TIME
|
||||
from flask_app import app, cache
|
||||
from models import Object
|
||||
from protocol import protocols
|
||||
from webmention import Webmention
|
||||
|
||||
|
@ -29,15 +31,19 @@ DESTS = frozenset((
|
|||
))
|
||||
|
||||
|
||||
@app.get(f'/convert/<any({",".join(SOURCES)}):src>/<any({",".join(DESTS)}):dest>/<path:url>')
|
||||
@app.get(f'/convert/<any({",".join(SOURCES)}):src>/<any({",".join(DESTS)}):dest>/<path:_>')
|
||||
@flask_util.cached(cache, CACHE_TIME, headers=['Accept'])
|
||||
def convert(src, dest, url):
|
||||
def convert(src, dest, _):
|
||||
"""Converts data from one protocol to another and serves it.
|
||||
|
||||
Fetches the source data if it's not already stored.
|
||||
"""
|
||||
if request.args:
|
||||
url += '?' + urllib.parse.urlencode(request.args)
|
||||
# don't use urllib.parse.urlencode(request.args) because that doesn't
|
||||
# guarantee us the same query param string as in the original URL, and we
|
||||
# want exactly the same thing since we're looking up the URL's Object by id
|
||||
path_prefix = f'convert/{src}/{dest}/'
|
||||
url = request.url.removeprefix(request.root_url).removeprefix(path_prefix)
|
||||
|
||||
# some browsers collapse repeated /s in the path down to a single slash.
|
||||
# if that happened to this URL, expand it back to two /s.
|
||||
url = re.sub(r'^(https?:/)([^/])', r'\1/\2', url)
|
||||
|
@ -45,5 +51,25 @@ def convert(src, dest, url):
|
|||
if not util.is_web(url):
|
||||
error(f'Expected fully qualified URL; got {url}')
|
||||
|
||||
# load, and maybe fetch. if it's a post/update, redirect to inner object.
|
||||
obj = protocols[src].load(url)
|
||||
if not obj.as1:
|
||||
error(f'Stored object for {id} has no data', status=404)
|
||||
|
||||
type = as1.object_type(obj.as1)
|
||||
if type in ('post', 'update', 'delete'):
|
||||
obj_id = as1.get_object(obj.as1).get('id')
|
||||
if obj_id:
|
||||
# TODO: protocols[src].load() this instead?
|
||||
obj_obj = Object.get_by_id(obj_id)
|
||||
if (obj_obj and obj_obj.as1 and
|
||||
not obj_obj.as1.keys() <= set(['id', 'url', 'objectType'])):
|
||||
logger.info(f'{type} activity, redirecting to Object {obj_id}')
|
||||
return redirect(f'/{path_prefix}{obj_id}', code=301)
|
||||
|
||||
# don't serve deletes or deleted objects
|
||||
if obj.deleted or type == 'delete':
|
||||
return '', 410
|
||||
|
||||
# convert and serve
|
||||
return protocols[dest].serve(obj)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
"""Unit tests for convert.py.
|
||||
"""
|
||||
import copy
|
||||
from unittest.mock import patch
|
||||
|
||||
from granary import as2
|
||||
from granary.tests.test_as1 import ACTOR, COMMENT, DELETE_OF_ID, UPDATE
|
||||
from models import Object
|
||||
from oauth_dropins.webutil.testutil import requests_response
|
||||
import requests
|
||||
|
||||
|
@ -13,38 +17,192 @@ from .test_redirect import (
|
|||
)
|
||||
from . import testutil
|
||||
|
||||
EXPECTED_HTML = """\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="0;url=https://fake.com/123456"></head>
|
||||
<body class="">
|
||||
<article class="h-entry">
|
||||
<span class="p-uid">tag:fake.com:123456</span>
|
||||
<time class="dt-published" datetime="2012-12-05T00:58:26+00:00">2012-12-05T00:58:26+00:00</time>
|
||||
<a class="u-url" href="https://fake.com/123456">fake.com/123456</a>
|
||||
<div class="e-content p-name">
|
||||
A ☕ reply
|
||||
</div>
|
||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
EXPECTED_AUTHOR_HTML = """\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="0;url=https://fake.com/123456"></head>
|
||||
<body class="">
|
||||
<article class="h-entry">
|
||||
<span class="p-uid">tag:fake.com:123456</span>
|
||||
<time class="dt-published" datetime="2012-12-05T00:58:26+00:00">2012-12-05T00:58:26+00:00</time>
|
||||
<span class="p-author h-card">
|
||||
<data class="p-uid" value="tag:fake.com:444"></data>
|
||||
<a class="p-name u-url" href="https://plus.google.com/bob">Bob</a>
|
||||
<img class="u-photo" src="https://bob/picture" alt="" />
|
||||
</span>
|
||||
<a class="u-url" href="https://fake.com/123456">fake.com/123456</a>
|
||||
<div class="e-content p-name">
|
||||
A ☕ reply
|
||||
</div>
|
||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@patch('requests.get')
|
||||
class ConvertTest(testutil.TestCase):
|
||||
|
||||
def test_unknown_source(self, _):
|
||||
got = self.client.get('/convert/nope/webmention/http://foo')
|
||||
self.assertEqual(404, got.status_code)
|
||||
def test_unknown_source(self):
|
||||
resp = self.client.get('/convert/nope/webmention/http://foo')
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_unknown_dest(self, _):
|
||||
got = self.client.get('/convert/activitypub/nope/http://foo')
|
||||
self.assertEqual(404, got.status_code)
|
||||
def test_unknown_dest(self):
|
||||
resp = self.client.get('/convert/activitypub/nope/http://foo')
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_missing_url(self, _):
|
||||
got = self.client.get('/convert/activitypub/webmention/')
|
||||
self.assertEqual(404, got.status_code)
|
||||
def test_missing_url(self):
|
||||
resp = self.client.get('/convert/activitypub/webmention/')
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_url_not_web(self, _):
|
||||
got = self.client.get('/convert/activitypub/webmention/git+ssh://foo/bar')
|
||||
self.assertEqual(400, got.status_code)
|
||||
def test_url_not_web(self):
|
||||
resp = self.client.get('/convert/activitypub/webmention/git+ssh://foo/bar')
|
||||
self.assertEqual(400, resp.status_code)
|
||||
|
||||
def test_activitypub_to_web(self, mock_get):
|
||||
mock_get.return_value = self.as2_resp(REPOST_AS2)
|
||||
def test_activitypub_to_web_object(self):
|
||||
url = 'https://user.com/bar?baz=baj&biff'
|
||||
with self.request_context:
|
||||
Object(id=url, our_as1=COMMENT).put()
|
||||
|
||||
got = self.client.get('/convert/activitypub/webmention/https://user.com/bar?baz=baj&biff')
|
||||
self.assertEqual(200, got.status_code)
|
||||
self.assertEqual(CONTENT_TYPE_HTML, got.content_type)
|
||||
resp = self.client.get('/convert/activitypub/webmention/https://user.com/bar?baz=baj&biff')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assert_multiline_equals(EXPECTED_HTML, resp.get_data(as_text=True),
|
||||
ignore_blanks=True)
|
||||
|
||||
mock_get.assert_has_calls((self.as2_req('https://user.com/bar?baz=baj&biff='),))
|
||||
def test_activitypub_to_web_object_empty(self):
|
||||
with self.request_context:
|
||||
Object(id='http://foo').put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
@patch('requests.get')
|
||||
def test_activitypub_to_web_fetch(self, mock_get):
|
||||
mock_get.return_value = self.as2_resp(as2.from_as1(COMMENT))
|
||||
url = 'https://user.com/bar?baz=baj&biff'
|
||||
|
||||
resp = self.client.get(f'/convert/activitypub/webmention/{url}')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual(CONTENT_TYPE_HTML, resp.content_type)
|
||||
self.assert_multiline_equals(EXPECTED_HTML, resp.get_data(as_text=True),
|
||||
ignore_blanks=True)
|
||||
|
||||
mock_get.assert_has_calls((self.as2_req(url),))
|
||||
|
||||
@patch('requests.get')
|
||||
def test_activitypub_to_web_fetch_fails(self, mock_get):
|
||||
mock_get.side_effect = [requests_response('', status=405)]
|
||||
|
||||
got = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(502, got.status_code)
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(502, resp.status_code)
|
||||
mock_get.assert_has_calls((self.as2_req('http://foo'),))
|
||||
|
||||
def test_activitypub_to_web_with_author(self):
|
||||
with self.request_context:
|
||||
Object(id='http://foo', our_as1={**COMMENT, 'author': 'http://bar'},
|
||||
source_protocol='activitypub').put()
|
||||
Object(id='http://bar', our_as1=ACTOR,
|
||||
source_protocol='activitypub').put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assert_multiline_equals(EXPECTED_AUTHOR_HTML, resp.get_data(as_text=True),
|
||||
ignore_blanks=True)
|
||||
|
||||
def test_activitypub_to_web_no_url(self):
|
||||
comment = copy.deepcopy(COMMENT)
|
||||
del comment['url']
|
||||
with self.request_context:
|
||||
Object(id='http://foo', our_as1=comment).put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
expected = EXPECTED_HTML.replace(
|
||||
'\n<meta http-equiv="refresh" content="0;url=https://fake.com/123456">', ''
|
||||
).replace('<a class="u-url" href="https://fake.com/123456">fake.com/123456</a>', '')
|
||||
self.assert_multiline_equals(expected, resp.get_data(as_text=True),
|
||||
ignore_blanks=True)
|
||||
|
||||
def test_activitypub_to_web_deleted_object(self):
|
||||
with self.request_context:
|
||||
Object(id='http://foo', as2={'content': 'foo'}, deleted=True).put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(410, resp.status_code)
|
||||
|
||||
def test_activitypub_to_web_delete_activity(self):
|
||||
with self.request_context:
|
||||
Object(id='http://foo', our_as1=DELETE_OF_ID).put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(410, resp.status_code)
|
||||
|
||||
def test_activitypub_to_web_update_inner_obj_exists_redirect(self):
|
||||
with self.request_context:
|
||||
# UPDATE's object field is a full object
|
||||
Object(id='http://foo', our_as1=UPDATE).put()
|
||||
Object(id=UPDATE['object']['id'], as2={'content': 'foo'}).put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(301, resp.status_code)
|
||||
self.assertEqual(f'/convert/activitypub/webmention/tag:fake.com:123456',
|
||||
resp.headers['Location'])
|
||||
|
||||
def test_activitypub_to_web_delete_inner_obj_exists_redirect(self):
|
||||
with self.request_context:
|
||||
# DELETE_OF_ID's object field is a bare string id
|
||||
Object(id='http://foo', our_as1=DELETE_OF_ID).put()
|
||||
Object(id=DELETE_OF_ID['object'], as2={'content': 'foo'}).put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(301, resp.status_code)
|
||||
self.assertEqual(f'/convert/activitypub/webmention/tag:fake.com:123456',
|
||||
resp.headers['Location'])
|
||||
|
||||
def test_activitypub_to_web_update_no_inner_obj_serve_as_is(self):
|
||||
with self.request_context:
|
||||
# UPDATE's object field is a full object
|
||||
Object(id='http://foo', our_as1=UPDATE).put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assert_multiline_in("""\
|
||||
<div class="e-content p-name">
|
||||
A ☕ reply
|
||||
</div>
|
||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
||||
""", resp.get_data(as_text=True), ignore_blanks=True)
|
||||
|
||||
def test_activitypub_to_web_update_inner_obj_too_minimal_serve_as_is(self):
|
||||
with self.request_context:
|
||||
# UPDATE's object field is a full object
|
||||
Object(id='http://foo', our_as1=UPDATE).put()
|
||||
Object(id=UPDATE['object']['id'], as2={'id': 'foo'}).put()
|
||||
|
||||
resp = self.client.get('/convert/activitypub/webmention/http://foo')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assert_multiline_in("""\
|
||||
<div class="e-content p-name">
|
||||
A ☕ reply
|
||||
</div>
|
||||
<a class="u-in-reply-to" href="https://fake.com/123"></a>
|
||||
""", resp.get_data(as_text=True), ignore_blanks=True)
|
||||
|
|
Ładowanie…
Reference in New Issue