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()
@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
def verify_signature(cls, activity):
"""Verifies the current request's HTTP Signature.

Wyświetl plik

@ -29,7 +29,8 @@ import common
WWW_DOMAINS = frozenset((
'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')
# 2048 bits makes tests slow, so use 1024 for them
KEY_BITS = 1024 if DEBUG else 2048

Wyświetl plik

@ -109,6 +109,25 @@ class Protocol(metaclass=ProtocolMeta):
"""
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
def receive(cls, id, **props):
"""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.util import json_dumps, json_loads
import activitypub
from activitypub import ActivityPub
from flask_app import app, cache
from common import CACHE_TIME, CONTENT_TYPE_HTML
from models import Object, User
@ -92,7 +92,7 @@ def redir(to):
obj = Webmention.load(to, check_backlink=False)
if not obj or obj.deleted:
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)}')
return ret, {
'Content-Type': accept_type,

Wyświetl plik

@ -13,6 +13,7 @@ import activitypub
from flask_app import app, cache
import common
from models import Object
from webmention import Webmention
logger = logging.getLogger(__name__)
@ -42,23 +43,4 @@ def render():
if obj.deleted or type == 'delete':
return '', 410
# fill in author/actor if available
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
return Webmention.serve(obj)

Wyświetl plik

@ -5,6 +5,7 @@ import copy
from datetime import datetime, timedelta
from hashlib import sha256
import logging
from unittest import skip
from unittest.mock import ANY, call, patch
import urllib.parse
@ -1413,6 +1414,12 @@ class ActivityPubUtilsTest(testutil.TestCase):
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):
g.user = self.make_user('foo.com')

Wyświetl plik

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

Wyświetl plik

@ -1452,3 +1452,20 @@ class WebmentionUtilTest(testutil.TestCase):
self.assert_req(mock_get, 'https://user.com/post')
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'))
@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):
maxDiff = None

Wyświetl plik

@ -23,7 +23,7 @@ from flask_app import app
import common
from models import Follower, Object, Target, User
import models
from protocol import Protocol
from protocol import Protocol, protocols
logger = logging.getLogger(__name__)
@ -125,6 +125,35 @@ class Webmention(Protocol):
obj.mf2 = entry
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')
def webmention_external():