kopia lustrzana https://github.com/snarfed/bridgy-fed
activitypub: follow HTML link rels that point to AS2 objects
...by reusing common.get_as2(). also lots of misc refactoring.pull/27/head
rodzic
e985516dc3
commit
c81a6b8ed2
|
@ -80,7 +80,7 @@ class InboxHandler(webapp2.RequestHandler):
|
|||
if activity.get('type') in ('Like', 'Announce'):
|
||||
actor = activity.get('actor')
|
||||
if actor:
|
||||
activity['actor'] = common.get_as2(actor)
|
||||
activity['actor'] = common.get_as2(actor).json()
|
||||
|
||||
# send webmentions to each target
|
||||
as1 = as2.to_as1(activity)
|
||||
|
|
44
common.py
44
common.py
|
@ -22,8 +22,6 @@ ACCT_RE = r'(?:acct:)?([^@]+)@' + DOMAIN_RE
|
|||
HEADERS = {
|
||||
'User-Agent': 'Bridgy Fed (https://fed.brid.gy/)',
|
||||
}
|
||||
ATOM_CONTENT_TYPE = 'application/atom+xml'
|
||||
MAGIC_ENVELOPE_CONTENT_TYPE = 'application/magic-envelope+xml'
|
||||
XML_UTF8 = "<?xml version='1.0' encoding='UTF-8'?>\n"
|
||||
USERNAME = 'me'
|
||||
# USERNAME_EMOJI = '🌎' # globe
|
||||
|
@ -35,11 +33,15 @@ CONTENT_TYPE_AS2_LD = 'application/ld+json; profile="https://www.w3.org/ns/activ
|
|||
CONTENT_TYPE_AS2 = 'application/activity+json'
|
||||
CONTENT_TYPE_AS1 = 'application/stream+json'
|
||||
CONTENT_TYPE_HTML = 'text/html'
|
||||
CONTENT_TYPE_ATOM = 'application/atom+xml'
|
||||
CONTENT_TYPE_MAGIC_ENVELOPE = 'application/magic-envelope+xml'
|
||||
|
||||
CONNEG_HEADERS_AS2 = {
|
||||
'Accept': '%s; q=0.9, %s; q=0.8' % (CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD),
|
||||
}
|
||||
CONNEG_HEADERS_AS2_HTML = copy.copy(CONNEG_HEADERS_AS2)
|
||||
CONNEG_HEADERS_AS2_HTML['Accept'] += ', %s; q=0.7' % CONTENT_TYPE_HTML
|
||||
CONNEG_HEADERS_AS2_HTML = {
|
||||
'Accept': CONNEG_HEADERS_AS2['Accept'] + ', %s; q=0.7' % CONTENT_TYPE_HTML,
|
||||
}
|
||||
|
||||
SUPPORTED_VERBS = (
|
||||
'checkin',
|
||||
|
@ -59,19 +61,20 @@ def requests_post(url, **kwargs):
|
|||
return _requests_fn(util.requests_post, url, **kwargs)
|
||||
|
||||
|
||||
def _requests_fn(fn, url, parse_json=False, log=False, **kwargs):
|
||||
def _requests_fn(fn, url, parse_json=False, **kwargs):
|
||||
"""Wraps requests.* and adds raise_for_status() and User-Agent."""
|
||||
kwargs.setdefault('headers', {}).update(HEADERS)
|
||||
|
||||
resp = fn(url, **kwargs)
|
||||
if log:
|
||||
logging.info('Got %s\n headers:%s\n%s', resp.status_code, resp.headers,
|
||||
resp.text)
|
||||
|
||||
logging.info('Got %s headers:%s', resp.status_code, resp.headers)
|
||||
type = resp.headers.get('Content-Type')
|
||||
if type and type.startswith('text/') and type != 'text/json':
|
||||
logging.info(resp.text)
|
||||
|
||||
if resp.status_code // 100 in (4, 5):
|
||||
msg = 'Received %s from %s:\n%s' % (resp.status_code, url, resp.text)
|
||||
logging.error(msg)
|
||||
raise exc.HTTPBadGateway(msg)
|
||||
raise exc.HTTPBadGateway('Received %s from %s:\n%s' %
|
||||
(resp.status_code, url, resp.text))
|
||||
|
||||
if parse_json:
|
||||
try:
|
||||
|
@ -95,32 +98,37 @@ def get_as2(url):
|
|||
url: string
|
||||
|
||||
Returns:
|
||||
dict, AS2 object parsed from JSON
|
||||
requests.Response
|
||||
|
||||
Raises:
|
||||
requests.HTTPError, webob.exc.HTTPException
|
||||
|
||||
If we raise webob HTTPException, it will have an additional response
|
||||
attribute with the last requests.Response we received.
|
||||
"""
|
||||
def _error():
|
||||
def _error(resp):
|
||||
msg = "Couldn't fetch %s as ActivityStreams 2" % url
|
||||
logging.error(msg)
|
||||
raise exc.HTTPBadGateway(msg)
|
||||
err = exc.HTTPBadGateway(msg)
|
||||
err.response = resp
|
||||
raise err
|
||||
|
||||
resp = requests_get(url, headers=CONNEG_HEADERS_AS2_HTML)
|
||||
if resp.headers.get('Content-Type') in (CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD):
|
||||
return resp.json()
|
||||
return resp
|
||||
|
||||
parsed = BeautifulSoup(resp.content, from_encoding=resp.encoding)
|
||||
as2 = parsed.find('link', rel=('alternate', 'self'), type=(
|
||||
CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD))
|
||||
if not (as2 and as2['href']):
|
||||
_error()
|
||||
_error(resp)
|
||||
|
||||
resp = requests_get(urlparse.urljoin(resp.url, as2['href']),
|
||||
headers=CONNEG_HEADERS_AS2)
|
||||
if resp.headers.get('Content-Type') in (CONTENT_TYPE_AS2, CONTENT_TYPE_AS2_LD):
|
||||
return resp.json()
|
||||
return resp
|
||||
|
||||
_error()
|
||||
_error(resp)
|
||||
|
||||
|
||||
def error(handler, msg, status=None, exc_info=False):
|
||||
|
|
|
@ -35,7 +35,7 @@ class CommonTest(testutil.TestCase):
|
|||
@mock.patch('requests.get', return_value=AS2)
|
||||
def test_get_as2_direct(self, mock_get):
|
||||
resp = common.get_as2('http://orig')
|
||||
self.assertEqual(AS2_OBJ, resp)
|
||||
self.assertEqual(AS2, resp)
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://orig', headers=common.CONNEG_HEADERS_AS2_HTML),
|
||||
))
|
||||
|
@ -43,7 +43,7 @@ class CommonTest(testutil.TestCase):
|
|||
@mock.patch('requests.get', side_effect=[HTML_WITH_AS2, AS2])
|
||||
def test_get_as2_via_html(self, mock_get):
|
||||
resp = common.get_as2('http://orig')
|
||||
self.assertEqual(AS2_OBJ, resp)
|
||||
self.assertEqual(AS2, resp)
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://orig', headers=common.CONNEG_HEADERS_AS2_HTML),
|
||||
self.req('http://as2', headers=common.CONNEG_HEADERS_AS2),
|
||||
|
|
|
@ -50,7 +50,7 @@ class SalmonTest(testutil.TestCase):
|
|||
# webmention post
|
||||
mock_post.return_value = requests_response()
|
||||
|
||||
slap = magicsigs.magic_envelope(atom_slap, common.ATOM_CONTENT_TYPE, self.key)
|
||||
slap = magicsigs.magic_envelope(atom_slap, common.CONTENT_TYPE_ATOM, self.key)
|
||||
got = app.get_response('/me@foo.com/salmon', method='POST', body=slap)
|
||||
self.assertEquals(200, got.status_int)
|
||||
|
||||
|
@ -140,7 +140,7 @@ class SalmonTest(testutil.TestCase):
|
|||
self.assertEquals(400, got.status_int)
|
||||
|
||||
def test_bad_inner_xml(self, mock_urlopen, mock_get, mock_post):
|
||||
slap = magicsigs.magic_envelope('not xml', common.ATOM_CONTENT_TYPE, self.key)
|
||||
slap = magicsigs.magic_envelope('not xml', common.CONTENT_TYPE_ATOM, self.key)
|
||||
got = app.get_response('/foo.com/salmon', method='POST', body=slap)
|
||||
self.assertEquals(400, got.status_int)
|
||||
|
||||
|
@ -152,6 +152,6 @@ class SalmonTest(testutil.TestCase):
|
|||
<uri>https://my/rsvp</uri>
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/rsvp</activity:verb>
|
||||
<activity:object>http://orig/event</activity:object>
|
||||
</entry>""", common.ATOM_CONTENT_TYPE, self.key)
|
||||
</entry>""", common.CONTENT_TYPE_ATOM, self.key)
|
||||
got = app.get_response('/foo.com/salmon', method='POST', body=slap)
|
||||
self.assertEquals(501, got.status_int)
|
||||
|
|
|
@ -6,7 +6,6 @@ TODO: test error handling
|
|||
from __future__ import unicode_literals
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
|
@ -22,7 +21,15 @@ from oauth_dropins.webutil.testutil import requests_response
|
|||
import requests
|
||||
|
||||
import activitypub
|
||||
import common
|
||||
from common import (
|
||||
AS2_PUBLIC_AUDIENCE,
|
||||
CONNEG_HEADERS_AS2,
|
||||
CONNEG_HEADERS_AS2_HTML,
|
||||
CONTENT_TYPE_AS2,
|
||||
CONTENT_TYPE_HTML,
|
||||
CONTENT_TYPE_MAGIC_ENVELOPE,
|
||||
HEADERS,
|
||||
)
|
||||
from models import MagicKey, Response
|
||||
import testutil
|
||||
import webmention
|
||||
|
@ -37,14 +44,21 @@ class WebmentionTest(testutil.TestCase):
|
|||
super(WebmentionTest, self).setUp()
|
||||
self.key = MagicKey.get_or_create('a')
|
||||
|
||||
self.orig = requests_response("""\
|
||||
self.orig_html_as2 = requests_response("""\
|
||||
<html>
|
||||
<meta>
|
||||
<link href='http://orig/atom' rel='alternate' type='application/atom+xml'>
|
||||
<link href='http://orig/as2' rel='alternate' type='application/activity+json'>
|
||||
</meta>
|
||||
</html>
|
||||
""", url='http://orig/post', content_type=CONTENT_TYPE_HTML)
|
||||
self.orig_html_atom = requests_response("""\
|
||||
<html>
|
||||
<meta>
|
||||
<link href='http://orig/atom' rel='alternate' type='application/atom+xml'>
|
||||
</meta>
|
||||
</html>
|
||||
""", url='http://orig/post', content_type='text/html; charset=utf-8')
|
||||
|
||||
""", url='http://orig/post', content_type=CONTENT_TYPE_HTML)
|
||||
self.orig_atom = requests_response("""\
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
|
@ -53,7 +67,14 @@ class WebmentionTest(testutil.TestCase):
|
|||
<content type="html">baz ☕ baj</content>
|
||||
</entry>
|
||||
""")
|
||||
|
||||
self.orig_as2 = requests_response({
|
||||
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||
'type': 'Article',
|
||||
'content': 'Lots of ☕ words...',
|
||||
'actor': {
|
||||
'url': 'http://orig/author',
|
||||
},
|
||||
}, url='http://orig/as2', content_type=CONTENT_TYPE_AS2)
|
||||
|
||||
self.reply_html = """\
|
||||
<html>
|
||||
|
@ -70,7 +91,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
</html>
|
||||
"""
|
||||
self.reply = requests_response(
|
||||
self.reply_html, content_type='text/html; charset=utf-8')
|
||||
self.reply_html, content_type=CONTENT_TYPE_HTML)
|
||||
self.reply_mf2 = mf2py.parse(self.reply_html, url='http://a/reply')
|
||||
self.reply_obj = microformats2.json_to_object(self.reply_mf2['items'][0])
|
||||
|
||||
|
@ -84,7 +105,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
</html>
|
||||
"""
|
||||
self.repost = requests_response(
|
||||
self.repost_html, content_type='text/html; charset=utf-8')
|
||||
self.repost_html, content_type=CONTENT_TYPE_HTML)
|
||||
self.repost_mf2 = mf2py.parse(self.repost_html, url='http://a/repost')
|
||||
|
||||
self.like_html = """\
|
||||
|
@ -98,24 +119,16 @@ class WebmentionTest(testutil.TestCase):
|
|||
</html>
|
||||
"""
|
||||
self.like = requests_response(
|
||||
self.like_html, content_type='text/html; charset=utf-8')
|
||||
self.like_html, content_type=CONTENT_TYPE_HTML)
|
||||
self.like_mf2 = mf2py.parse(self.like_html, url='http://a/like')
|
||||
|
||||
self.article = requests_response({
|
||||
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||
'type': 'Article',
|
||||
'content': 'Lots of ☕ words...',
|
||||
'actor': {
|
||||
'url': 'http://orig/author',
|
||||
},
|
||||
})
|
||||
self.actor = requests_response({
|
||||
'objectType' : 'person',
|
||||
'displayName': 'Mrs. ☕ Foo',
|
||||
'url': 'https://foo.com/about-me',
|
||||
'inbox': 'https://foo.com/inbox',
|
||||
})
|
||||
self.activitypub_gets = [self.reply, self.article, self.actor]
|
||||
}, content_type=CONTENT_TYPE_AS2)
|
||||
self.activitypub_gets = [self.reply, self.orig_as2, self.actor]
|
||||
|
||||
self.as2_create = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
|
@ -129,7 +142,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
'content': ' <a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a> <a href="https://fed.brid.gy/"></a> ',
|
||||
'inReplyTo': 'http://orig/post',
|
||||
'cc': [
|
||||
common.AS2_PUBLIC_AUDIENCE,
|
||||
AS2_PUBLIC_AUDIENCE,
|
||||
'http://orig/post',
|
||||
],
|
||||
'attributedTo': [{
|
||||
|
@ -146,7 +159,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
def verify_salmon(self, mock_post):
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('http://orig/salmon',), args)
|
||||
self.assertEqual(common.MAGIC_ENVELOPE_CONTENT_TYPE,
|
||||
self.assertEqual(CONTENT_TYPE_MAGIC_ENVELOPE,
|
||||
kwargs['headers']['Content-Type'])
|
||||
|
||||
env = utils.parse_magic_envelope(kwargs['data'])
|
||||
|
@ -167,23 +180,22 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEquals(200, got.status_int)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
call('http://a/reply', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/post', headers=activitypub.CONNEG_HEADER,
|
||||
timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/author', headers=activitypub.CONNEG_HEADER,
|
||||
timeout=util.HTTP_TIMEOUT),))
|
||||
self.req('http://a/reply'),
|
||||
self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
self.req('http://orig/author', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
))
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('https://foo.com/inbox',), args)
|
||||
self.assertEqual(self.as2_create, kwargs['json'])
|
||||
|
||||
headers = kwargs['headers']
|
||||
self.assertEqual(activitypub.CONTENT_TYPE_AS, headers['Content-Type'])
|
||||
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())
|
||||
|
||||
resp = Response.get_by_id('http://a/reply http://orig/post')
|
||||
resp = Response.get_by_id('http://a/reply http://orig/as2')
|
||||
self.assertEqual('out', resp.direction)
|
||||
self.assertEqual('activitypub', resp.protocol)
|
||||
self.assertEqual('complete', resp.status)
|
||||
|
@ -196,7 +208,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
# self.assertEqual(['abc xyz'], resp.responses)
|
||||
|
||||
def test_activitypub_update_reply(self, mock_get, mock_post):
|
||||
Response(id='http://a/reply http://orig/post', status='complete').put()
|
||||
Response(id='http://a/reply http://orig/as2', status='complete').put()
|
||||
|
||||
mock_get.side_effect = self.activitypub_gets
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
@ -213,7 +225,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEqual(self.as2_update, kwargs['json'])
|
||||
|
||||
def test_activitypub_create_repost(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [self.repost, self.article, self.actor]
|
||||
mock_get.side_effect = [self.repost, self.orig_as2, self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
||||
got = app.get_response(
|
||||
|
@ -224,11 +236,10 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEquals(200, got.status_int)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
call('http://a/repost', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/post', headers=activitypub.CONNEG_HEADER,
|
||||
timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/author', headers=activitypub.CONNEG_HEADER,
|
||||
timeout=util.HTTP_TIMEOUT),))
|
||||
self.req('http://a/repost'),
|
||||
self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
self.req('http://orig/author', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
))
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('https://foo.com/inbox',), args)
|
||||
|
@ -238,7 +249,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
'url': 'http://a/repost',
|
||||
'name': 'reposted!',
|
||||
'object': 'http://orig/post',
|
||||
'cc': [common.AS2_PUBLIC_AUDIENCE],
|
||||
'cc': [AS2_PUBLIC_AUDIENCE],
|
||||
'actor': {
|
||||
'type': 'Person',
|
||||
'url': 'http://orig',
|
||||
|
@ -247,19 +258,42 @@ class WebmentionTest(testutil.TestCase):
|
|||
}, kwargs['json'])
|
||||
|
||||
headers = kwargs['headers']
|
||||
self.assertEqual(activitypub.CONTENT_TYPE_AS, headers['Content-Type'])
|
||||
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())
|
||||
|
||||
resp = Response.get_by_id('http://a/repost http://orig/post')
|
||||
resp = Response.get_by_id('http://a/repost http://orig/as2')
|
||||
self.assertEqual('out', resp.direction)
|
||||
self.assertEqual('activitypub', resp.protocol)
|
||||
self.assertEqual('complete', resp.status)
|
||||
self.assertEqual(self.repost_mf2, json.loads(resp.source_mf2))
|
||||
|
||||
def test_activitypub_link_rel_alternate_as2(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [self.reply, self.orig_html_as2, self.orig_as2,
|
||||
self.actor]
|
||||
mock_post.return_value = requests_response('abc xyz')
|
||||
|
||||
got = app.get_response(
|
||||
'/webmention', method='POST', body=urllib.urlencode({
|
||||
'source': 'http://a/reply',
|
||||
'target': 'https://fed.brid.gy/',
|
||||
}))
|
||||
self.assertEquals(200, got.status_int)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.req('http://a/reply'),
|
||||
self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
self.req('http://orig/as2', headers=CONNEG_HEADERS_AS2),
|
||||
self.req('http://orig/author', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
))
|
||||
|
||||
args, kwargs = mock_post.call_args
|
||||
self.assertEqual(('https://foo.com/inbox',), args)
|
||||
self.assertEqual(self.as2_create, kwargs['json'])
|
||||
|
||||
def test_salmon_reply(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [self.reply, self.orig, self.orig_atom]
|
||||
mock_get.side_effect = [self.reply, self.orig_html_atom, self.orig_atom]
|
||||
|
||||
got = app.get_response(
|
||||
'/webmention', method='POST', body=urllib.urlencode({
|
||||
|
@ -269,10 +303,9 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEquals(200, got.status_int)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
call('http://a/reply', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/post', headers=activitypub.CONNEG_HEADER,
|
||||
timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/atom', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
||||
self.req('http://a/reply'),
|
||||
self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
self.req('http://orig/atom'),
|
||||
))
|
||||
|
||||
data = self.verify_salmon(mock_post)
|
||||
|
@ -301,7 +334,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEqual(self.reply_mf2, json.loads(resp.source_mf2))
|
||||
|
||||
def test_salmon_like(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [self.like, self.orig, self.orig_atom]
|
||||
mock_get.side_effect = [self.like, self.orig_html_atom, self.orig_atom]
|
||||
|
||||
got = app.get_response(
|
||||
'/webmention', method='POST', body=urllib.urlencode({
|
||||
|
@ -311,10 +344,9 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEquals(200, got.status_int)
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
call('http://a/like', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/post', headers=activitypub.CONNEG_HEADER,
|
||||
timeout=util.HTTP_TIMEOUT),
|
||||
call('http://orig/atom', headers=common.HEADERS, timeout=util.HTTP_TIMEOUT),
|
||||
self.req('http://a/like'),
|
||||
self.req('http://orig/post', headers=CONNEG_HEADERS_AS2_HTML),
|
||||
self.req('http://orig/atom'),
|
||||
))
|
||||
|
||||
data = self.verify_salmon(mock_post)
|
||||
|
@ -353,7 +385,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
'href': 'http://orig/@ryan/salmon',
|
||||
}],
|
||||
})
|
||||
mock_get.side_effect = [self.reply, self.orig, orig_atom, webfinger]
|
||||
mock_get.side_effect = [self.reply, self.orig_html_atom, orig_atom, webfinger]
|
||||
|
||||
got = app.get_response('/webmention', method='POST', body=urllib.urlencode({
|
||||
'source': 'http://a/reply',
|
||||
|
@ -363,7 +395,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
|
||||
mock_get.assert_any_call(
|
||||
'http://orig/.well-known/webfinger?resource=acct:ryan@orig',
|
||||
headers=common.HEADERS, timeout=util.HTTP_TIMEOUT, verify=False)
|
||||
headers=HEADERS, timeout=util.HTTP_TIMEOUT, verify=False)
|
||||
self.assertEqual(('http://orig/@ryan/salmon',), mock_post.call_args[0])
|
||||
|
||||
def test_salmon_no_target_atom(self, mock_get, mock_post):
|
||||
|
|
|
@ -65,7 +65,7 @@ representative h-card</a> on %s""" % resp.url)
|
|||
canonical_url = urls[0]
|
||||
|
||||
# discover atom feed, if any
|
||||
atom = parsed.find('link', rel='alternate', type=common.ATOM_CONTENT_TYPE)
|
||||
atom = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM)
|
||||
if atom and atom['href']:
|
||||
atom = urlparse.urljoin(resp.url, atom['href'])
|
||||
else:
|
||||
|
@ -120,7 +120,7 @@ representative h-card</a> on %s""" % resp.url)
|
|||
# OStatus
|
||||
{
|
||||
'rel': 'http://schemas.google.com/g/2010#updates-from',
|
||||
'type': common.ATOM_CONTENT_TYPE,
|
||||
'type': common.CONTENT_TYPE_ATOM,
|
||||
'href': atom,
|
||||
}, {
|
||||
'rel': 'hub',
|
||||
|
|
|
@ -23,6 +23,7 @@ import mf2util
|
|||
from oauth_dropins.webutil import util
|
||||
import requests
|
||||
import webapp2
|
||||
from webob import exc
|
||||
|
||||
import activitypub
|
||||
import common
|
||||
|
@ -51,12 +52,12 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
logging.warning('Error sending email', exc_info=True)
|
||||
|
||||
# fetch source page, convert to ActivityStreams
|
||||
resp = common.requests_get(source)
|
||||
mf2 = mf2py.parse(resp.text, url=resp.url)
|
||||
# logging.debug('Parsed mf2 for %s: %s', resp.url, json.dumps(mf2, indent=2))
|
||||
source_url = resp.url or source
|
||||
source_resp = common.requests_get(source)
|
||||
source_url = source_resp.url or source
|
||||
source_mf2 = mf2py.parse(source_resp.text, url=source_url)
|
||||
# logging.debug('Parsed mf2 for %s: %s', source_resp.url, json.dumps(mf2, indent=2))
|
||||
|
||||
entry = mf2util.find_first_entry(mf2, ['h-entry'])
|
||||
entry = mf2util.find_first_entry(source_mf2, ['h-entry'])
|
||||
logging.info('First entry: %s', json.dumps(entry, indent=2))
|
||||
source_obj = microformats2.json_to_object(entry)
|
||||
logging.info('Converted to AS: %s', json.dumps(source_obj, indent=2))
|
||||
|
@ -70,22 +71,20 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
'found in %s' % source_url)
|
||||
|
||||
try:
|
||||
resp = common.requests_get(target, headers=activitypub.CONNEG_HEADER,
|
||||
log=True)
|
||||
target_url = resp.url or target
|
||||
except requests.HTTPError as e:
|
||||
if e.response.status_code // 100 == 4:
|
||||
return self.send_salmon(source_obj, target_url=target_url)
|
||||
target_resp = common.get_as2(target)
|
||||
except (requests.HTTPError, exc.HTTPBadGateway) as e:
|
||||
if (e.response.status_code // 100 == 2 and
|
||||
e.response.headers.get('Content-Type').startswith('text/html')):
|
||||
return self.send_salmon(source_obj, source_mf2, target_resp=e.response)
|
||||
raise
|
||||
|
||||
self.response = Response.get_or_create(
|
||||
target_url = target_resp.url or target
|
||||
stored_response = Response.get_or_create(
|
||||
source=source_url, target=target_url, direction='out',
|
||||
source_mf2=json.dumps(mf2))
|
||||
if resp.headers.get('Content-Type').startswith('text/html'):
|
||||
return self.send_salmon(source_obj, target_resp=resp)
|
||||
source_mf2=json.dumps(source_mf2))
|
||||
|
||||
# find actor's inbox
|
||||
target_obj = resp.json()
|
||||
target_obj = target_resp.json()
|
||||
inbox_url = target_obj.get('inbox')
|
||||
|
||||
if not inbox_url:
|
||||
|
@ -99,22 +98,21 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
|
||||
if not inbox_url:
|
||||
# fetch actor as AS object
|
||||
actor = common.requests_get(actor, parse_json=True,
|
||||
headers=activitypub.CONNEG_HEADER)
|
||||
actor = common.get_as2(actor).json()
|
||||
inbox_url = actor.get('inbox')
|
||||
|
||||
if not inbox_url:
|
||||
# TODO: probably need a way to save errors like this so that we can
|
||||
# return them if ostatus fails too.
|
||||
# common.error(self, 'Target actor has no inbox')
|
||||
return self.send_salmon(source_obj, target_url=target_url)
|
||||
return self.send_salmon(source_obj, source_mf2, target_resp=target_resp)
|
||||
|
||||
# convert to AS2
|
||||
source_domain = urlparse.urlparse(source_url).netloc
|
||||
key = MagicKey.get_or_create(source_domain)
|
||||
source_activity = common.postprocess_as2(as2.from_as1(source_obj), key=key)
|
||||
|
||||
if self.response.status == 'complete':
|
||||
if stored_response.status == 'complete':
|
||||
source_activity['type'] = 'Update'
|
||||
|
||||
# prepare HTTP Signature (required by Mastodon)
|
||||
|
@ -127,33 +125,32 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
|
||||
# deliver source object to target actor's inbox.
|
||||
headers = {
|
||||
'Content-Type': activitypub.CONTENT_TYPE_AS,
|
||||
'Content-Type': common.CONTENT_TYPE_AS2,
|
||||
# required for HTTP Signature
|
||||
# https://tools.ietf.org/html/draft-cavage-http-signatures-07#section-2.1.3
|
||||
'Date': datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
|
||||
}
|
||||
common.requests_post(
|
||||
urlparse.urljoin(target_url, inbox_url), json=source_activity, auth=auth,
|
||||
headers=headers, log=True)
|
||||
headers=headers)
|
||||
|
||||
self.response.status = 'complete'
|
||||
self.response.protocol = 'activitypub'
|
||||
self.response.put()
|
||||
stored_response.status = 'complete'
|
||||
stored_response.protocol = 'activitypub'
|
||||
stored_response.put()
|
||||
|
||||
def send_salmon(self, source_obj, target_url=None, target_resp=None):
|
||||
def send_salmon(self, source_obj, source_mf2, target_url=None, target_resp=None):
|
||||
# fetch target HTML page, extract Atom rel-alternate link
|
||||
if target_url:
|
||||
assert not target_resp
|
||||
target_resp = common.requests_get(target_url)
|
||||
else:
|
||||
assert target_resp
|
||||
# TODO: this could be different due to redirects
|
||||
target_url = target_resp.url
|
||||
|
||||
parsed = BeautifulSoup(target_resp.content, from_encoding=target_resp.encoding)
|
||||
atom_url = parsed.find('link', rel='alternate', type=common.ATOM_CONTENT_TYPE)
|
||||
if not atom_url or not atom_url['href']:
|
||||
common.error(self, 'Target post %s has no Atom link' % target_resp.url,
|
||||
atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM)
|
||||
if not atom_url or not atom_url.get('href'):
|
||||
common.error(self, 'Target post %s has no Atom link' % target_url,
|
||||
status=400)
|
||||
|
||||
# fetch Atom target post, extract and inject id into source object
|
||||
|
@ -194,8 +191,7 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
# TODO: always https?
|
||||
resp = common.requests_get(
|
||||
'%s://%s/.well-known/webfinger?resource=acct:%s' %
|
||||
(parsed.scheme, parsed.netloc, email),
|
||||
log=True, verify=False)
|
||||
(parsed.scheme, parsed.netloc, email), verify=False)
|
||||
endpoint = django_salmon.get_salmon_replies_link(resp.json())
|
||||
except requests.HTTPError as e:
|
||||
pass
|
||||
|
@ -216,16 +212,16 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
key = MagicKey.get_or_create(domain)
|
||||
logging.info('Using key for %s: %s', domain, key)
|
||||
magic_envelope = magicsigs.magic_envelope(
|
||||
entry, common.ATOM_CONTENT_TYPE, key)
|
||||
entry, common.CONTENT_TYPE_ATOM, key)
|
||||
|
||||
logging.info('Sending Salmon slap to %s', endpoint)
|
||||
common.requests_post(
|
||||
endpoint, data=common.XML_UTF8 + magic_envelope, log=True,
|
||||
headers={'Content-Type': common.MAGIC_ENVELOPE_CONTENT_TYPE})
|
||||
endpoint, data=common.XML_UTF8 + magic_envelope,
|
||||
headers={'Content-Type': common.CONTENT_TYPE_MAGIC_ENVELOPE})
|
||||
|
||||
self.response.status = 'complete'
|
||||
self.response.protocol = 'ostatus'
|
||||
self.response.put()
|
||||
Response(source=source_url, target=target_url, direction='out',
|
||||
protocol = 'ostatus', status = 'complete',
|
||||
source_mf2=json.dumps(source_mf2)).put()
|
||||
|
||||
|
||||
app = webapp2.WSGIApplication([
|
||||
|
|
Ładowanie…
Reference in New Issue