From 8f7c291e33dea2e76b0a2372292cf01f3f57be25 Mon Sep 17 00:00:00 2001 From: Oleg Silkin Date: Mon, 26 Aug 2019 12:06:18 -0400 Subject: [PATCH] Adds comment_hide functionality --- lbry/lbry/extras/daemon/Daemon.py | 92 ++++++-- lbry/lbry/extras/daemon/comment_client.py | 3 +- .../integration/test_comment_commands.py | 213 +++++++++++++++--- 3 files changed, 254 insertions(+), 54 deletions(-) diff --git a/lbry/lbry/extras/daemon/Daemon.py b/lbry/lbry/extras/daemon/Daemon.py index ea4619eb0..2716b4718 100644 --- a/lbry/lbry/extras/daemon/Daemon.py +++ b/lbry/lbry/extras/daemon/Daemon.py @@ -3424,7 +3424,8 @@ class Daemon(metaclass=JSONRPCServerType): @requires(WALLET_COMPONENT) async def jsonrpc_comment_list(self, claim_id, parent_id=None, page=1, page_size=50, - include_replies=True, is_channel_signature_valid=False): + include_replies=True, is_channel_signature_valid=False, + hidden=False, visible=False): """ List comments associated with a claim. @@ -3433,6 +3434,7 @@ class Daemon(metaclass=JSONRPCServerType): [(--page= --page_size=)] [--parent_id=] [--include_replies] [--is_channel_signature_valid] + [--visible | --hidden] Options: --claim_id= : (str) The claim on which the comment will be made on @@ -3443,6 +3445,8 @@ class Daemon(metaclass=JSONRPCServerType): --is_channel_signature_valid : (bool) Only include comments with valid signatures. [Warning: Paginated total size will not change, even if list reduces] + --visible : (bool) Select only Visisble Comments + --hidden : (bool) Select only Hidden Comments Returns: (dict) Containing the list, and information about the paginated content: @@ -3467,15 +3471,25 @@ class Daemon(metaclass=JSONRPCServerType): ] } """ - result = await comment_client.jsonrpc_post( - self.conf.comment_server, - "get_claim_comments", - claim_id=claim_id, - parent_id=parent_id, - page=page, - page_size=page_size, - top_level=not include_replies - ) + if hidden ^ visible: + result = await comment_client.jsonrpc_post( + self.conf.comment_server, + 'get_claim_hidden_comments', + claim_id=claim_id, + hidden=hidden, + page=page, + page_size=page_size + ) + else: + result = await comment_client.jsonrpc_post( + self.conf.comment_server, + 'get_claim_comments', + claim_id=claim_id, + parent_id=parent_id, + page=page, + page_size=page_size, + top_level=not include_replies + ) for comment in result.get('items', []): channel_url = comment.get('channel_url') if not channel_url: @@ -3554,13 +3568,13 @@ class Daemon(metaclass=JSONRPCServerType): comment_abandon ( | --comment_id=) Options: - --comment_id= : (str) The ID of the comment to be deleted. + --comment_id= : (str) The ID of the comment to be abandoned. Returns: - (dict) Object with the `comment_id` passed in as the key, and a flag indicating if it was deleted + (dict) Object with the `comment_id` passed in as the key, and a flag indicating if it was abandoned { (str): { - "deleted": (bool) + "abandoned": (bool) } } """ @@ -3569,14 +3583,54 @@ class Daemon(metaclass=JSONRPCServerType): self.conf.comment_server, 'get_channel_from_comment_id', comment_id=comment_id ) if 'error' in channel: - return {comment_id: {'deleted': False}} + return {comment_id: {'abandoned': False}} channel = await self.get_channel_or_none(None, **channel) abandon_comment_body.update({ 'channel_id': channel.claim_id, 'channel_name': channel.claim_name, }) comment_client.sign_comment(abandon_comment_body, channel, abandon=True) - return await comment_client.jsonrpc_post(self.conf.comment_server, 'delete_comment', abandon_comment_body) + return await comment_client.jsonrpc_post(self.conf.comment_server, 'abandon_comment', abandon_comment_body) + + @requires(WALLET_COMPONENT) + async def jsonrpc_comment_hide(self, comment_ids: typing.Union[str, list]): + """ + Hide a comment published to a claim you control. + + Usage: + comment_hide ... + + Options: + --comment_ids= : (str, list) one or more comment_id to hide. + + Returns: + (dict) keyed by comment_id, containing success info + '': { + "hidden": (bool) flag indicating if comment_id was hidden + } + """ + if isinstance(comment_ids, str): + comment_ids = [comment_ids] + + comments = await comment_client.jsonrpc_post( + self.conf.comment_server, 'get_comments_by_id', comment_ids=comment_ids + ) + claim_ids = {comment['claim_id'] for comment in comments} + claims = {cid: await self.ledger.get_claim_by_claim_id(claim_id=cid) for cid in claim_ids} + pieces = [] + for comment in comments: + claim = claims.get(comment['claim_id']) + if claim: + channel = await self.get_channel_or_none( + account_ids=[], + channel_id=claim.channel.claim_id, + channel_name=claim.channel.claim_name, + for_signing=True + ) + piece = {'comment_id': comment['comment_id']} + comment_client.sign_comment(piece, channel, abandon=True) + pieces.append(piece) + return await comment_client.jsonrpc_post(self.conf.comment_server, 'hide_comments', pieces=pieces) async def broadcast_or_release(self, tx, blocking=False): try: @@ -3597,18 +3651,14 @@ class Daemon(metaclass=JSONRPCServerType): def valid_stream_name_or_error(name: str): try: if not name: - raise Exception( - "Stream name cannot be blank." - ) + raise Exception('Stream name cannot be blank.') parsed = URL.parse(name) if parsed.has_channel: raise Exception( "Stream names cannot start with '@' symbol. This is reserved for channels claims." ) if not parsed.has_stream or parsed.stream.name != name: - raise Exception( - "Stream name has invalid characters." - ) + raise Exception('Stream name has invalid characters.') except (TypeError, ValueError): raise Exception("Invalid stream name.") diff --git a/lbry/lbry/extras/daemon/comment_client.py b/lbry/lbry/extras/daemon/comment_client.py index 9de1b4f94..26b2b3b18 100644 --- a/lbry/lbry/extras/daemon/comment_client.py +++ b/lbry/lbry/extras/daemon/comment_client.py @@ -57,8 +57,7 @@ async def jsonrpc_post(url: str, method: str, params: dict = None, **kwargs) -> params = params or {} params.update(kwargs) json_body = {'jsonrpc': '2.0', 'id': None, 'method': method, 'params': params} - headers = {'Content-Type': 'application/json'} - async with utils.aiohttp_request('POST', url, json=json_body, headers=headers) as response: + async with utils.aiohttp_request('POST', url, json=json_body) as response: try: result = await response.json() return result['result'] if 'result' in result else result diff --git a/lbry/tests/integration/test_comment_commands.py b/lbry/tests/integration/test_comment_commands.py index 34a04240d..d3db2b16d 100644 --- a/lbry/tests/integration/test_comment_commands.py +++ b/lbry/tests/integration/test_comment_commands.py @@ -26,6 +26,7 @@ class MockedCommentServer: 'signing_ts': None, 'timestamp': None, 'channel_url': None, + 'is_hidden': False, } def __init__(self, port=2903): @@ -45,10 +46,9 @@ class MockedCommentServer: @staticmethod def clean(d: dict): - return {k: v for k, v in d.items() if v} + return {k: v for k, v in d.items() if v or isinstance(v, bool)} def create_comment(self, channel_name=None, channel_id=None, **kwargs): - self.comment_id += 1 comment_id = self.comment_id channel_url = 'lbry://' + channel_name + '#' + channel_id if channel_id else None comment = self._create_comment( @@ -60,57 +60,97 @@ class MockedCommentServer: **kwargs ) self.comments.append(comment) + self.comment_id += 1 return self.clean(comment) - def delete_comment(self, comment_id: int, channel_id: str, **kwargs): + def abandon_comment(self, comment_id: int, channel_id: str, **kwargs): deleted = False try: - if 0 <= comment_id <= len(self.comments) and self.comments[comment_id - 1]['channel_id'] == channel_id: - self.comments.pop(comment_id - 1) + if 0 <= comment_id < len(self.comments) and self.comments[comment_id]['channel_id'] == channel_id: + self.comments.pop(comment_id) deleted = True finally: return { str(comment_id): { - 'deleted': deleted + 'abandoned': deleted } } - def get_claim_comments(self, page=1, page_size=50, **kwargs): + def hide_comment(self, comment_id, signing_ts, signature): + comment_id = int(comment_id) if not isinstance(comment_id, int) else comment_id + if 0 <= comment_id < len(self.comments) and len(signature) == 128 and signing_ts.isalnum(): + self.comments[comment_id]['is_hidden'] = True + return True + return False + + def hide_comments(self, pieces: list): + comments_hidden = [] + for p in pieces: + if self.hide_comment(**p): + comments_hidden.append(p['comment_id']) + return {'hidden': comments_hidden} + + def get_claim_comments(self, claim_id, page=1, page_size=50,**kwargs): + comments = list(filter(lambda c: c['claim_id'] == claim_id, self.comments)) return { 'page': page, 'page_size': page_size, - 'total_pages': ceil(len(self.comments)/page_size), - 'total_items': len(self.comments), - 'items': [self.clean(c) for c in (self.comments[::-1])[(page - 1) * page_size: page * page_size]] + 'total_pages': ceil(len(comments)/page_size), + 'total_items': len(comments), + 'items': [self.clean(c) for c in (comments[::-1])[(page - 1) * page_size: page * page_size]], + 'has_hidden_comments': bool(list(filter(lambda x: x['is_hidden'], comments))) + } + + def get_claim_hidden_comments(self, claim_id, hidden=True, page=1, page_size=50): + comments = list(filter(lambda c: c['claim_id'] == claim_id, self.comments)) + select_comments = list(filter(lambda c: c['is_hidden'] == hidden, comments)) + return { + 'page': page, + 'page_size': page_size, + 'total_pages': ceil(len(select_comments) / page_size), + 'total_items': len(select_comments), + 'items': [self.clean(c) for c in (select_comments[::-1])[(page - 1) * page_size: page * page_size]], + 'has_hidden_comments': bool(list(filter(lambda c: c['is_hidden'], comments))) } def get_comment_channel_by_id(self, comment_id: int, **kwargs): - comment = self.comments[comment_id - 1] + comment = self.comments[comment_id] return { 'channel_id': comment.get('channel_id'), 'channel_name': comment.get('channel_name') } + def get_comments_by_id(self, comment_ids: list): + comment_ids = [int(c) if not isinstance(c, int) else c for c in comment_ids] + comments = [self.comments[cmnt_id] for cmnt_id in comment_ids if 0 <= cmnt_id < len(self.comments)] + return comments + methods = { 'get_claim_comments': get_claim_comments, + 'get_comments_by_id': get_comments_by_id, 'create_comment': create_comment, - 'delete_comment': delete_comment, + 'abandon_comment': abandon_comment, 'get_channel_from_comment_id': get_comment_channel_by_id, + 'get_claim_hidden_comments': get_claim_hidden_comments, + 'hide_comments': hide_comments, } def process_json(self, body) -> dict: response = {'jsonrpc': '2.0', 'id': body['id']} try: if body['method'] in self.methods: - params = body.get('params', {}) - if 'comment_id' in params and type(params['comment_id']) is str: - params['comment_id'] = int(params['comment_id']) + params: dict = body.get('params', {}) + comment_id = params.get('comment_id') + if comment_id and not isinstance(comment_id, int): + params['comment_id'] = int(comment_id) + result = self.methods[body['method']](self, **params) response['result'] = result else: response['error'] = self.ERRORS['INVALID_METHOD'] - except Exception: + except Exception as err: response['error'] = self.ERRORS['UNKNOWN'] + response['error'].update({'exception': f'{type(err).__name__}: {err}'}) return response async def start(self): @@ -181,20 +221,19 @@ class CommentCommands(CommandTestCase): async def test02_unsigned_comment_list(self): stream = (await self.stream_create())['outputs'][0] comments = [] - for i in range(28): + num_items = 28 + for i in range(num_items): comment = await self.daemon.jsonrpc_comment_create( comment=f'{i}', claim_id=stream['claim_id'], ) self.assertIn('comment_id', comment) comments.append(comment) - - comment_list = await self.daemon.jsonrpc_comment_list( - claim_id=stream['claim_id'] - ) - self.assertIs(comment_list['page_size'], 50) - self.assertIs(comment_list['page'], 1) - self.assertIs(comment_list['total_items'], 28) + list_fields = ['items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'] + comment_list = await self.daemon.jsonrpc_comment_list(stream['claim_id']) + for field in list_fields: + self.assertIn(field, comment_list) + self.assertEqual(comment_list['total_items'], num_items) for comment in comment_list['items']: self.assertEqual(comment['comment'], comments.pop()['comment']) @@ -216,10 +255,12 @@ class CommentCommands(CommandTestCase): ) self.assertIn('comment_id', comment) comments.append(comment) - + list_fields = ['items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'] comment_list = await self.daemon.jsonrpc_comment_list( claim_id=stream['claim_id'] ) + for field in list_fields: + self.assertIn(field, comment_list) self.assertIs(comment_list['page_size'], 50) self.assertIs(comment_list['page'], 1) self.assertIs(comment_list['total_items'], 28) @@ -241,10 +282,120 @@ class CommentCommands(CommandTestCase): channel_id=rswanson['claim_id'] ) self.assertIn('signature', comment) - deleted = await self.daemon.jsonrpc_comment_abandon(comment['comment_id']) - self.assertIn(comment['comment_id'], deleted) - self.assertTrue(deleted[comment['comment_id']]['deleted']) + abandoned = await self.daemon.jsonrpc_comment_abandon(comment['comment_id']) + self.assertIn(comment['comment_id'], abandoned) + self.assertTrue(abandoned[comment['comment_id']]['abandoned']) - deleted = await self.daemon.jsonrpc_comment_abandon(comment['comment_id']) - self.assertIn(comment['comment_id'], deleted) - self.assertFalse(deleted[comment['comment_id']]['deleted']) + abandoned = await self.daemon.jsonrpc_comment_abandon(comment['comment_id']) + self.assertFalse(abandoned[comment['comment_id']]['abandoned']) + + async def test05_comment_hide(self): + moth = (await self.channel_create('@InconspicuousMoth'))['outputs'][0] + bee = (await self.channel_create('@LazyBumblebee'))['outputs'][0] + moth_id = moth['claim_id'] + stream = await self.stream_create('Cool Lamps to Sit On', channel_id=moth_id) + claim_id = stream['outputs'][0]['claim_id'] + + comment1 = await self.daemon.jsonrpc_comment_create( + comment='Who on earth would want to sit around on a lamp all day', + claim_id=claim_id, + channel_id=bee['claim_id'] + ) + self.assertFalse(comment1['is_hidden']) + + comment2 = await self.daemon.jsonrpc_comment_create( + comment='silence mortal', + claim_id=claim_id, + channel_id=moth_id, + ) + self.assertFalse(comment2['is_hidden']) + + comments = await self.daemon.jsonrpc_comment_list(claim_id) + self.assertIn('has_hidden_comments', comments) + self.assertFalse(comments['has_hidden_comments']) + + hidden = await self.daemon.jsonrpc_comment_hide([comment1['comment_id']]) + self.assertIn('hidden', hidden) + hidden = hidden['hidden'] + self.assertIn(comment1['comment_id'], hidden) + + comments = await self.daemon.jsonrpc_comment_list(claim_id) + self.assertIn('has_hidden_comments', comments) + self.assertTrue(comments['has_hidden_comments']) + hidden_cmts1 = list(filter(lambda c: c['is_hidden'], comments['items'])) + self.assertTrue(len(hidden_cmts1) == 1) + hidden_comment = hidden_cmts1[0] + self.assertEqual(hidden_comment['comment_id'], hidden[0]) + + hidden_comments = await self.daemon.jsonrpc_comment_list(claim_id, hidden=True) + self.assertIn('has_hidden_comments', hidden_comments) + self.assertTrue(hidden_comments['has_hidden_comments']) + self.assertLess(hidden_comments['total_items'], comments['total_items']) + self.assertListEqual(hidden_comments['items'], hidden_cmts1) + + visible_comments = await self.daemon.jsonrpc_comment_list(claim_id, visible=True) + self.assertIn('has_hidden_comments', visible_comments) + self.assertTrue(visible_comments['has_hidden_comments']) + self.assertLess(visible_comments['total_items'], comments['total_items']) + total_hidden = hidden_comments['total_items'] + total_visible = visible_comments['total_items'] + self.assertEqual(total_hidden + total_visible, comments['total_items']) + + items_hidden = hidden_comments['items'] + items_visible = visible_comments['items'] + for item in items_visible + items_hidden: + self.assertIn(item, comments['items']) + + async def test06_comment_list_test(self): + moth = (await self.channel_create('@InconspicuousMoth'))['outputs'][0] + bee = (await self.channel_create('@LazyBumblebee'))['outputs'][0] + moth_id = moth['claim_id'] + stream = await self.stream_create('Cool Lamps to Sit On', channel_id=moth_id) + claim_id = stream['outputs'][0]['claim_id'] + hidden_comment = await self.daemon.jsonrpc_comment_create( + comment='Who on earth would want to sit around on a lamp all day', + claim_id=claim_id, + channel_id=bee['claim_id'] + ) + await self.daemon.jsonrpc_comment_hide([hidden_comment['comment_id']]) + owner_comment = await self.daemon.jsonrpc_comment_create( + comment='Go away you yellow freak', + claim_id=claim_id, + channel_id=moth_id, + ) + other_comment = await self.daemon.jsonrpc_comment_create( + comment='I got my swim trunks and my flippy-floppies', + claim_id=claim_id, + channel_id=bee['claim_id'] + ) + anon_comment = await self.daemon.jsonrpc_comment_create( + claim_id=claim_id, + comment='Anonymous comment' + ) + all_comments = [anon_comment, other_comment, owner_comment, hidden_comment] + list_fields = ['items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'] + normal_list = await self.daemon.jsonrpc_comment_list(claim_id) + for field in list_fields: + self.assertIn(field, normal_list) + self.assertEqual(normal_list['total_items'], 4) + self.assertTrue(normal_list['has_hidden_comments']) + for i, cmnt in enumerate(all_comments): + self.assertEqual(cmnt['comment_id'], normal_list['items'][i]['comment_id']) + + hidden = await self.daemon.jsonrpc_comment_list(claim_id, hidden=True) + self.assertTrue(hidden['has_hidden_comments']) + for field in list_fields: + self.assertIn(field, hidden) + self.assertEqual(hidden['total_items'], 1) + + visible = await self.daemon.jsonrpc_comment_list(claim_id, visible=True) + for field in list_fields: + self.assertIn(field, visible) + self.assertTrue(visible['has_hidden_comments']) + self.assertEqual(visible['total_items'], normal_list['total_items'] - hidden['total_items']) + + valid_list = await self.daemon.jsonrpc_comment_list(claim_id, is_channel_signature_valid=True) + for field in list_fields: + self.assertIn(field, valid_list) + self.assertTrue(visible['has_hidden_comments']) + self.assertEqual(len(valid_list['items']), len(normal_list['items']) - 1)