support comment pinning

This commit is contained in:
jessop 2020-10-08 15:48:37 -04:00 committed by Lex Berezhny
parent 3047649650
commit 7384609e74
4 changed files with 124 additions and 11 deletions

View file

@ -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)

View file

@ -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

View file

@ -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]

View file

@ -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))