From 56c5909b84021989986b63f8cfc47b68a55b6ca7 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Thu, 28 Sep 2023 14:42:18 -0700 Subject: [PATCH] add /bridge-user page to propagate a new user into ATProto for #647 --- pages.py | 30 ++++++++++++++++++++++++- templates/bridge_user.html | 15 +++++++++++++ tests/test_pages.py | 45 +++++++++++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 templates/bridge_user.html diff --git a/pages.py b/pages.py index bc11e41..8bd4b90 100644 --- a/pages.py +++ b/pages.py @@ -12,12 +12,13 @@ from google.cloud.ndb.stats import KindStat from granary import as1, as2, atom, microformats2, rss import humanize from oauth_dropins.webutil import flask_util, logs, util -from oauth_dropins.webutil.flask_util import error, redirect +from oauth_dropins.webutil.flask_util import error, flash, redirect import common from common import DOMAIN_RE from flask_app import app, cache from models import fetch_page, Follower, Object, PAGE_SIZE, PROTOCOLS +from protocol import Protocol FOLLOWERS_UI_LIMIT = 999 @@ -197,6 +198,33 @@ def feed(protocol, id): return body, {'Content-Type': rss.CONTENT_TYPE} +@app.get('/bridge-user') +@flask_util.cached(cache, datetime.timedelta(days=1)) +def bridge_user_page(): + return render_template('bridge_user.html') + + +@app.post('/bridge-user') +def bridge_user(): + handle = request.values['handle'] + + proto, id = Protocol.for_handle(handle) + if not proto: + flash(f"Couldn't determine protocol for {handle}") + return render_template('bridge_user.html'), 400 + + if not id: + id = proto.handle_to_id(handle) + if not id: + flash(f"Couldn't resolve {proto.__name__} handle {handle}") + return render_template('bridge_user.html'), 400 + + proto.get_or_create(id=id, propagate=True) + + flash('Bridging fake:user into Bluesky. Try searching for them in a minute!') + return render_template('bridge_user.html') + + def fetch_objects(query): """Fetches a page of Object entities from a datastore query. diff --git a/templates/bridge_user.html b/templates/bridge_user.html new file mode 100644 index 0000000..de40e01 --- /dev/null +++ b/templates/bridge_user.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block content %} + +

Enter a user to bridge

+

May be a web site domain or @user@server fediverse address. Bridgy Fed will federate the user into the Bluesky sandbox.

+ +
+
+ + +
+
+ +{% endblock %} diff --git a/tests/test_pages.py b/tests/test_pages.py index 7060ab8..d4be7d9 100644 --- a/tests/test_pages.py +++ b/tests/test_pages.py @@ -1,15 +1,23 @@ """Unit tests for pages.py.""" +from unittest.mock import patch + +import arroba.server +from flask import get_flashed_messages from google.cloud import ndb +from google.cloud.tasks_v2.types import Task from granary import atom, microformats2, rss from oauth_dropins.webutil import util +from oauth_dropins.webutil.appengine_config import tasks_client +from oauth_dropins.webutil.testutil import requests_response # import first so that Fake is defined before URL routes are registered from .testutil import Fake, TestCase, ACTOR, COMMENT, MENTION, NOTE from activitypub import ActivityPub -from models import Object, Follower +from models import Object, Follower, Target from web import Web +from granary.tests.test_bluesky import ACTOR_AS, ACTOR_PROFILE_VIEW_BSKY from .test_web import ACTOR_AS2, REPOST_AS2 ACTOR_WITH_PREFERRED_USERNAME = { @@ -311,6 +319,41 @@ class PagesTest(TestCase): # COMMENT's author self.assertIn('Dr. Eve', got.text) + @patch.object(tasks_client, 'create_task', return_value=Task(name='my task')) + @patch('requests.post', + return_value=requests_response('OK')) # create DID on PLC + def test_bridge_user(self, mock_post, mock_create_task): + Fake.fetchable = {'fake:user': ACTOR_AS} + + got = self.client.post('/bridge-user', data={'handle': 'fake:handle:user'}) + self.assertEqual(200, got.status_code) + self.assertEqual( + ['Bridging fake:user into Bluesky. Try searching for them in a minute!'], + get_flashed_messages()) + + # check user, repo + user = Fake.get_by_id('fake:user') + self.assertEqual('fake:handle:user', user.handle) + self.assertEqual([Target(uri=user.atproto_did, protocol='atproto')], + user.copies) + repo = arroba.server.storage.load_repo(user.atproto_did) + + # check profile + profile = repo.get_record('app.bsky.actor.profile', 'self') + self.assertEqual(ACTOR_PROFILE_VIEW_BSKY, profile) + + at_uri = f'at://{user.atproto_did}/app.bsky.actor.profile/self' + self.assertEqual([Target(uri=at_uri, protocol='atproto')], + Object.get_by_id(id='fake:user').copies) + + mock_create_task.assert_called() + + def test_bridge_user_bad_handle(self): + got = self.client.post('/bridge-user', data={'handle': 'bad xyz'}) + self.assertEqual(400, got.status_code) + self.assertEqual(["Couldn't determine protocol for bad xyz"], + get_flashed_messages()) + def test_nodeinfo(self): # just check that it doesn't crash self.client.get('/nodeinfo.json')