From 60a3f7b1cbb9a391ad1ce50b399b0732fde6cfa0 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Sun, 13 Aug 2017 00:12:16 -0700 Subject: [PATCH] activitypub actor endpoint: fetch mf2, convert to AS --- activitypub.py | 51 ++++++++++++++++++++++++++++++++++++++++ app.yaml | 6 +++-- requirements.txt | 1 + test/__init__.py | 3 +++ test/test_activitypub.py | 38 ++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 activitypub.py create mode 100644 test/__init__.py create mode 100644 test/test_activitypub.py diff --git a/activitypub.py b/activitypub.py new file mode 100644 index 0000000..f78e120 --- /dev/null +++ b/activitypub.py @@ -0,0 +1,51 @@ +# coding=utf-8 +"""Handles requests for ActivityPub endpoints: actors, inbox, etc. +""" +import json +import logging + +import appengine_config + +from granary import microformats2 +import mf2py +import mf2util +import requests +import webapp2 + + +# https://www.w3.org/TR/activitypub/#retrieving-objects +CONTENT_TYPE = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' +USER_AGENT = 'bridgy-activitypub (https://activitypub.brid.gy/)' + + +class ActorHandler(webapp2.RequestHandler): + """Serves /[DOMAIN], fetches its mf2, converts to AS Actor, and serves it.""" + + def get(self, domain): + url = 'https://%s/' % domain + 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)) + + hcard = mf2util.representative_hcard(mf2, resp.url) + logging.info('Representative h-card: %s', json.dumps(hcard, indent=2)) + + obj = microformats2.json_to_object(hcard) + obj.update({ + 'inbox': '%s/%s/inbox' % (self.request.host_url, domain), + }) + logging.info('Returning: %s', json.dumps(obj, indent=2)) + + self.response.headers.update({ + 'Content-Type': CONTENT_TYPE, + 'Access-Control-Allow-Origin': '*', + }) + self.response.write(json.dumps(obj, indent=2)) + + +app = webapp2.WSGIApplication( + [(r'/([^/]+\.[^/]+)/?', ActorHandler), + ], debug=appengine_config.DEBUG) diff --git a/app.yaml b/app.yaml index 98b7cbe..3f95c33 100644 --- a/app.yaml +++ b/app.yaml @@ -10,6 +10,8 @@ builtins: - remote_api: on libraries: +- name: lxml + version: latest - name: ssl version: latest - name: webob @@ -33,8 +35,8 @@ handlers: upload: static/index.html # dynamic -- url: /[^/]+ - script: app.application +- url: /[^/]+/? + script: activitypub.app secure: always skip_files: diff --git a/requirements.txt b/requirements.txt index 3dd6596..b775dd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ granary mf2py>=1.0.4 mf2util>=0.5.0 +mock requests==2.10.0 requests-toolbelt==0.6.2 -e git+https://github.com/snarfed/webmention-tools.git#egg=webmentiontools diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..3e1d7ca --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,3 @@ +# webutil/test/__init__.py has setup code that makes App Engine SDK's +# bundled libraries importable. +import oauth_dropins.webutil.test diff --git a/test/test_activitypub.py b/test/test_activitypub.py new file mode 100644 index 0000000..b6a4405 --- /dev/null +++ b/test/test_activitypub.py @@ -0,0 +1,38 @@ +# coding=utf-8 +"""Unit tests for activitypub.py. +""" +import json +import unittest + +import mock +import requests + +import activitypub + + +@mock.patch('requests.get') +class ActivityPubTest(unittest.TestCase): + + def test_actor_handler(self, mock_get): + html = u""" + +Mrs. ☕ Foo + +""" + resp = requests.Response() + resp.status_code = 200 + resp._text = html + resp._content = html.encode('utf-8') + resp.encoding = 'utf-8' + resp.url = 'https://foo.com/' + mock_get.return_value = resp + + got = activitypub.app.get_response('/foo.com') + self.assertEquals(200, got.status_int) + self.assertEquals(activitypub.CONTENT_TYPE, got.headers['Content-Type']) + self.assertEquals({ + 'objectType' : 'person', + 'displayName': u'Mrs. ☕ Foo', + 'url': 'https://foo.com/about-me', + 'inbox': 'http://localhost/foo.com/inbox', + }, json.loads(got.body))