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