AP users: extract out Protocol.serve() method

#512
pull/517/head
Ryan Barrett 2023-05-23 21:30:57 -07:00
rodzic 892047a10c
commit cf86f4d808
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
10 zmienionych plików z 96 dodań i 26 usunięć

Wyświetl plik

@ -133,6 +133,12 @@ class ActivityPub(Protocol):
_error() _error()
@classmethod
def serve(cls, obj):
"""Serves an :class:`Object` as AS2."""
return (postprocess_as2(as2.from_as1(obj.as1)),
{'Content-Type': as2.CONTENT_TYPE})
@classmethod @classmethod
def verify_signature(cls, activity): def verify_signature(cls, activity):
"""Verifies the current request's HTTP Signature. """Verifies the current request's HTTP Signature.

Wyświetl plik

@ -29,7 +29,8 @@ import common
WWW_DOMAINS = frozenset(( WWW_DOMAINS = frozenset((
'www.jvt.me', 'www.jvt.me',
)) ))
# TODO: eventually load from Protocol subclasses' IDs instead? # TODO: eventually load from protocol.protocols instead, if/when we can get
# around the circular import
PROTOCOLS = ('activitypub', 'bluesky', 'ostatus', 'webmention', 'ui') PROTOCOLS = ('activitypub', 'bluesky', 'ostatus', 'webmention', 'ui')
# 2048 bits makes tests slow, so use 1024 for them # 2048 bits makes tests slow, so use 1024 for them
KEY_BITS = 1024 if DEBUG else 2048 KEY_BITS = 1024 if DEBUG else 2048

Wyświetl plik

@ -109,6 +109,25 @@ class Protocol(metaclass=ProtocolMeta):
""" """
raise NotImplementedError() raise NotImplementedError()
@classmethod
def serve(cls, obj):
"""Returns this protocol's Flask response for a given :class:`Object`.
For example, an HTML string and `'text/html'` for :class:`Webmention`,
or a dict with AS2 JSON and `'application/activity+json'` for
:class:`ActivityPub.
To be implemented by subclasses.
Args:
obj: :class:`Object`
Returns:
(response body, dict with HTTP headers) tuple appropriate to be
returned from a Flask handler
"""
raise NotImplementedError()
@classmethod @classmethod
def receive(cls, id, **props): def receive(cls, id, **props):
"""Handles an incoming activity. """Handles an incoming activity.

Wyświetl plik

@ -21,7 +21,7 @@ from oauth_dropins.webutil import flask_util, util
from oauth_dropins.webutil.flask_util import error from oauth_dropins.webutil.flask_util import error
from oauth_dropins.webutil.util import json_dumps, json_loads from oauth_dropins.webutil.util import json_dumps, json_loads
import activitypub from activitypub import ActivityPub
from flask_app import app, cache from flask_app import app, cache
from common import CACHE_TIME, CONTENT_TYPE_HTML from common import CACHE_TIME, CONTENT_TYPE_HTML
from models import Object, User from models import Object, User
@ -92,7 +92,7 @@ def redir(to):
obj = Webmention.load(to, check_backlink=False) obj = Webmention.load(to, check_backlink=False)
if not obj or obj.deleted: if not obj or obj.deleted:
return f'Object not found: {to}', 404 return f'Object not found: {to}', 404
ret = activitypub.postprocess_as2(as2.from_as1(obj.as1)) ret, _ = ActivityPub.serve(obj)
logger.info(f'Returning: {json_dumps(ret, indent=2)}') logger.info(f'Returning: {json_dumps(ret, indent=2)}')
return ret, { return ret, {
'Content-Type': accept_type, 'Content-Type': accept_type,

Wyświetl plik

@ -13,6 +13,7 @@ import activitypub
from flask_app import app, cache from flask_app import app, cache
import common import common
from models import Object from models import Object
from webmention import Webmention
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -42,23 +43,4 @@ def render():
if obj.deleted or type == 'delete': if obj.deleted or type == 'delete':
return '', 410 return '', 410
# fill in author/actor if available return Webmention.serve(obj)
obj_as1 = obj.as1
for field in 'author', 'actor':
val = as1.get_object(obj.as1, field)
if val.keys() == set(['id']) and val['id']:
# TODO: abstract on obj.source_protocol
loaded = activitypub.ActivityPub.load(val['id'])
if loaded and loaded.as1:
obj_as1 = {**obj_as1, field: loaded.as1}
# add HTML meta redirect to source page. should trigger for end users in
# browsers but not for webmention receivers (hopefully).
html = microformats2.activities_to_html([obj_as1])
utf8 = '<meta charset="utf-8">'
url = util.get_url(obj_as1)
if url:
refresh = f'<meta http-equiv="refresh" content="0;url={url}">'
html = html.replace(utf8, utf8 + '\n' + refresh)
return html

Wyświetl plik

@ -5,6 +5,7 @@ import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from hashlib import sha256 from hashlib import sha256
import logging import logging
from unittest import skip
from unittest.mock import ANY, call, patch from unittest.mock import ANY, call, patch
import urllib.parse import urllib.parse
@ -1413,6 +1414,12 @@ class ActivityPubUtilsTest(testutil.TestCase):
mock_get.assert_has_calls([self.as2_req('http://the/id')]) mock_get.assert_has_calls([self.as2_req('http://the/id')])
@skip
def test_serve(self):
obj = Object(id='http://orig', as2=LIKE)
self.assertEqual((LIKE_WRAPPED, {'Content-Type': 'application/activity+json'}),
ActivityPub.serve(obj))
def test_postprocess_as2_idempotent(self): def test_postprocess_as2_idempotent(self):
g.user = self.make_user('foo.com') g.user = self.make_user('foo.com')

Wyświetl plik

@ -74,8 +74,11 @@ class RenderTest(testutil.TestCase):
def test_render_with_author(self): def test_render_with_author(self):
with self.request_context: with self.request_context:
Object(id='abc', as2=as2.from_as1({**COMMENT, 'author': 'def'})).put() Object(id='abc', as2=as2.from_as1({**COMMENT, 'author': 'def'}),
Object(id='def', as2=as2.from_as1(ACTOR)).put() source_protocol='activitypub').put()
Object(id='def', as2=as2.from_as1(ACTOR),
source_protocol='activitypub').put()
resp = self.client.get('/render?id=abc') resp = self.client.get('/render?id=abc')
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
self.assert_multiline_equals( self.assert_multiline_equals(

Wyświetl plik

@ -1452,3 +1452,20 @@ class WebmentionUtilTest(testutil.TestCase):
self.assert_req(mock_get, 'https://user.com/post') self.assert_req(mock_get, 'https://user.com/post')
mock_post.assert_not_called() mock_post.assert_not_called()
def test_serve(self, _, __):
obj = Object(id='http://orig', mf2=ACTOR_MF2)
html, headers = Webmention.serve(obj)
self.assert_multiline_equals("""\
<!DOCTYPE html>
<html>
<head><meta charset="utf-8">
<meta http-equiv="refresh" content="0;url=https://user.com/"></head>
<body class="">
<span class="h-card">
<a class="p-name u-url" href="https://user.com/">Ms. Baz</a>
</span>
</body>
</html>
""", html, ignore_blanks=True)
self.assertEqual({'Content-Type': 'text/html; charset=utf-8'}, headers)

Wyświetl plik

@ -72,6 +72,12 @@ class FakeProtocol(protocol.Protocol):
raise requests.HTTPError(response=util.Struct(status_code='410')) raise requests.HTTPError(response=util.Struct(status_code='410'))
@classmethod
def serve(cls, obj):
logger.info(f'FakeProtocol.load {obj.key.id()}')
return (f'FakeProtocol object {obj.key.id()}',
{'Accept': 'fake/protocol'})
class TestCase(unittest.TestCase, testutil.Asserts): class TestCase(unittest.TestCase, testutil.Asserts):
maxDiff = None maxDiff = None

Wyświetl plik

@ -23,7 +23,7 @@ from flask_app import app
import common import common
from models import Follower, Object, Target, User from models import Follower, Object, Target, User
import models import models
from protocol import Protocol from protocol import Protocol, protocols
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -125,6 +125,35 @@ class Webmention(Protocol):
obj.mf2 = entry obj.mf2 = entry
return obj return obj
@classmethod
def serve(cls, obj):
"""Serves an :class:`Object` as HTML."""
obj_as1 = obj.as1
from_proto = protocols.get(obj.source_protocol)
if from_proto:
# fill in author/actor if available
for field in 'author', 'actor':
val = as1.get_object(obj.as1, field)
if val.keys() == set(['id']) and val['id']:
loaded = from_proto.load(val['id'])
if loaded and loaded.as1:
obj_as1 = {**obj_as1, field: loaded.as1}
else:
logger.debug(f'Not hydrating actor or author due to source_protocol {obj.source_protocol}')
html = microformats2.activities_to_html([obj_as1])
# add HTML meta redirect to source page. should trigger for end users in
# browsers but not for webmention receivers (hopefully).
url = util.get_url(obj_as1)
if url:
utf8 = '<meta charset="utf-8">'
refresh = f'<meta http-equiv="refresh" content="0;url={url}">'
html = html.replace(utf8, utf8 + '\n' + refresh)
return html, {'Content-Type': common.CONTENT_TYPE_HTML}
@app.post('/webmention') @app.post('/webmention')
def webmention_external(): def webmention_external():