add webmention endpoint and conversion to AP inbox delivery

mastodon
Ryan Barrett 2017-08-15 07:39:22 -07:00
rodzic 576410005b
commit 04e65d612d
4 zmienionych plików z 166 dodań i 18 usunięć

Wyświetl plik

@ -9,7 +9,6 @@ from granary import microformats2
import mf2py
import mf2util
from oauth_dropins.webutil import util
import requests
import webapp2
from webmentiontools import send

Wyświetl plik

@ -3,8 +3,9 @@
import json
import logging
import mf2py
import requests
from webob import exc
HEADERS = {
'User-Agent': 'bridgy-federated (https://fed.brid.gy/)',
@ -12,22 +13,25 @@ HEADERS = {
def requests_get(url, **kwargs):
return _requests_fn(requests.get, url, **kwargs)
def requests_post(url, **kwargs):
return _requests_fn(requests.post, url, **kwargs)
def _requests_fn(fn, url, json=False, **kwargs):
"""Wraps requests.* and adds raise_for_status() and User-Agent."""
kwargs.setdefault('headers', {}).update(HEADERS)
resp = requests.get(url, **kwargs)
resp = fn(url, **kwargs)
resp.raise_for_status()
if json:
try:
return resp.json()
except ValueError:
msg = "Couldn't parse response as JSON"
logging.error(msg, exc_info=True)
raise exc.HTTPBadRequest(400, msg)
return resp
# def fetch_mf2(url):
# """Fetches a URL and parses and returns its mf2.
# Args:
# url: string
# Returns: dict, parsed mf2
# """
# resp = requests.get(url=url, headers={'User-Agent': USER_AGENT})
# resp.raise_for_status()
# mf2 = mf2py.parse(resp.text, url=resp.url)
# logging.info('Parsed mf2 for %s: %s', resp.url, json.dumps(mf2, indent=2))
# return mf2

Wyświetl plik

@ -0,0 +1,84 @@
# coding=utf-8
"""Unit tests for webmention.py.
TODO: test error handling
"""
import copy
import json
import unittest
import urllib
import mock
import requests
import activitypub
import common
import webmention
from webmention import app
@mock.patch('requests.post')
@mock.patch('requests.get')
class WebmentionTest(unittest.TestCase):
def test_webmention(self, mock_get, mock_post):
reply_html = u"""
<html><body>
<div class="h-entry">
<p class="e-content">
<a class="u-in-reply-to" href="http://orig/post">foo bar</a>
</p>
</div>
</body></html>
"""
reply = requests.Response()
reply.status_code = 200
reply._text = reply_html
reply._content = reply._text.encode('utf-8')
reply.encoding = 'utf-8'
article_as = {
'@context': ['https://www.w3.org/ns/activitystreams'],
'type': 'Article',
'content': u'Lots of ☕ words...',
'actor': 'http://orig/author',
}
article = requests.Response()
article.status_code = 200
article._text = json.dumps(article_as)
article._content = article._text.encode('utf-8')
article.encoding = 'utf-8'
actor_as = {
'objectType' : 'person',
'displayName': u'Mrs. ☕ Foo',
'url': 'https://foo.com/about-me',
'inbox': 'https://foo.com/inbox',
}
actor = requests.Response()
actor.status_code = 200
actor._text = json.dumps(actor_as)
actor._content = actor._text.encode('utf-8')
actor.encoding = 'utf-8'
mock_get.side_effect = [reply, article, actor]
got = app.get_response(
'/webmention', method='POST', body=urllib.urlencode({
'source': 'http://a/reply',
'target': 'http://orig/post',
}))
self.assertEquals(200, got.status_int)
args, kwargs = mock_post.call_args
self.assertEqual(('https://foo.com/inbox',), args)
self.assertEqual({
'objectType': 'comment',
'displayName': u'foo ☕ bar',
'content': u' <a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a> ',
'inReplyTo': [{'url': 'http://orig/post'}],
}, kwargs['json'])
expected_headers = copy.copy(common.HEADERS)
expected_headers['Content-Type'] = activitypub.CONTENT_TYPE_AS
self.assertEqual(expected_headers, kwargs['headers'])

61
webmention.py 100644
Wyświetl plik

@ -0,0 +1,61 @@
"""Handles inbound webmentions.
"""
import copy
import json
import logging
import appengine_config
from granary import microformats2
import mf2py
import mf2util
from oauth_dropins.webutil import util
import webapp2
import activitypub
import common
class WebmentionHandler(webapp2.RequestHandler):
"""Handles inbound webmention, converts to ActivityPub inbox delivery."""
def post(self):
logging.info('Params: %s', self.request.params.items())
source = util.get_required_param(self, 'source')
target = util.get_required_param(self, 'target')
# fetch source page, convert to ActivityStreams
resp = common.requests_get(source)
mf2 = mf2py.parse(resp.text, url=resp.url)
logging.info('Parsed mf2 for %s: %s', resp.url, json.dumps(mf2, indent=2))
entry = mf2util.find_first_entry(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))
# fetch target page as AS object
target_obj = common.requests_get(target, json=True,
headers=activitypub.CONNEG_HEADER)
# 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')
actor = common.requests_get(actor_url, 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')
headers = copy.copy(common.HEADERS)
headers['Content-Type'] = activitypub.CONTENT_TYPE_AS
requests.post(inbox_url, json=source_obj, headers=headers)
app = webapp2.WSGIApplication([
('/webmention', WebmentionHandler),
], debug=appengine_config.DEBUG)