forked from LBRYCommunity/lbry-sdk
support comment pinning
This commit is contained in:
parent
3047649650
commit
7384609e74
4 changed files with 124 additions and 11 deletions
|
@ -22,10 +22,10 @@ def cid2hash(claim_id: str) -> bytes:
|
||||||
return binascii.unhexlify(claim_id.encode())[::-1]
|
return binascii.unhexlify(claim_id.encode())[::-1]
|
||||||
|
|
||||||
|
|
||||||
def is_comment_signed_by_channel(comment: dict, channel: Output, abandon=False):
|
def is_comment_signed_by_channel(comment: dict, channel: Output, sign_comment_id=False):
|
||||||
if isinstance(channel, Output):
|
if isinstance(channel, Output):
|
||||||
try:
|
try:
|
||||||
signing_field = comment['comment_id'] if abandon else comment['comment']
|
signing_field = comment['comment_id'] if sign_comment_id else comment['comment']
|
||||||
pieces = [
|
pieces = [
|
||||||
comment['signing_ts'].encode(),
|
comment['signing_ts'].encode(),
|
||||||
cid2hash(comment['channel_id']),
|
cid2hash(comment['channel_id']),
|
||||||
|
@ -41,9 +41,9 @@ def is_comment_signed_by_channel(comment: dict, channel: Output, abandon=False):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def sign_comment(comment: dict, channel: Output, abandon=False):
|
def sign_comment(comment: dict, channel: Output, sign_comment_id=False):
|
||||||
timestamp = str(int(time.time()))
|
timestamp = str(int(time.time()))
|
||||||
signing_field = comment['comment_id'] if abandon else comment['comment']
|
signing_field = comment['comment_id'] if sign_comment_id else comment['comment']
|
||||||
pieces = [timestamp.encode(), channel.claim_hash, signing_field.encode()]
|
pieces = [timestamp.encode(), channel.claim_hash, signing_field.encode()]
|
||||||
digest = sha256(b''.join(pieces))
|
digest = sha256(b''.join(pieces))
|
||||||
signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
|
signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
|
||||||
|
|
|
@ -5110,8 +5110,10 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
{
|
{
|
||||||
"comment": (str) The actual string as inputted by the user,
|
"comment": (str) The actual string as inputted by the user,
|
||||||
"comment_id": (str) The Comment's unique identifier,
|
"comment_id": (str) The Comment's unique identifier,
|
||||||
|
"claim_id": (str) The claim commented on,
|
||||||
"channel_name": (str) Name of the channel this was posted under, prepended with a '@',
|
"channel_name": (str) Name of the channel this was posted under, prepended with a '@',
|
||||||
"channel_id": (str) The Channel Claim ID that this comment was posted under,
|
"channel_id": (str) The Channel Claim ID that this comment was posted under,
|
||||||
|
"is_pinned": (boolean) Channel owner has pinned this comment,
|
||||||
"signature": (str) The signature of the comment,
|
"signature": (str) The signature of the comment,
|
||||||
"signing_ts": (str) The timestamp used to sign the comment,
|
"signing_ts": (str) The timestamp used to sign the comment,
|
||||||
"channel_url": (str) Channel's URI in the ClaimTrie,
|
"channel_url": (str) Channel's URI in the ClaimTrie,
|
||||||
|
@ -5159,11 +5161,13 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
{
|
{
|
||||||
"comment": (str) The actual string as inputted by the user,
|
"comment": (str) The actual string as inputted by the user,
|
||||||
"comment_id": (str) The Comment's unique identifier,
|
"comment_id": (str) The Comment's unique identifier,
|
||||||
|
"claim_id": (str) The claim commented on,
|
||||||
"channel_name": (str) Name of the channel this was posted under, prepended with a '@',
|
"channel_name": (str) Name of the channel this was posted under, prepended with a '@',
|
||||||
"channel_id": (str) The Channel Claim ID that this comment was posted under,
|
"channel_id": (str) The Channel Claim ID that this comment was posted under,
|
||||||
"signature": (str) The signature of the comment,
|
"signature": (str) The signature of the comment,
|
||||||
"signing_ts": (str) Timestamp used to sign the most recent signature,
|
"signing_ts": (str) Timestamp used to sign the most recent signature,
|
||||||
"channel_url": (str) Channel's URI in the ClaimTrie,
|
"channel_url": (str) Channel's URI in the ClaimTrie,
|
||||||
|
"is_pinned": (boolean) Channel owner has pinned this comment,
|
||||||
"parent_id": (str) Comment this is replying to, (None) if this is the root,
|
"parent_id": (str) Comment this is replying to, (None) if this is the root,
|
||||||
"timestamp": (int) The time at which comment was entered into the server at, in nanoseconds.
|
"timestamp": (int) The time at which comment was entered into the server at, in nanoseconds.
|
||||||
}
|
}
|
||||||
|
@ -5222,7 +5226,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
'channel_id': channel.claim_id,
|
'channel_id': channel.claim_id,
|
||||||
'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, sign_comment_id=True)
|
||||||
return await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Abandon', abandon_comment_body)
|
return await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Abandon', abandon_comment_body)
|
||||||
|
|
||||||
@requires(WALLET_COMPONENT)
|
@requires(WALLET_COMPONENT)
|
||||||
|
@ -5267,10 +5271,51 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
for_signing=True
|
for_signing=True
|
||||||
)
|
)
|
||||||
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, sign_comment_id=True)
|
||||||
pieces.append(piece)
|
pieces.append(piece)
|
||||||
return await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Hide', pieces=pieces)
|
return await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Hide', pieces=pieces)
|
||||||
|
|
||||||
|
@requires(WALLET_COMPONENT)
|
||||||
|
async def jsonrpc_comment_pin(self, comment_id=None, channel_id=None, channel_name=None,
|
||||||
|
channel_account_id=None, remove=False, wallet_id=None):
|
||||||
|
"""
|
||||||
|
Pin a comment published to a claim you control.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
comment_pin (<comment_id> | --comment_id=<comment_id>)
|
||||||
|
(--channel_id=<channel_id>)
|
||||||
|
(--channel_name=<channel_name>)
|
||||||
|
[--remove]
|
||||||
|
[--channel_account_id=<channel_account_id>...] [--wallet_id=<wallet_id>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--comment_id=<comment_id> : (str) Hash identifying the comment to pin
|
||||||
|
--channel_id=<claim_id> : (str) The ID of channel owning the commented claim
|
||||||
|
--channel_name=<claim_name> : (str) The name of channel owning the commented claim
|
||||||
|
--remove : (bool) remove the pin
|
||||||
|
--channel_account_id=<channel_account_id> : (str) one or more account ids for accounts to look in
|
||||||
|
--wallet_id=<wallet_id : (str) restrict operation to specific wallet
|
||||||
|
|
||||||
|
Returns: lists containing the ids comments that are hidden and visible.
|
||||||
|
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
comment_pin_args = {
|
||||||
|
'comment_id': comment_id,
|
||||||
|
'channel_name': channel_name,
|
||||||
|
'channel_id': channel_id,
|
||||||
|
'remove': remove,
|
||||||
|
}
|
||||||
|
comment_client.sign_comment(comment_pin_args, channel, sign_comment_id=True)
|
||||||
|
return await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Pin', comment_pin_args)
|
||||||
|
|
||||||
@requires(WALLET_COMPONENT)
|
@requires(WALLET_COMPONENT)
|
||||||
async def jsonrpc_comment_react(
|
async def jsonrpc_comment_react(
|
||||||
self, comment_ids, channel_name=None, channel_id=None,
|
self, comment_ids, channel_name=None, channel_id=None,
|
||||||
|
@ -5291,6 +5336,7 @@ class Daemon(metaclass=JSONRPCServerType):
|
||||||
--channel_id=<claim_id> : (str) The ID of channel reacting
|
--channel_id=<claim_id> : (str) The ID of channel reacting
|
||||||
--channel_name=<claim_name> : (str) The name of the channel reacting
|
--channel_name=<claim_name> : (str) The name of the channel reacting
|
||||||
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
|
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
|
||||||
|
--channel_account_id=<channel_account_id> : (str) one or more account ids for accounts to look in
|
||||||
--react_type=<react_type> : (str) name of reaction type
|
--react_type=<react_type> : (str) name of reaction type
|
||||||
--remove : (bool) remove specified react_type
|
--remove : (bool) remove specified react_type
|
||||||
--clear_types=<clear_types> : (str) types to clear when adding another type
|
--clear_types=<clear_types> : (str) types to clear when adding another type
|
||||||
|
|
|
@ -29,6 +29,7 @@ class MockedCommentServer:
|
||||||
'timestamp': None,
|
'timestamp': None,
|
||||||
'channel_url': None,
|
'channel_url': None,
|
||||||
'is_hidden': False,
|
'is_hidden': False,
|
||||||
|
'is_pinned': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
REACT_SCHEMA = {
|
REACT_SCHEMA = {
|
||||||
|
@ -122,6 +123,21 @@ class MockedCommentServer:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def pin_comment(
|
||||||
|
self,
|
||||||
|
comment_id: typing.Union[int, str],
|
||||||
|
channel_name: str, channel_id: str, remove: bool,
|
||||||
|
signing_ts: str, signature: str
|
||||||
|
):
|
||||||
|
comment_id = self.get_comment_id(comment_id)
|
||||||
|
if self.is_signable(signature, signing_ts):
|
||||||
|
if remove:
|
||||||
|
self.comments[comment_id]['is_pinned'] = False
|
||||||
|
else:
|
||||||
|
self.comments[comment_id]['is_pinned'] = True
|
||||||
|
return self.comments[comment_id]
|
||||||
|
return False
|
||||||
|
|
||||||
def hide_comments(self, pieces: list):
|
def hide_comments(self, pieces: list):
|
||||||
hidden = []
|
hidden = []
|
||||||
for p in pieces:
|
for p in pieces:
|
||||||
|
@ -254,6 +270,7 @@ class MockedCommentServer:
|
||||||
'comment.Abandon': abandon_comment,
|
'comment.Abandon': abandon_comment,
|
||||||
'comment.GetChannelFromCommentID': get_comment_channel_by_id,
|
'comment.GetChannelFromCommentID': get_comment_channel_by_id,
|
||||||
'comment.Hide': hide_comments,
|
'comment.Hide': hide_comments,
|
||||||
|
'comment.Pin': pin_comment,
|
||||||
'comment.Edit': edit_comment,
|
'comment.Edit': edit_comment,
|
||||||
'reaction.React': react,
|
'reaction.React': react,
|
||||||
'reaction.List': list_reacts,
|
'reaction.List': list_reacts,
|
||||||
|
@ -518,6 +535,56 @@ class CommentCommands(CommandTestCase):
|
||||||
for item in items_visible + items_hidden:
|
for item in items_visible + items_hidden:
|
||||||
self.assertIn(item, comments['items'])
|
self.assertIn(item, comments['items'])
|
||||||
|
|
||||||
|
async def test05_comment_pin(self):
|
||||||
|
# wherein a bee makes a sick burn on moth's channel and moth pins it
|
||||||
|
moth = (await self.channel_create('@InconspicuousMoth'))['outputs'][0]
|
||||||
|
bee = (await self.channel_create('@LazyBumblebee'))['outputs'][0]
|
||||||
|
moth_id = moth['claim_id']
|
||||||
|
moth_stream = await self.stream_create('Cool_Lamps_to_Sit_On', channel_id=moth_id)
|
||||||
|
moth_claim_id = moth_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=moth_claim_id,
|
||||||
|
channel_id=bee['claim_id']
|
||||||
|
)
|
||||||
|
self.assertFalse(comment1['is_pinned'])
|
||||||
|
|
||||||
|
comment2 = await self.daemon.jsonrpc_comment_create(
|
||||||
|
comment='sick burn, brah',
|
||||||
|
claim_id=moth_claim_id,
|
||||||
|
channel_id=moth_id,
|
||||||
|
)
|
||||||
|
self.assertFalse(comment2['is_pinned'])
|
||||||
|
|
||||||
|
comments = await self.daemon.jsonrpc_comment_list(moth_claim_id)
|
||||||
|
comments_items = comments['items']
|
||||||
|
self.assertIn('is_pinned', comments_items[0])
|
||||||
|
self.assertFalse(comments_items[0]['is_pinned'])
|
||||||
|
|
||||||
|
pinned = await self.daemon.jsonrpc_comment_pin(
|
||||||
|
comment_id=comment1['comment_id'],
|
||||||
|
channel_id=moth['claim_id'],
|
||||||
|
channel_name=moth['name']
|
||||||
|
)
|
||||||
|
self.assertTrue(pinned['is_pinned'])
|
||||||
|
|
||||||
|
comments_after_pinning = await self.daemon.jsonrpc_comment_list(moth_claim_id)
|
||||||
|
items_after_pin = comments_after_pinning['items']
|
||||||
|
self.assertTrue(items_after_pin[1]['is_pinned'])
|
||||||
|
|
||||||
|
unpinned = await self.daemon.jsonrpc_comment_pin(
|
||||||
|
comment_id=comment1['comment_id'],
|
||||||
|
channel_id=moth['claim_id'],
|
||||||
|
channel_name=moth['name'],
|
||||||
|
remove=True
|
||||||
|
)
|
||||||
|
self.assertFalse(unpinned['is_pinned'])
|
||||||
|
|
||||||
|
comments_after_unpinning = await self.daemon.jsonrpc_comment_list(moth_claim_id)
|
||||||
|
items_after_unpin = comments_after_unpinning['items']
|
||||||
|
self.assertFalse(items_after_unpin[1]['is_pinned'])
|
||||||
|
|
||||||
async def test06_comment_list(self):
|
async def test06_comment_list(self):
|
||||||
moth = (await self.channel_create('@InconspicuousMoth'))['outputs'][0]
|
moth = (await self.channel_create('@InconspicuousMoth'))['outputs'][0]
|
||||||
bee = (await self.channel_create('@LazyBumblebee'))['outputs'][0]
|
bee = (await self.channel_create('@LazyBumblebee'))['outputs'][0]
|
||||||
|
|
|
@ -37,8 +37,8 @@ class TestSigningComments(AsyncioTestCase):
|
||||||
rswanson = await get_channel('@RonSwanson')
|
rswanson = await get_channel('@RonSwanson')
|
||||||
dsilver = get_stream('Welcome to the Pawnee, and give a big round for Ron Swanson, AKA Duke Silver')
|
dsilver = get_stream('Welcome to the Pawnee, and give a big round for Ron Swanson, AKA Duke Silver')
|
||||||
comment_body = self.create_claim_comment_body('COMPUTER, DELETE ALL VIDEOS OF RON.', dsilver, rswanson)
|
comment_body = self.create_claim_comment_body('COMPUTER, DELETE ALL VIDEOS OF RON.', dsilver, rswanson)
|
||||||
sign_comment(comment_body, rswanson, abandon=True)
|
sign_comment(comment_body, rswanson, sign_comment_id=True)
|
||||||
self.assertTrue(is_comment_signed_by_channel(comment_body, rswanson, abandon=True))
|
self.assertTrue(is_comment_signed_by_channel(comment_body, rswanson, sign_comment_id=True))
|
||||||
|
|
||||||
async def test04_invalid_signature(self):
|
async def test04_invalid_signature(self):
|
||||||
rswanson = await get_channel('@RonSwanson')
|
rswanson = await get_channel('@RonSwanson')
|
||||||
|
@ -53,7 +53,7 @@ class TestSigningComments(AsyncioTestCase):
|
||||||
self.assertTrue(is_comment_signed_by_channel(chair_comment, rswanson))
|
self.assertTrue(is_comment_signed_by_channel(chair_comment, rswanson))
|
||||||
self.assertFalse(is_comment_signed_by_channel(chair_comment, jeanralphio))
|
self.assertFalse(is_comment_signed_by_channel(chair_comment, jeanralphio))
|
||||||
fake_abandon_signal = chair_comment.copy()
|
fake_abandon_signal = chair_comment.copy()
|
||||||
sign_comment(fake_abandon_signal, jeanralphio, abandon=True)
|
sign_comment(fake_abandon_signal, jeanralphio, sign_comment_id=True)
|
||||||
self.assertFalse(is_comment_signed_by_channel(fake_abandon_signal, rswanson, abandon=True))
|
self.assertFalse(is_comment_signed_by_channel(fake_abandon_signal, rswanson, sign_comment_id=True))
|
||||||
self.assertFalse(is_comment_signed_by_channel(fake_abandon_signal, jeanralphio, abandon=True))
|
self.assertFalse(is_comment_signed_by_channel(fake_abandon_signal, jeanralphio, sign_comment_id=True))
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue