diff --git a/lbry/error/README.md b/lbry/error/README.md index 2295ad8b9..09352b52f 100644 --- a/lbry/error/README.md +++ b/lbry/error/README.md @@ -51,11 +51,12 @@ Code | Name | Message 405 | ChannelKeyNotFound | Channel signing key not found. 406 | ChannelKeyInvalid | Channel signing key is out of date. -- For example, channel was updated but you don't have the updated key. 407 | DataDownload | Failed to download blob. *generic* -408 | Resolve | Failed to resolve '{url}'. -409 | ResolveTimeout | Failed to resolve '{url}' within the timeout. -410 | KeyFeeAboveMaxAllowed | {message} -411 | InvalidPassword | Password is invalid. -412 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version. +410 | Resolve | Failed to resolve '{url}'. +411 | ResolveTimeout | Failed to resolve '{url}' within the timeout. +411 | ResolveCensored | Resolve of '{url}' was censored by channel with claim id '{censor_id}'. +420 | KeyFeeAboveMaxAllowed | {message} +421 | InvalidPassword | Password is invalid. +422 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version. **5xx** | Blob | **Blobs** 500 | BlobNotFound | Blob not found. 501 | BlobPermissionDenied | Permission denied to read blob. diff --git a/lbry/error/__init__.py b/lbry/error/__init__.py index 567b3cb17..66247f4e0 100644 --- a/lbry/error/__init__.py +++ b/lbry/error/__init__.py @@ -197,6 +197,12 @@ class ResolveTimeoutError(WalletError): super().__init__(f"Failed to resolve '{url}' within the timeout.") +class ResolveCensoredError(WalletError): + + def __init__(self, url, censor_id): + super().__init__(f"Resolve of '{url}' was censored by channel with claim id '{censor_id}'.") + + class KeyFeeAboveMaxAllowedError(WalletError): def __init__(self, message): diff --git a/lbry/error/generate.py b/lbry/error/generate.py index 3d28b0008..9c1cef501 100644 --- a/lbry/error/generate.py +++ b/lbry/error/generate.py @@ -50,7 +50,7 @@ class ErrorClass: def get_arguments(self): args = ['self'] - for arg in re.findall('{([a-z0-1]+)}', self.message): + for arg in re.findall('{([a-z0-1_]+)}', self.message): args.append(arg) return args diff --git a/lbry/schema/result.py b/lbry/schema/result.py index b9d47d3fe..13d683b56 100644 --- a/lbry/schema/result.py +++ b/lbry/schema/result.py @@ -4,7 +4,13 @@ from typing import List from binascii import hexlify from itertools import chain +from lbry.error import ResolveCensoredError from lbry.schema.types.v2.result_pb2 import Outputs as OutputsMessage +from lbry.schema.types.v2.result_pb2 import Error as ErrorMessage + +INVALID = ErrorMessage.Code.Name(ErrorMessage.INVALID) +NOT_FOUND = ErrorMessage.Code.Name(ErrorMessage.NOT_FOUND) +BLOCKED = ErrorMessage.Code.Name(ErrorMessage.BLOCKED) class Censor: @@ -73,7 +79,12 @@ class Outputs: def message_to_txo(self, txo_message, tx_map): if txo_message.WhichOneof('meta') == 'error': - return None + return { + 'error': { + 'name': txo_message.error.Code.Name(txo_message.error.code).lower(), + 'text': txo_message.error.text, + } + } txo = tx_map[txo_message.tx_hash].outputs[txo_message.nout] if txo_message.WhichOneof('meta') == 'claim': claim = txo_message.claim @@ -146,9 +157,11 @@ class Outputs: if isinstance(txo, Exception): txo_message.error.text = txo.args[0] if isinstance(txo, ValueError): - txo_message.error.code = txo_message.error.INVALID + txo_message.error.code = ErrorMessage.INVALID elif isinstance(txo, LookupError): - txo_message.error.code = txo_message.error.NOT_FOUND + txo_message.error.code = ErrorMessage.NOT_FOUND + elif isinstance(txo, ResolveCensoredError): + txo_message.error.code = ErrorMessage.BLOCKED return txo_message.tx_hash = txo['txo_hash'][:32] txo_message.nout, = struct.unpack(' Tuple[List[Output], dict, int, int]: diff --git a/lbry/wallet/server/db/reader.py b/lbry/wallet/server/db/reader.py index 7fd4ca1f3..f2a045ac6 100644 --- a/lbry/wallet/server/db/reader.py +++ b/lbry/wallet/server/db/reader.py @@ -4,14 +4,14 @@ import apsw import logging from operator import itemgetter from typing import Tuple, List, Dict, Union, Type, Optional -from binascii import unhexlify +from binascii import unhexlify, hexlify from decimal import Decimal from contextvars import ContextVar from functools import wraps from dataclasses import dataclass from lbry.wallet.database import query, interpolate - +from lbry.error import ResolveCensoredError from lbry.schema.url import URL, normalize_name from lbry.schema.tags import clean_tags from lbry.schema.result import Outputs, Censor @@ -451,6 +451,8 @@ def resolve_url(raw_url): matches = search_claims(censor, **query, limit=1) if matches: channel = matches[0] + elif censor.censored: + return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode()) else: return LookupError(f'Could not find channel in "{raw_url}".') @@ -469,8 +471,10 @@ def resolve_url(raw_url): matches = search_claims(censor, **query, limit=1) if matches: return matches[0] + elif censor.censored: + return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode()) else: - return LookupError(f'Could not find stream in "{raw_url}".') + return LookupError(f'Could not find claim at "{raw_url}".') return channel diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py index b1e391522..2a93cb3d7 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -763,29 +763,48 @@ class StreamCommands(ClaimTestCase): bad_content_id = self.get_claim_id( await self.stream_create('bad_content', '1.1', channel_name='@some_channel', tags=['bad']) ) - blocking_channel_id = self.get_claim_id( + filtering_channel_id = self.get_claim_id( await self.channel_create('@filtering', '1.0') ) self.conductor.spv_node.server.db.sql.filtering_channel_hashes.add( - unhexlify(blocking_channel_id)[::-1] + unhexlify(filtering_channel_id)[::-1] ) await self.stream_repost(bad_content_id, 'filter1', '1.1', channel_name='@filtering') # search for blocked content directly result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content')) self.assertEqual([], result['items']) - self.assertEqual({"channels": {blocking_channel_id: 1}, "total": 1}, result['blocked']) + self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked']) # search channel containing blocked content result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel')) self.assertEqual(1, len(result['items'])) - self.assertEqual({"channels": {blocking_channel_id: 1}, "total": 1}, result['blocked']) + self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked']) # content was filtered by not_tag before censoring result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel', not_tags=["good", "bad"])) self.assertEqual(0, len(result['items'])) self.assertEqual({"channels": {}, "total": 0}, result['blocked']) + blocking_channel_id = self.get_claim_id( + await self.channel_create('@blocking', '1.0') + ) + self.conductor.spv_node.server.db.sql.blocking_channel_hashes.add( + unhexlify(blocking_channel_id)[::-1] + ) + + # filtered content can still be resolved + result = await self.out(self.daemon.jsonrpc_resolve('lbry://@some_channel/bad_content')) + self.assertEqual(bad_content_id, result['lbry://@some_channel/bad_content']['claim_id']) + + await self.stream_repost(bad_content_id, 'block1', '1.1', channel_name='@blocking') + + # blocked content is not resolveable + result = await self.out(self.daemon.jsonrpc_resolve('lbry://@some_channel/bad_content')) + error = result['lbry://@some_channel/bad_content']['error'] + self.assertTrue(error['name'], 'blocked') + self.assertTrue(error['text'].startswith("Resolve of 'lbry://@some_channel/bad_content' was censored")) + async def test_publish_updates_file_list(self): tx = await self.stream_create(title='created') txo = tx['outputs'][0] diff --git a/tests/integration/blockchain/test_internal_transaction_api.py b/tests/integration/blockchain/test_internal_transaction_api.py index f71522380..2bb4ac944 100644 --- a/tests/integration/blockchain/test_internal_transaction_api.py +++ b/tests/integration/blockchain/test_internal_transaction_api.py @@ -72,6 +72,7 @@ class BasicTransactionTest(IntegrationTestCase): self.assertIn('error', response['lbry://@bar/foo']) # checks for expected format in inexistent URIs - response = await self.ledger.resolve([], ['lbry://404', 'lbry://@404']) - self.assertEqual('lbry://404 did not resolve to a claim', response['lbry://404']['error']) - self.assertEqual('lbry://@404 did not resolve to a claim', response['lbry://@404']['error']) + response = await self.ledger.resolve([], ['lbry://404', 'lbry://@404', 'lbry://@404/404']) + self.assertEqual('Could not find claim at "lbry://404".', response['lbry://404']['error']['text']) + self.assertEqual('Could not find channel in "lbry://@404".', response['lbry://@404']['error']['text']) + self.assertEqual('Could not find channel in "lbry://@404/404".', response['lbry://@404/404']['error']['text']) diff --git a/tests/integration/blockchain/test_resolve_command.py b/tests/integration/blockchain/test_resolve_command.py index ba43b3b23..04e2b2528 100644 --- a/tests/integration/blockchain/test_resolve_command.py +++ b/tests/integration/blockchain/test_resolve_command.py @@ -15,6 +15,7 @@ class BaseResolveTestCase(CommandTestCase): other = (await self.resolve(name))[name] if claim_id is None: self.assertIn('error', other) + self.assertEqual(other['error']['name'], 'not_found') else: self.assertEqual(claim_id, other['claim_id']) @@ -183,7 +184,12 @@ class ResolveCommand(BaseResolveTestCase): # only possible outside a channel response = await self.resolve('lbry://@abc/on-channel-claim') self.assertEqual(response, { - 'lbry://@abc/on-channel-claim': {'error': 'lbry://@abc/on-channel-claim did not resolve to a claim'} + 'lbry://@abc/on-channel-claim': { + 'error': { + 'name': 'not_found', + 'text': 'Could not find claim at "lbry://@abc/on-channel-claim".', + } + } }) response = (await self.resolve('lbry://on-channel-claim'))['lbry://on-channel-claim'] self.assertFalse(response['is_channel_signature_valid']) @@ -257,7 +263,12 @@ class ResolveCommand(BaseResolveTestCase): self.assertFalse(response['bad_example']['is_channel_signature_valid']) response = await self.resolve('@olds/bad_example') self.assertEqual(response, { - '@olds/bad_example': {'error': '@olds/bad_example did not resolve to a claim'} + '@olds/bad_example': { + 'error': { + 'name': 'not_found', + 'text': 'Could not find claim at "@olds/bad_example".', + } + } }) diff --git a/tests/integration/other/test_chris45.py b/tests/integration/other/test_chris45.py index 1f7813e02..e71322435 100644 --- a/tests/integration/other/test_chris45.py +++ b/tests/integration/other/test_chris45.py @@ -87,7 +87,10 @@ class EpicAdventuresOfChris45(CommandTestCase): response = await self.resolve('lbry://@spam/hovercraft') self.assertEqual( response['lbry://@spam/hovercraft'], - {'error': 'lbry://@spam/hovercraft did not resolve to a claim'} + {'error': { + 'name': 'not_found', + 'text': 'Could not find claim at "lbry://@spam/hovercraft".' + }} ) # After abandoning he just waits for his LBCs to be returned to his account @@ -186,4 +189,10 @@ class EpicAdventuresOfChris45(CommandTestCase): # He them checks that the claim doesn't resolve anymore. response = await self.resolve(uri) - self.assertEqual(response[uri], {'error': f'{uri} did not resolve to a claim'}) + self.assertEqual( + response[uri], + {'error': { + 'name': 'not_found', + 'text': f'Could not find claim at "{uri}".' + }} + ) diff --git a/tests/unit/wallet/server/test_sqldb.py b/tests/unit/wallet/server/test_sqldb.py index b46c4f11e..af5566a77 100644 --- a/tests/unit/wallet/server/test_sqldb.py +++ b/tests/unit/wallet/server/test_sqldb.py @@ -617,7 +617,10 @@ class TestContentBlocking(TestSQLDB): self.assertEqual(1, censor.total) self.assertEqual({blocking_channel.claim_hash: 1}, censor.censored) results, _ = reader.resolve([claim1.claim_name]) - self.assertEqual('Could not find stream in "claim1".', results[0].args[0]) + self.assertEqual( + f"Resolve of 'claim1' was censored by channel with claim id '{blocking_channel.claim_id}'.", + results[0].args[0] + ) results, _ = reader.resolve([ claim2.claim_name, regular_channel.claim_name # claim2 and channel still resolved ]) @@ -654,8 +657,14 @@ class TestContentBlocking(TestSQLDB): results, _ = reader.resolve([ claim2.claim_name, regular_channel.claim_name # claim2 and channel don't resolve ]) - self.assertEqual('Could not find stream in "claim2".', results[0].args[0]) - self.assertEqual('Could not find channel in "@channel1".', results[1].args[0]) + self.assertEqual( + f"Resolve of 'claim2' was censored by channel with claim id '{blocking_channel.claim_id}'.", + results[0].args[0] + ) + self.assertEqual( + f"Resolve of '@channel1' was censored by channel with claim id '{blocking_channel.claim_id}'.", + results[1].args[0] + ) results, _ = reader.resolve([claim3.claim_name]) # claim3 still resolved self.assertEqual(claim3.claim_hash, results[0]['claim_hash'])