kopia lustrzana https://github.com/snarfed/bridgy-fed
add webmention endpoint and conversion to AP inbox delivery
rodzic
576410005b
commit
04e65d612d
|
@ -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
|
||||
|
||||
|
|
38
common.py
38
common.py
|
@ -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
|
||||
|
|
|
@ -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'])
|
|
@ -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)
|
Ładowanie…
Reference in New Issue