kopia lustrzana https://github.com/snarfed/bridgy-fed
misc tweaks after testing against distbin, mastodon, etc
- salmon: use top-level <entry>, not <feed> - activitypub: actor and attributedTo are objects, not string URLs - activitypub: cc public audience - activitypub: use inbox in target post object if available etc...mastodon
rodzic
541d97d2a4
commit
c9090401b0
|
@ -21,7 +21,7 @@ CONTENT_TYPE_AS = 'application/activity+json'
|
|||
CONNEG_HEADER = {
|
||||
'Accept': '%s; q=0.9, %s; q=0.8' % (CONTENT_TYPE_AS2, CONTENT_TYPE_AS),
|
||||
}
|
||||
|
||||
PUBLIC_AUDIENCE = 'https://www.w3.org/ns/activitystreams#Public'
|
||||
|
||||
class ActorHandler(webapp2.RequestHandler):
|
||||
"""Serves /[DOMAIN], fetches its mf2, converts to AS Actor, and serves it."""
|
||||
|
|
|
@ -42,14 +42,16 @@ class WebmentionTest(testutil.TestCase):
|
|||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
""", content_type='text/html; charset=utf-8')
|
||||
|
||||
def test_webmention_activitypub(self, mock_get, mock_post):
|
||||
article = requests_response({
|
||||
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||
'type': 'Article',
|
||||
'content': u'Lots of ☕ words...',
|
||||
'actor': 'http://orig/author',
|
||||
'actor': {
|
||||
'url': 'http://orig/author',
|
||||
},
|
||||
})
|
||||
actor = requests_response({
|
||||
'objectType' : 'person',
|
||||
|
@ -82,6 +84,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
'displayName': u'foo ☕ bar',
|
||||
'content': u' <a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a> ',
|
||||
'inReplyTo': [{'url': 'http://orig/post'}],
|
||||
'cc': [activitypub.PUBLIC_AUDIENCE],
|
||||
}, kwargs['json'])
|
||||
|
||||
expected_headers = copy.copy(common.HEADERS)
|
||||
|
@ -95,7 +98,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
<link href='http://orig/atom' rel='alternate' type='application/atom+xml'>
|
||||
</meta>
|
||||
</html>
|
||||
""")
|
||||
""", content_type='text/html; charset=utf-8')
|
||||
atom = requests_response("""\
|
||||
<?xml version="1.0"?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom">
|
||||
|
@ -128,16 +131,16 @@ class WebmentionTest(testutil.TestCase):
|
|||
envelope = utils.parse_magic_envelope(kwargs['data'])
|
||||
assert envelope['sig']
|
||||
|
||||
feed = utils.decode(envelope['data'])
|
||||
parsed = feedparser.parse(feed)
|
||||
data = utils.decode(envelope['data'])
|
||||
parsed = feedparser.parse(data)
|
||||
entry = parsed.entries[0]
|
||||
|
||||
self.assertEquals('http://a/reply', entry.id)
|
||||
self.assertEquals('http://a/reply', entry['id'])
|
||||
self.assertIn({
|
||||
'rel': 'alternate',
|
||||
'href': 'http://a/reply',
|
||||
'type': 'text/html',
|
||||
}, entry.links)
|
||||
}, entry['links'])
|
||||
self.assertEquals({
|
||||
'type': 'text/html',
|
||||
'href': 'http://orig/post',
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
"""Handles inbound webmentions.
|
||||
|
||||
TODO: mastodon doesn't advertise salmon endpoint in their individual post atom?!
|
||||
https://mastodon.technology/users/snarfed/updates/73978.atom
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
@ -48,26 +51,40 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
return self.send_salmon(source_obj, target_url=target)
|
||||
raise
|
||||
|
||||
if resp.headers.get('Content-Type') == 'text/html':
|
||||
if resp.headers.get('Content-Type').startswith('text/html'):
|
||||
return self.send_salmon(source_obj, target_resp=resp)
|
||||
|
||||
logging.info('Got %s', resp.headers.get('Content-Type'))
|
||||
target_obj = resp.json()
|
||||
logging.info(json.dumps(target_obj, indent=2))
|
||||
|
||||
# fetch actor as AS object
|
||||
actor_url = target_obj.get('actor') or target_obj.get('attributedTo')
|
||||
if not actor_url:
|
||||
self.abort(400, 'Target object has no actor or attributedTo')
|
||||
# find actor's inbox
|
||||
inbox_url = target_obj.get('inbox')
|
||||
|
||||
actor = common.requests_get(actor_url, parse_json=True,
|
||||
headers=activitypub.CONNEG_HEADER)
|
||||
|
||||
# deliver source object to target actor's inbox
|
||||
inbox_url = actor.get('inbox')
|
||||
if not inbox_url:
|
||||
self.abort(400, 'Target actor has no inbox')
|
||||
# fetch actor as AS object
|
||||
actor = target_obj.get('actor') or target_obj.get('attributedTo') or {}
|
||||
actor_url = actor.get('url')
|
||||
if not actor_url:
|
||||
self.abort(400, 'Target object has no actor or attributedTo URL')
|
||||
|
||||
common.requests_post(inbox_url, json=source_obj,
|
||||
headers={'Content-Type': activitypub.CONTENT_TYPE_AS})
|
||||
actor = common.requests_get(actor_url, parse_json=True,
|
||||
headers=activitypub.CONNEG_HEADER)
|
||||
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.
|
||||
# self.abort(400, 'Target actor has no inbox')
|
||||
return self.send_salmon(source_obj, target_url=target)
|
||||
|
||||
# deliver source object to target actor's inbox and public
|
||||
source_obj.setdefault('cc', []).append(activitypub.PUBLIC_AUDIENCE)
|
||||
|
||||
resp = common.requests_post(
|
||||
urlparse.urljoin(target, inbox_url), json=source_obj,
|
||||
headers={'Content-Type': activitypub.CONTENT_TYPE_AS})
|
||||
logging.info('Got: %s\n%s', resp.headers, resp.text)
|
||||
|
||||
def send_salmon(self, source_obj, target_url=None, target_resp=None):
|
||||
# fetch target HTML page, extract Atom rel-alternate link
|
||||
|
@ -81,6 +98,7 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
|
||||
parsed = BeautifulSoup(target_resp.content, from_encoding=target_resp.encoding)
|
||||
atom_url = parsed.find('link', rel='alternate', type=common.ATOM_CONTENT_TYPE)
|
||||
assert atom_url # TODO
|
||||
assert atom_url['href'] # TODO
|
||||
|
||||
# fetch Atom target post, extract id and salmon endpoint
|
||||
|
@ -92,26 +110,20 @@ class WebmentionHandler(webapp2.RequestHandler):
|
|||
logging.info('Discovering Salmon endpoint in %s', atom_url['href'])
|
||||
endpoint = django_salmon.discover_salmon_endpoint(feed)
|
||||
if not endpoint:
|
||||
author = source_obj.get('author') or {}
|
||||
common.error(self,
|
||||
'No salmon endpoint found for %s' %
|
||||
(author.get('id') or author.get('url')),
|
||||
status=400)
|
||||
common.error(self, 'No salmon endpoint found!', status=400)
|
||||
logging.info('Discovered Salmon endpoint %s', endpoint)
|
||||
|
||||
# construct reply Atom object
|
||||
source_url = self.request.get('source')
|
||||
feed = atom.activities_to_atom(
|
||||
[{'object': source_obj}], {}, host_url=source_url,
|
||||
xml_base=source_url)
|
||||
logging.info('Converted %s to Atom:\n%s', source_url, feed)
|
||||
entry = atom.activity_to_atom({'object': source_obj}, xml_base=source_url)
|
||||
logging.info('Converted %s to Atom:\n%s', source_url, entry)
|
||||
|
||||
# sign reply and wrap in magic envelope
|
||||
# TODO: use author h-card's u-url?
|
||||
domain = urlparse.urlparse(source_url).netloc.split(':')[0]
|
||||
key = models.MagicKey.get_or_create(domain)
|
||||
magic_envelope = magicsigs.magic_envelope(
|
||||
feed, common.ATOM_CONTENT_TYPE, key)
|
||||
entry, common.ATOM_CONTENT_TYPE, key)
|
||||
|
||||
logging.info('Sending Salmon slap to %s', endpoint)
|
||||
common.requests_post(
|
||||
|
|
Ładowanie…
Reference in New Issue