store responses in new Response datastore model

pull/27/head
Ryan Barrett 2017-10-09 19:29:50 -05:00
rodzic c34103b41a
commit 19a686edc3
7 zmienionych plików z 98 dodań i 22 usunięć

Wyświetl plik

@ -13,7 +13,7 @@ import webapp2
from webmentiontools import send
import common
import models
from models import MagicKey, Response
# https://www.w3.org/TR/activitypub/#retrieving-objects
@ -39,7 +39,7 @@ class ActorHandler(webapp2.RequestHandler):
Couldn't find a <a href="http://microformats.org/wiki/representative-hcard-parsing">\
representative h-card</a> on %s""" % resp.url)
key = models.MagicKey.get_or_create(domain)
key = MagicKey.get_or_create(domain)
obj = common.postprocess_as2(as2.from_as1(microformats2.json_to_object(hcard)),
key=key)
obj.update({
@ -83,13 +83,18 @@ class InboxHandler(webapp2.RequestHandler):
errors = []
for target in targets:
response = Response.get_or_insert(
'%s %s' % (source, target), direction='in', protocol='activitypub')
logging.info('Sending webmention from %s to %s', source, target)
wm = send.WebmentionSend(source, target)
if wm.send(headers=common.HEADERS):
logging.info('Success: %s', wm.response)
response.status = 'complete'
else:
logging.warning('Failed: %s', wm.error)
errors.append(wm.error)
response.status = 'error'
response.put()
if errors:
msg = 'Errors:\n' + '\n'.join(json.dumps(e, indent=2) for e in errors)

Wyświetl plik

@ -55,3 +55,20 @@ class MagicKey(StringIdModel):
magicsigs.base64_to_long(str(self.public_exponent)),
magicsigs.base64_to_long(str(self.private_exponent))))
return rsa.exportKey(format='PEM')
class Response(StringIdModel):
"""A reply, like, repost, or other interaction that we've relayed.
Key name is 'SOURCE_URL TARGET_URL', e.g. 'http://a/reply http://orig/post'.
"""
STATUSES = ('new', 'complete', 'error')
PROTOCOLS = ('activitypub', 'ostatus')
DIRECTIONS = ('out', 'in')
status = ndb.StringProperty(choices=STATUSES, default='new')
protocol = ndb.StringProperty(choices=PROTOCOLS)
direction = ndb.StringProperty(choices=DIRECTIONS)
created = ndb.DateTimeProperty(auto_now_add=True)
updated = ndb.DateTimeProperty(auto_now=True)

Wyświetl plik

@ -14,6 +14,7 @@ import webapp2
from webmentiontools import send
import common
from models import Response
# from django_salmon.feeds
ATOM_NS = 'http://www.w3.org/2005/Atom'
@ -70,13 +71,18 @@ class SlapHandler(webapp2.RequestHandler):
# send webmentions!
errors = []
for target in targets:
response = Response.get_or_insert(
'%s %s' % (source, target), direction='in', protocol='ostatus')
logging.info('Sending webmention from %s to %s', source, target)
wm = send.WebmentionSend(source, target)
if wm.send(headers=common.HEADERS):
logging.info('Success: %s', wm.response)
response.status = 'complete'
else:
logging.warning('Failed: %s', wm.error)
errors.append(wm.error)
response.status = 'error'
response.put()
if errors:
self.abort(errors[0].get('http_status') or 400,

Wyświetl plik

@ -16,7 +16,7 @@ import requests
import activitypub
from activitypub import app
import common
import models
from models import MagicKey, Response
import testutil
@ -44,7 +44,7 @@ class ActivityPubTest(testutil.TestCase):
'url': 'https://foo.com/about-me',
'inbox': 'http://localhost/foo.com/inbox',
'publicKey': {
'publicKeyPem': models.MagicKey.get_by_id('foo.com').public_pem(),
'publicKeyPem': MagicKey.get_by_id('foo.com').public_pem(),
},
}, json.loads(got.body))
@ -93,6 +93,11 @@ class ActivityPubTest(testutil.TestCase):
headers=expected_headers,
verify=False)
resp = Response.get_by_id('http://this/reply http://orig/post')
self.assertEqual('in', resp.direction)
self.assertEqual('activitypub', resp.protocol)
self.assertEqual('complete', resp.status)
def test_inbox_like_not_supported(self, mock_get, mock_post):
got = app.get_response('/foo.com/inbox', method='POST',
body=json.dumps({
@ -101,3 +106,5 @@ class ActivityPubTest(testutil.TestCase):
'object': 'http://orig/post',
}))
self.assertEquals(400, got.status_int)
self.assertIsNone(Response.get_by_id('http://a/reply http://orig/post'))

Wyświetl plik

@ -14,7 +14,7 @@ from oauth_dropins.webutil.testutil import requests_response, UrlopenResult
import requests
import common
import models
from models import MagicKey, Response
from salmon import app
import testutil
@ -26,7 +26,7 @@ class SalmonTest(testutil.TestCase):
def test_slap(self, mock_urlopen, mock_get, mock_post):
# salmon magic key discovery. first host-meta, then webfinger
key = models.MagicKey.get_or_create('alice')
key = MagicKey.get_or_create('alice')
mock_urlopen.side_effect = [
UrlopenResult(200, """\
<?xml version='1.0' encoding='UTF-8'?>
@ -88,3 +88,8 @@ class SalmonTest(testutil.TestCase):
allow_redirects=False,
headers=expected_headers,
verify=False)
resp = Response.get_by_id('https://my/reply http://orig/post')
self.assertEqual('in', resp.direction)
self.assertEqual('ostatus', resp.protocol)
self.assertEqual('complete', resp.status)

Wyświetl plik

@ -23,7 +23,7 @@ import requests
import activitypub
import common
from models import MagicKey
from models import MagicKey, Response
import testutil
import webmention
from webmention import app
@ -77,8 +77,8 @@ class WebmentionTest(testutil.TestCase):
'url': 'https://foo.com/about-me',
'inbox': 'https://foo.com/inbox',
})
mock_get.side_effect = [self.reply, article, actor]
mock_post.return_value = requests_response('abc xyz')
got = app.get_response(
'/webmention', method='POST', body=urllib.urlencode({
@ -94,9 +94,7 @@ class WebmentionTest(testutil.TestCase):
call('http://orig/author', headers=activitypub.CONNEG_HEADER,
timeout=util.HTTP_TIMEOUT),))
args, kwargs = mock_post.call_args
self.assertEqual(('https://foo.com/inbox',), args)
self.assertEqual({
expected_as2 = {
'@context': 'https://www.w3.org/ns/activitystreams',
'type': 'Create',
'object': {
@ -118,7 +116,10 @@ class WebmentionTest(testutil.TestCase):
'displayName': 'Ms. ☕ Baz',
}],
},
}, kwargs['json'])
}
args, kwargs = mock_post.call_args
self.assertEqual(('https://foo.com/inbox',), args)
self.assertEqual(expected_as2, kwargs['json'])
headers = kwargs['headers']
self.assertEqual(activitypub.CONTENT_TYPE_AS, headers['Content-Type'])
@ -127,6 +128,17 @@ class WebmentionTest(testutil.TestCase):
rsa_key = kwargs['auth'].header_signer._rsa._key
self.assertEqual(expected_key.private_pem(), rsa_key.exportKey())
resp = Response.get_by_id('http://a/reply http://orig/post')
self.assertEqual('out', resp.direction)
self.assertEqual('activitypub', resp.protocol)
self.assertEqual('complete', resp.status)
# TODO: if i do this, maybe switch to separate HttpRequest model and
# foreign key
# self.assertEqual([expected_as2], resp.request_statuses)
# self.assertEqual([expected_as2], resp.requests)
# self.assertEqual(['abc xyz'], resp.responses)
def test_salmon(self, mock_get, mock_post):
orig_atom = requests_response("""\
<?xml version="1.0"?>
@ -183,6 +195,11 @@ class WebmentionTest(testutil.TestCase):
'<a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a> <a href="https://fed.brid.gy/"></a>',
entry.content[0]['value'])
resp = Response.get_by_id('http://a/reply http://orig/post')
self.assertEqual('out', resp.direction)
self.assertEqual('ostatus', resp.protocol)
self.assertEqual('complete', resp.status)
def test_salmon_get_salmon_from_webfinger(self, mock_get, mock_post):
orig_atom = requests_response("""\
<?xml version="1.0"?>
@ -227,3 +244,6 @@ class WebmentionTest(testutil.TestCase):
}))
self.assertEquals(400, got.status_int)
self.assertIn('Target post http://orig/url has no Atom link', got.body)
self.assertIsNone(Response.get_by_id('http://a/reply http://orig/post'))

Wyświetl plik

@ -25,11 +25,15 @@ import webapp2
import activitypub
import common
import models
from models import MagicKey, Response
class WebmentionHandler(webapp2.RequestHandler):
"""Handles inbound webmention, converts to ActivityPub or Salmon."""
"""Handles inbound webmention, converts to ActivityPub or Salmon.
Instance attributes:
response: Response
"""
def post(self):
logging.info('Params: %s', self.request.params.items())
@ -40,6 +44,7 @@ class WebmentionHandler(webapp2.RequestHandler):
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
entry = mf2util.find_first_entry(mf2, ['h-entry'])
logging.info('First entry: %s', json.dumps(entry, indent=2))
@ -52,16 +57,19 @@ class WebmentionHandler(webapp2.RequestHandler):
if isinstance(target, dict):
target = target.get('url')
if not target:
self.abort(400, 'No u-in-reply-to found in %s' % source)
self.abort(400, 'No u-in-reply-to 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)
return self.send_salmon(source_obj, target_url=target_url)
raise
self.response = Response.get_or_insert('%s %s' % (source_url, target_url),
direction='out')
if resp.headers.get('Content-Type').startswith('text/html'):
return self.send_salmon(source_obj, target_resp=resp)
@ -88,11 +96,11 @@ class WebmentionHandler(webapp2.RequestHandler):
# TODO: probably need a way to save errors like this so that we can
# return them if ostatus fails too.
# self.abort(400, 'Target actor has no inbox')
return self.send_salmon(source_obj, target_url=target)
return self.send_salmon(source_obj, target_url=target_url)
# convert to AS2
source_domain = urlparse.urlparse(source).netloc
key = models.MagicKey.get_or_create(source_domain)
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)
# prepare HTTP Signature (required by Mastodon)
@ -110,10 +118,14 @@ class WebmentionHandler(webapp2.RequestHandler):
# 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'),
}
resp = common.requests_post(
urlparse.urljoin(target, inbox_url), json=source_activity, auth=auth,
common.requests_post(
urlparse.urljoin(target_url, inbox_url), json=source_activity, auth=auth,
headers=headers, log=True)
self.response.status = 'complete'
self.response.protocol = 'activitypub'
self.response.put()
def send_salmon(self, source_obj, target_url=None, target_resp=None):
# fetch target HTML page, extract Atom rel-alternate link
if target_url:
@ -179,7 +191,7 @@ class WebmentionHandler(webapp2.RequestHandler):
# sign reply and wrap in magic envelope
domain = urlparse.urlparse(source_url).netloc
key = models.MagicKey.get_or_create(domain)
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)
@ -189,6 +201,10 @@ class WebmentionHandler(webapp2.RequestHandler):
endpoint, data=common.XML_UTF8 + magic_envelope, log=True,
headers={'Content-Type': common.MAGIC_ENVELOPE_CONTENT_TYPE})
self.response.status = 'complete'
self.response.protocol = 'ostatus'
self.response.put()
app = webapp2.WSGIApplication([
('/webmention', WebmentionHandler),