From 91c4200bb3531ae3e9751243b7df0bb5c8b27921 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Sat, 7 Jan 2023 18:00:19 -0800 Subject: [PATCH] follow UI: synthesize Follow activity id, store an Activity, link address, form bug fix --- follow.py | 24 +++++++++++++++--------- models.py | 4 ++-- templates/following.html | 2 +- tests/test_follow.py | 21 ++++++++++++++++----- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/follow.py b/follow.py index 9a448ee..336ccc1 100644 --- a/follow.py +++ b/follow.py @@ -13,11 +13,12 @@ from oauth_dropins import indieauth from oauth_dropins.webutil import flask_util, util from oauth_dropins.webutil.flask_util import error, flash from oauth_dropins.webutil import util +from oauth_dropins.webutil.testutil import NOW from oauth_dropins.webutil.util import json_dumps, json_loads from app import app import common -from models import Follower, User +from models import Activity, Follower, User logger = logging.getLogger(__name__) @@ -120,9 +121,6 @@ class FollowCallback(indieauth.Callback): if not User.get_by_id(domain): error(f'No user for domain {domain}') - # addr = state.get('address') - # if not addr: - # error(f'state missing address field') addr = state assert addr webfinger = fetch_webfinger(addr) @@ -146,18 +144,25 @@ class FollowCallback(indieauth.Callback): flash(f"AS2 profile {as2_url} missing id or inbox") return redirect(f'/user/{domain}/following') - common.signed_post(inbox, data={ + timestamp = NOW.replace(microsecond=0, tzinfo=None).isoformat() + follow_as2 = { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Follow', - 'id': 'TODO', + 'id': common.host_url(f'/user/{domain}/following#{timestamp}-{addr}'), 'object': id, 'actor': common.host_url(domain), 'to': [as2.PUBLIC_AUDIENCE], - }) + } + common.signed_post(inbox, data=follow_as2) Follower.get_or_create(dest=id, src=domain, status='active', last_follow=json_dumps({})) - flash(f'Followed {addr}.') + Activity.get_or_create(source='UI', target=id, domain=[domain], + direction='out', protocol='activitypub', status='complete', + source_as2=json_dumps(follow_as2, sort_keys=True)) + + link = util.pretty_link(obj.get('url') or id, text=addr) + flash(f'Followed {link}.') return redirect(f'/user/{domain}/following') @@ -165,4 +170,5 @@ 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')) + view_func=FollowCallback.as_view('follow_callback', 'unused'), + methods=['GET']) diff --git a/models.py b/models.py index 6b8f22e..fa43119 100644 --- a/models.py +++ b/models.py @@ -333,8 +333,8 @@ class Follower(StringIdModel): src = ndb.StringProperty() dest = ndb.StringProperty() - # most recent AP Follow activity (JSON). must have a composite actor object - # with an inbox, publicInbox, or sharedInbox! + # Most recent AP Follow activity (JSON). If this is an inbound follow, must + # have a composite actor object with an inbox, publicInbox, or sharedInbox. last_follow = ndb.TextProperty() status = ndb.StringProperty(choices=STATUSES, default='active') diff --git a/templates/following.html b/templates/following.html index 79a3b4f..f616726 100644 --- a/templates/following.html +++ b/templates/following.html @@ -9,7 +9,7 @@
Following
-
+

@foo@bar.'], get_flashed_messages()) mock_get.assert_has_calls(( self.req('https://bar/.well-known/webfinger?resource=acct:foo@bar'), @@ -150,19 +151,29 @@ class AddFollowerTest(testutil.TestCase): )) inbox_args, inbox_kwargs = mock_post.call_args_list[-1] self.assertEqual(('http://bar/inbox',), inbox_args) - self.assert_equals({ + + expected_follow = { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Follow', - 'id': 'TODO', + 'id': 'http://localhost/user/snarfed.org/following#2022-01-02T03:04:05-@foo@bar', 'actor': 'http://localhost/snarfed.org', 'object': 'https://bar/id', 'to': [as2.PUBLIC_AUDIENCE], - }, json_loads(inbox_kwargs['data'])) + } + self.assert_equals(expected_follow, json_loads(inbox_kwargs['data'])) followers = Follower.query().fetch() self.assertEqual(1, len(followers)) self.assertEqual('https://bar/id snarfed.org', followers[0].key.id()) + activities = Activity.query().fetch() + self.assert_entities_equal( + [Activity(id='UI https://bar/id', domain=['snarfed.org'], + status='complete', protocol='activitypub', direction='out', + source_as2=json_dumps(expected_follow, sort_keys=True))], + activities, + ignore=['created', 'updated']) + def test_callback_missing_user(self, mock_get, mock_post): mock_post.return_value = requests_response('me=https://snarfed.org')