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]
|
||||
|
||||
|
||||
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):
|
||||
try:
|
||||
signing_field = comment['comment_id'] if abandon else comment['comment']
|
||||
signing_field = comment['comment_id'] if sign_comment_id else comment['comment']
|
||||
pieces = [
|
||||
comment['signing_ts'].encode(),
|
||||
cid2hash(comment['channel_id']),
|
||||
|
@ -41,9 +41,9 @@ def is_comment_signed_by_channel(comment: dict, channel: Output, abandon=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()))
|
||||
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()]
|
||||
digest = sha256(b''.join(pieces))
|
||||
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_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_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,
|
||||
"signing_ts": (str) The timestamp used to sign the comment,
|
||||
"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_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_id": (str) The Channel Claim ID that this comment was posted under,
|
||||
"signature": (str) The signature of the comment,
|
||||
"signing_ts": (str) Timestamp used to sign the most recent signature,
|
||||
"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,
|
||||
"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_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)
|
||||
|
||||
@requires(WALLET_COMPONENT)
|
||||
|
@ -5267,10 +5271,51 @@ class Daemon(metaclass=JSONRPCServerType):
|
|||
for_signing=True
|
||||
)
|
||||
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)
|
||||
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)
|
||||
async def jsonrpc_comment_react(
|
||||
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_name=<claim_name> : (str) The name of the channel reacting
|
||||
--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
|
||||
--remove : (bool) remove specified react_type
|
||||
--clear_types=<clear_types> : (str) types to clear when adding another type
|
||||
|
|
|
@ -29,6 +29,7 @@ class MockedCommentServer:
|
|||
'timestamp': None,
|
||||
'channel_url': None,
|
||||
'is_hidden': False,
|
||||
'is_pinned': False,
|
||||
}
|
||||
|
||||
REACT_SCHEMA = {
|
||||
|
@ -122,6 +123,21 @@ class MockedCommentServer:
|
|||
return True
|
||||
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):
|
||||
hidden = []
|
||||
for p in pieces:
|
||||
|
@ -254,6 +270,7 @@ class MockedCommentServer:
|
|||
'comment.Abandon': abandon_comment,
|
||||
'comment.GetChannelFromCommentID': get_comment_channel_by_id,
|
||||
'comment.Hide': hide_comments,
|
||||
'comment.Pin': pin_comment,
|
||||
'comment.Edit': edit_comment,
|
||||
'reaction.React': react,
|
||||
'reaction.List': list_reacts,
|
||||
|
@ -518,6 +535,56 @@ class CommentCommands(CommandTestCase):
|
|||
for item in items_visible + items_hidden:
|
||||
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):
|
||||
moth = (await self.channel_create('@InconspicuousMoth'))['outputs'][0]
|
||||
bee = (await self.channel_create('@LazyBumblebee'))['outputs'][0]
|
||||
|
|
|
@ -37,8 +37,8 @@ class TestSigningComments(AsyncioTestCase):
|
|||
rswanson = await get_channel('@RonSwanson')
|
||||
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)
|
||||
sign_comment(comment_body, rswanson, abandon=True)
|
||||
self.assertTrue(is_comment_signed_by_channel(comment_body, rswanson, abandon=True))
|
||||
sign_comment(comment_body, rswanson, sign_comment_id=True)
|
||||
self.assertTrue(is_comment_signed_by_channel(comment_body, rswanson, sign_comment_id=True))
|
||||
|
||||
async def test04_invalid_signature(self):
|
||||
rswanson = await get_channel('@RonSwanson')
|
||||
|
@ -53,7 +53,7 @@ class TestSigningComments(AsyncioTestCase):
|
|||
self.assertTrue(is_comment_signed_by_channel(chair_comment, rswanson))
|
||||
self.assertFalse(is_comment_signed_by_channel(chair_comment, jeanralphio))
|
||||
fake_abandon_signal = chair_comment.copy()
|
||||
sign_comment(fake_abandon_signal, jeanralphio, abandon=True)
|
||||
self.assertFalse(is_comment_signed_by_channel(fake_abandon_signal, rswanson, abandon=True))
|
||||
self.assertFalse(is_comment_signed_by_channel(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, sign_comment_id=True))
|
||||
self.assertFalse(is_comment_signed_by_channel(fake_abandon_signal, jeanralphio, sign_comment_id=True))
|
||||
|
||||
|
|
Loading…
Reference in a new issue