diff --git a/lbry/error/README.md b/lbry/error/README.md index 09352b52f..ecb59d7d3 100644 --- a/lbry/error/README.md +++ b/lbry/error/README.md @@ -53,7 +53,7 @@ Code | Name | Message 407 | DataDownload | Failed to download blob. *generic* 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}'. +411 | ResolveCensored | Resolve of '{url}' was censored by channel with claim id '{claim_id(censor_hash)}'. 420 | KeyFeeAboveMaxAllowed | {message} 421 | InvalidPassword | Password is invalid. 422 | IncompatibleWalletServer | '{server}:{port}' has an incompatibly old version. diff --git a/lbry/error/__init__.py b/lbry/error/__init__.py index 66247f4e0..78fa339da 100644 --- a/lbry/error/__init__.py +++ b/lbry/error/__init__.py @@ -1,4 +1,4 @@ -from .base import BaseError +from .base import BaseError, claim_id class UserInputError(BaseError): @@ -16,18 +16,22 @@ class CommandError(UserInputError): class CommandDoesNotExistError(CommandError): def __init__(self, command): + self.command = command super().__init__(f"Command '{command}' does not exist.") class CommandDeprecatedError(CommandError): def __init__(self, command): + self.command = command super().__init__(f"Command '{command}' is deprecated.") class CommandInvalidArgumentError(CommandError): def __init__(self, argument, command): + self.argument = argument + self.command = command super().__init__(f"Invalid argument '{argument}' to command '{command}'.") @@ -37,6 +41,7 @@ class CommandTemporarilyUnavailableError(CommandError): """ def __init__(self, command): + self.command = command super().__init__(f"Command '{command}' is temporarily unavailable.") @@ -46,6 +51,7 @@ class CommandPermanentlyUnavailableError(CommandError): """ def __init__(self, command): + self.command = command super().__init__(f"Command '{command}' is permanently unavailable.") @@ -58,12 +64,15 @@ class InputValueError(UserInputError, ValueError): class GenericInputValueError(InputValueError): def __init__(self, value, argument): + self.value = value + self.argument = argument super().__init__(f"The value '{value}' for argument '{argument}' is not valid.") class InputValueIsNoneError(InputValueError): def __init__(self, argument): + self.argument = argument super().__init__(f"None or null is not valid value for argument '{argument}'.") @@ -79,6 +88,7 @@ class ConfigWriteError(ConfigurationError): """ def __init__(self, path): + self.path = path super().__init__(f"Cannot write configuration file '{path}'.") @@ -88,6 +98,7 @@ class ConfigReadError(ConfigurationError): """ def __init__(self, path): + self.path = path super().__init__(f"Cannot find provided configuration file '{path}'.") @@ -97,18 +108,21 @@ class ConfigParseError(ConfigurationError): """ def __init__(self, path): + self.path = path super().__init__(f"Failed to parse the configuration file '{path}'.") class ConfigMissingError(ConfigurationError): def __init__(self, path): + self.path = path super().__init__(f"Configuration file '{path}' is missing setting that has no default / fallback.") class ConfigInvalidError(ConfigurationError): def __init__(self, path): + self.path = path super().__init__(f"Configuration file '{path}' has setting with invalid value.") @@ -188,24 +202,29 @@ class DataDownloadError(WalletError): class ResolveError(WalletError): def __init__(self, url): + self.url = url super().__init__(f"Failed to resolve '{url}'.") class ResolveTimeoutError(WalletError): def __init__(self, url): + self.url = url 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}'.") + def __init__(self, url, censor_hash): + self.url = url + self.censor_hash = censor_hash + super().__init__(f"Resolve of '{url}' was censored by channel with claim id '{claim_id(censor_hash)}'.") class KeyFeeAboveMaxAllowedError(WalletError): def __init__(self, message): + self.message = message super().__init__(f"{message}") @@ -218,6 +237,8 @@ class InvalidPasswordError(WalletError): class IncompatibleWalletServerError(WalletError): def __init__(self, server, port): + self.server = server + self.port = port super().__init__(f"'{server}:{port}' has an incompatibly old version.") @@ -278,30 +299,35 @@ class DownloadCancelledError(BlobError): class DownloadSDTimeoutError(BlobError): def __init__(self, download): + self.download = download super().__init__(f"Failed to download sd blob {download} within timeout.") class DownloadDataTimeoutError(BlobError): def __init__(self, download): + self.download = download super().__init__(f"Failed to download data blobs for sd hash {download} within timeout.") class InvalidStreamDescriptorError(BlobError): def __init__(self, message): + self.message = message super().__init__(f"{message}") class InvalidDataError(BlobError): def __init__(self, message): + self.message = message super().__init__(f"{message}") class InvalidBlobHashError(BlobError): def __init__(self, message): + self.message = message super().__init__(f"{message}") @@ -314,12 +340,14 @@ class ComponentError(BaseError): class ComponentStartConditionNotMetError(ComponentError): def __init__(self, components): + self.components = components super().__init__(f"Unresolved dependencies for: {components}") class ComponentsNotStartedError(ComponentError): def __init__(self, message): + self.message = message super().__init__(f"{message}") @@ -332,16 +360,20 @@ class CurrencyExchangeError(BaseError): class InvalidExchangeRateResponseError(CurrencyExchangeError): def __init__(self, source, reason): + self.source = source + self.reason = reason super().__init__(f"Failed to get exchange rate from {source}: {reason}") class CurrencyConversionError(CurrencyExchangeError): def __init__(self, message): + self.message = message super().__init__(f"{message}") class InvalidCurrencyError(CurrencyExchangeError): def __init__(self, currency): + self.currency = currency super().__init__(f"Invalid currency: {currency} is not a supported currency.") diff --git a/lbry/error/base.py b/lbry/error/base.py index 1d2f0f30a..fce1be2ca 100644 --- a/lbry/error/base.py +++ b/lbry/error/base.py @@ -1,2 +1,9 @@ +from binascii import hexlify + + +def claim_id(claim_hash): + return hexlify(claim_hash[::-1]).decode() + + class BaseError(Exception): pass diff --git a/lbry/error/generate.py b/lbry/error/generate.py index 9c1cef501..1752e8452 100644 --- a/lbry/error/generate.py +++ b/lbry/error/generate.py @@ -13,10 +13,12 @@ class {name}({parents}):{doc} """ INIT = """ - def __init__({args}): + def __init__({args}):{fields} super().__init__({format}"{message}") """ +FUNCTIONS = ['claim_id'] + class ErrorClass: @@ -50,10 +52,20 @@ 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): + for func in FUNCTIONS: + if arg.startswith(f'{func}('): + arg = arg[len(f'{func}('):-1] + break args.append(arg) return args + @staticmethod + def get_fields(args): + if len(args) > 1: + return f''.join(f'\n{INDENT*2}self.{field} = {field}' for field in args[1:]) + return '' + @staticmethod def get_doc_string(doc): if doc: @@ -69,7 +81,8 @@ class ErrorClass: args = self.get_arguments() if self.is_leaf: out.write((CLASS + INIT).format( - name=self.class_name, parents=', '.join(parents), args=', '.join(args), + name=self.class_name, parents=', '.join(parents), + args=', '.join(args), fields=self.get_fields(args), message=self.message, doc=self.get_doc_string(self.comment), format='f' if len(args) > 1 else '' )) else: @@ -102,7 +115,7 @@ def find_parent(stack, child): def generate(out): - out.write('from .base import BaseError\n') + out.write(f"from .base import BaseError, {', '.join(FUNCTIONS)}\n") stack = {} for error in get_errors(): error.render(out, find_parent(stack, error)) diff --git a/lbry/schema/result.py b/lbry/schema/result.py index 13d683b56..3469a9c4c 100644 --- a/lbry/schema/result.py +++ b/lbry/schema/result.py @@ -13,6 +13,16 @@ NOT_FOUND = ErrorMessage.Code.Name(ErrorMessage.NOT_FOUND) BLOCKED = ErrorMessage.Code.Name(ErrorMessage.BLOCKED) +def set_reference(reference, claim_hash, rows): + if claim_hash: + for txo in rows: + if claim_hash == txo['claim_hash']: + reference.tx_hash = txo['txo_hash'][:32] + reference.nout = struct.unpack(' List: ) -def _get_referenced_rows(censor: Censor, txo_rows: List[dict]): +def _get_referenced_rows(txo_rows: List[dict], censor_channels: List[bytes]): + censor = ctx.get().get_resolve_censor() repost_hashes = set(filter(None, map(itemgetter('reposted_claim_hash'), txo_rows))) - channel_hashes = set(filter(None, map(itemgetter('channel_hash'), txo_rows))) + channel_hashes = set(chain( + filter(None, map(itemgetter('channel_hash'), txo_rows)), + censor_channels + )) reposted_txos = [] if repost_hashes: @@ -418,7 +423,7 @@ def search(constraints) -> Tuple[List, List, int, int, Censor]: context = ctx.get() search_censor = context.get_search_censor() txo_rows = search_claims(search_censor, **constraints) - extra_txo_rows = _get_referenced_rows(context.get_resolve_censor(), txo_rows) + extra_txo_rows = _get_referenced_rows(txo_rows, search_censor.censored.keys()) return txo_rows, extra_txo_rows, constraints['offset'], total, search_censor @@ -426,7 +431,8 @@ def search(constraints) -> Tuple[List, List, int, int, Censor]: def resolve(urls) -> Tuple[List, List]: txo_rows = [resolve_url(raw_url) for raw_url in urls] extra_txo_rows = _get_referenced_rows( - ctx.get().get_resolve_censor(), [r for r in txo_rows if isinstance(r, dict)] + [txo for txo in txo_rows if isinstance(txo, dict)], + [txo.censor_hash for txo in txo_rows if isinstance(txo, ResolveCensoredError)] ) return txo_rows, extra_txo_rows @@ -452,7 +458,7 @@ def resolve_url(raw_url): if matches: channel = matches[0] elif censor.censored: - return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode()) + return ResolveCensoredError(raw_url, next(iter(censor.censored))) else: return LookupError(f'Could not find channel in "{raw_url}".') @@ -472,7 +478,7 @@ def resolve_url(raw_url): if matches: return matches[0] elif censor.censored: - return ResolveCensoredError(raw_url, hexlify(next(iter(censor.censored))[::-1]).decode()) + return ResolveCensoredError(raw_url, next(iter(censor.censored))) else: return LookupError(f'Could not find claim at "{raw_url}".') diff --git a/lbry/wallet/server/db/writer.py b/lbry/wallet/server/db/writer.py index fbccb06ee..db2cda525 100644 --- a/lbry/wallet/server/db/writer.py +++ b/lbry/wallet/server/db/writer.py @@ -241,10 +241,10 @@ class SQLDB: streams, channels = {}, {} if channel_hashes: sql = query( - "SELECT claim.channel_hash, claim.reposted_claim_hash, reposted.claim_type " - "FROM claim JOIN claim AS reposted ON (reposted.claim_hash=claim.reposted_claim_hash)", **{ - 'claim.reposted_claim_hash__is_not_null': 1, - 'claim.channel_hash__in': channel_hashes + "SELECT repost.channel_hash, repost.reposted_claim_hash, target.claim_type " + "FROM claim as repost JOIN claim AS target ON (target.claim_hash=repost.reposted_claim_hash)", **{ + 'repost.reposted_claim_hash__is_not_null': 1, + 'repost.channel_hash__in': channel_hashes } ) for blocked_claim in self.execute(*sql): diff --git a/tests/integration/blockchain/test_claim_commands.py b/tests/integration/blockchain/test_claim_commands.py index cd8593307..24d605dee 100644 --- a/tests/integration/blockchain/test_claim_commands.py +++ b/tests/integration/blockchain/test_claim_commands.py @@ -405,10 +405,10 @@ class ClaimCommands(ClaimTestCase): await self.ledger.wait(channel_tx) r = await self.claim_list(resolve=True) - self.assertEqual('not_found', r[0]['meta']['error']['name']) + self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name']) self.assertTrue(r[1]['meta']['is_controlling']) r = await self.channel_list(resolve=True) - self.assertEqual('not_found', r[0]['meta']['error']['name']) + self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name']) self.assertTrue(r[1]['meta']['is_controlling']) # confirm it @@ -430,10 +430,10 @@ class ClaimCommands(ClaimTestCase): await self.ledger.wait(stream_tx) r = await self.claim_list(resolve=True) - self.assertEqual('not_found', r[0]['meta']['error']['name']) + self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name']) self.assertTrue(r[1]['meta']['is_controlling']) r = await self.stream_list(resolve=True) - self.assertEqual('not_found', r[0]['meta']['error']['name']) + self.assertEqual('NOT_FOUND', r[0]['meta']['error']['name']) self.assertTrue(r[1]['meta']['is_controlling']) # confirm it @@ -845,18 +845,26 @@ class StreamCommands(ClaimTestCase): # search for blocked content directly result = await self.out(self.daemon.jsonrpc_claim_search(name='bad_content')) + blocked = result['blocked'] self.assertEqual([], result['items']) - self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked']) + self.assertEqual(1, blocked['total']) + self.assertEqual(1, len(blocked['channels'])) + self.assertEqual(1, blocked['channels'][0]['blocked']) + self.assertTrue(blocked['channels'][0]['channel']['short_url'].startswith('lbry://@filtering#')) # search channel containing blocked content result = await self.out(self.daemon.jsonrpc_claim_search(channel='@some_channel')) + blocked = result['blocked'] self.assertEqual(1, len(result['items'])) - self.assertEqual({"channels": {filtering_channel_id: 1}, "total": 1}, result['blocked']) + self.assertEqual(1, blocked['total']) + self.assertEqual(1, len(blocked['channels'])) + self.assertEqual(1, blocked['channels'][0]['blocked']) + self.assertTrue(blocked['channels'][0]['channel']['short_url'].startswith('lbry://@filtering#')) # 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']) + self.assertEqual({"channels": [], "total": 0}, result['blocked']) blocking_channel_id = self.get_claim_id( await self.channel_create('@blocking', '1.0') @@ -874,8 +882,9 @@ class StreamCommands(ClaimTestCase): # 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.assertEqual(error['name'], 'BLOCKED') self.assertTrue(error['text'].startswith("Resolve of 'lbry://@some_channel/bad_content' was censored")) + self.assertTrue(error['censor']['short_url'].startswith('lbry://@blocking#')) async def test_publish_updates_file_list(self): tx = await self.stream_create(title='created') diff --git a/tests/integration/blockchain/test_resolve_command.py b/tests/integration/blockchain/test_resolve_command.py index 04e2b2528..1809803d7 100644 --- a/tests/integration/blockchain/test_resolve_command.py +++ b/tests/integration/blockchain/test_resolve_command.py @@ -15,7 +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') + self.assertEqual(other['error']['name'], 'NOT_FOUND') else: self.assertEqual(claim_id, other['claim_id']) @@ -186,7 +186,7 @@ class ResolveCommand(BaseResolveTestCase): self.assertEqual(response, { 'lbry://@abc/on-channel-claim': { 'error': { - 'name': 'not_found', + 'name': 'NOT_FOUND', 'text': 'Could not find claim at "lbry://@abc/on-channel-claim".', } } @@ -265,7 +265,7 @@ class ResolveCommand(BaseResolveTestCase): self.assertEqual(response, { '@olds/bad_example': { 'error': { - 'name': 'not_found', + '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 e71322435..319a0dc21 100644 --- a/tests/integration/other/test_chris45.py +++ b/tests/integration/other/test_chris45.py @@ -88,7 +88,7 @@ class EpicAdventuresOfChris45(CommandTestCase): self.assertEqual( response['lbry://@spam/hovercraft'], {'error': { - 'name': 'not_found', + 'name': 'NOT_FOUND', 'text': 'Could not find claim at "lbry://@spam/hovercraft".' }} ) @@ -192,7 +192,7 @@ class EpicAdventuresOfChris45(CommandTestCase): self.assertEqual( response[uri], {'error': { - 'name': 'not_found', + 'name': 'NOT_FOUND', 'text': f'Could not find claim at "{uri}".' }} )