kopia lustrzana https://github.com/snarfed/bridgy-fed
flask: port activitypub
rodzic
f26aed5cbb
commit
b8be570d66
301
activitypub.py
301
activitypub.py
|
@ -4,7 +4,9 @@ from base64 import b64encode
|
|||
import datetime
|
||||
from hashlib import sha256
|
||||
import logging
|
||||
import re
|
||||
|
||||
from flask import request
|
||||
from google.cloud import ndb
|
||||
from granary import as2, microformats2
|
||||
import mf2util
|
||||
|
@ -13,7 +15,9 @@ from oauth_dropins.webutil.handlers import cache_response
|
|||
from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
import webapp2
|
||||
|
||||
from app import app, cache
|
||||
import common
|
||||
from common import error, redirect_unwrap, redirect_wrap
|
||||
from models import Follower, MagicKey
|
||||
from httpsig.requests_auth import HTTPSignatureAuth
|
||||
|
||||
|
@ -75,182 +79,185 @@ def send(activity, inbox_url, user_domain):
|
|||
headers=headers)
|
||||
|
||||
|
||||
class ActorHandler():
|
||||
@app.route('/<string:domain>')
|
||||
@cache.cached(CACHE_TIME.total_seconds())
|
||||
def actor(domain):
|
||||
"""Serves /[DOMAIN], fetches its mf2, converts to AS Actor, and serves it."""
|
||||
if not re.match(common.DOMAIN_RE, domain):
|
||||
return error(f'{acct} is not a domain', 404)
|
||||
|
||||
@cache_response(CACHE_TIME)
|
||||
def get(self, domain):
|
||||
tld = domain.split('.')[-1]
|
||||
if tld in common.TLD_BLOCKLIST:
|
||||
self.error('', status=404)
|
||||
tld = domain.split('.')[-1]
|
||||
if tld in common.TLD_BLOCKLIST:
|
||||
return error('', status=404)
|
||||
|
||||
mf2 = util.fetch_mf2('http://%s/' % domain, gateway=True,
|
||||
headers=common.HEADERS)
|
||||
# logging.info('Parsed mf2 for %s: %s', resp.url, json_dumps(mf2, indent=2))
|
||||
mf2 = util.fetch_mf2('http://%s/' % domain, gateway=True,
|
||||
headers=common.HEADERS)
|
||||
# logging.info('Parsed mf2 for %s: %s', resp.url, json_dumps(mf2, indent=2))
|
||||
|
||||
hcard = mf2util.representative_hcard(mf2, mf2['url'])
|
||||
logging.info('Representative h-card: %s', json_dumps(hcard, indent=2))
|
||||
if not hcard:
|
||||
self.error("""\
|
||||
Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % mf2['url'])
|
||||
hcard = mf2util.representative_hcard(mf2, mf2['url'])
|
||||
logging.info('Representative h-card: %s', json_dumps(hcard, indent=2))
|
||||
if not hcard:
|
||||
return error("""\
|
||||
Coul find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % mf2['url'])
|
||||
|
||||
key = MagicKey.get_or_create(domain)
|
||||
obj = self.postprocess_as2(as2.from_as1(microformats2.json_to_object(hcard)),
|
||||
key=key)
|
||||
obj.update({
|
||||
'inbox': f'{request.host_url}{domain}/inbox',
|
||||
'outbox': f'{request.host_url}{domain}/outbox',
|
||||
'following': f'{request.host_url}{domain}/following',
|
||||
'followers': f'{request.host_url}{domain}/followers',
|
||||
})
|
||||
logging.info('Returning: %s', json_dumps(obj, indent=2))
|
||||
key = MagicKey.get_or_create(domain)
|
||||
obj = common.postprocess_as2(
|
||||
as2.from_as1(microformats2.json_to_object(hcard)), key=key)
|
||||
obj.update({
|
||||
'inbox': f'{request.host_url}{domain}/inbox',
|
||||
'outbox': f'{request.host_url}{domain}/outbox',
|
||||
'following': f'{request.host_url}{domain}/following',
|
||||
'followers': f'{request.host_url}{domain}/followers',
|
||||
})
|
||||
logging.info('Returning: %s', json_dumps(obj, indent=2))
|
||||
|
||||
self.response.headers.update({
|
||||
'Content-Type': common.CONTENT_TYPE_AS2,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
self.response.write(json_dumps(obj, indent=2))
|
||||
return (obj, {
|
||||
'Content-Type': common.CONTENT_TYPE_AS2,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
|
||||
|
||||
class InboxHandler():
|
||||
@app.route('/<string:domain>/inbox', methods=['POST'])
|
||||
def inbox(domain):
|
||||
"""Accepts POSTs to /[DOMAIN]/inbox and converts to outbound webmentions."""
|
||||
def post(self, domain):
|
||||
logging.info('Got: %s', self.request.body)
|
||||
body = request.get_data(as_text=True)
|
||||
logging.info(f'Got: {body}')
|
||||
|
||||
# parse and validate AS2 activity
|
||||
try:
|
||||
activity = json_loads(self.request.body)
|
||||
assert activity
|
||||
except (TypeError, ValueError, AssertionError):
|
||||
self.error("Couldn't parse body as JSON", exc_info=True)
|
||||
if not re.match(common.DOMAIN_RE, domain):
|
||||
return error(f'{acct} is not a domain', 404)
|
||||
|
||||
obj = activity.get('object') or {}
|
||||
if isinstance(obj, str):
|
||||
obj = {'id': obj}
|
||||
# parse and validate AS2 activity
|
||||
try:
|
||||
activity = request.json
|
||||
assert activity
|
||||
except (TypeError, ValueError, AssertionError):
|
||||
return error("Couldn't parse body as JSON", exc_info=True)
|
||||
|
||||
type = activity.get('type')
|
||||
if type == 'Accept': # eg in response to a Follow
|
||||
return # noop
|
||||
if type == 'Create':
|
||||
type = obj.get('type')
|
||||
elif type not in SUPPORTED_TYPES:
|
||||
self.error('Sorry, %s activities are not supported yet.' % type,
|
||||
status=501)
|
||||
obj = activity.get('object') or {}
|
||||
if isinstance(obj, str):
|
||||
obj = {'id': obj}
|
||||
|
||||
# TODO: verify signature if there is one
|
||||
type = activity.get('type')
|
||||
if type == 'Accept': # eg in response to a Follow
|
||||
return # noop
|
||||
if type == 'Create':
|
||||
type = obj.get('type')
|
||||
elif type not in SUPPORTED_TYPES:
|
||||
return error('Sorry, %s activities are not supported yet.' % type,
|
||||
status=501)
|
||||
|
||||
if type == 'Undo' and obj.get('type') == 'Follow':
|
||||
# skip actor fetch below; we don't need it to undo a follow
|
||||
return self.undo_follow(self.redirect_unwrap(activity))
|
||||
elif type == 'Delete':
|
||||
id = obj.get('id')
|
||||
# TODO: verify signature if there is one
|
||||
|
||||
# !!! temporarily disabled actually deleting Followers below because
|
||||
# mastodon.social sends Deletes for every Bridgy Fed account, all at
|
||||
# basically the same time, and we have many Follower objects, so we
|
||||
# have to do this table scan for each one, so the requests take a
|
||||
# long time and end up spawning extra App Engine instances that we
|
||||
# get billed for. and the Delete requests are almost never for
|
||||
# followers we have. TODO: revisit this and do it right.
|
||||
if type == 'Undo' and obj.get('type') == 'Follow':
|
||||
# skip actor fetch below; we don't need it to undo a follow
|
||||
undo_follow(redirect_unwrap(activity))
|
||||
return ''
|
||||
elif type == 'Delete':
|
||||
id = obj.get('id')
|
||||
|
||||
# if isinstance(id, str):
|
||||
# # assume this is an actor
|
||||
# # https://github.com/snarfed/bridgy-fed/issues/63
|
||||
# for key in Follower.query().iter(keys_only=True):
|
||||
# if key.id().split(' ')[-1] == id:
|
||||
# key.delete()
|
||||
return
|
||||
# !!! temporarily disabled actually deleting Followers below because
|
||||
# mastodon.social sends Deletes for every Bridgy Fed account, all at
|
||||
# basically the same time, and we have many Follower objects, so we
|
||||
# have to do this table scan for each one, so the requests take a
|
||||
# long time and end up spawning extra App Engine instances that we
|
||||
# get billed for. and the Delete requests are almost never for
|
||||
# followers we have. TODO: revisit this and do it right.
|
||||
|
||||
# fetch actor if necessary so we have name, profile photo, etc
|
||||
for elem in obj, activity:
|
||||
actor = elem.get('actor')
|
||||
if actor and isinstance(actor, str):
|
||||
elem['actor'] = common.get_as2(actor).json()
|
||||
# if isinstance(id, str):
|
||||
# # assume this is an actor
|
||||
# # https://github.com/snarfed/bridgy-fed/issues/63
|
||||
# for key in Follower.query().iter(keys_only=True):
|
||||
# if key.id().split(' ')[-1] == id:
|
||||
# key.delete()
|
||||
return ''
|
||||
|
||||
activity_unwrapped = self.redirect_unwrap(activity)
|
||||
if type == 'Follow':
|
||||
return self.accept_follow(activity, activity_unwrapped)
|
||||
# fetch actor if necessary so we have name, profile photo, etc
|
||||
for elem in obj, activity:
|
||||
actor = elem.get('actor')
|
||||
if actor and isinstance(actor, str):
|
||||
elem['actor'] = common.get_as2(actor).json()
|
||||
|
||||
# send webmentions to each target
|
||||
as1 = as2.to_as1(activity)
|
||||
self.send_webmentions(as1, proxy=True, protocol='activitypub',
|
||||
source_as2=json_dumps(activity_unwrapped))
|
||||
activity_unwrapped = redirect_unwrap(activity)
|
||||
if type == 'Follow':
|
||||
return accept_follow(activity, activity_unwrapped)
|
||||
|
||||
def accept_follow(self, follow, follow_unwrapped):
|
||||
"""Replies to an AP Follow request with an Accept request.
|
||||
# send webmentions to each target
|
||||
as1 = as2.to_as1(activity)
|
||||
common.send_webmentions(as1, proxy=True, protocol='activitypub',
|
||||
source_as2=json_dumps(activity_unwrapped))
|
||||
|
||||
Args:
|
||||
follow: dict, AP Follow activity
|
||||
follow_unwrapped: dict, same, except with redirect URLs unwrapped
|
||||
"""
|
||||
logging.info('Replying to Follow with Accept')
|
||||
return ''
|
||||
|
||||
followee = follow.get('object')
|
||||
followee_unwrapped = follow_unwrapped.get('object')
|
||||
follower = follow.get('actor')
|
||||
if not followee or not followee_unwrapped or not follower:
|
||||
self.error('Follow activity requires object and actor. Got: %s' % follow)
|
||||
|
||||
inbox = follower.get('inbox')
|
||||
follower_id = follower.get('id')
|
||||
if not inbox or not follower_id:
|
||||
self.error('Follow actor requires id and inbox. Got: %s', follower)
|
||||
def accept_follow(follow, follow_unwrapped):
|
||||
"""Replies to an AP Follow request with an Accept request.
|
||||
|
||||
# store Follower
|
||||
user_domain = util.domain_from_link(followee_unwrapped)
|
||||
Follower.get_or_create(user_domain, follower_id, last_follow=json_dumps(follow))
|
||||
Args:
|
||||
follow: dict, AP Follow activity
|
||||
follow_unwrapped: dict, same, except with redirect URLs unwrapped
|
||||
"""
|
||||
logging.info('Replying to Follow with Accept')
|
||||
|
||||
# send AP Accept
|
||||
accept = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': util.tag_uri(self.request.host, 'accept/%s/%s' % (
|
||||
(user_domain, follow.get('id')))),
|
||||
'type': 'Accept',
|
||||
'actor': followee,
|
||||
'object': {
|
||||
'type': 'Follow',
|
||||
'actor': follower_id,
|
||||
'object': followee,
|
||||
}
|
||||
followee = follow.get('object')
|
||||
followee_unwrapped = follow_unwrapped.get('object')
|
||||
follower = follow.get('actor')
|
||||
if not followee or not followee_unwrapped or not follower:
|
||||
return error('Follow activity requires object and actor. Got: %s' % follow)
|
||||
|
||||
inbox = follower.get('inbox')
|
||||
follower_id = follower.get('id')
|
||||
if not inbox or not follower_id:
|
||||
return error('Follow actor requires id and inbox. Got: %s', follower)
|
||||
|
||||
# store Follower
|
||||
user_domain = util.domain_from_link(followee_unwrapped)
|
||||
Follower.get_or_create(user_domain, follower_id, last_follow=json_dumps(follow))
|
||||
|
||||
# send AP Accept
|
||||
accept = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'id': util.tag_uri(request.host, 'accept/%s/%s' % (
|
||||
(user_domain, follow.get('id')))),
|
||||
'type': 'Accept',
|
||||
'actor': followee,
|
||||
'object': {
|
||||
'type': 'Follow',
|
||||
'actor': follower_id,
|
||||
'object': followee,
|
||||
}
|
||||
resp = send(accept, inbox, user_domain)
|
||||
self.response.status_int = resp.status_code
|
||||
self.response.write(resp.text)
|
||||
}
|
||||
resp = send(accept, inbox, user_domain)
|
||||
|
||||
# send webmention
|
||||
self.send_webmentions(as2.to_as1(follow), proxy=True, protocol='activitypub',
|
||||
source_as2=json_dumps(follow_unwrapped))
|
||||
# send webmention
|
||||
common.send_webmentions(as2.to_as1(follow), proxy=True, protocol='activitypub',
|
||||
source_as2=json_dumps(follow_unwrapped))
|
||||
|
||||
@ndb.transactional()
|
||||
def undo_follow(self, undo_unwrapped):
|
||||
"""Replies to an AP Follow request with an Accept request.
|
||||
|
||||
Args:
|
||||
undo_unwrapped: dict, AP Undo activity with redirect URLs unwrapped
|
||||
"""
|
||||
logging.info('Undoing Follow')
|
||||
|
||||
follow = undo_unwrapped.get('object', {})
|
||||
follower = follow.get('actor')
|
||||
followee = follow.get('object')
|
||||
if not follower or not followee:
|
||||
self.error('Undo of Follow requires object with actor and object. Got: %s' % follow)
|
||||
|
||||
# deactivate Follower
|
||||
user_domain = util.domain_from_link(followee)
|
||||
follower_obj = Follower.get_by_id(Follower._id(user_domain, follower))
|
||||
if follower_obj:
|
||||
logging.info('Marking %s as inactive' % follower_obj.key)
|
||||
follower_obj.status = 'inactive'
|
||||
follower_obj.put()
|
||||
else:
|
||||
logging.warning('No Follower found for %s %s', user_domain, follower)
|
||||
return resp.text, resp.status_code
|
||||
|
||||
|
||||
# TODO send webmention with 410 of u-follow
|
||||
@ndb.transactional()
|
||||
def undo_follow(undo_unwrapped):
|
||||
"""Replies to an AP Follow request with an Accept request.
|
||||
|
||||
Args:
|
||||
undo_unwrapped: dict, AP Undo activity with redirect URLs unwrapped
|
||||
"""
|
||||
logging.info('Undoing Follow')
|
||||
|
||||
ROUTES = [
|
||||
(r'/%s/?' % common.DOMAIN_RE, ActorHandler),
|
||||
(r'/%s/inbox' % common.DOMAIN_RE, InboxHandler),
|
||||
]
|
||||
follow = undo_unwrapped.get('object', {})
|
||||
follower = follow.get('actor')
|
||||
followee = follow.get('object')
|
||||
if not follower or not followee:
|
||||
return error('Undo of Follow requires object with actor and object. Got: %s' % follow)
|
||||
|
||||
# deactivate Follower
|
||||
user_domain = util.domain_from_link(followee)
|
||||
follower_obj = Follower.get_by_id(Follower._id(user_domain, follower))
|
||||
if follower_obj:
|
||||
logging.info('Marking %s as inactive' % follower_obj.key)
|
||||
follower_obj.status = 'inactive'
|
||||
follower_obj.put()
|
||||
else:
|
||||
logging.warning('No Follower found for %s %s', user_domain, follower)
|
||||
|
||||
# TODO send webmention with 410 of u-follow
|
||||
|
|
|
@ -9,6 +9,7 @@ import requests
|
|||
|
||||
from app import app, cache
|
||||
import common
|
||||
from common import error
|
||||
|
||||
LINK_HEADER = '<%s>; rel="webmention"'
|
||||
CACHE_TIME = datetime.timedelta(seconds=15)
|
||||
|
@ -21,14 +22,14 @@ def add_wm(url=None):
|
|||
"""Proxies HTTP requests and adds Link header to our webmention endpoint."""
|
||||
url = urllib.parse.unquote(url)
|
||||
if not url.startswith('http://') and not url.startswith('https://'):
|
||||
common.error('URL must start with http:// or https://')
|
||||
return error('URL must start with http:// or https://')
|
||||
|
||||
try:
|
||||
got = common.requests_get(url)
|
||||
except requests.exceptions.Timeout as e:
|
||||
common.error(str(e), status=504, exc_info=True)
|
||||
return error(str(e), status=504, exc_info=True)
|
||||
except requests.exceptions.RequestException as e:
|
||||
common.error(str(e), status=502, exc_info=True)
|
||||
return error(str(e), status=502, exc_info=True)
|
||||
|
||||
resp = flask.make_response(got.content, got.status_code, dict(got.headers))
|
||||
resp.headers.add('Link', LINK_HEADER % (request.args.get('endpoint') or
|
||||
|
|
10
common.py
10
common.py
|
@ -169,7 +169,7 @@ def error(msg, status=None, exc_info=False):
|
|||
if not status:
|
||||
status = 400
|
||||
logging.info('Returning %s: %s' % (status, msg), exc_info=exc_info)
|
||||
abort(status, msg)
|
||||
return (msg, status)
|
||||
|
||||
|
||||
def send_webmentions(activity_wrapped, proxy=None, **response_props):
|
||||
|
@ -244,7 +244,7 @@ def send_webmentions(activity_wrapped, proxy=None, **response_props):
|
|||
|
||||
if errors:
|
||||
msg = 'Errors:\n' + '\n'.join(str(e) for e in errors)
|
||||
return error(msg, status=errors[0].get('http_status'))
|
||||
return error(msg, status=getattr(errors[0], 'http_status', None))
|
||||
|
||||
|
||||
def postprocess_as2(activity, target=None, key=None):
|
||||
|
@ -418,9 +418,9 @@ def redirect_unwrap(val):
|
|||
elif isinstance(val, str):
|
||||
prefix = urllib.parse.urljoin(request.host_url, '/r/')
|
||||
if val.startswith(prefix):
|
||||
return val[len(prefix):]
|
||||
return util.follow_redirects(val[len(prefix):]).url
|
||||
elif val.startswith(request.host_url):
|
||||
return util.follow_redirects(
|
||||
util.domain_from_link(urllib.parse.urlparse(val).path.strip('/'))).url
|
||||
domain = util.domain_from_link(urllib.parse.urlparse(val).path.strip('/'))
|
||||
return util.follow_redirects(domain).url
|
||||
|
||||
return val
|
||||
|
|
|
@ -43,7 +43,7 @@ def redir(to=None):
|
|||
to = re.sub(r'^(https?:/)([^/])', r'\1/\2', to)
|
||||
|
||||
if not to.startswith('http://') and not to.startswith('https://'):
|
||||
error(f'Expected fully qualified URL; got {to}')
|
||||
return error(f'Expected fully qualified URL; got {to}')
|
||||
|
||||
# check that we've seen this domain before so we're not an open redirect
|
||||
domains = set((util.domain_from_link(to),
|
||||
|
|
|
@ -76,7 +76,7 @@ def slap(acct):
|
|||
#
|
||||
# updated = utils.parse_updated_from_atom(data)
|
||||
# if not utils.verify_timestamp(updated):
|
||||
# self.error('Timestamp is more than 1h old.')
|
||||
# return error('Timestamp is more than 1h old.')
|
||||
|
||||
# send webmentions to each target
|
||||
activity = atom.atom_to_activity(data)
|
||||
|
|
|
@ -12,12 +12,11 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
|
|||
import requests
|
||||
|
||||
import activitypub
|
||||
from app import application
|
||||
from app import app, cache
|
||||
import common
|
||||
from models import Follower, MagicKey, Response
|
||||
from . import testutil
|
||||
|
||||
|
||||
REPLY_OBJECT = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Note',
|
||||
|
@ -28,7 +27,7 @@ REPLY_OBJECT = {
|
|||
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
}
|
||||
REPLY_OBJECT_WRAPPED = copy.deepcopy(REPLY_OBJECT)
|
||||
REPLY_OBJECT_WRAPPED['inReplyTo'] = 'http://localhost:80/r/orig/post'
|
||||
REPLY_OBJECT_WRAPPED['inReplyTo'] = 'http://localhost/r/orig/post'
|
||||
REPLY = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Create',
|
||||
|
@ -113,7 +112,7 @@ FOLLOW_WRAPPED_WITH_ACTOR['actor'] = FOLLOW_WITH_ACTOR['actor']
|
|||
ACCEPT = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Accept',
|
||||
'id': 'tag:localhost:80:accept/realize.be/https://mastodon.social/6d1a',
|
||||
'id': 'tag:localhost:accept/realize.be/https://mastodon.social/6d1a',
|
||||
'actor': 'http://localhost/realize.be',
|
||||
'object': {
|
||||
'type': 'Follow',
|
||||
|
@ -138,6 +137,8 @@ DELETE = {
|
|||
'object': 'https://mastodon.social/users/swentel',
|
||||
}
|
||||
|
||||
client = app.test_client()
|
||||
|
||||
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
|
@ -146,19 +147,20 @@ class ActivityPubTest(testutil.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
super(ActivityPubTest, self).setUp()
|
||||
activitypub.ActorHandler.get.cache_clear()
|
||||
app.testing = True
|
||||
cache.clear()
|
||||
|
||||
def test_actor_handler(self, _, mock_get, __):
|
||||
def test_actor(self, _, mock_get, __):
|
||||
mock_get.return_value = requests_response("""
|
||||
<body>
|
||||
<a class="h-card u-url" rel="me" href="/about-me">Mrs. ☕ Foo</a>
|
||||
</body>
|
||||
""", url='https://foo.com/', content_type=common.CONTENT_TYPE_HTML)
|
||||
|
||||
got = application.get_response('/foo.com')
|
||||
got = client.get('/foo.com')
|
||||
mock_get.assert_called_once_with('http://foo.com/', headers=common.HEADERS,
|
||||
stream=True, timeout=util.HTTP_TIMEOUT)
|
||||
self.assertEqual(200, got.status_int)
|
||||
self.assertEqual(200, got.status_code)
|
||||
type = got.headers['Content-Type']
|
||||
self.assertTrue(type.startswith(common.CONTENT_TYPE_AS2), type)
|
||||
self.assertEqual({
|
||||
|
@ -180,9 +182,9 @@ class ActivityPubTest(testutil.TestCase):
|
|||
'id': 'foo.com',
|
||||
'publicKeyPem': MagicKey.get_by_id('foo.com').public_pem().decode(),
|
||||
},
|
||||
}, json_loads(got.body))
|
||||
}, got.json)
|
||||
|
||||
def test_actor_handler_no_hcard(self, _, mock_get, __):
|
||||
def test_actor_no_hcard(self, _, mock_get, __):
|
||||
mock_get.return_value = requests_response("""
|
||||
<body>
|
||||
<div class="h-entry">
|
||||
|
@ -191,15 +193,15 @@ class ActivityPubTest(testutil.TestCase):
|
|||
</body>
|
||||
""")
|
||||
|
||||
got = application.get_response('/foo.com')
|
||||
got = client.get('/foo.com')
|
||||
mock_get.assert_called_once_with('http://foo.com/', headers=common.HEADERS,
|
||||
stream=True, timeout=util.HTTP_TIMEOUT)
|
||||
self.assertEqual(400, got.status_int)
|
||||
self.assertIn('representative h-card', got.body.decode())
|
||||
self.assertEqual(400, got.status_code)
|
||||
self.assertIn('representative h-card', got.get_data(as_text=True))
|
||||
|
||||
def test_actor_blocked_tld(self, _, __, ___):
|
||||
got = application.get_response('/foo.json')
|
||||
self.assertEqual(404, got.status_int)
|
||||
got = client.get('/foo.json')
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
||||
def test_inbox_reply_object(self, *mocks):
|
||||
self._test_inbox_reply(REPLY_OBJECT, REPLY_OBJECT, *mocks)
|
||||
|
@ -216,9 +218,8 @@ class ActivityPubTest(testutil.TestCase):
|
|||
'<html><head><link rel="webmention" href="/webmention"></html>')
|
||||
mock_post.return_value = requests_response()
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(as2).encode())
|
||||
self.assertEqual(200, got.status_int, got.body)
|
||||
got = client.post('/foo.com/inbox', json=as2)
|
||||
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
||||
mock_get.assert_called_once_with(
|
||||
'http://orig/post', headers=common.HEADERS, timeout=15, stream=True)
|
||||
|
||||
|
@ -246,9 +247,8 @@ class ActivityPubTest(testutil.TestCase):
|
|||
|
||||
mock_head.return_value = requests_response(url='http://this/')
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(reply).encode())
|
||||
self.assertEqual(200, got.status_int, got.body)
|
||||
got = client.post('/foo.com/inbox', json=reply)
|
||||
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
||||
|
||||
mock_head.assert_called_once_with(
|
||||
'http://this', allow_redirects=True, stream=True, timeout=15)
|
||||
|
@ -268,28 +268,28 @@ class ActivityPubTest(testutil.TestCase):
|
|||
'<html><head><link rel="webmention" href="/webmention"></html>')
|
||||
mock_post.return_value = requests_response()
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(as2).encode())
|
||||
self.assertEqual(200, got.status_int, got.body)
|
||||
mock_get.assert_called_once_with(
|
||||
'http://target/', headers=common.HEADERS, timeout=15, stream=True)
|
||||
with app.test_client() as test_client:
|
||||
got = test_client.post('/foo.com/inbox', json=as2)
|
||||
self.assertEqual(200, got.status_code, got.get_data(as_text=True))
|
||||
mock_get.assert_called_once_with(
|
||||
'http://target/', headers=common.HEADERS, timeout=15, stream=True)
|
||||
|
||||
expected_headers = copy.deepcopy(common.HEADERS)
|
||||
expected_headers['Accept'] = '*/*'
|
||||
mock_post.assert_called_once_with(
|
||||
'http://target/webmention',
|
||||
data={
|
||||
'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Fmention&target=http%3A%2F%2Ftarget%2F',
|
||||
'target': 'http://target/',
|
||||
},
|
||||
allow_redirects=False, timeout=15, stream=True,
|
||||
headers=expected_headers)
|
||||
expected_headers = copy.deepcopy(common.HEADERS)
|
||||
expected_headers['Accept'] = '*/*'
|
||||
mock_post.assert_called_once_with(
|
||||
'http://target/webmention',
|
||||
data={
|
||||
'source': 'http://localhost/render?source=http%3A%2F%2Fthis%2Fmention&target=http%3A%2F%2Ftarget%2F',
|
||||
'target': 'http://target/',
|
||||
},
|
||||
allow_redirects=False, timeout=15, stream=True,
|
||||
headers=expected_headers)
|
||||
|
||||
resp = Response.get_by_id('http://this/mention http://target/')
|
||||
self.assertEqual('in', resp.direction)
|
||||
self.assertEqual('activitypub', resp.protocol)
|
||||
self.assertEqual('complete', resp.status)
|
||||
self.assertEqual(self.handler.redirect_unwrap(as2), json_loads(resp.source_as2))
|
||||
resp = Response.get_by_id('http://this/mention http://target/')
|
||||
self.assertEqual('in', resp.direction)
|
||||
self.assertEqual('activitypub', resp.protocol)
|
||||
self.assertEqual('complete', resp.status)
|
||||
self.assertEqual(common.redirect_unwrap(as2), json_loads(resp.source_as2))
|
||||
|
||||
def test_inbox_like(self, mock_head, mock_get, mock_post):
|
||||
mock_head.return_value = requests_response(url='http://orig/post')
|
||||
|
@ -302,9 +302,8 @@ class ActivityPubTest(testutil.TestCase):
|
|||
]
|
||||
mock_post.return_value = requests_response()
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(LIKE_WRAPPED).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/foo.com/inbox', json=LIKE)
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
as2_headers = copy.deepcopy(common.HEADERS)
|
||||
as2_headers.update(common.CONNEG_HEADERS_AS2_HTML)
|
||||
|
@ -339,9 +338,8 @@ class ActivityPubTest(testutil.TestCase):
|
|||
]
|
||||
mock_post.return_value = requests_response()
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(FOLLOW_WRAPPED).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/foo.com/inbox', json=FOLLOW_WRAPPED)
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
as2_headers = copy.deepcopy(common.HEADERS)
|
||||
as2_headers.update(common.CONNEG_HEADERS_AS2_HTML)
|
||||
|
@ -379,9 +377,8 @@ class ActivityPubTest(testutil.TestCase):
|
|||
|
||||
Follower(id=Follower._id('realize.be', FOLLOW['actor'])).put()
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(UNDO_FOLLOW_WRAPPED).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/foo.com/inbox', json=UNDO_FOLLOW_WRAPPED)
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
follower = Follower.get_by_id('realize.be %s' % FOLLOW['actor'])
|
||||
self.assertEqual('inactive', follower.status)
|
||||
|
@ -389,28 +386,26 @@ class ActivityPubTest(testutil.TestCase):
|
|||
def test_inbox_undo_follow_doesnt_exist(self, mock_head, mock_get, mock_post):
|
||||
mock_head.return_value = requests_response(url='https://realize.be/')
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(UNDO_FOLLOW_WRAPPED).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/foo.com/inbox', json=UNDO_FOLLOW_WRAPPED)
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
def test_inbox_undo_follow_inactive(self, mock_head, mock_get, mock_post):
|
||||
mock_head.return_value = requests_response(url='https://realize.be/')
|
||||
Follower(id=Follower._id('realize.be', 'https://mastodon.social/users/swentel'),
|
||||
status='inactive').put()
|
||||
|
||||
got = application.get_response('/foo.com/inbox', method='POST',
|
||||
body=json_dumps(UNDO_FOLLOW_WRAPPED).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/foo.com/inbox', json=UNDO_FOLLOW_WRAPPED)
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
def test_inbox_unsupported_type(self, *_):
|
||||
got = application.get_response('/foo.com/inbox', method='POST', body=json_dumps({
|
||||
got = client.post('/foo.com/inbox', json={
|
||||
'@context': ['https://www.w3.org/ns/activitystreams'],
|
||||
'id': 'https://xoxo.zone/users/aaronpk#follows/40',
|
||||
'type': 'Block',
|
||||
'actor': 'https://xoxo.zone/users/aaronpk',
|
||||
'object': 'http://snarfed.org/',
|
||||
}).encode())
|
||||
self.assertEqual(501, got.status_int)
|
||||
})
|
||||
self.assertEqual(501, got.status_code)
|
||||
|
||||
def test_inbox_delete_actor(self, mock_head, mock_get, mock_post):
|
||||
follower = Follower.get_or_create('realize.be', DELETE['actor'])
|
||||
|
@ -419,9 +414,8 @@ class ActivityPubTest(testutil.TestCase):
|
|||
other = Follower.get_or_create('realize.be', 'https://mas.to/users/other')
|
||||
self.assertEqual(3, Follower.query().count())
|
||||
|
||||
got = application.get_response('/realize.be/inbox', method='POST',
|
||||
body=json_dumps(DELETE).encode())
|
||||
self.assertEqual(200, got.status_int)
|
||||
got = client.post('/realize.be/inbox', json=DELETE)
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
||||
# TODO: bring back
|
||||
# self.assertEqual([other], Follower.query().fetch())
|
||||
|
|
Ładowanie…
Reference in New Issue