From c81a6b8ed266b4a2471b2d1178b280bda01a2167 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Fri, 20 Oct 2017 07:49:25 -0700 Subject: [PATCH] activitypub: follow HTML link rels that point to AS2 objects ...by reusing common.get_as2(). also lots of misc refactoring. --- activitypub.py | 2 +- common.py | 44 ++++++++------ test/test_common.py | 4 +- test/test_salmon.py | 6 +- test/test_webmention.py | 132 +++++++++++++++++++++++++--------------- webfinger.py | 4 +- webmention.py | 72 +++++++++++----------- 7 files changed, 150 insertions(+), 114 deletions(-) diff --git a/activitypub.py b/activitypub.py index bf69c52..a08c6bb 100644 --- a/activitypub.py +++ b/activitypub.py @@ -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) diff --git a/common.py b/common.py index 7b6a45c..493e9f4 100644 --- a/common.py +++ b/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 = "\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): diff --git a/test/test_common.py b/test/test_common.py index 45db5d5..ec6b2da 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -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), diff --git a/test/test_salmon.py b/test/test_salmon.py index 01f7e08..adccb64 100644 --- a/test/test_salmon.py +++ b/test/test_salmon.py @@ -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): https://my/rsvp http://activitystrea.ms/schema/1.0/rsvp http://orig/event -""", common.ATOM_CONTENT_TYPE, self.key) +""", common.CONTENT_TYPE_ATOM, self.key) got = app.get_response('/foo.com/salmon', method='POST', body=slap) self.assertEquals(501, got.status_int) diff --git a/test/test_webmention.py b/test/test_webmention.py index df42ed8..9b0fef5 100644 --- a/test/test_webmention.py +++ b/test/test_webmention.py @@ -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("""\ + + + + + + +""", url='http://orig/post', content_type=CONTENT_TYPE_HTML) + self.orig_html_atom = requests_response("""\ -""", 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("""\ @@ -53,7 +67,14 @@ class WebmentionTest(testutil.TestCase): baz ☕ baj """) - + 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 = """\ @@ -70,7 +91,7 @@ class WebmentionTest(testutil.TestCase): """ 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): """ 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): """ 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': ' foo ☕ bar ', '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): diff --git a/webfinger.py b/webfinger.py index 5120160..4a77d24 100644 --- a/webfinger.py +++ b/webfinger.py @@ -65,7 +65,7 @@ representative h-card 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 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', diff --git a/webmention.py b/webmention.py index 3561140..d6be908 100644 --- a/webmention.py +++ b/webmention.py @@ -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([