diff --git a/app.yaml b/app.yaml index 1e19331..dabf977 100644 --- a/app.yaml +++ b/app.yaml @@ -28,11 +28,16 @@ env_variables: # PLC_HOST: plc.bsky-sandbox.dev # APPVIEW_HOST: api.bsky-sandbox.dev # BGS_HOST: bgs.bsky-sandbox.dev + # MOD_SERVICE_HOST: ? + # MOD_SERVICE_DID: ? # prod PLC_HOST: plc.directory APPVIEW_HOST: api.bsky.app BGS_HOST: bsky.network + MOD_SERVICE_HOST: mod.bsky.app + MOD_SERVICE_DID: did:plc:ar7c4by46qjdydhdevvrndac + # ...or test against labeler.dholms.xyz / did:plc:vzxheqfwpbi3lxbgdh22js66 handlers: diff --git a/atproto.py b/atproto.py index 53d44a8..2f68582 100644 --- a/atproto.py +++ b/atproto.py @@ -375,6 +375,9 @@ class ATProto(User, Protocol): assert repo repo.callback = lambda _: common.create_task(queue='atproto-commit') + if verb == 'flag': + return to_cls.create_report(record, user) + # write commit type = record['$type'] lex_type = LEXICONS[type]['type'] @@ -473,6 +476,7 @@ class ATProto(User, Protocol): obj.key = ndb.Key(Object, id.replace(f'at://{handle}', f'at://{repo}')) try: + appview.address = f'https://{os.environ["APPVIEW_HOST"]}' ret = appview.com.atproto.repo.getRecord( repo=repo, collection=collection, rkey=rkey) except RequestException as e: @@ -540,7 +544,9 @@ class ATProto(User, Protocol): }) match ret.get('$type'): - case 'app.bsky.feed.like' | 'app.bsky.feed.repost': + case ('app.bsky.feed.like' + | 'app.bsky.feed.repost' + | 'com.atproto.moderation.createReport#input'): populate_cid(ret['subject']) case 'app.bsky.feed.post' if ret.get('reply'): populate_cid(ret['reply']['root']) @@ -549,6 +555,35 @@ class ATProto(User, Protocol): return ret + @classmethod + def create_report(cls, input, from_user): + """Sends a ``createReport`` for a ``flag`` activity. + + Args: + input (dict): ``createReport`` input + from_user (models.User): user (actor) this flag is from + + Returns: + bool: True if the report was sent successfully, False if the flag's + actor is not bridged into ATProto + """ + assert input['$type'] == 'com.atproto.moderation.createReport#input' + + repo_did = from_user.get_copy(ATProto) + if not repo_did: + return False + repo = arroba.server.storage.load_repo(repo_did) + mod_host = os.environ['MOD_SERVICE_HOST'] + token = service_jwt(host=mod_host, + aud=os.environ['MOD_SERVICE_DID'], + repo_did=repo_did, + privkey=repo.signing_key) + + client = Client(f'https://{mod_host}', headers={'User-Agent': USER_AGENT}) + output = client.com.atproto.moderation.createReport(input) + logger.info(f'Created report on {mod_host}: {json_dumps(output)}') + return True + # URL route is registered in hub.py def poll_notifications(): """Fetches and enqueueus new activities from the AppView for our users. diff --git a/hub.yaml b/hub.yaml index 52ecef0..0edf088 100644 --- a/hub.yaml +++ b/hub.yaml @@ -23,11 +23,16 @@ env_variables: # PLC_HOST: plc.bsky-sandbox.dev # APPVIEW_HOST: api.bsky-sandbox.dev # BGS_HOST: bgs.bsky-sandbox.dev + # MOD_SERVICE_HOST: ? + # MOD_SERVICE_DID: ? # prod PLC_HOST: plc.directory APPVIEW_HOST: api.bsky.app BGS_HOST: bsky.network + MOD_SERVICE_HOST: mod.bsky.app + MOD_SERVICE_DID: did:plc:ar7c4by46qjdydhdevvrndac + # ...or test against labeler.dholms.xyz / did:plc:vzxheqfwpbi3lxbgdh22js66 # need only one instance so that new commits can be delivered to subscribeRepos # subscribers in memory diff --git a/tests/test_atproto.py b/tests/test_atproto.py index 31ffecd..1687dec 100644 --- a/tests/test_atproto.py +++ b/tests/test_atproto.py @@ -283,7 +283,7 @@ class ATProtoTest(TestCase): }, obj.bsky) # eg https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=did:plc:s2koow7r6t7tozgd4slc3dsg&collection=app.bsky.feed.post&rkey=3jqcpv7bv2c2q mock_get.assert_called_once_with( - 'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Aabc&collection=app.bsky.feed.post&rkey=123', + 'https://appview.local/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Aabc&collection=app.bsky.feed.post&rkey=123', json=None, data=None, headers={ 'Content-Type': 'application/json', @@ -299,7 +299,7 @@ class ATProtoTest(TestCase): obj = Object(id='at://did:plc:abc/app.bsky.feed.post/123') self.assertFalse(ATProto.fetch(obj)) mock_get.assert_called_once_with( - 'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Aabc&collection=app.bsky.feed.post&rkey=123', + 'https://appview.local/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Aabc&collection=app.bsky.feed.post&rkey=123', json=None, data=None, headers=ANY) def test_fetch_bsky_app_url_fails(self): @@ -346,7 +346,7 @@ class ATProtoTest(TestCase): }, obj.bsky) mock_get.assert_any_call( - 'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=789', + 'https://appview.local/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=789', json=None, data=None, headers={ 'Content-Type': 'application/json', 'User-Agent': common.USER_AGENT, @@ -370,7 +370,7 @@ class ATProtoTest(TestCase): }, obj.bsky) mock_get.assert_called_with( - 'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.actor.profile&rkey=self', + 'https://appview.local/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.actor.profile&rkey=self', json=None, data=None, headers={ 'Content-Type': 'application/json', 'User-Agent': common.USER_AGENT, @@ -472,7 +472,7 @@ class ATProtoTest(TestCase): 'object': 'at://han.dull/app.bsky.feed.post/tid', }))) mock_get.assert_called_with( - 'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=tid', + 'https://appview.local/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=tid', json=None, data=None, headers=ANY) @patch('dns.resolver.resolve', side_effect=NXDOMAIN()) @@ -521,7 +521,7 @@ class ATProtoTest(TestCase): }))) mock_get.assert_called_with( - 'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=tid', + 'https://appview.local/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3Auser&collection=app.bsky.feed.post&rkey=tid', json=None, data=None, headers=ANY) def test_convert_blobs_false(self): @@ -957,7 +957,7 @@ class ATProtoTest(TestCase): Object.get_by_id(id='fake:repost').copies) mock_get.assert_called_with( - 'https://api.bsky-sandbox.dev/xrpc/com.atproto.repo.getRecord?repo=did%3Abob&collection=app.bsky.feed.post&rkey=tid', + 'https://appview.local/xrpc/com.atproto.repo.getRecord?repo=did%3Abob&collection=app.bsky.feed.post&rkey=tid', json=None, data=None, headers=ANY) mock_create_task.assert_called() @@ -1109,6 +1109,47 @@ class ATProtoTest(TestCase): mock_create_task.assert_called() + # createReport + @patch('requests.post', return_value=requests_response({'id': 3})) + # did:plc:eve + @patch('requests.get', return_value=requests_response({ + **DID_DOC, + 'id': 'did:plc:eve', + })) + def test_send_flag_createReport(self, _, mock_post): + user = self.make_user_and_repo() + + uri = 'at://did:plc:eve/app.bsky.feed.post/123' + obj = self.store_object(id='fake:flag', source_protocol='fake', our_as1={ + 'objectType': 'activity', + 'verb': 'flag', + 'actor': 'fake:user', + 'object': uri, + 'content': 'foo bar', + }) + self.store_object(id=uri, source_protocol='bsky', bsky={ + '$type': 'app.bsky.feed.post', + 'cid': 'bafy...', + }) + + self.assertTrue(ATProto.send(obj, 'https://bsky.brid.gy/')) + + repo = self.storage.load_repo(user.get_copy(ATProto)) + self.assertEqual({}, repo.get_contents()) + + mock_post.assert_called_with( + 'https://mod.service.local/xrpc/com.atproto.moderation.createReport', + json={ + '$type': 'com.atproto.moderation.createReport#input', + 'reasonType': 'com.atproto.moderation.defs#reasonOther', + 'reason': 'foo bar', + 'subject': { + '$type': 'com.atproto.repo.strongRef', + 'uri': uri, + 'cid': 'bafy...', + }, + }, data=None, headers=ANY) + @patch.object(tasks_client, 'create_task', return_value=Task(name='my task')) @patch('requests.get') def test_poll_notifications(self, mock_get, mock_create_task): @@ -1192,7 +1233,7 @@ class ATProtoTest(TestCase): self.assertEqual(200, resp.status_code) expected_list_notifs = call( - 'https://api.bsky-sandbox.dev/xrpc/app.bsky.notification.listNotifications?limit=10', + 'https://appview.local/xrpc/app.bsky.notification.listNotifications?limit=10', json=None, data=None, headers={ 'Content-Type': 'application/json', @@ -1290,7 +1331,7 @@ class ATProtoTest(TestCase): self.assertEqual(200, resp.status_code) get_timeline = call( - 'https://api.bsky-sandbox.dev/xrpc/app.bsky.feed.getTimeline?limit=10', + 'https://appview.local/xrpc/app.bsky.feed.getTimeline?limit=10', json=None, data=None, headers={ 'Content-Type': 'application/json', diff --git a/tests/test_integrations.py b/tests/test_integrations.py index 3740eef..13f74dc 100644 --- a/tests/test_integrations.py +++ b/tests/test_integrations.py @@ -418,7 +418,6 @@ class IntegrationTests(TestCase): 'object': 'https://bsky.brid.gy/bsky.brid.gy', }, }, json_loads(kwargs['data']), ignore=['to', '@context']) - util.d(mock_post.calls) @patch('requests.post') @patch('requests.get') diff --git a/tests/testutil.py b/tests/testutil.py index 70aae02..5e5447d 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -265,8 +265,12 @@ class TestCase(unittest.TestCase, testutil.Asserts): # arroba config os.environ.update({ + 'APPVIEW_HOST': 'appview.local', + 'BGS_HOST': 'bgs.local', 'PDS_HOST': 'pds.local', 'PLC_HOST': 'plc.local', + 'MOD_SERVICE_HOST': 'mod.service.local', + 'MOD_SERVICE_DID': 'did:mod-service', }) def tearDown(self):