kopia lustrzana https://github.com/snarfed/bridgy-fed
store responses in new Response datastore model
rodzic
c34103b41a
commit
19a686edc3
|
@ -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)
|
||||
|
|
17
models.py
17
models.py
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
Ładowanie…
Reference in New Issue