new comment and reactions api

This commit is contained in:
jessop 2020-09-23 16:43:28 -04:00
parent eba8856261
commit 9dc4559aba
4 changed files with 259 additions and 32 deletions

View file

@ -611,7 +611,7 @@ class Config(CLIConfig):
('lbrynet4.lbry.com', 4444) # ASIA ('lbrynet4.lbry.com', 4444) # ASIA
]) ])
comment_server = String("Comment server API URL", "https://comments.lbry.com/api") comment_server = String("Comment server API URL", "https://comments.lbry.com/api/v2")
# blockchain # blockchain
blockchain_name = String("Blockchain name - lbrycrd_main, lbrycrd_regtest, or lbrycrd_testnet", 'lbrycrd_main') blockchain_name = String("Blockchain name - lbrycrd_main, lbrycrd_regtest, or lbrycrd_testnet", 'lbrycrd_main')

View file

@ -52,11 +52,21 @@ def sign_comment(comment: dict, channel: Output, abandon=False):
'signing_ts': timestamp 'signing_ts': timestamp
}) })
def sign_reaction(reaction: dict, channel: Output):
timestamp = str(int(time.time()))
signing_field = reaction['channel_name']
pieces = [timestamp.encode(), channel.claim_hash, signing_field.encode()]
digest = sha256(b''.join(pieces))
signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
reaction.update({
'signature': binascii.hexlify(signature).decode(),
'signing_ts': timestamp
})
async def jsonrpc_post(url: str, method: str, params: dict = None, **kwargs) -> any: async def jsonrpc_post(url: str, method: str, params: dict = None, **kwargs) -> any:
params = params or {} params = params or {}
params.update(kwargs) params.update(kwargs)
json_body = {'jsonrpc': '2.0', 'id': None, 'method': method, 'params': params} json_body = {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}
async with utils.aiohttp_request('POST', url, json=json_body) as response: async with utils.aiohttp_request('POST', url, json=json_body) as response:
try: try:
result = await response.json() result = await response.json()

View file

@ -5048,8 +5048,9 @@ class Daemon(metaclass=JSONRPCServerType):
if hidden ^ visible: if hidden ^ visible:
result = await comment_client.jsonrpc_post( result = await comment_client.jsonrpc_post(
self.conf.comment_server, self.conf.comment_server,
'get_claim_hidden_comments', 'comment.List',
claim_id=claim_id, claim_id=claim_id,
visible=visible,
hidden=hidden, hidden=hidden,
page=page, page=page,
page_size=page_size page_size=page_size
@ -5057,7 +5058,7 @@ class Daemon(metaclass=JSONRPCServerType):
else: else:
result = await comment_client.jsonrpc_post( result = await comment_client.jsonrpc_post(
self.conf.comment_server, self.conf.comment_server,
'get_claim_comments', 'comment.List',
claim_id=claim_id, claim_id=claim_id,
parent_id=parent_id, parent_id=parent_id,
page=page, page=page,
@ -5132,7 +5133,7 @@ class Daemon(metaclass=JSONRPCServerType):
} }
comment_client.sign_comment(comment_body, channel) comment_client.sign_comment(comment_body, channel)
response = await comment_client.jsonrpc_post(self.conf.comment_server, 'create_comment', comment_body) response = await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Create', comment_body)
response.update({ response.update({
'is_claim_signature_valid': comment_client.is_comment_signed_by_channel(response, channel) 'is_claim_signature_valid': comment_client.is_comment_signed_by_channel(response, channel)
}) })
@ -5169,7 +5170,7 @@ class Daemon(metaclass=JSONRPCServerType):
""" """
channel = await comment_client.jsonrpc_post( channel = await comment_client.jsonrpc_post(
self.conf.comment_server, self.conf.comment_server,
'get_channel_from_comment_id', 'comment.GetChannelFromCommentID',
comment_id=comment_id comment_id=comment_id
) )
if 'error' in channel: if 'error' in channel:
@ -5186,7 +5187,7 @@ class Daemon(metaclass=JSONRPCServerType):
} }
comment_client.sign_comment(edited_comment, channel_claim) comment_client.sign_comment(edited_comment, channel_claim)
return await comment_client.jsonrpc_post( return await comment_client.jsonrpc_post(
self.conf.comment_server, 'edit_comment', edited_comment self.conf.comment_server, 'comment.Edit', edited_comment
) )
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
@ -5212,7 +5213,7 @@ class Daemon(metaclass=JSONRPCServerType):
wallet = self.wallet_manager.get_wallet_or_default(wallet_id) wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
abandon_comment_body = {'comment_id': comment_id} abandon_comment_body = {'comment_id': comment_id}
channel = await comment_client.jsonrpc_post( channel = await comment_client.jsonrpc_post(
self.conf.comment_server, 'get_channel_from_comment_id', comment_id=comment_id self.conf.comment_server, 'comment.GetChannelFromCommentID', comment_id=comment_id
) )
if 'error' in channel: if 'error' in channel:
return {comment_id: {'abandoned': False}} return {comment_id: {'abandoned': False}}
@ -5222,7 +5223,7 @@ class Daemon(metaclass=JSONRPCServerType):
'channel_name': channel.claim_name, 'channel_name': channel.claim_name,
}) })
comment_client.sign_comment(abandon_comment_body, channel, abandon=True) comment_client.sign_comment(abandon_comment_body, channel, abandon=True)
return await comment_client.jsonrpc_post(self.conf.comment_server, 'abandon_comment', abandon_comment_body) return await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Abandon', abandon_comment_body)
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
async def jsonrpc_comment_hide(self, comment_ids: typing.Union[str, list], wallet_id=None): async def jsonrpc_comment_hide(self, comment_ids: typing.Union[str, list], wallet_id=None):
@ -5268,7 +5269,96 @@ class Daemon(metaclass=JSONRPCServerType):
piece = {'comment_id': comment['comment_id']} piece = {'comment_id': comment['comment_id']}
comment_client.sign_comment(piece, channel, abandon=True) comment_client.sign_comment(piece, channel, abandon=True)
pieces.append(piece) pieces.append(piece)
return await comment_client.jsonrpc_post(self.conf.comment_server, 'hide_comments', pieces=pieces) return await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Hide', pieces=pieces)
@requires(WALLET_COMPONENT)
async def jsonrpc_comment_react(self, comment_id, channel_name=None, channel_id=None,
channel_account_id=None, remove=False, clear_types=False, react_type=None, wallet_id=None):
"""
Create and associate a reaction emoji with a comment using your channel identity.
Usage:
comment_react (<comment_id> | --comment_id=<comment_id>)
(--channel_id=<channel_id>)
(--channel_name=<channel_name>)
(--react_type=<react_type>)
[(--remove) | (--clear_types=<clear_types>)]
[--channel_account_id=<channel_account_id>...] [--wallet_id=<wallet_id>]
Options:
--comment_id=<comment_id> : (str) The comment id reacted to
--channel_id=<claim_id> : (str) The ID of channel reacting
--channel_name=<claim_name> : (str) The name of the channel reacting
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
--react_type=<react_type> : (str) name of reaction type
--remove : (bool) remove specified react_type
--clear_types=<clear_types> : (str) types to clear when adding another type
Returns:
(dict) Reaction object if successfully made, (None) otherwise
{
"hidden": (list) IDs of hidden comments.
"visible": (list) IDs of visible comments.
}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
channel = await self.get_channel_or_error(
wallet, channel_account_id, channel_id, channel_name, for_signing=True
)
print(channel)
react_body = {
'comment_ids': comment_id,
'channel_id': channel_id,
'channel_name': channel.claim_name,
'type': react_type,
}
comment_client.sign_reaction(react_body, channel)
response = await comment_client.jsonrpc_post(self.conf.comment_server, 'reaction.React', react_body)
return response
@requires(WALLET_COMPONENT)
async def jsonrpc_comment_react_list(self, comment_id, channel_name=None, channel_id=None,
channel_account_id=None, react_type=None, wallet_id=None):
"""
List reactions emoji with a claim using your channel identity.
Usage:
comment_react_list (<comment_id> | --comment_id=<comment_id>)
Options:
--comment_id=<comment_id> : (str) The comment id reacted to
--channel_id=<claim_id> : (str) The ID of channel reacting
--channel_name=<claim_name> : (str) The name of the channel reacting
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
--react_type=<react_type> : (str) name of reaction type
Returns:
(dict) Comment object if successfully made, (None) otherwise
{
"hidden": (list) IDs of hidden comments.
"visible": (list) IDs of visible comments.
}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
channel = await self.get_channel_or_error(
wallet, channel_account_id, channel_id, channel_name, for_signing=True
)
react_list_body = {
'comment_id': comment_id,
'channel_id': channel_id,
'channel_name': channel.claim_name,
'type': react_type,
}
comment_client.sign_reaction(react_list_body, channel)
response = await comment_client.jsonrpc_post(self.conf.comment_server, 'reaction.List', react_list_body)
return response
async def broadcast_or_release(self, tx, blocking=False): async def broadcast_or_release(self, tx, blocking=False):
await self.wallet_manager.broadcast_or_release(tx, blocking) await self.wallet_manager.broadcast_or_release(tx, blocking)

View file

@ -32,6 +32,15 @@ class MockedCommentServer:
'is_hidden': False, 'is_hidden': False,
} }
REACT_SCHEMA = {
'comment_id': None,
'reaction_type': None,
'timestamp': None,
'signature': None,
'channel_id': None,
'channel_name': None
}
def __init__(self, port=2903): def __init__(self, port=2903):
self.port = port self.port = port
self.app = web.Application(debug=True) self.app = web.Application(debug=True)
@ -39,7 +48,9 @@ class MockedCommentServer:
self.runner = None self.runner = None
self.server = None self.server = None
self.comments = [] self.comments = []
self.reacts = []
self.comment_id = 0 self.comment_id = 0
self.react_id = 0
@classmethod @classmethod
def _create_comment(cls, **kwargs): def _create_comment(cls, **kwargs):
@ -47,6 +58,12 @@ class MockedCommentServer:
schema.update(**kwargs) schema.update(**kwargs)
return schema return schema
@classmethod
def _react(cls, **kwargs):
schema = cls.REACT_SCHEMA.copy()
schema.update(**kwargs)
return schema
def create_comment(self, claim_id=None, parent_id=None, channel_name=None, channel_id=None, **kwargs): def create_comment(self, claim_id=None, parent_id=None, channel_name=None, channel_id=None, **kwargs):
comment_id = self.comment_id comment_id = self.comment_id
channel_url = 'lbry://' + channel_name + '#' + channel_id if channel_id else None channel_url = 'lbry://' + channel_name + '#' + channel_id if channel_id else None
@ -117,27 +134,18 @@ class MockedCommentServer:
'visible': list(comment_ids - set(hidden)) 'visible': list(comment_ids - set(hidden))
} }
def get_claim_comments(self, claim_id, page=1, page_size=50,**kwargs): def get_claim_comments(self, claim_id, hidden=False, visible=False, page=1, page_size=50,**kwargs):
comments = list(filter(lambda c: c['claim_id'] == claim_id, self.comments)) comments = list(filter(lambda c: c['claim_id'] == claim_id, self.comments))
if hidden ^ visible:
comments = list(filter(lambda c: c['is_hidden'] == hidden, comments))
return { return {
'page': page, 'page': page,
'page_size': page_size, 'page_size': page_size,
'total_pages': ceil(len(comments)/page_size), 'total_pages': ceil(len(comments)/page_size),
'total_items': len(comments), 'total_items': len(comments),
'items': [self.clean(c) for c in (comments[::-1])[(page - 1) * page_size: page * page_size]], '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))) 'has_hidden_comments': bool(list(filter(lambda x: x['is_hidden'], self.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): def get_comment_channel_by_id(self, comment_id: int, **kwargs):
@ -157,15 +165,83 @@ class MockedCommentServer:
'has_hidden_comments': bool({c for c in comments if c['is_hidden']}) 'has_hidden_comments': bool({c for c in comments if c['is_hidden']})
} }
def react(
self,
comment_ids=None,
channel_name=None,
channel_id=None,
remove=None,
clear_types=None,
type=None,
signing_ts=None,
**kwargs
):
comment_batch = comment_ids.split(',')
for item in comment_batch:
c_id = item.strip()
reacts_for_comment_id = [r for r in self.reacts if r['comment_id'] == c_id]
channels_reacts_for_comment_id = [r for r in reacts_for_comment_id if ['channel_id'] == channel_id]
if remove:
matching_react = None
for reaction in channels_reacts_for_comment_id:
if reaction['reaction_type'] == type:
matching_react = reaction
break
if matching_react:
self.reacts.pop(matching_react['id'])
else:
if clear_types:
for r_type in clear_types:
for reaction in channels_reacts_for_comment_id:
if reaction['reaction_type'] == r_type:
self.reacts.pop(reaction['id'])
react = self._react(
comment_id=str(c_id),
channel_name=channel_name,
channel_id=channel_id,
reaction_type=type,
timestamp=str(int(time.time())),
**kwargs
)
self.reacts.append(react)
self.react_id += 1
return self.clean(react)
def list_reacts(self, comment_id, channel_id, **kwargs):
reacts_for_comment = list(filter(lambda c: c['comment_id'] == comment_id, self.reacts))
own_reacts_for_comment = list(filter(lambda c: c['channel_id'] == channel_id, reacts_for_comment))
other_reacts_for_comment = list(filter(lambda c: c['channel_id'] != channel_id, reacts_for_comment))
print('seriously wtf', own_reacts_for_comment)
own_counts = {}
if own_reacts_for_comment:
for react in own_reacts_for_comment:
own_counts[react['reaction_type']] = own_counts[react['reaction_type']] + 1 if react['reaction_type'] in own_counts else 1
print('eh??')
other_counts = {}
if other_reacts_for_comment:
for react in other_reacts_for_comment:
other_counts[react['reaction_type']] = other_counts[react['reaction_type']] + 1 if react['reaction_type'] in other_counts else 1
return {
'my_reactions': {
comment_id: own_counts,
},
'others_reactions': {
comment_id: other_counts,
}
}
methods = { methods = {
'get_claim_comments': get_claim_comments, 'comment.List': get_claim_comments,
'get_comments_by_id': get_comments_by_id, 'get_comments_by_id': get_comments_by_id,
'create_comment': create_comment, 'comment.Create': create_comment,
'abandon_comment': abandon_comment, 'comment.Abandon': abandon_comment,
'get_channel_from_comment_id': get_comment_channel_by_id, 'comment.GetChannelFromCommentID': get_comment_channel_by_id,
'get_claim_hidden_comments': get_claim_hidden_comments, 'comment.Hide': hide_comments,
'hide_comments': hide_comments, 'comment.Edit': edit_comment,
'edit_comment': edit_comment, 'reaction.React': react,
'reaction.List': list_reacts,
} }
def process_json(self, body) -> dict: def process_json(self, body) -> dict:
@ -450,7 +526,6 @@ class CommentCommands(CommandTestCase):
channel_id=bee['claim_id'] channel_id=bee['claim_id']
) )
all_comments = [other_comment, owner_comment, hidden_comment] all_comments = [other_comment, owner_comment, hidden_comment]
normal_list = await self.daemon.jsonrpc_comment_list(claim_id) normal_list = await self.daemon.jsonrpc_comment_list(claim_id)
self.assertEqual( self.assertEqual(
{'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'}, {'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'},
@ -532,3 +607,55 @@ class CommentCommands(CommandTestCase):
comment='If you see it and you mean then you know you have to go', comment='If you see it and you mean then you know you have to go',
comment_id=original_cid comment_id=original_cid
) )
async def test06_reactions(self):
moth = (await self.channel_create('@InconspicuousMoth'))['outputs'][0]
bee = (await self.channel_create('@LazyBumblebee'))['outputs'][0]
stream = await self.stream_create('Cool_Lamps_to_Sit_On', channel_id=moth['claim_id'])
claim_id = stream['outputs'][0]['claim_id']
first_comment = await self.daemon.jsonrpc_comment_create(
comment='Go away you yellow freak',
claim_id=claim_id,
channel_id=moth['claim_id'],
)
second_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']
)
all_comments = [second_comment, first_comment]
comment_list = await self.daemon.jsonrpc_comment_list(claim_id)
self.assertEqual(
{'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'},
set(comment_list)
)
self.assertEqual(comment_list['total_items'], 2)
# bee likes [0]
# moth likes [0,1]
# moth smiles [0,1,2]
# bee dislikes (cleartypes) [1,2,3]
# bee undislikes (remove) [1,2]
# list likes
bee_like_reaction = await self.daemon.jsonrpc_comment_react(
comment_id=first_comment['comment_id'],
channel_id=bee['claim_id'],
channel_name=bee['name'],
react_type='dislike',
)
bee_like_reaction = await self.daemon.jsonrpc_comment_react(
comment_id=first_comment['comment_id'],
channel_id=moth['claim_id'],
channel_name=moth['name'],
react_type='like',
)
reactions = await self.daemon.jsonrpc_comment_react_list(
comment_id=first_comment['comment_id'],
channel_id=moth['claim_id'],
channel_name=moth['name'],
)
# test assertions