kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
e6308543a9
commit
d2eda25375
63
follow.py
63
follow.py
|
@ -102,6 +102,7 @@ class FollowStart(indieauth.Start):
|
|||
"""Starts the IndieAuth flow to add a follower to an existing user."""
|
||||
def dispatch_request(self):
|
||||
address = request.form['address']
|
||||
domain = request.form['me']
|
||||
|
||||
try:
|
||||
return redirect(self.redirect_url(state=address))
|
||||
|
@ -173,9 +174,71 @@ class FollowCallback(indieauth.Callback):
|
|||
return redirect(f'/user/{domain}/following')
|
||||
|
||||
|
||||
class UnfollowStart(indieauth.Start):
|
||||
"""Starts the IndieAuth flow to add a follower to an existing user."""
|
||||
def dispatch_request(self):
|
||||
key = request.form['key']
|
||||
domain = request.form['me']
|
||||
|
||||
try:
|
||||
return redirect(self.redirect_url(state=key))
|
||||
except Exception as e:
|
||||
if util.is_connection_failure(e) or util.interpret_http_exception(e)[0]:
|
||||
flash(f"Couldn't fetch your web site: {e}")
|
||||
return redirect(f'/user/{domain}/following')
|
||||
raise
|
||||
|
||||
|
||||
class UnfollowCallback(indieauth.Callback):
|
||||
"""IndieAuth callback to add a follower to an existing user."""
|
||||
def finish(self, auth_entity, state=None):
|
||||
if not auth_entity:
|
||||
return
|
||||
|
||||
domain = util.domain_from_link(auth_entity.key.id())
|
||||
if not User.get_by_id(domain):
|
||||
error(f'No user for domain {domain}')
|
||||
|
||||
follower = Follower.get_by_id(state)
|
||||
if not follower:
|
||||
error(f'Bad state {state}')
|
||||
|
||||
followee_id = follower.dest
|
||||
last_follow = json_loads(follower.last_follow)
|
||||
followee = last_follow['object']
|
||||
inbox = last_follow['object']['inbox']
|
||||
|
||||
timestamp = NOW.replace(microsecond=0, tzinfo=None).isoformat()
|
||||
unfollow_id = common.host_url(f'/user/{domain}/following#undo-{timestamp}-{followee_id}')
|
||||
unfollow_as2 = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Undo',
|
||||
'id': unfollow_id,
|
||||
'actor': common.host_url(domain),
|
||||
'object': last_follow,
|
||||
}
|
||||
common.signed_post(inbox, data=unfollow_as2)
|
||||
|
||||
follower.status = 'inactive'
|
||||
follower.put()
|
||||
Activity.get_or_create(source=unfollow_id, target=followee_id, domain=[domain],
|
||||
direction='out', protocol='activitypub', status='complete',
|
||||
source_as2=json_dumps(unfollow_as2))
|
||||
|
||||
link = util.pretty_link(util.get_url(followee) or followee_id)
|
||||
flash(f'Unfollowed {link}.')
|
||||
return redirect(f'/user/{domain}/following')
|
||||
|
||||
|
||||
app.add_url_rule('/follow/start',
|
||||
view_func=FollowStart.as_view('follow_start', '/follow/callback'),
|
||||
methods=['POST'])
|
||||
app.add_url_rule('/follow/callback',
|
||||
view_func=FollowCallback.as_view('follow_callback', 'unused'),
|
||||
methods=['GET'])
|
||||
app.add_url_rule('/unfollow/start',
|
||||
view_func=UnfollowStart.as_view('unfollow_start', '/unfollow/callback'),
|
||||
methods=['POST'])
|
||||
app.add_url_rule('/unfollow/callback',
|
||||
view_func=UnfollowCallback.as_view('unfollow_callback', 'unused'),
|
||||
methods=['GET'])
|
||||
|
|
|
@ -3,13 +3,21 @@
|
|||
|
||||
{% for f in followers %}
|
||||
<li class="row">
|
||||
<a class="follower col-xs-12 col-sm-6 col-lg-6" href="{{ f.url }}">
|
||||
<a class="follower col-xs-10 col-sm-6 col-lg-6" href="{{ f.url }}">
|
||||
{% if f.picture %}
|
||||
<img class="profile u-photo" src="{{ f.picture }}" width="48px">
|
||||
{% endif %}
|
||||
{{ f.name or '' }}
|
||||
{{ f.name or '' }}
|
||||
{{ f.handle }}
|
||||
</a>
|
||||
{% if page_name == 'following' %}
|
||||
<form method="post" action="/unfollow/start" class="col-xs-2 col-sm-1 col-lg-1">
|
||||
<input type="hidden" name="me" value="{{ domain }}" />
|
||||
<input type="hidden" name="key" value="{{ f.key.id() }}" />
|
||||
<input type="submit" title="Unfollow (requires IndieAuth)" value="✖"
|
||||
class="btn delete-website" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% else %}
|
||||
None yet. Check back soon!
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
<div class="row big">Followers</div>
|
||||
|
||||
{% include "_followers.html" %}
|
||||
{% with page_name="followers" %}
|
||||
{% include "_followers.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div class="row">
|
||||
<form method="post" action="/follow/start">
|
||||
<p>
|
||||
<label for="follower-address">Follow another account (requires <a href="https://indieauth.net/">IndieAuth</a>):</label>
|
||||
<label for="follower-address">Follow someone (requires <a href="https://indieauth.net/">IndieAuth</a>):</label>
|
||||
<input id="follower-address" name="address" type="text" required
|
||||
placeholder="@user@domain.social" alt="fediverse address"
|
||||
value="{{ address or '' }}"></input>
|
||||
|
@ -21,6 +21,8 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
{% include "_followers.html" %}
|
||||
{% with page_name="following" %}
|
||||
{% include "_followers.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Unit tests for follow.py.
|
||||
"""
|
||||
import copy
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask import get_flashed_messages
|
||||
|
@ -27,6 +28,29 @@ WEBFINGER = requests_response({
|
|||
'href': 'https://bar/actor'
|
||||
}],
|
||||
})
|
||||
FOLLOWEE = {
|
||||
'type': 'Person',
|
||||
'id': 'https://bar/id',
|
||||
'url': 'https://bar/url',
|
||||
'inbox': 'http://bar/inbox',
|
||||
}
|
||||
FOLLOW_ADDRESS = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Follow',
|
||||
'id': f'http://localhost/user/snarfed.org/following#2022-01-02T03:04:05-@foo@bar',
|
||||
'actor': 'http://localhost/snarfed.org',
|
||||
'object': FOLLOWEE,
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}
|
||||
FOLLOW_URL = copy.deepcopy(FOLLOW_ADDRESS)
|
||||
FOLLOW_URL['id'] = f'http://localhost/user/snarfed.org/following#2022-01-02T03:04:05-https://bar/actor'
|
||||
UNDO_FOLLOW = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Undo',
|
||||
'id': f'http://localhost/user/snarfed.org/following#undo-2022-01-02T03:04:05-https://bar/id',
|
||||
'actor': 'http://localhost/snarfed.org',
|
||||
'object': FOLLOW_ADDRESS,
|
||||
}
|
||||
|
||||
|
||||
@patch('requests.get')
|
||||
|
@ -96,7 +120,7 @@ class RemoteFollowTest(testutil.TestCase):
|
|||
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
class AddFollowerTest(testutil.TestCase):
|
||||
class FollowTest(testutil.TestCase):
|
||||
|
||||
def test_start(self, mock_get, _):
|
||||
resp = self.client.post('/follow/start', data={
|
||||
|
@ -108,36 +132,31 @@ class AddFollowerTest(testutil.TestCase):
|
|||
resp.headers['Location'])
|
||||
|
||||
def test_callback_address(self, mock_get, mock_post):
|
||||
self._test_callback('@foo@bar', WEBFINGER, mock_get, mock_post)
|
||||
mock_get.side_effect = (
|
||||
# oauth-dropins indieauth https://snarfed.org fetch for user json
|
||||
requests_response(''),
|
||||
WEBFINGER,
|
||||
self.as2_resp(FOLLOWEE),
|
||||
)
|
||||
self._test_callback('@foo@bar', FOLLOW_ADDRESS, mock_get, mock_post)
|
||||
mock_get.assert_has_calls((
|
||||
self.req('https://bar/.well-known/webfinger?resource=acct:foo@bar'),
|
||||
))
|
||||
|
||||
def test_callback_url(self, mock_get, mock_post):
|
||||
self._test_callback('https://bar/actor', None, mock_get, mock_post)
|
||||
mock_get.side_effect = (
|
||||
requests_response(''),
|
||||
self.as2_resp(FOLLOWEE),
|
||||
)
|
||||
self._test_callback('https://bar/actor', FOLLOW_URL, mock_get, mock_post)
|
||||
|
||||
def _test_callback(self, input, webfinger_data, mock_get, mock_post):
|
||||
followee = {
|
||||
'type': 'Person',
|
||||
'id': 'https://bar/id',
|
||||
'url': 'https://bar/url',
|
||||
'inbox': 'http://bar/inbox',
|
||||
}
|
||||
def _test_callback(self, input, expected_follow, mock_get, mock_post):
|
||||
User.get_or_create('snarfed.org')
|
||||
|
||||
mock_post.side_effect = (
|
||||
requests_response('me=https://snarfed.org'),
|
||||
requests_response('OK'), # AP Follow to inbox
|
||||
)
|
||||
gets = [
|
||||
# oauth-dropins indieauth https://snarfed.org fetch for user json
|
||||
requests_response(''),
|
||||
self.as2_resp(followee),
|
||||
]
|
||||
if webfinger_data:
|
||||
gets.insert(1, webfinger_data)
|
||||
mock_get.side_effect = gets
|
||||
|
||||
User.get_or_create('snarfed.org')
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
|
@ -148,47 +167,30 @@ class AddFollowerTest(testutil.TestCase):
|
|||
resp = self.client.get(f'/follow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertEqual('/user/snarfed.org/following',resp.headers['Location'])
|
||||
self.assertEqual([f'Followed <a href="https://bar/url">{input}</a>.'], get_flashed_messages())
|
||||
self.assertEqual([f'Followed <a href="https://bar/url">{input}</a>.'],
|
||||
get_flashed_messages())
|
||||
|
||||
mock_get.assert_has_calls((
|
||||
self.as2_req('https://bar/actor'),
|
||||
))
|
||||
mock_post.assert_has_calls((
|
||||
self.req('http://auth/endpoint', data={
|
||||
'me': 'https://snarfed.org',
|
||||
'state': input,
|
||||
'code': 'my_code',
|
||||
'client_id': indieauth.INDIEAUTH_CLIENT_ID,
|
||||
'redirect_uri': 'http://localhost/follow/callback',
|
||||
}),
|
||||
))
|
||||
inbox_args, inbox_kwargs = mock_post.call_args_list[-1]
|
||||
self.assertEqual(input, mock_post.call_args_list[0][1]['data']['state'])
|
||||
inbox_args, inbox_kwargs = mock_post.call_args_list[1]
|
||||
self.assertEqual(('http://bar/inbox',), inbox_args)
|
||||
|
||||
expected_follow = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Follow',
|
||||
'id': f'http://localhost/user/snarfed.org/following#2022-01-02T03:04:05-{input}',
|
||||
'actor': 'http://localhost/snarfed.org',
|
||||
'object': followee,
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}
|
||||
self.assert_equals(expected_follow, json_loads(inbox_kwargs['data']))
|
||||
|
||||
expected_follow_json = json_dumps(expected_follow, sort_keys=True)
|
||||
follow_json = json_dumps(expected_follow, sort_keys=True)
|
||||
followers = Follower.query().fetch()
|
||||
self.assert_entities_equal(
|
||||
Follower(id='https://bar/id snarfed.org', last_follow=expected_follow_json,
|
||||
Follower(id='https://bar/id snarfed.org', last_follow=follow_json,
|
||||
src='snarfed.org', dest='https://bar/id', status='active'),
|
||||
followers,
|
||||
ignore=['created', 'updated'])
|
||||
|
||||
id = f'http://localhost/user/snarfed.org/following__2022-01-02T03:04:05-{input} https://bar/id'
|
||||
activities = Activity.query().fetch()
|
||||
self.assert_entities_equal(
|
||||
[Activity(id=f'http://localhost/user/snarfed.org/following__2022-01-02T03:04:05-{input} https://bar/id',
|
||||
domain=['snarfed.org'], status='complete',
|
||||
protocol='activitypub', direction='out',
|
||||
source_as2=expected_follow_json)],
|
||||
[Activity(id=id, domain=['snarfed.org'], status='complete',
|
||||
protocol='activitypub', direction='out', source_as2=follow_json)],
|
||||
activities,
|
||||
ignore=['created', 'updated'])
|
||||
|
||||
|
@ -203,3 +205,72 @@ class AddFollowerTest(testutil.TestCase):
|
|||
with self.client:
|
||||
resp = self.client.get(f'/follow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(400, resp.status_code)
|
||||
|
||||
|
||||
@patch('requests.post')
|
||||
@patch('requests.get')
|
||||
class UnfollowTest(testutil.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.follower = Follower(
|
||||
id='https://bar/id snarfed.org', last_follow=json_dumps(FOLLOW_ADDRESS),
|
||||
src='snarfed.org', dest='https://bar/id', status='active',
|
||||
).put()
|
||||
|
||||
def test_start(self, mock_get, _):
|
||||
resp = self.client.post('/unfollow/start', data={
|
||||
'me': 'https://snarfed.org',
|
||||
'key': self.follower.id(),
|
||||
})
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertTrue(resp.headers['Location'].startswith(indieauth.INDIEAUTH_URL),
|
||||
resp.headers['Location'])
|
||||
|
||||
def test_callback(self, mock_get, mock_post):
|
||||
User.get_or_create('snarfed.org')
|
||||
mock_post.side_effect = (
|
||||
requests_response('me=https://snarfed.org'),
|
||||
requests_response('OK'), # AP Undo Follow to inbox
|
||||
)
|
||||
# oauth-dropins indieauth https://snarfed.org fetch for user json
|
||||
mock_get.return_value = requests_response('')
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
'me': 'https://snarfed.org',
|
||||
'state': self.follower.id(),
|
||||
})
|
||||
with self.client:
|
||||
resp = self.client.get(f'/unfollow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertEqual('/user/snarfed.org/following',resp.headers['Location'])
|
||||
self.assertEqual([f'Unfollowed <a href="https://bar/url">bar/url</a>.'],
|
||||
get_flashed_messages())
|
||||
|
||||
inbox_args, inbox_kwargs = mock_post.call_args_list[1]
|
||||
self.assertEqual(('http://bar/inbox',), inbox_args)
|
||||
self.assert_equals(UNDO_FOLLOW, json_loads(inbox_kwargs['data']))
|
||||
|
||||
follower = Follower.get_by_id('https://bar/id snarfed.org')
|
||||
self.assertEqual('inactive', follower.status)
|
||||
|
||||
activities = Activity.query().fetch()
|
||||
self.assert_entities_equal(
|
||||
[Activity(id='http://localhost/user/snarfed.org/following__undo-2022-01-02T03:04:05-https://bar/id https://bar/id', domain=['snarfed.org'],
|
||||
status='complete', protocol='activitypub', direction='out',
|
||||
source_as2=json_dumps(UNDO_FOLLOW))],
|
||||
activities,
|
||||
ignore=['created', 'updated'])
|
||||
|
||||
def test_callback_missing_user(self, mock_get, mock_post):
|
||||
mock_post.return_value = requests_response('me=https://snarfed.org')
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
'me': 'https://snarfed.org',
|
||||
'state': 'https://bar/id',
|
||||
})
|
||||
with self.client:
|
||||
resp = self.client.get(f'/unfollow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(400, resp.status_code)
|
||||
|
|
Ładowanie…
Reference in New Issue