flask: port activitypub

flask
Ryan Barrett 2021-07-10 08:07:40 -07:00
rodzic f26aed5cbb
commit b8be570d66
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
6 zmienionych plików z 221 dodań i 219 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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),

Wyświetl plik

@ -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)

Wyświetl plik

@ -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())