Merge pull request #3414 from cristi-zz/remove_comment_api

removed `comment` API endoints
This commit is contained in:
Lex Berezhny 2021-09-09 13:07:12 -04:00 committed by GitHub
commit 4626d42d08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 177 additions and 1916 deletions

File diff suppressed because one or more lines are too long

View file

@ -698,8 +698,6 @@ 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/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

@ -1,79 +0,0 @@
import logging
import time
import hashlib
import binascii
import ecdsa
from lbry import utils
from lbry.crypto.hash import sha256
from lbry.wallet.transaction import Output
log = logging.getLogger(__name__)
def get_encoded_signature(signature):
signature = signature.encode() if isinstance(signature, str) else signature
r = int(signature[:int(len(signature) / 2)], 16)
s = int(signature[int(len(signature) / 2):], 16)
return ecdsa.util.sigencode_der(r, s, len(signature) * 4)
def cid2hash(claim_id: str) -> bytes:
return binascii.unhexlify(claim_id.encode())[::-1]
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 sign_comment_id else comment['comment']
return verify(channel, signing_field.encode(), comment, cid2hash(comment['channel_id']))
except KeyError:
pass
return False
def verify(channel, data, signature, channel_hash=None):
pieces = [
signature['signing_ts'].encode(),
channel_hash or channel.claim_hash,
data
]
return Output.is_signature_valid(
get_encoded_signature(signature['signature']),
sha256(b''.join(pieces)),
channel.claim.channel.public_key_bytes
)
def sign_comment(comment: dict, channel: Output, sign_comment_id=False):
signing_field = comment['comment_id'] if sign_comment_id else comment['comment']
comment.update(sign(channel, signing_field.encode()))
def sign(channel, data):
timestamp = str(int(time.time()))
pieces = [timestamp.encode(), channel.claim_hash, data]
digest = sha256(b''.join(pieces))
signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
return {
'signature': binascii.hexlify(signature).decode(),
'signing_ts': timestamp
}
def sign_reaction(reaction: dict, channel: Output):
signing_field = reaction['channel_name']
reaction.update(sign(channel, signing_field.encode()))
async def jsonrpc_post(url: str, method: str, params: dict = None, **kwargs) -> any:
params = params or {}
params.update(kwargs)
json_body = {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}
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
except Exception as cte:
log.exception('Unable to decode response from server: %s', cte)
return await response.text()

View file

@ -46,7 +46,6 @@ from lbry.extras.daemon.components import EXCHANGE_RATE_MANAGER_COMPONENT, UPNP_
from lbry.extras.daemon.componentmanager import RequiredCondition from lbry.extras.daemon.componentmanager import RequiredCondition
from lbry.extras.daemon.componentmanager import ComponentManager from lbry.extras.daemon.componentmanager import ComponentManager
from lbry.extras.daemon.json_response_encoder import JSONResponseEncoder from lbry.extras.daemon.json_response_encoder import JSONResponseEncoder
from lbry.extras.daemon import comment_client
from lbry.extras.daemon.undecorated import undecorated from lbry.extras.daemon.undecorated import undecorated
from lbry.extras.daemon.security import ensure_request_allowed from lbry.extras.daemon.security import ensure_request_allowed
from lbry.file_analysis import VideoFileAnalyzer from lbry.file_analysis import VideoFileAnalyzer
@ -2837,7 +2836,12 @@ class Daemon(metaclass=JSONRPCServerType):
signing_channel = await self.get_channel_or_error( signing_channel = await self.get_channel_or_error(
wallet, channel_account_id, channel_id, channel_name, for_signing=True wallet, channel_account_id, channel_id, channel_name, for_signing=True
) )
return comment_client.sign(signing_channel, unhexlify(hexdata)) timestamp = str(int(time.time()))
signature = signing_channel.sign_data(unhexlify(hexdata), timestamp)
return {
'signature': signature,
'signing_ts': timestamp
}
@requires(WALLET_COMPONENT) @requires(WALLET_COMPONENT)
async def jsonrpc_channel_abandon( async def jsonrpc_channel_abandon(
@ -5166,440 +5170,6 @@ class Daemon(metaclass=JSONRPCServerType):
break break
return results return results
COMMENT_DOC = """
View, create and abandon comments.
"""
async def jsonrpc_comment_list(self, claim_id, parent_id=None, page=1, page_size=50,
include_replies=False, skip_validation=False,
is_channel_signature_valid=False, hidden=False, visible=False):
"""
List comments associated with a claim.
Usage:
comment_list (<claim_id> | --claim_id=<claim_id>)
[(--page=<page> --page_size=<page_size>)]
[--parent_id=<parent_id>] [--include_replies]
[--skip_validation] [--is_channel_signature_valid]
[--visible | --hidden]
Options:
--claim_id=<claim_id> : (str) The claim on which the comment will be made on
--parent_id=<parent_id> : (str) CommentId of a specific thread you'd like to see
--page=<page> : (int) The page you'd like to see in the comment list.
--page_size=<page_size> : (int) The amount of comments that you'd like to retrieve
--skip_validation : (bool) Skip resolving comments to validate channel names
--include_replies : (bool) Whether or not you want to include replies in list
--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 Visible Comments
--hidden : (bool) Select only Hidden Comments
Returns:
(dict) Containing the list, and information about the paginated content:
{
"page": "Page number of the current items.",
"page_size": "Number of items to show on a page.",
"total_pages": "Total number of pages.",
"total_items": "Total number of items.",
"items": "A List of dict objects representing comments."
[
{
"comment": (str) The actual string as inputted by the user,
"comment_id": (str) The Comment's unique identifier,
"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,
"channel_url": (str) Channel's URI in the ClaimTrie,
"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.
},
...
]
}
"""
if hidden ^ visible:
result = await comment_client.jsonrpc_post(
self.conf.comment_server,
'comment.List',
claim_id=claim_id,
visible=visible,
hidden=hidden,
page=page,
page_size=page_size
)
else:
result = await comment_client.jsonrpc_post(
self.conf.comment_server,
'comment.List',
claim_id=claim_id,
parent_id=parent_id,
page=page,
page_size=page_size,
top_level=not include_replies
)
if not skip_validation:
for comment in result.get('items', []):
channel_url = comment.get('channel_url')
if not channel_url:
continue
resolve_response = await self.resolve([], [channel_url])
if isinstance(resolve_response[channel_url], Output):
comment['is_channel_signature_valid'] = comment_client.is_comment_signed_by_channel(
comment, resolve_response[channel_url]
)
else:
comment['is_channel_signature_valid'] = False
if is_channel_signature_valid:
result['items'] = [
c for c in result.get('items', []) if c.get('is_channel_signature_valid', False)
]
return result
@requires(WALLET_COMPONENT)
async def jsonrpc_comment_create(self, comment, claim_id=None, parent_id=None, channel_account_id=None,
channel_name=None, channel_id=None, wallet_id=None):
"""
Create and associate a comment with a claim using your channel identity.
Usage:
comment_create (<comment> | --comment=<comment>)
(<claim_id> | --claim_id=<claim_id>) [--parent_id=<parent_id>]
(--channel_id=<channel_id> | --channel_name=<channel_name>)
[--channel_account_id=<channel_account_id>...] [--wallet_id=<wallet_id>]
Options:
--comment=<comment> : (str) Comment to be made, should be at most 2000 characters.
--claim_id=<claim_id> : (str) The ID of the claim to comment on
--parent_id=<parent_id> : (str) The ID of a comment to make a response to
--channel_id=<channel_id> : (str) The ID of the channel you want to post under
--channel_name=<channel_name> : (str) The channel you want to post as, prepend with a '@'
--channel_account_id=<channel_account_id> : (str) one or more account ids for accounts to look in
for channel certificates, defaults to all accounts
--wallet_id=<wallet_id> : (str) restrict operation to specific wallet
Returns:
(dict) Comment object if successfully made, (None) otherwise
{
"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,
"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.
}
"""
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_body = {
'comment': comment.strip(),
'claim_id': claim_id,
'parent_id': parent_id,
'channel_id': channel.claim_id,
'channel_name': channel.claim_name,
}
comment_client.sign_comment(comment_body, channel)
response = await comment_client.jsonrpc_post(self.conf.comment_server, 'comment.Create', comment_body)
response.update({
'is_claim_signature_valid': comment_client.is_comment_signed_by_channel(response, channel)
})
return response
@requires(WALLET_COMPONENT)
async def jsonrpc_comment_update(self, comment, comment_id, wallet_id=None):
"""
Edit a comment published as one of your channels.
Usage:
comment_update (<comment> | --comment=<comment>)
(<comment_id> | --comment_id=<comment_id>)
[--wallet_id=<wallet_id>]
Options:
--comment=<comment> : (str) New comment replacing the old one
--comment_id=<comment_id> : (str) Hash identifying the comment to edit
--wallet_id=<wallet_id : (str) restrict operation to specific wallet
Returns:
(dict) Comment object if edit was successful, (None) otherwise
{
"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.
}
"""
channel = await comment_client.jsonrpc_post(
self.conf.comment_server,
'comment.GetChannelFromCommentID',
comment_id=comment_id
)
if 'error' in channel:
# TODO: use error from lbry.error
raise ValueError(channel['error'])
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
# channel = await self.get_channel_or_none(wallet, None, **channel)
channel_claim = await self.get_channel_or_error(wallet, [], **channel)
edited_comment = {
'comment_id': comment_id,
'comment': comment,
'channel_id': channel_claim.claim_id,
'channel_name': channel_claim.claim_name
}
comment_client.sign_comment(edited_comment, channel_claim)
return await comment_client.jsonrpc_post(
self.conf.comment_server, 'comment.Edit', edited_comment
)
@requires(WALLET_COMPONENT)
async def jsonrpc_comment_abandon(self, comment_id, wallet_id=None):
"""
Abandon a comment published under your channel identity.
Usage:
comment_abandon (<comment_id> | --comment_id=<comment_id>) [--wallet_id=<wallet_id>]
Options:
--comment_id=<comment_id> : (str) The ID of the comment to be abandoned.
--wallet_id=<wallet_id : (str) restrict operation to specific wallet
Returns:
(dict) Object with the `comment_id` passed in as the key, and a flag indicating if it was abandoned
{
<comment_id> (str): {
"abandoned": (bool)
}
}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
abandon_comment_body = {'comment_id': comment_id}
channel = await comment_client.jsonrpc_post(
self.conf.comment_server, 'comment.GetChannelFromCommentID', comment_id=comment_id
)
if 'error' in channel:
return {comment_id: {'abandoned': False}}
channel = await self.get_channel_or_none(wallet, None, **channel)
abandon_comment_body.update({
'channel_id': channel.claim_id,
'channel_name': channel.claim_name,
})
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)
async def jsonrpc_comment_hide(self, comment_ids: typing.Union[str, list], wallet_id=None):
"""
Hide a comment published to a claim you control.
Usage:
comment_hide <comment_ids>... [--wallet_id=<wallet_id>]
Options:
--comment_ids=<comment_ids> : (str, list) one or more comment_id to hide.
--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)
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
)
comments = comments['items']
claim_ids = {comment['claim_id'] for comment in comments}
claims = {cid: await self.ledger.get_claim_by_claim_id(wallet.accounts, 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(
wallet,
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, 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,
channel_account_id=None, remove=False, clear_types=None, react_type=None, wallet_id=None):
"""
Create and associate a reaction emoji with a comment using your channel identity.
Usage:
comment_react (--comment_ids=<comment_ids>)
(--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_ids=<comment_ids> : (str) one or more comment id reacted to, comma delimited
--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
Returns:
(dict) Reaction object if successfully made, (None) otherwise
{
"Reactions": {
<comment_id>: {
<reaction_type>: (int) Count for this reaction
...
}
}
"""
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_body = {
'comment_ids': comment_ids,
'channel_id': channel_id,
'channel_name': channel.claim_name,
'type': react_type,
'remove': remove,
'clear_types': clear_types,
}
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_ids, channel_name=None, channel_id=None,
channel_account_id=None, react_types=None, wallet_id=None):
"""
List reactions emoji with a claim using your channel identity.
Usage:
comment_react_list (--comment_ids=<comment_ids>)
[(--channel_id=<channel_id>)(--channel_name=<channel_name>)]
[--react_types=<react_types>]
Options:
--comment_ids=<comment_ids> : (str) The comment ids reacted to, comma delimited
--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_types=<react_type> : (str) comma delimited reaction types
Returns:
(dict) Comment object if successfully made, (None) otherwise
{
"my_reactions": {
<comment_id>: {
<reaction_type>: (int) Count for this reaction type
...
}
}
"other_reactions": {
<comment_id>: {
<reaction_type>: (int) Count for this reaction type
...
}
}
}
"""
wallet = self.wallet_manager.get_wallet_or_default(wallet_id)
react_list_body = {
'comment_ids': comment_ids,
}
if channel_id:
channel = await self.get_channel_or_error(
wallet, channel_account_id, channel_id, channel_name, for_signing=True
)
react_list_body['channel_id'] = channel_id
react_list_body['channel_name'] = channel.claim_name
if react_types:
react_list_body['types'] = react_types
if channel_id:
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

@ -459,6 +459,12 @@ class Output(InputOutput):
self.signable.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256) self.signable.signature = channel.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
self.script.generate() self.script.generate()
def sign_data(self, data:bytes, timestamp:str) -> str:
pieces = [timestamp.encode(), self.claim_hash, data]
digest = sha256(b''.join(pieces))
signature = self.private_key.sign_digest_deterministic(digest, hashfunc=hashlib.sha256)
return hexlify(signature).decode()
def clear_signature(self): def clear_signature(self):
self.channel = None self.channel = None
self.signable.clear_signature() self.signable.clear_signature()

View file

@ -13,7 +13,6 @@ from lbry.extras.cli import set_kwargs, get_argument_parser
from lbry.extras.daemon.daemon import ( from lbry.extras.daemon.daemon import (
Daemon, jsonrpc_dumps_pretty, encode_pagination_doc Daemon, jsonrpc_dumps_pretty, encode_pagination_doc
) )
from tests.integration.other.test_comment_commands import MockedCommentServer
from lbry.extras.daemon.json_response_encoder import ( from lbry.extras.daemon.json_response_encoder import (
encode_tx_doc, encode_txo_doc, encode_account_doc, encode_file_doc, encode_tx_doc, encode_txo_doc, encode_account_doc, encode_file_doc,
encode_wallet_doc encode_wallet_doc
@ -67,10 +66,6 @@ class Examples(CommandTestCase):
async def asyncSetUp(self): async def asyncSetUp(self):
await super().asyncSetUp() await super().asyncSetUp()
self.daemon.conf.comment_server = 'http://localhost:2903/api'
self.comment_server = MockedCommentServer(2903)
await self.comment_server.start()
self.addCleanup(self.comment_server.stop)
self.recorder = ExampleRecorder(self) self.recorder = ExampleRecorder(self)
async def play(self): async def play(self):
@ -330,41 +325,6 @@ class Examples(CommandTestCase):
await self.daemon.jsonrpc_channel_abandon(self.get_claim_id(big_stream)) await self.daemon.jsonrpc_channel_abandon(self.get_claim_id(big_stream))
await self.generate(1) await self.generate(1)
# comments
comment = await r(
'Posting a comment as your channel',
'comment', 'create', '--comment="Thank you Based God"',
'--channel_name=@channel', f'--claim_id={stream_id}'
)
reply = await r(
'Use the parent_id param to make replies',
'comment', 'create',
'--comment="I have photographic evidence confirming Sasquatch exists"',
f'--channel_name=@channel', f'--parent_id={comment["comment_id"]}',
f'--claim_id={stream_id}'
)
await r(
'List all comments on a claim',
'comment', 'list', stream_id, '--include_replies'
)
await r(
'List a comment thread replying to a top level comment',
'comment', 'list', stream_id,
f'--parent_id={comment["comment_id"]}'
)
await r(
'Edit the contents of a comment',
'comment', 'update', 'Where there was once sasquatch, there is not',
f'--comment_id={comment["comment_id"]}'
)
await self.daemon.jsonrpc_comment_abandon(reply['comment_id'])
# collections # collections
collection = await r( collection = await r(
'Create a collection of one stream', 'Create a collection of one stream',
@ -431,11 +391,6 @@ class Examples(CommandTestCase):
# abandon all the things # abandon all the things
await r(
'Abandon a comment',
'comment', 'abandon', comment['comment_id']
)
abandon_stream = await r( abandon_stream = await r(
'Abandon a stream claim', 'Abandon a stream claim',
'stream', 'abandon', stream_id 'stream', 'abandon', stream_id

View file

@ -5,19 +5,38 @@ import asyncio
from binascii import unhexlify from binascii import unhexlify
from unittest import skip from unittest import skip
from urllib.request import urlopen from urllib.request import urlopen
import ecdsa
from lbry.error import InsufficientFundsError from lbry.error import InsufficientFundsError
from lbry.extras.daemon.comment_client import verify
from lbry.extras.daemon.daemon import DEFAULT_PAGE_SIZE from lbry.extras.daemon.daemon import DEFAULT_PAGE_SIZE
from lbry.testcase import CommandTestCase from lbry.testcase import CommandTestCase
from lbry.wallet.orchstr8.node import SPVNode from lbry.wallet.orchstr8.node import SPVNode
from lbry.wallet.transaction import Transaction from lbry.wallet.transaction import Transaction, Output
from lbry.wallet.util import satoshis_to_coins as lbc from lbry.wallet.util import satoshis_to_coins as lbc
from lbry.crypto.hash import sha256
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def get_encoded_signature(signature):
signature = signature.encode() if isinstance(signature, str) else signature
r = int(signature[:int(len(signature) / 2)], 16)
s = int(signature[int(len(signature) / 2):], 16)
return ecdsa.util.sigencode_der(r, s, len(signature) * 4)
def verify(channel, data, signature, channel_hash=None):
pieces = [
signature['signing_ts'].encode(),
channel_hash or channel.claim_hash,
data
]
return Output.is_signature_valid(
get_encoded_signature(signature['signature']),
sha256(b''.join(pieces)),
channel.claim.channel.public_key_bytes
)
class ClaimTestCase(CommandTestCase): class ClaimTestCase(CommandTestCase):

View file

@ -1,812 +0,0 @@
import re
import time
import typing
from math import ceil
import secrets
from aiohttp import web
from lbry.testcase import CommandTestCase
class MockedCommentServer:
ERRORS = {
'INVALID_PARAMS': {'code': -32602, 'message': 'Invalid parameters'},
'INTERNAL': {'code': -32603, 'message': 'An internal error'},
'UNKNOWN': {'code': -1, 'message': 'An unknown or very miscellaneous error'},
'INVALID_METHOD': {'code': -32604, 'message': 'The Requested method does not exist'}
}
COMMENT_SCHEMA = {
'comment': None,
'comment_id': None,
'claim_id': None,
'parent_id': None,
'channel_name': None,
'channel_id': None,
'signature': None,
'signing_ts': None,
'timestamp': None,
'channel_url': None,
'is_hidden': False,
'is_pinned': False,
}
REACT_SCHEMA = {
'react_id': None,
'comment_id': None,
'reaction_type': None,
'timestamp': None,
'signature': None,
'channel_id': None,
'channel_name': None
}
def __init__(self, port=2903):
self.port = port
self.app = web.Application(debug=True)
self.app.add_routes([web.post('/api', self.api)])
self.runner = None
self.server = None
self.comments = []
self.reacts = {}
self.index = 0
self.react_id = 0
@classmethod
def _create_comment(cls, **kwargs):
schema = cls.COMMENT_SCHEMA.copy()
schema.update(**kwargs)
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):
comment_id = secrets.token_hex(64)
channel_url = 'lbry://' + channel_name + '#' + channel_id if channel_id else None
if parent_id:
parent_comment = list(filter(lambda c: c['comment_id'] == parent_id, self.comments))[0]
claim_id = parent_comment['claim_id']
comment = self._create_comment(
comment_id=comment_id,
channel_name=channel_name,
channel_id=channel_id,
channel_url=channel_url,
timestamp=str(int(time.time())),
claim_id=claim_id,
parent_id=parent_id,
**kwargs
)
self.comments.append(comment)
return self.clean(comment)
def abandon_comment(self, comment_id: str, channel_id: str, **kwargs):
deleted = False
index = self.get_index_for_comment_id(comment_id)
try:
if index >= 0:
self.comments.pop(index)
deleted = True
finally:
return {
str(comment_id): {
'abandoned': deleted
}
}
def edit_comment(self, comment_id: str, comment: str, channel_id: str,
channel_name: str, signature: str, signing_ts: str) -> dict:
edited = False
if self.credentials_are_valid(channel_id, channel_name, signature, signing_ts) \
and self.is_valid_body(comment):
index = self.get_index_for_comment_id(comment_id)
if self.comments[index]['channel_id'] == channel_id:
self.comments[index].update({
'comment': comment,
'signature': signature,
'signing_ts': signing_ts
})
edited = True
return self.comments[index] if edited else None
def hide_comment(self, comment_id: str, signing_ts: str, signature: str):
if self.is_signable(signature, signing_ts):
self.comments[self.get_index_for_comment_id(comment_id)]['is_hidden'] = True
return True
return False
def pin_comment(
self,
comment_id: str,
channel_name: str, channel_id: str, remove: bool,
signing_ts: str, signature: str
):
index = self.get_index_for_comment_id(comment_id)
if self.is_signable(signature, signing_ts):
if remove:
self.comments[index]['is_pinned'] = False
else:
self.comments[index]['is_pinned'] = True
return self.comments[index]
return False
def hide_comments(self, pieces: list):
hidden = []
for p in pieces:
if self.hide_comment(**p):
hidden.append(p['comment_id'])
comment_ids = {c['comment_id'] for c in pieces}
return {
'hidden': hidden,
'visible': list(comment_ids - set(hidden))
}
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))
if hidden ^ visible:
comments = list(filter(lambda c: c['is_hidden'] == hidden, comments))
return {
'page': page,
'page_size': 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'], self.comments)))
}
def get_comment_channel_by_id(self, comment_id: str, **kwargs):
comment = self.comments[self.get_index_for_comment_id(comment_id)]
return {
'channel_id': comment['channel_id'],
'channel_name': comment['channel_name'],
}
def get_comments_by_id(self, comment_ids: list):
comments = [self.comments[self.get_index_for_comment_id(cid)] for cid in comment_ids]
return {
'page': 1,
'page_size': len(comment_ids),
'total_pages': 1,
'items': comments,
'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:
react_id = self.react_id
c_id = item.strip()
reacts_for_comment_id = [r for r in list(self.reacts.values()) if r['comment_id'] == c_id]
channels_reacts_for_comment_id = [r for r in reacts_for_comment_id if r['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(int(matching_react['react_id']), None)
else:
if clear_types:
for r_type in clear_types.split(','):
for reaction in channels_reacts_for_comment_id:
if reaction['reaction_type'] == r_type:
x = self.reacts.pop(int(reaction['react_id']), None)
react = self._react(
react_id=str(react_id),
comment_id=str(c_id),
channel_name=channel_name,
channel_id=channel_id,
reaction_type=type,
timestamp=str(int(time.time())),
**kwargs
)
self.reacts[self.react_id] = react
self.react_id += 1
self.clean(react)
return True
def list_reacts(self, comment_ids, channel_id, channel_name, types=None, **kwargs):
all_types = list(set([r['reaction_type'] for r in list(self.reacts.values())]))
comment_id_list = comment_ids.split(',')
# _reacts: {'a1': {'like': 0, 'dislike': 0}, 'a2': {'like': 0, 'dislike': 0}}
own_reacts = {}
other_reacts = {}
# for each comment_id
# add comment_id: {} to own_reacts and other_reacts
# for each react in own_reacts
# for each react in other_reacts
for cid in comment_id_list:
own_reacts[cid] = {}
other_reacts[cid] = {}
for r_type in all_types:
own_reacts[cid][r_type] = 0
other_reacts[cid][r_type] = 0
# process that comment ids reactions for own and other categories
reacts_for_comment = list(filter(lambda c: c['comment_id'] == cid, list(self.reacts.values())))
if types:
reacts_for_comment = list(filter(lambda c: c['reaction_type'] in types.split(','), reacts_for_comment))
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))
if own_reacts_for_comment:
for react in own_reacts_for_comment:
own_reacts[cid][react['reaction_type']] += 1
if other_reacts_for_comment:
for react in other_reacts_for_comment:
other_reacts[cid][react['reaction_type']] += 1
return {
'my_reactions': own_reacts,
'others_reactions': other_reacts,
}
methods = {
'comment.List': get_claim_comments,
'get_comments_by_id': get_comments_by_id,
'comment.Create': create_comment,
'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,
}
def process_json(self, body) -> dict:
response = {'jsonrpc': '2.0', 'id': body['id']}
error = None
try:
if body['method'] in self.methods:
params: dict = body.get('params', {})
result = self.methods[body['method']](self, **params)
response['result'] = result
else:
response['error'] = self.ERRORS['INVALID_METHOD']
except (ValueError, TypeError) as err:
error = err
response['error'] = self.ERRORS['INVALID_PARAMS']
except Exception as err:
error = err
response['error'] = self.ERRORS['UNKNOWN']
finally:
if 'error' in response:
response['error'].update({'exception': f'{type(error).__name__}: {error}'})
return response
async def start(self):
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.server = web.TCPSite(self.runner, 'localhost', self.port)
await self.server.start()
async def stop(self):
await self.runner.shutdown()
await self.runner.cleanup()
async def api(self, request):
body = await request.json()
if type(body) is list or type(body) is dict:
if type(body) is list:
response = [self.process_json(part) for part in body]
else:
response = self.process_json(body)
return web.json_response(response)
else:
raise TypeError('invalid type passed')
@staticmethod
def clean(d: dict):
return {k: v for k, v in d.items() if v or isinstance(v, bool)}
@staticmethod
def is_valid_body(comment) -> bool:
return 0 < len(comment) <= 2000
def is_valid_comment_id(self, comment_id: typing.Union[int, str]) -> bool:
if isinstance(comment_id, str):
return True
return False
def get_comment_for_id(self, cid: str) -> dict:
return list(filter(lambda c: c['comment_id'] == cid, self.comments))[0]
def get_index_for_comment_id(self, value: str):
for i, dic in enumerate(self.comments):
if dic['comment_id'] == value:
return i
return -1
@staticmethod
def claim_id_is_valid(claim_id: str) -> bool:
return re.fullmatch('([a-z0-9]{40}|[A-Z0-9]{40})', claim_id) is not None
@staticmethod
def channel_name_is_valid(channel_name: str) -> bool:
return re.fullmatch(
'@(?:(?![\x00-\x08\x0b\x0c\x0e-\x1f\x23-\x26'
'\x2f\x3a\x3d\x3f-\x40\uFFFE-\U0000FFFF]).){1,255}',
channel_name
) is not None
@staticmethod
def is_valid_channel(channel_id: str, channel_name: str) -> bool:
return channel_id and MockedCommentServer.claim_id_is_valid(channel_id) and \
channel_name and MockedCommentServer.channel_name_is_valid(channel_name)
@staticmethod
def is_signable(signature: str, signing_ts: str) -> bool:
return signing_ts and signing_ts.isalnum() and \
signature and len(signature) == 128
@staticmethod
def credentials_are_valid(channel_id: str = None, channel_name: str = None,
signature: str = None, signing_ts: str = None) -> bool:
if channel_id or channel_name or signature or signing_ts:
try:
assert channel_id and channel_name and signature and signing_ts
assert MockedCommentServer.is_valid_channel(channel_id, channel_name)
assert MockedCommentServer.is_signable(signature, signing_ts)
except Exception:
return False
return True
def is_valid_base_comment(self, comment: str, claim_id: str, parent_id: int = None, **kwargs) -> bool:
return comment is not None and self.is_valid_body(comment) and \
claim_id is not None and self.claim_id_is_valid(claim_id) and \
(parent_id is None or self.is_valid_comment_id(parent_id))
class CommentCommands(CommandTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()
self.daemon.conf.comment_server = 'http://localhost:2903/api'
self.comment_server = MockedCommentServer(2903)
await self.comment_server.start()
self.addCleanup(self.comment_server.stop)
async def test01_comment_create(self):
channel = (await self.channel_create('@JimmyBuffett'))['outputs'][0]
stream = (await self.stream_create())['outputs'][0]
empty_list = await self.daemon.jsonrpc_comment_list(stream['claim_id'])
self.assertEqual(0, len(empty_list['items']))
comment = await self.daemon.jsonrpc_comment_create(
claim_id=stream['claim_id'],
channel_id=channel['claim_id'],
comment="It's 5 O'Clock Somewhere"
)
comments = (await self.daemon.jsonrpc_comment_list(stream['claim_id']))['items']
self.assertEqual(1, len(comments))
self.assertEqual(comment['comment_id'], comments[0]['comment_id'])
self.assertEqual(stream['claim_id'], comments[0]['claim_id'])
channel2 = (await self.channel_create('@BuffettJimmy'))['outputs'][0]
comment2 = await self.daemon.jsonrpc_comment_create(
claim_id=stream['claim_id'],
channel_name=channel2['name'],
comment='Let\'s all go to Margaritaville',
parent_id=comments[0]['comment_id']
)
comments = (await self.daemon.jsonrpc_comment_list(stream['claim_id']))['items']
self.assertEqual(2, len(comments))
self.assertEqual(comments[0]['channel_id'], channel2['claim_id'])
self.assertEqual(comments[0]['parent_id'], comments[1]['comment_id'])
async def test03_signed_comments_list(self):
channel = (await self.channel_create('@JimmyBuffett'))['outputs'][0]
stream = (await self.stream_create())['outputs'][0]
comments = []
for i in range(28):
comment = await self.daemon.jsonrpc_comment_create(
comment=f'{i}',
claim_id=stream['claim_id'],
channel_id=channel['claim_id'],
)
comments.append(comment)
self.assertIn('comment_id', comment)
comment_list = await self.daemon.jsonrpc_comment_list(
claim_id=stream['claim_id']
)
self.assertEqual(
{'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'},
set(comment_list)
)
self.assertIs(comment_list['page_size'], 50)
self.assertIs(comment_list['page'], 1)
self.assertIs(comment_list['total_items'], 28)
for comment in comment_list['items']:
comment_temp = comments.pop()
self.assertEqual(comment['comment'], comment_temp['comment'])
signed_comment_list = await self.daemon.jsonrpc_comment_list(
claim_id=stream['claim_id'],
is_channel_signature_valid=True
)
self.assertIs(len(signed_comment_list['items']), 28)
async def test04_comment_abandons(self):
rswanson = (await self.channel_create('@RonSwanson'))['outputs'][0]
stream = (await self.stream_create('Pawnee_Tow_Hall_of_Fame_by_Leslie_Knope'))['outputs'][0]
comment = await self.daemon.jsonrpc_comment_create(
comment='KNOPE! WHAT DID I TELL YOU ABOUT PUTTING MY INFORMATION UP LIKE THAT',
claim_id=stream['claim_id'],
channel_id=rswanson['claim_id']
)
self.assertIn('signature', comment)
abandoned = await self.daemon.jsonrpc_comment_abandon(comment['comment_id'])
self.assertIn(comment['comment_id'], abandoned)
self.assertTrue(abandoned[comment['comment_id']]['abandoned'])
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.assertEqual(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 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]
stream = await self.stream_create('Cool_Lamps_to_Sit_On', channel_id=moth['claim_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['claim_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']
)
all_comments = [other_comment, owner_comment, hidden_comment]
normal_list = await self.daemon.jsonrpc_comment_list(claim_id)
self.assertEqual(
{'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'},
set(normal_list)
)
self.assertEqual(normal_list['total_items'], 3)
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.assertEqual(
{'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'},
set(hidden)
)
self.assertTrue(hidden['has_hidden_comments'])
self.assertEqual(hidden['total_items'], 1)
visible = await self.daemon.jsonrpc_comment_list(claim_id, visible=True)
self.assertEqual(
{'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'},
set(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)
self.assertEqual(
{'items', 'page', 'page_size', 'has_hidden_comments', 'total_items', 'total_pages'},
set(valid_list)
)
self.assertTrue(visible['has_hidden_comments'])
self.assertEqual(len(valid_list['items']), len(normal_list['items']))
async def test07_edit_comments(self):
luda = (await self.channel_create('@Ludacris'))['outputs'][0]
juicy = (await self.channel_create('@JuicyJ'))['outputs'][0]
stream = await self.stream_create('Chicken-n-beer', channel_id=luda['claim_id'])
claim_id = stream['outputs'][0]['claim_id']
# Editing a comment made by a channel you own
og_comment = await self.daemon.jsonrpc_comment_create(
comment='This is a masterp[iece',
claim_id=claim_id,
channel_id=juicy['claim_id']
)
original_cid = og_comment.get('comment_id')
original_sig = og_comment.get('signature')
self.assertIsNotNone(original_cid, 'comment wasnt properly made')
self.assertIsNotNone(original_sig, 'comment should have a signature')
edited = await self.daemon.jsonrpc_comment_update(
comment='This is a masterpiece, need more like it!',
comment_id=original_cid
)
edited_cid = edited.get('comment_id')
edited_sig = edited.get('signature')
self.assertIsNotNone(edited_sig, 'comment wasnt properly edited!')
self.assertIsNotNone(edited_sig, 'edited comment should have a signature!')
self.assertEqual(original_cid, edited_cid, 'Comment ID should not change!')
self.assertNotEqual(original_sig, edited_sig, 'New signature should not be the same as the old!')
# editing a comment made by a channel you don't own
og_comment = await self.daemon.jsonrpc_comment_create(
comment='I wonder if you know, how they live in tokyo',
claim_id=claim_id,
channel_id=juicy['claim_id']
)
original_cid = og_comment.get('comment_id')
self.assertIsNotNone(original_cid, 'Comment should be able to be made')
# Now abandon the channel
await self.daemon.jsonrpc_channel_abandon(juicy['claim_id'])
# this should error out
with self.assertRaises(ValueError):
await self.daemon.jsonrpc_comment_update(
comment='If you see it and you mean then you know you have to go',
comment_id=original_cid
)
async def test06_reactions(self):
# wherein a bee insulted by a rude moth accidentally
# 1) likes an insult
# 2) clicks dislike instead, testing "clear_types"
# 3) then clicks dislike again, testing "remove"
# calls from the point of view of the moth
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']
)
first_comment_id = first_comment['comment_id']
second_comment_id = second_comment['comment_id']
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_like_reaction = await self.daemon.jsonrpc_comment_react(
comment_ids=first_comment_id,
channel_id=bee['claim_id'],
channel_name=bee['name'],
react_type='like',
)
moth_like_reaction = await self.daemon.jsonrpc_comment_react(
comment_ids=first_comment_id,
channel_id=moth['claim_id'],
channel_name=moth['name'],
react_type='like',
)
reactions = await self.daemon.jsonrpc_comment_react_list(
comment_ids=first_comment_id,
channel_id=moth['claim_id'],
channel_name=moth['name'],
)
# {'my_reactions': {'0': {'like': 1}}, 'others_reactions': {'0': {'like': 1}}}
self.assertEqual(reactions['my_reactions'][first_comment_id]['like'], 1)
self.assertEqual(reactions['others_reactions'][first_comment_id]['like'], 1)
bee_dislike_reaction = await self.daemon.jsonrpc_comment_react(
comment_ids=first_comment_id,
channel_id=bee['claim_id'],
channel_name=bee['name'],
react_type='dislike',
clear_types='like',
)
reactions_after_bee_dislikes = await self.daemon.jsonrpc_comment_react_list(
comment_ids=first_comment_id,
channel_id=moth['claim_id'],
channel_name=moth['name'],
)
# {'my_reactions': {'0': {'like': 1, 'dislike', 0}}, 'others_reactions': {'0': {'like': 0, 'dislike': 1}}}
self.assertEqual(reactions_after_bee_dislikes['my_reactions'][first_comment_id]['like'], 1)
self.assertEqual(reactions_after_bee_dislikes['my_reactions'][first_comment_id]['dislike'], 0)
self.assertEqual(reactions_after_bee_dislikes['others_reactions'][first_comment_id]['dislike'], 1)
self.assertEqual(reactions_after_bee_dislikes['others_reactions'][first_comment_id]['like'], 0)
only_likes_after_bee_dislikes = await self.daemon.jsonrpc_comment_react_list(
comment_ids=first_comment_id,
channel_id=moth['claim_id'],
channel_name=moth['name'],
react_types='like',
)
self.assertEqual(only_likes_after_bee_dislikes['my_reactions'][first_comment_id]['like'], 1)
self.assertEqual(only_likes_after_bee_dislikes['my_reactions'][first_comment_id]['dislike'], 0)
self.assertEqual(only_likes_after_bee_dislikes['others_reactions'][first_comment_id]['dislike'], 0)
self.assertEqual(only_likes_after_bee_dislikes['others_reactions'][first_comment_id]['like'], 0)
bee_un_dislike_reaction = await self.daemon.jsonrpc_comment_react(
comment_ids=first_comment_id,
channel_id=bee['claim_id'],
channel_name=bee['name'],
remove=True,
react_type='dislike',
)
reactions_after_bee_absconds = await self.daemon.jsonrpc_comment_react_list(
comment_ids=first_comment_id,
channel_id=moth['claim_id'],
channel_name=moth['name'],
)
self.assertEqual(reactions_after_bee_absconds['my_reactions'][first_comment_id]['like'], 1)
self.assertNotIn('dislike', reactions_after_bee_absconds['my_reactions'][first_comment_id])
self.assertEqual(reactions_after_bee_absconds['others_reactions'][first_comment_id]['like'], 0)
self.assertNotIn('dislike', reactions_after_bee_absconds['others_reactions'][first_comment_id])
bee_reacts_to_both_comments = await self.daemon.jsonrpc_comment_react(
comment_ids=first_comment_id + ',' + second_comment_id,
channel_id=bee['claim_id'],
channel_name=bee['name'],
react_type='frozen_tom',
)
reactions_after_double_frozen_tom = await self.daemon.jsonrpc_comment_react_list(
comment_ids=first_comment_id + ',' + second_comment_id,
channel_id=moth['claim_id'],
channel_name=moth['name'],
)
self.assertEqual(reactions_after_double_frozen_tom['my_reactions'][first_comment_id]['like'], 1)
self.assertNotIn('dislike', reactions_after_double_frozen_tom['my_reactions'][first_comment_id])
self.assertEqual(reactions_after_double_frozen_tom['others_reactions'][first_comment_id]['frozen_tom'], 1)
self.assertEqual(reactions_after_double_frozen_tom['others_reactions'][second_comment_id]['frozen_tom'], 1)

View file

@ -1,59 +0,0 @@
from lbry.testcase import AsyncioTestCase
import hashlib
from lbry.extras.daemon.comment_client import sign_comment
from lbry.extras.daemon.comment_client import is_comment_signed_by_channel
from tests.unit.wallet.test_schema_signing import get_stream, get_channel
class TestSigningComments(AsyncioTestCase):
@staticmethod
def create_claim_comment_body(comment, claim, channel):
return {
'claim_id': claim.claim_id,
'channel_name': channel.claim_name,
'channel_id': channel.claim_id,
'comment': comment,
'comment_id': hashlib.sha256(comment.encode()).hexdigest()
}
async def test01_successful_create_sign_and_validate_comment(self):
channel = await get_channel('@BusterBluth')
stream = get_stream('pop secret')
comment = self.create_claim_comment_body('Cool stream', stream, channel)
sign_comment(comment, channel)
self.assertTrue(is_comment_signed_by_channel(comment, channel))
async def test02_fail_to_validate_spoofed_channel(self):
pdiddy = await get_channel('@PDitty')
channel2 = await get_channel('@TomHaverford')
stream = get_stream()
comment = self.create_claim_comment_body('Woahh This is Sick!! Shout out 2 my boy Tommy H', stream, pdiddy)
sign_comment(comment, channel2)
self.assertFalse(is_comment_signed_by_channel(comment, pdiddy))
async def test03_successful_sign_abandon_comment(self):
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, 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')
jeanralphio = await get_channel('@JeanRalphio')
chair = get_stream('This is a nice chair. I made it with Mahogany wood and this electric saw')
chair_comment = self.create_claim_comment_body(
'Hah. You use an electric saw? Us swansons have been making chairs with handsaws just three after birth.',
chair,
rswanson
)
sign_comment(chair_comment, rswanson)
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, 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))

View file

@ -2,10 +2,9 @@ from binascii import unhexlify
from lbry.testcase import AsyncioTestCase from lbry.testcase import AsyncioTestCase
from lbry.wallet.constants import CENT, NULL_HASH32 from lbry.wallet.constants import CENT, NULL_HASH32
from lbry.wallet import Ledger, Database, Headers, Transaction, Input, Output from lbry.wallet import Ledger, Database, Headers, Transaction, Input, Output
from lbry.schema.claim import Claim from lbry.schema.claim import Claim
from lbry.crypto.hash import sha256
def get_output(amount=CENT, pubkey_hash=NULL_HASH32): def get_output(amount=CENT, pubkey_hash=NULL_HASH32):
return Transaction() \ return Transaction() \
@ -114,3 +113,21 @@ class TestValidatingOldSignatures(AsyncioTestCase):
}) })
self.assertTrue(stream.is_signed_by(channel, ledger)) self.assertTrue(stream.is_signed_by(channel, ledger))
class TestValidateSignContent(AsyncioTestCase):
async def test_sign_some_content(self):
some_content = "MEANINGLESS CONTENT AEE3353320".encode()
timestamp_str = "1630564175"
channel = await get_channel()
stream = get_stream()
signature = channel.sign_data(some_content, timestamp_str)
stream.signable.signature = unhexlify(signature.encode())
encoded_signature = stream.get_encoded_signature()
pieces = [timestamp_str.encode(), channel.claim_hash, some_content]
self.assertTrue(Output.is_signature_valid(
encoded_signature,
sha256(b''.join(pieces)),
channel.claim.channel.public_key_bytes
))