kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
892047a10c
commit
cf86f4d808
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
19
protocol.py
19
protocol.py
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
22
render.py
22
render.py
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
Ładowanie…
Reference in New Issue