kopia lustrzana https://github.com/snarfed/bridgy-fed
flask: port webmention, add exception handler
rodzic
c17cb3394b
commit
4fffc073d2
10
app.py
10
app.py
|
@ -16,4 +16,14 @@ app.wsgi_app = handlers.ndb_context_middleware(
|
|||
|
||||
cache = Cache(app)
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_exception(e):
|
||||
"""A Flask error handler that propagates HTTP exceptions into the response."""
|
||||
code, body = util.interpret_http_exception(e)
|
||||
if code:
|
||||
return ((f'Upstream server request failed: {e}' if code in ('502', '504')
|
||||
else f'HTTP Error {code}: {body}'),
|
||||
int(code))
|
||||
return e
|
||||
|
||||
import activitypub, add_webmention, logs, redirect, render, salmon, superfeedr, webfinger, webmention
|
||||
|
|
|
@ -152,11 +152,11 @@ def content_type(resp):
|
|||
return type.split(';')[0]
|
||||
|
||||
|
||||
def get_required_param(request, name):
|
||||
def get_required_param(name):
|
||||
try:
|
||||
val = request.args.get(name)
|
||||
val = request.args.get(name) or request.form.get(name)
|
||||
except (UnicodeDecodeError, UnicodeEncodeError) as e:
|
||||
abort(400, f"Couldn't decode query parameters as UTF-8: {e}")
|
||||
abort(400, f"Couldn't decode parameters as UTF-8: {e}")
|
||||
|
||||
if not val:
|
||||
abort(400, f'Missing required parameter: {name}')
|
||||
|
|
|
@ -18,8 +18,8 @@ CACHE_TIME = datetime.timedelta(minutes=15)
|
|||
response_filter=common.not_5xx)
|
||||
def render():
|
||||
"""Fetches a stored Response and renders it as HTML."""
|
||||
source = common.get_required_param(request, 'source')
|
||||
target = common.get_required_param(request, 'target')
|
||||
source = common.get_required_param('source')
|
||||
target = common.get_required_param('target')
|
||||
|
||||
id = f'{source} {target}'
|
||||
resp = Response.get_by_id(id)
|
||||
|
|
|
@ -64,13 +64,16 @@ REPOST_AS2 = {
|
|||
},
|
||||
}
|
||||
|
||||
client = app.test_client()
|
||||
|
||||
|
||||
@mock.patch('requests.post')
|
||||
@mock.patch('requests.get')
|
||||
class WebmentionTest(testutil.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(WebmentionTest, self).setUp()
|
||||
self.key = MagicKey.get_or_create('a')
|
||||
app.testing = True
|
||||
|
||||
self.orig_html_as2 = requests_response("""\
|
||||
<html>
|
||||
|
@ -276,13 +279,12 @@ class WebmentionTest(testutil.TestCase):
|
|||
return env['data']
|
||||
|
||||
def test_bad_source_url(self, mock_get, mock_post):
|
||||
got = application.get_response('/webmention', method='POST', body=b'')
|
||||
self.assertEqual(400, got.status_int)
|
||||
got = client.post('/webmention', data=b'')
|
||||
self.assertEqual(400, got.status_code)
|
||||
|
||||
mock_get.side_effect = ValueError('foo bar')
|
||||
got = application.get_response('/webmention', method='POST',
|
||||
body=urlencode({'source': 'bad'}).encode())
|
||||
self.assertEqual(400, got.status_int)
|
||||
got = client.post('/webmention', data={'source': 'bad'})
|
||||
self.assertEqual(400, got.status_code)
|
||||
|
||||
def test_no_source_entry(self, mock_get, mock_post):
|
||||
mock_get.return_value = requests_response("""
|
||||
|
@ -292,12 +294,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
</body>
|
||||
</html>""", content_type=CONTENT_TYPE_HTML)
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(400, got.status_int)
|
||||
got = client.post( '/webmention', data={
|
||||
'source': 'http://a/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(400, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((self.req('http://a/post'),))
|
||||
|
||||
|
@ -309,12 +310,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
</body>
|
||||
</html>""", content_type=CONTENT_TYPE_HTML)
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((self.req('http://a/post'),))
|
||||
|
||||
|
@ -324,43 +324,36 @@ class WebmentionTest(testutil.TestCase):
|
|||
content_type=CONTENT_TYPE_HTML),
|
||||
ValueError('foo bar'))
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST',
|
||||
body=urlencode({'source': 'http://a/post'}).encode())
|
||||
self.assertEqual(400, got.status_int)
|
||||
got = client.post('/webmention', data={'source': 'http://a/post'})
|
||||
self.assertEqual(400, got.status_code)
|
||||
|
||||
def test_target_fetch_fails(self, mock_get, mock_post):
|
||||
def test_source_fetch_fails(self, mock_get, mock_post):
|
||||
mock_get.side_effect = (
|
||||
requests_response(self.reply_html.replace('http://orig/post', 'bad'),
|
||||
content_type=CONTENT_TYPE_HTML),
|
||||
requests.Timeout('foo bar'))
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST',
|
||||
body=urlencode({'source': 'http://a/post'}).encode())
|
||||
self.assertEqual(502, got.status_int)
|
||||
got = client.post('/webmention', data={'source': 'http://a/post'})
|
||||
self.assertEqual(502, got.status_code)
|
||||
|
||||
def test_target_fetch_has_no_content_type(self, mock_get, mock_post):
|
||||
def test_source_fetch_has_no_content_type(self, mock_get, mock_post):
|
||||
mock_get.side_effect = (
|
||||
requests_response(self.reply_html),
|
||||
requests_response(self.reply_html, content_type='None')
|
||||
)
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST',
|
||||
body=urlencode({'source': 'http://a/post'}).encode())
|
||||
self.assertEqual(502, got.status_int)
|
||||
got = client.post('/webmention', data={'source': 'http://a/post'})
|
||||
self.assertEqual(502, got.status_code)
|
||||
|
||||
def test_no_backlink(self, mock_get, mock_post):
|
||||
mock_get.return_value = requests_response(
|
||||
self.reply_html.replace('<a href="http://localhost/"></a>', ''),
|
||||
content_type=CONTENT_TYPE_HTML)
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(400, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(400, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((self.req('http://a/post'),))
|
||||
|
||||
|
@ -368,12 +361,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = self.activitypub_gets
|
||||
mock_post.return_value = requests_response('abc xyz', status=203)
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(203, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(203, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/reply'),
|
||||
|
@ -410,12 +402,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = self.activitypub_gets
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('https://foo.com/inbox',), args)
|
||||
|
@ -438,12 +429,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz', status=203)
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(203, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(203, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/reply'),
|
||||
|
@ -462,12 +452,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = self.activitypub_gets
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('https://foo.com/inbox',), args)
|
||||
|
@ -477,12 +466,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [self.repost, self.orig_as2, self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/repost',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/repost',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/repost'),
|
||||
|
@ -511,12 +499,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.orig_html_as2, self.orig_as2, self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/reply'),
|
||||
|
@ -544,11 +531,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [missing_url, self.orig_as2, self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz', status=203)
|
||||
|
||||
got = application.get_response('/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/repost',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(203, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/repost',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(203, got.status_code)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('https://foo.com/inbox',), args)
|
||||
|
@ -579,11 +566,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [repost, author, self.orig_as2, self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz', status=201)
|
||||
|
||||
got = application.get_response('/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/repost',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(201, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/repost',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(201, got.status_code)
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('https://foo.com/inbox',), args)
|
||||
|
@ -624,12 +611,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
'inbox': 'https://inbox',
|
||||
}}))
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://orig/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://orig/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://orig/post'),
|
||||
|
@ -661,12 +647,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
'orig', 'https://mastodon/aaa',
|
||||
last_follow=json_dumps({'actor': {'inbox': 'https://inbox'}}))
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://orig/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://orig/post',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
self.assertEqual(('https://inbox',), mock_post.call_args[0])
|
||||
create = copy.deepcopy(self.create_as2)
|
||||
|
@ -680,12 +665,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [self.follow, self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/follow',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/follow',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/follow'),
|
||||
|
@ -713,15 +697,15 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_post.return_value = requests_response(
|
||||
'abc xyz', status=405, url='https://foo.com/inbox')
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/follow',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}).encode())
|
||||
self.assertEqual(502, got.status_int, got.text)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/follow',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
})
|
||||
body = got.get_data(as_text=True)
|
||||
self.assertEqual(502, got.status_code, body)
|
||||
self.assertEqual(
|
||||
'405 Client Error: None for url: https://foo.com/inbox ; abc xyz',
|
||||
got.text)
|
||||
body)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/follow'),
|
||||
|
@ -748,12 +732,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [self.reply, self.not_fediverse,
|
||||
self.orig_html_atom, self.orig_atom]
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/reply',
|
||||
'target': 'http://orig/post',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'http://orig/post',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/reply'),
|
||||
|
@ -792,12 +775,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
def test_salmon_like(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [self.like, self.orig_html_atom, self.orig_atom]
|
||||
|
||||
got = application.get_response(
|
||||
'/webmention', method='POST', body=urlencode({
|
||||
'source': 'http://a/like',
|
||||
'target': 'http://orig/post',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/like',
|
||||
'target': 'http://orig/post',
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/like'),
|
||||
|
@ -844,11 +826,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [self.reply, self.not_fediverse,
|
||||
self.orig_html_atom, orig_atom, webfinger]
|
||||
|
||||
got = application.get_response('/webmention', method='POST', body=urlencode({
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'http://orig/post',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_any_call(
|
||||
'http://orig/.well-known/webfinger?resource=acct:ryan@orig',
|
||||
|
@ -862,12 +844,13 @@ class WebmentionTest(testutil.TestCase):
|
|||
</html>""", 'http://orig/url')
|
||||
mock_get.side_effect = [self.reply, self.not_fediverse, orig_no_atom]
|
||||
|
||||
got = application.get_response('/webmention', method='POST', body=urlencode({
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'http://orig/post',
|
||||
}).encode())
|
||||
self.assertEqual(400, got.status_int)
|
||||
self.assertIn('Target post http://orig/url has no Atom link', got.body.decode())
|
||||
})
|
||||
self.assertEqual(400, got.status_code)
|
||||
self.assertIn('Target post http://orig/url has no Atom link',
|
||||
got.get_data(as_text=True))
|
||||
|
||||
self.assertIsNone(Response.get_by_id('http://a/reply http://orig/post'))
|
||||
|
||||
|
@ -881,11 +864,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [self.reply, self.not_fediverse, orig_relative,
|
||||
self.orig_atom]
|
||||
|
||||
got = application.get_response('/webmention', method='POST', body=urlencode({
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'http://orig/post',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_any_call('http://orig/atom/1', headers=HEADERS,
|
||||
stream=True, timeout=util.HTTP_TIMEOUT)
|
||||
|
@ -902,11 +885,11 @@ class WebmentionTest(testutil.TestCase):
|
|||
mock_get.side_effect = [self.reply, self.not_fediverse, orig_base,
|
||||
self.orig_atom]
|
||||
|
||||
got = application.get_response('/webmention', method='POST', body=urlencode({
|
||||
got = client.post('/webmention', data={
|
||||
'source': 'http://a/reply',
|
||||
'target': 'http://orig/post',
|
||||
}).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
})
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
mock_get.assert_any_call('http://orig/base/atom/1', headers=HEADERS,
|
||||
stream=True, timeout=util.HTTP_TIMEOUT)
|
||||
|
|
|
@ -155,7 +155,7 @@ class WebfingerHandler(UserHandler):
|
|||
https://tools.ietf.org/html/rfc7033#section-4
|
||||
"""
|
||||
def template_vars(self):
|
||||
resource = util.get_required_param(self, 'resource')
|
||||
resource = common.get_required_param('resource')
|
||||
try:
|
||||
user, domain = util.parse_acct_uri(resource)
|
||||
if domain in common.DOMAINS:
|
||||
|
|
|
@ -6,11 +6,13 @@ TODO tests:
|
|||
"""
|
||||
import logging
|
||||
import urllib.parse
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import django_salmon
|
||||
from django_salmon import magicsigs
|
||||
import feedparser
|
||||
from flask import request
|
||||
from flask.views import View
|
||||
from google.cloud.ndb import Key
|
||||
from granary import as2, atom, microformats2, source
|
||||
import mf2util
|
||||
|
@ -21,13 +23,15 @@ import webapp2
|
|||
from webob import exc
|
||||
|
||||
import activitypub
|
||||
from app import app
|
||||
import common
|
||||
from common import error
|
||||
from models import Follower, MagicKey, Response
|
||||
|
||||
SKIP_EMAIL_DOMAINS = frozenset(('localhost', 'snarfed.org'))
|
||||
|
||||
|
||||
class WebmentionHandler():
|
||||
class Webmention(View):
|
||||
"""Handles inbound webmention, converts to ActivityPub or Salmon."""
|
||||
source_url = None # string
|
||||
source_domain = None # string
|
||||
|
@ -35,11 +39,11 @@ class WebmentionHandler():
|
|||
source_obj = None # parsed AS1 dict
|
||||
target_resp = None # requests.Response
|
||||
|
||||
def post(self):
|
||||
logging.info('Params: %s', list(self.request.params.items()))
|
||||
def dispatch_request(self):
|
||||
logging.info(f'Params: {list(request.form.items())}')
|
||||
|
||||
# fetch source page
|
||||
source = util.get_required_param(self, 'source')
|
||||
source = common.get_required_param('source')
|
||||
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]
|
||||
|
@ -49,16 +53,16 @@ class WebmentionHandler():
|
|||
|
||||
# check for backlink to bridgy fed (for webmention spec and to confirm
|
||||
# source's intent to federate to mastodon)
|
||||
if (self.request.host_url not in source_resp.text and
|
||||
urllib.parse.quote(self.request.host_url, safe='') not in source_resp.text):
|
||||
self.error("Couldn't find link to {request.host_url}")
|
||||
if (request.host_url not in source_resp.text and
|
||||
urllib.parse.quote(request.host_url, safe='') not in source_resp.text):
|
||||
return error("Couldn't find link to {request.host_url}")
|
||||
|
||||
# convert source page to ActivityStreams
|
||||
entry = mf2util.find_first_entry(self.source_mf2, ['h-entry'])
|
||||
if not entry:
|
||||
self.error('No microformats2 found on %s' % self.source_url)
|
||||
return error('No microformats2 found on {self.source_url}')
|
||||
|
||||
logging.info('First entry: %s', json_dumps(entry, indent=2))
|
||||
logging.info(f'First entry: {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', {})
|
||||
|
@ -66,17 +70,20 @@ class WebmentionHandler():
|
|||
props['url'] = [self.source_url]
|
||||
|
||||
self.source_obj = microformats2.json_to_object(entry, fetch_mf2=True)
|
||||
logging.info('Converted to AS1: %s', json_dumps(self.source_obj, indent=2))
|
||||
logging.info(f'Converted to AS1: {json_dumps(self.source_obj, indent=2)}')
|
||||
|
||||
tried_ap = self.try_activitypub()
|
||||
if tried_ap is None:
|
||||
self.try_salmon()
|
||||
for method in self.try_activitypub, self.try_salmon:
|
||||
ret = method()
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
return ''
|
||||
|
||||
def try_activitypub(self):
|
||||
"""Attempts ActivityPub delivery.
|
||||
|
||||
Returns True if we succeeded, False if we failed, None if ActivityPub
|
||||
was not available.
|
||||
Returns Flask response (string body or tuple) if we succeeded or failed,
|
||||
None if ActivityPub was not available.
|
||||
"""
|
||||
targets = self._activitypub_targets()
|
||||
if not targets:
|
||||
|
@ -90,7 +97,7 @@ class WebmentionHandler():
|
|||
|
||||
for resp, inbox in targets:
|
||||
target_obj = json_loads(resp.target_as2) if resp.target_as2 else None
|
||||
source_activity = self.postprocess_as2(
|
||||
source_activity = common.postprocess_as2(
|
||||
as2.from_as1(self.source_obj), target=target_obj, key=key)
|
||||
|
||||
if resp.status == 'complete':
|
||||
|
@ -108,15 +115,11 @@ class WebmentionHandler():
|
|||
|
||||
# Pass the AP response status code and body through as our response
|
||||
if last_success:
|
||||
self.response.status_int = last_success.status_code
|
||||
self.response.write(last_success.text)
|
||||
return last_success.text, last_success.status_code
|
||||
elif isinstance(error, (requests.HTTPError, exc.HTTPBadGateway)):
|
||||
self.response.status_int = error.status_code
|
||||
self.response.write(str(error))
|
||||
return str(error), error.status_code
|
||||
else:
|
||||
self.response.write(str(error))
|
||||
|
||||
return bool(last_success)
|
||||
return str(error)
|
||||
|
||||
def _targets(self):
|
||||
"""
|
||||
|
@ -188,9 +191,9 @@ class WebmentionHandler():
|
|||
inbox_url = actor.get('inbox')
|
||||
actor = actor.get('url') or actor.get('id')
|
||||
if not inbox_url and not actor:
|
||||
self.error('Target object has no actor or attributedTo with URL or id.')
|
||||
return error('Target object has no actor or attributedTo with URL or id.')
|
||||
elif not isinstance(actor, str):
|
||||
self.error('Target actor or attributedTo has unexpected url or id object: %r' % actor)
|
||||
return error(f'Target actor or attributedTo has unexpected url or id object: {actor}')
|
||||
|
||||
if not inbox_url:
|
||||
# fetch actor as AS object
|
||||
|
@ -200,7 +203,7 @@ class WebmentionHandler():
|
|||
if not inbox_url:
|
||||
# TODO: probably need a way to save errors like this so that we can
|
||||
# return them if ostatus fails too.
|
||||
# self.error('Target actor has no inbox')
|
||||
# return error('Target actor has no inbox')
|
||||
continue
|
||||
|
||||
inbox_url = urllib.parse.urljoin(target_url, inbox_url)
|
||||
|
@ -209,7 +212,11 @@ class WebmentionHandler():
|
|||
return resps_and_inbox_urls
|
||||
|
||||
def try_salmon(self):
|
||||
"""Returns True if we attempted OStatus delivery. Raises otherwise."""
|
||||
"""
|
||||
Returns Flask response (string body or tuple) if we attempted OStatus
|
||||
delivery (whether successful or not), None if we didn't attempt, raises
|
||||
an exception otherwise.
|
||||
"""
|
||||
target = None
|
||||
if self.target_resp:
|
||||
target = self.target_resp.url
|
||||
|
@ -219,7 +226,7 @@ class WebmentionHandler():
|
|||
target = targets[0]
|
||||
if not target:
|
||||
logging.warning("No targets or followers. Ignoring.")
|
||||
return False
|
||||
return
|
||||
|
||||
resp = Response.get_or_create(
|
||||
source=self.source_url, target=target, direction='out',
|
||||
|
@ -249,7 +256,7 @@ class WebmentionHandler():
|
|||
parsed = util.parse_html(self.target_resp)
|
||||
atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM)
|
||||
if not atom_url or not atom_url.get('href'):
|
||||
self.error('Target post %s has no Atom link' % resp.target(), status=400)
|
||||
return error(f'Target post {resp.target()} has no Atom link')
|
||||
|
||||
# fetch Atom target post, extract and inject id into source object
|
||||
base_url = ''
|
||||
|
@ -262,7 +269,7 @@ class WebmentionHandler():
|
|||
|
||||
feed = common.requests_get(atom_url).text
|
||||
parsed = feedparser.parse(feed)
|
||||
logging.info('Parsed: %s', json_dumps(parsed, indent=2))
|
||||
logging.info(f'Parsed: {json_dumps(parsed, indent=2)}')
|
||||
entry = parsed.entries[0]
|
||||
target_id = entry.id
|
||||
in_reply_to = self.source_obj.get('inReplyTo')
|
||||
|
@ -285,7 +292,7 @@ class WebmentionHandler():
|
|||
self.source_obj.setdefault('tags', []).append({'url': url})
|
||||
|
||||
# extract and discover salmon endpoint
|
||||
logging.info('Discovering Salmon endpoint in %s', atom_url)
|
||||
logging.info(f'Discovering Salmon endpoint in {atom_url}')
|
||||
endpoint = django_salmon.discover_salmon_endpoint(feed)
|
||||
|
||||
if not endpoint:
|
||||
|
@ -305,8 +312,8 @@ class WebmentionHandler():
|
|||
pass
|
||||
|
||||
if not endpoint:
|
||||
self.error('No salmon endpoint found!', status=400)
|
||||
logging.info('Discovered Salmon endpoint %s', endpoint)
|
||||
return error('No salmon endpoint found!')
|
||||
logging.info(f'Discovered Salmon endpoint {endpoint}')
|
||||
|
||||
# construct reply Atom object
|
||||
self.source_url = resp.source()
|
||||
|
@ -314,22 +321,22 @@ class WebmentionHandler():
|
|||
if self.source_obj.get('verb') not in source.VERBS_WITH_OBJECT:
|
||||
activity = {'object': self.source_obj}
|
||||
entry = atom.activity_to_atom(activity, xml_base=self.source_url)
|
||||
logging.info('Converted %s to Atom:\n%s', self.source_url, entry)
|
||||
logging.info(f'Converted {self.source_url} to Atom:\n{entry}')
|
||||
|
||||
# sign reply and wrap in magic envelope
|
||||
domain = urllib.parse.urlparse(self.source_url).netloc
|
||||
key = MagicKey.get_or_create(domain)
|
||||
logging.info('Using key for %s: %s', domain, key)
|
||||
logging.info(f'Using key for {domain}: {key}')
|
||||
magic_envelope = magicsigs.magic_envelope(
|
||||
entry, common.CONTENT_TYPE_ATOM, key).decode()
|
||||
|
||||
logging.info('Sending Salmon slap to %s', endpoint)
|
||||
logging.info(f'Sending Salmon slap to {endpoint}')
|
||||
common.requests_post(
|
||||
endpoint, data=common.XML_UTF8 + magic_envelope,
|
||||
headers={'Content-Type': common.CONTENT_TYPE_MAGIC_ENVELOPE})
|
||||
return True
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
ROUTES = [
|
||||
('/webmention', WebmentionHandler),
|
||||
]
|
||||
app.add_url_rule('/webmention', view_func=Webmention.as_view('webmention'),
|
||||
methods=['POST'])
|
||||
|
|
Ładowanie…
Reference in New Issue