2018-09-17 22:13:30 +02:00
|
|
|
import json
|
2019-03-14 00:47:13 +01:00
|
|
|
from collections import OrderedDict
|
2019-03-15 06:33:41 +01:00
|
|
|
from typing import List, Tuple
|
|
|
|
from decimal import Decimal
|
|
|
|
from binascii import hexlify, unhexlify
|
2019-03-18 05:59:13 +01:00
|
|
|
from hashlib import sha256
|
|
|
|
|
|
|
|
import ecdsa
|
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
|
|
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
|
|
|
from cryptography.hazmat.primitives import hashes
|
|
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
|
|
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
|
|
|
from ecdsa.util import sigencode_der
|
2019-03-14 00:47:13 +01:00
|
|
|
|
2018-09-17 22:13:30 +02:00
|
|
|
from google.protobuf import json_format # pylint: disable=no-name-in-module
|
|
|
|
from google.protobuf.message import DecodeError as DecodeError_pb # pylint: disable=no-name-in-module,import-error
|
|
|
|
|
2019-03-14 00:47:13 +01:00
|
|
|
from torba.client.constants import COIN
|
2018-09-17 22:13:30 +02:00
|
|
|
|
2018-12-15 07:25:46 +01:00
|
|
|
from lbrynet.schema.signature import Signature
|
2018-09-17 22:31:44 +02:00
|
|
|
from lbrynet.schema.validator import get_validator
|
|
|
|
from lbrynet.schema.signer import get_signer
|
2019-02-28 22:54:37 +01:00
|
|
|
from lbrynet.schema.constants import CURVE_NAMES, SECP256k1
|
2018-09-17 22:31:44 +02:00
|
|
|
from lbrynet.schema.encoding import decode_fields, decode_b64_fields, encode_fields
|
|
|
|
from lbrynet.schema.error import DecodeError
|
2019-03-15 06:33:41 +01:00
|
|
|
from lbrynet.schema.types.v2.claim_pb2 import Claim as ClaimMessage, Fee as FeeMessage
|
|
|
|
from lbrynet.schema.base import b58decode, b58encode
|
|
|
|
from lbrynet.schema import compat
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
class ClaimDict(OrderedDict):
|
2018-12-15 07:25:46 +01:00
|
|
|
def __init__(self, claim_dict=None, detached_signature: Signature=None):
|
2019-03-06 03:37:44 +01:00
|
|
|
if isinstance(claim_dict, legacy_claim_pb2.Claim):
|
2018-09-17 22:13:30 +02:00
|
|
|
raise Exception("To initialize %s with a Claim protobuf use %s.load_protobuf" %
|
|
|
|
(self.__class__.__name__, self.__class__.__name__))
|
2018-12-15 07:25:46 +01:00
|
|
|
self.detached_signature = detached_signature
|
2018-09-17 22:13:30 +02:00
|
|
|
OrderedDict.__init__(self, claim_dict or [])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def protobuf_dict(self):
|
|
|
|
"""Claim dictionary using base64 to represent bytes"""
|
|
|
|
|
|
|
|
return json.loads(json_format.MessageToJson(self.protobuf, True))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def protobuf(self):
|
|
|
|
"""Claim message object"""
|
|
|
|
|
2019-03-14 00:47:13 +01:00
|
|
|
return LegacyClaim.load(self)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def serialized(self):
|
|
|
|
"""Serialized Claim protobuf"""
|
2019-01-18 01:24:24 +01:00
|
|
|
if self.detached_signature:
|
2019-01-05 10:02:43 +01:00
|
|
|
return self.detached_signature.serialized
|
2018-09-17 22:13:30 +02:00
|
|
|
return self.protobuf.SerializeToString()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def serialized_no_signature(self):
|
|
|
|
"""Serialized Claim protobuf without publisherSignature field"""
|
|
|
|
claim = self.protobuf
|
|
|
|
claim.ClearField("publisherSignature")
|
|
|
|
return ClaimDict.load_protobuf(claim).serialized
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_signature(self):
|
2019-01-18 01:24:24 +01:00
|
|
|
return self.protobuf.HasField("publisherSignature") or (
|
|
|
|
self.detached_signature and self.detached_signature.raw_signature
|
|
|
|
)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_certificate(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
return CLAIM_TYPE_NAMES[claim.claimType] == "certificate"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_stream(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
return CLAIM_TYPE_NAMES[claim.claimType] == "stream"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def source_hash(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
if not CLAIM_TYPE_NAMES[claim.claimType] == "stream":
|
|
|
|
return None
|
|
|
|
return binascii.hexlify(claim.stream.source.source)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_fee(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
if not CLAIM_TYPE_NAMES[claim.claimType] == "stream":
|
|
|
|
return None
|
|
|
|
if claim.stream.metadata.HasField("fee"):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def source_fee(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
if not CLAIM_TYPE_NAMES[claim.claimType] == "stream":
|
|
|
|
return None
|
|
|
|
if claim.stream.metadata.HasField("fee"):
|
|
|
|
return Fee.load_protobuf(claim.stream.metadata.fee)
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
2019-01-18 03:37:37 +01:00
|
|
|
def certificate_id(self) -> str:
|
2019-01-18 01:24:24 +01:00
|
|
|
if self.protobuf.HasField("publisherSignature"):
|
2019-01-18 03:37:37 +01:00
|
|
|
return binascii.hexlify(self.protobuf.publisherSignature.certificateId).decode()
|
2019-01-05 10:02:43 +01:00
|
|
|
if self.detached_signature and self.detached_signature.certificate_id:
|
2019-01-18 03:37:37 +01:00
|
|
|
return binascii.hexlify(self.detached_signature.certificate_id).decode()
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def signature(self):
|
|
|
|
if not self.has_signature:
|
|
|
|
return None
|
|
|
|
return binascii.hexlify(self.protobuf.publisherSignature.signature)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def protobuf_len(self):
|
|
|
|
"""Length of serialized string"""
|
|
|
|
|
|
|
|
return self.protobuf.ByteSize()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def json_len(self):
|
|
|
|
"""Length of json encoded string"""
|
|
|
|
|
|
|
|
return len(json.dumps(self.claim_dict))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def claim_dict(self):
|
|
|
|
"""Claim dictionary with bytes represented as hex and base58"""
|
|
|
|
|
2019-01-18 02:20:47 +01:00
|
|
|
return dict(encode_fields(self, self.detached_signature))
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
2018-12-15 07:25:46 +01:00
|
|
|
def load_protobuf_dict(cls, protobuf_dict, detached_signature=None):
|
2018-09-17 22:13:30 +02:00
|
|
|
"""
|
|
|
|
Load a ClaimDict from a dictionary with base64 encoded bytes
|
|
|
|
(as returned by the protobuf json formatter)
|
|
|
|
"""
|
|
|
|
|
2018-12-15 07:25:46 +01:00
|
|
|
return cls(decode_b64_fields(protobuf_dict), detached_signature=detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
2018-12-15 07:25:46 +01:00
|
|
|
def load_protobuf(cls, protobuf_claim, detached_signature=None):
|
2018-09-17 22:13:30 +02:00
|
|
|
"""Load ClaimDict from a protobuf Claim message"""
|
2018-12-15 07:25:46 +01:00
|
|
|
return cls.load_protobuf_dict(json.loads(json_format.MessageToJson(protobuf_claim, True)), detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def load_dict(cls, claim_dict):
|
|
|
|
"""Load ClaimDict from a dictionary with hex and base58 encoded bytes"""
|
|
|
|
try:
|
2019-01-18 02:20:47 +01:00
|
|
|
claim_dict, detached_signature = decode_fields(claim_dict)
|
|
|
|
return cls.load_protobuf(cls(claim_dict).protobuf, detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
except json_format.ParseError as err:
|
|
|
|
raise DecodeError(str(err))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def deserialize(cls, serialized):
|
|
|
|
"""Load a ClaimDict from a serialized protobuf string"""
|
2019-01-05 10:02:43 +01:00
|
|
|
detached_signature = Signature.flagged_parse(serialized)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
2019-03-06 03:37:44 +01:00
|
|
|
temp_claim = legacy_claim_pb2.Claim()
|
2018-09-17 22:13:30 +02:00
|
|
|
try:
|
2019-01-05 10:02:43 +01:00
|
|
|
temp_claim.ParseFromString(detached_signature.payload)
|
2018-09-17 22:13:30 +02:00
|
|
|
except DecodeError_pb:
|
|
|
|
raise DecodeError(DecodeError_pb)
|
2018-12-15 07:25:46 +01:00
|
|
|
return cls.load_protobuf(temp_claim, detached_signature=detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
2018-12-31 05:08:02 +01:00
|
|
|
def generate_certificate(cls, private_key, curve=SECP256k1):
|
2018-09-17 22:13:30 +02:00
|
|
|
signer = get_signer(curve).load_pem(private_key)
|
|
|
|
return cls.load_protobuf(signer.certificate)
|
|
|
|
|
2019-01-04 20:06:18 +01:00
|
|
|
def sign(self, private_key, claim_address, cert_claim_id, curve=SECP256k1, name=None, force_detached=False):
|
2018-09-17 22:13:30 +02:00
|
|
|
signer = get_signer(curve).load_pem(private_key)
|
2019-01-04 20:06:18 +01:00
|
|
|
signed, signature = signer.sign_stream_claim(self, claim_address, cert_claim_id, name, force_detached)
|
2018-12-31 01:23:03 +01:00
|
|
|
return ClaimDict.load_protobuf(signed, signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
2018-12-15 07:25:46 +01:00
|
|
|
def validate_signature(self, claim_address, certificate, name=None):
|
2018-09-17 22:13:30 +02:00
|
|
|
if isinstance(certificate, ClaimDict):
|
|
|
|
certificate = certificate.protobuf
|
|
|
|
curve = CURVE_NAMES[certificate.certificate.keyType]
|
|
|
|
validator = get_validator(curve).load_from_certificate(certificate, self.certificate_id)
|
2018-12-31 01:23:03 +01:00
|
|
|
return validator.validate_claim_signature(self, claim_address, name)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
def validate_private_key(self, private_key, certificate_id):
|
|
|
|
certificate = self.protobuf
|
|
|
|
if CLAIM_TYPE_NAMES[certificate.claimType] != "certificate":
|
|
|
|
return
|
|
|
|
curve = CURVE_NAMES[certificate.certificate.keyType]
|
|
|
|
validator = get_validator(curve).load_from_certificate(certificate, certificate_id)
|
|
|
|
signing_key = validator.signing_key_from_pem(private_key)
|
|
|
|
return validator.validate_private_key(signing_key)
|
|
|
|
|
|
|
|
def get_validator(self, certificate_id):
|
|
|
|
"""
|
2018-09-17 22:31:44 +02:00
|
|
|
Get a lbrynet.schema.validator.Validator object for a certificate claim
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
:param certificate_id: claim id of this certificate claim
|
2018-09-17 22:31:44 +02:00
|
|
|
:return: None or lbrynet.schema.validator.Validator object
|
2018-09-17 22:13:30 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
claim = self.protobuf
|
|
|
|
if CLAIM_TYPE_NAMES[claim.claimType] != "certificate":
|
|
|
|
return
|
|
|
|
curve = CURVE_NAMES[claim.certificate.keyType]
|
|
|
|
return get_validator(curve).load_from_certificate(claim, certificate_id)
|
2019-03-14 00:47:13 +01:00
|
|
|
|
|
|
|
|
2019-03-15 06:33:41 +01:00
|
|
|
class Claim:
|
2019-03-14 00:47:13 +01:00
|
|
|
|
2019-03-18 05:59:13 +01:00
|
|
|
__slots__ = '_claim', 'signature', 'certificate_id', 'signature_type', 'unsigned_payload'
|
2019-03-15 06:33:41 +01:00
|
|
|
|
|
|
|
def __init__(self, claim_message=None):
|
|
|
|
self._claim = claim_message or ClaimMessage()
|
2019-03-18 05:59:13 +01:00
|
|
|
self.signature = None
|
|
|
|
self.signature_type = 'SECP256k1'
|
|
|
|
self.certificate_id = None
|
|
|
|
self.unsigned_payload = None
|
2019-03-15 06:33:41 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_undetermined(self):
|
|
|
|
return self._claim.WhichOneof('type') is None
|
2019-03-14 00:47:13 +01:00
|
|
|
|
2019-03-15 06:33:41 +01:00
|
|
|
@property
|
|
|
|
def is_stream(self):
|
|
|
|
return self._claim.WhichOneof('type') == 'stream'
|
2019-03-14 00:47:13 +01:00
|
|
|
|
2019-03-15 06:33:41 +01:00
|
|
|
@property
|
|
|
|
def is_channel(self):
|
|
|
|
return self._claim.WhichOneof('type') == 'channel'
|
|
|
|
|
2019-03-18 05:59:13 +01:00
|
|
|
@property
|
|
|
|
def is_signed(self):
|
|
|
|
return self.signature is not None
|
|
|
|
|
|
|
|
def is_signed_by(self, channel: 'Channel', claim_address):
|
|
|
|
if self.unsigned_payload:
|
|
|
|
digest = sha256(b''.join([
|
|
|
|
claim_address, self.unsigned_payload, self.certificate_id
|
|
|
|
])).digest()
|
|
|
|
public_key = load_der_public_key(channel.public_key_bytes, default_backend())
|
|
|
|
hash = hashes.SHA256()
|
|
|
|
signature = hexlify(self.signature)
|
|
|
|
r = int(signature[:int(len(signature)/2)], 16)
|
|
|
|
s = int(signature[int(len(signature)/2):], 16)
|
|
|
|
encoded_sig = sigencode_der(r, s, len(signature)*4)
|
|
|
|
public_key.verify(encoded_sig, digest, ec.ECDSA(Prehashed(hash)))
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
digest = sha256(b''.join([
|
|
|
|
self.certificate_id.encode(),
|
|
|
|
first_input_txid_nout.encode(),
|
|
|
|
self.to_bytes()
|
|
|
|
])).digest()
|
|
|
|
|
|
|
|
def sign(self, certificate_id: str, private_key_text: str, first_input_txid_nout):
|
|
|
|
digest = sha256(b''.join([
|
|
|
|
certificate_id.encode(),
|
|
|
|
first_input_txid_nout.encode(),
|
|
|
|
self.to_bytes()
|
|
|
|
])).digest()
|
|
|
|
private_key = ecdsa.SigningKey.from_pem(private_key_text, hashfunc="sha256")
|
|
|
|
self.signature = private_key.sign_digest_deterministic(digest, hashfunc="sha256")
|
|
|
|
self.certificate_id = certificate_id
|
|
|
|
|
2019-03-15 06:33:41 +01:00
|
|
|
@property
|
|
|
|
def stream_message(self):
|
|
|
|
if self.is_undetermined:
|
|
|
|
self._claim.stream.SetInParent()
|
|
|
|
if not self.is_stream:
|
|
|
|
raise ValueError('Claim is not a stream.')
|
|
|
|
return self._claim.stream
|
|
|
|
|
|
|
|
@property
|
|
|
|
def stream(self) -> 'Stream':
|
|
|
|
return Stream(self)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def channel_message(self):
|
|
|
|
if self.is_undetermined:
|
|
|
|
self._claim.channel.SetInParent()
|
|
|
|
if not self.is_channel:
|
|
|
|
raise ValueError('Claim is not a channel.')
|
|
|
|
return self._claim.channel
|
|
|
|
|
|
|
|
@property
|
|
|
|
def channel(self) -> 'Channel':
|
|
|
|
return Channel(self)
|
|
|
|
|
|
|
|
def to_bytes(self) -> bytes:
|
|
|
|
return self._claim.SerializeToString()
|
2019-03-14 00:47:13 +01:00
|
|
|
|
|
|
|
@classmethod
|
2019-03-15 06:33:41 +01:00
|
|
|
def from_bytes(cls, data: bytes) -> 'Claim':
|
|
|
|
claim = ClaimMessage()
|
|
|
|
if data[0] == 0:
|
|
|
|
claim.ParseFromString(data[1:])
|
|
|
|
return cls(claim)
|
|
|
|
elif data[0] == 1:
|
|
|
|
claim.ParseFromString(data[85:])
|
|
|
|
return cls(claim).from_message(payload[1:21], payload[21:85])
|
|
|
|
elif data[0] == ord('{'):
|
|
|
|
return compat.from_old_json_schema(cls(claim), data)
|
2019-03-14 00:47:13 +01:00
|
|
|
else:
|
2019-03-15 06:33:41 +01:00
|
|
|
return compat.from_types_v1(cls(claim), data)
|
|
|
|
|
|
|
|
|
|
|
|
class Video:
|
|
|
|
|
|
|
|
__slots__ = '_video',
|
|
|
|
|
|
|
|
def __init__(self, video_message):
|
|
|
|
self._video = video_message
|
|
|
|
|
|
|
|
@property
|
|
|
|
def width(self) -> int:
|
|
|
|
return self._video.width
|
|
|
|
|
|
|
|
@width.setter
|
|
|
|
def width(self, width: int):
|
|
|
|
self._video.width = width
|
|
|
|
|
|
|
|
@property
|
|
|
|
def height(self) -> int:
|
|
|
|
return self._video.height
|
|
|
|
|
|
|
|
@height.setter
|
|
|
|
def height(self, height: int):
|
|
|
|
self._video.height = height
|
|
|
|
|
|
|
|
@property
|
|
|
|
def dimensions(self) -> Tuple[int, int]:
|
|
|
|
return self.width, self.height
|
|
|
|
|
|
|
|
@dimensions.setter
|
|
|
|
def dimensions(self, dimensions: Tuple[int, int]):
|
|
|
|
self._video.width, self._video.height = dimensions
|
|
|
|
|
|
|
|
|
|
|
|
class File:
|
|
|
|
|
|
|
|
__slots__ = '_file',
|
|
|
|
|
|
|
|
def __init__(self, file_message):
|
|
|
|
self._file = file_message
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
return self._file.name
|
|
|
|
|
|
|
|
@name.setter
|
|
|
|
def name(self, name: str):
|
|
|
|
self._file.name = name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def size(self) -> int:
|
|
|
|
return self._file.size
|
|
|
|
|
|
|
|
@size.setter
|
|
|
|
def size(self, size: int):
|
|
|
|
self._file.size = size
|
|
|
|
|
|
|
|
|
|
|
|
class Fee:
|
|
|
|
|
|
|
|
__slots__ = '_fee',
|
|
|
|
|
|
|
|
def __init__(self, fee_message):
|
|
|
|
self._fee = fee_message
|
|
|
|
|
|
|
|
@property
|
|
|
|
def currency(self) -> str:
|
|
|
|
return FeeMessage.Currency.Name(self._fee.currency)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def address(self) -> str:
|
|
|
|
return b58encode(self._fee.address).decode()
|
|
|
|
|
|
|
|
@address.setter
|
|
|
|
def address(self, address: str):
|
|
|
|
self._fee.address = b58decode(address)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def address_bytes(self) -> bytes:
|
|
|
|
return self._fee.address
|
|
|
|
|
|
|
|
@address_bytes.setter
|
|
|
|
def address_bytes(self, address: bytes):
|
|
|
|
self._fee.address = address
|
|
|
|
|
2019-03-15 06:41:22 +01:00
|
|
|
@property
|
|
|
|
def amount(self) -> Decimal:
|
|
|
|
if self.currency == 'LBC':
|
|
|
|
return self.lbc
|
|
|
|
if self.currency == 'USD':
|
|
|
|
return self.usd
|
|
|
|
|
2019-03-15 14:05:14 +01:00
|
|
|
DEWIES = Decimal(COIN)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def lbc(self) -> Decimal:
|
|
|
|
if self._fee.currency != FeeMessage.LBC:
|
|
|
|
raise ValueError('LBC can only be returned for LBC fees.')
|
|
|
|
return Decimal(self._fee.amount / self.DEWIES)
|
|
|
|
|
|
|
|
@lbc.setter
|
|
|
|
def lbc(self, amount: Decimal):
|
|
|
|
self.dewies = int(amount * self.DEWIES)
|
|
|
|
|
2019-03-15 06:33:41 +01:00
|
|
|
@property
|
|
|
|
def dewies(self) -> int:
|
|
|
|
if self._fee.currency != FeeMessage.LBC:
|
|
|
|
raise ValueError('Dewies can only be returned for LBC fees.')
|
|
|
|
return self._fee.amount
|
|
|
|
|
|
|
|
@dewies.setter
|
|
|
|
def dewies(self, amount: int):
|
|
|
|
self._fee.amount = amount
|
|
|
|
self._fee.currency = FeeMessage.LBC
|
|
|
|
|
2019-03-15 14:05:14 +01:00
|
|
|
PENNIES = Decimal(100.0)
|
2019-03-15 06:33:41 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def usd(self) -> Decimal:
|
|
|
|
if self._fee.currency != FeeMessage.USD:
|
|
|
|
raise ValueError('USD can only be returned for USD fees.')
|
2019-03-15 14:05:14 +01:00
|
|
|
return Decimal(self._fee.amount / self.PENNIES)
|
2019-03-15 06:33:41 +01:00
|
|
|
|
|
|
|
@usd.setter
|
|
|
|
def usd(self, amount: Decimal):
|
2019-03-15 14:05:14 +01:00
|
|
|
self.pennies = int(amount * self.PENNIES)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def pennies(self) -> int:
|
|
|
|
if self._fee.currency != FeeMessage.USD:
|
|
|
|
raise ValueError('Pennies can only be returned for USD fees.')
|
|
|
|
return self._fee.amount
|
|
|
|
|
|
|
|
@pennies.setter
|
|
|
|
def pennies(self, amount: int):
|
|
|
|
self._fee.amount = amount
|
2019-03-15 06:33:41 +01:00
|
|
|
self._fee.currency = FeeMessage.USD
|
|
|
|
|
|
|
|
|
2019-03-18 05:59:13 +01:00
|
|
|
class Channel:
|
|
|
|
|
|
|
|
__slots__ = '_claim', '_channel'
|
|
|
|
|
|
|
|
def __init__(self, claim: Claim = None):
|
|
|
|
self._claim = claim or Claim()
|
|
|
|
self._channel = self._claim.channel_message
|
|
|
|
|
|
|
|
@property
|
|
|
|
def claim(self) -> Claim:
|
|
|
|
return self._claim
|
|
|
|
|
|
|
|
@property
|
|
|
|
def tags(self) -> List:
|
|
|
|
return self._channel.tags
|
|
|
|
|
|
|
|
@property
|
|
|
|
def public_key(self) -> str:
|
|
|
|
return hexlify(self._channel.public_key).decode()
|
|
|
|
|
|
|
|
@public_key.setter
|
|
|
|
def public_key(self, sd_public_key: str):
|
|
|
|
self._channel.public_key = unhexlify(sd_public_key.encode())
|
|
|
|
|
|
|
|
@property
|
|
|
|
def public_key_bytes(self) -> bytes:
|
|
|
|
return self._channel.public_key
|
|
|
|
|
|
|
|
@public_key_bytes.setter
|
|
|
|
def public_key_bytes(self, public_key: bytes):
|
|
|
|
self._channel.public_key = public_key
|
|
|
|
|
|
|
|
@property
|
|
|
|
def language(self) -> str:
|
|
|
|
return self._channel.language
|
|
|
|
|
|
|
|
@language.setter
|
|
|
|
def language(self, language: str):
|
|
|
|
self._channel.language = language
|
|
|
|
|
|
|
|
@property
|
|
|
|
def title(self) -> str:
|
|
|
|
return self._channel.title
|
|
|
|
|
|
|
|
@title.setter
|
|
|
|
def title(self, title: str):
|
|
|
|
self._channel.title = title
|
|
|
|
|
|
|
|
@property
|
|
|
|
def description(self) -> str:
|
|
|
|
return self._channel.description
|
|
|
|
|
|
|
|
@description.setter
|
|
|
|
def description(self, description: str):
|
|
|
|
self._channel.description = description
|
|
|
|
|
|
|
|
@property
|
|
|
|
def contact_email(self) -> str:
|
|
|
|
return self._channel.contact_email
|
|
|
|
|
|
|
|
@contact_email.setter
|
|
|
|
def contact_email(self, contact_email: str):
|
|
|
|
self._channel.contact_email = contact_email
|
|
|
|
|
|
|
|
@property
|
|
|
|
def homepage_url(self) -> str:
|
|
|
|
return self._channel.homepage_url
|
|
|
|
|
|
|
|
@homepage_url.setter
|
|
|
|
def homepage_url(self, homepage_url: str):
|
|
|
|
self._channel.homepage_url = homepage_url
|
|
|
|
|
|
|
|
@property
|
|
|
|
def thumbnail_url(self) -> str:
|
|
|
|
return self._channel.thumbnail_url
|
|
|
|
|
|
|
|
@thumbnail_url.setter
|
|
|
|
def thumbnail_url(self, thumbnail_url: str):
|
|
|
|
self._channel.thumbnail_url = thumbnail_url
|
|
|
|
|
|
|
|
@property
|
|
|
|
def cover_url(self) -> str:
|
|
|
|
return self._channel.cover_url
|
|
|
|
|
|
|
|
@cover_url.setter
|
|
|
|
def cover_url(self, cover_url: str):
|
|
|
|
self._channel.cover_url = cover_url
|
|
|
|
|
|
|
|
|
2019-03-15 06:33:41 +01:00
|
|
|
class Stream:
|
|
|
|
|
|
|
|
__slots__ = '_claim', '_stream'
|
|
|
|
|
|
|
|
def __init__(self, claim: Claim = None):
|
|
|
|
self._claim = claim or Claim()
|
|
|
|
self._stream = self._claim.stream_message
|
|
|
|
|
|
|
|
@property
|
|
|
|
def claim(self) -> Claim:
|
|
|
|
return self._claim
|
|
|
|
|
|
|
|
@property
|
|
|
|
def video(self) -> Video:
|
|
|
|
return Video(self._stream.video)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def file(self) -> File:
|
|
|
|
return File(self._stream.file)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def fee(self) -> Fee:
|
|
|
|
return Fee(self._stream.fee)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def tags(self) -> List:
|
|
|
|
return self._stream.tags
|
|
|
|
|
|
|
|
@property
|
|
|
|
def hash(self) -> str:
|
|
|
|
return hexlify(self._stream.hash).decode()
|
|
|
|
|
|
|
|
@hash.setter
|
|
|
|
def hash(self, sd_hash: str):
|
|
|
|
self._stream.hash = unhexlify(sd_hash.encode())
|
|
|
|
|
|
|
|
@property
|
|
|
|
def hash_bytes(self) -> bytes:
|
|
|
|
return self._stream.hash
|
|
|
|
|
|
|
|
@hash_bytes.setter
|
|
|
|
def hash_bytes(self, hash: bytes):
|
|
|
|
self._stream.hash = hash
|
|
|
|
|
|
|
|
@property
|
|
|
|
def language(self) -> str:
|
|
|
|
return self._stream.language
|
|
|
|
|
|
|
|
@language.setter
|
|
|
|
def language(self, language: str):
|
|
|
|
self._stream.language = language
|
|
|
|
|
|
|
|
@property
|
|
|
|
def title(self) -> str:
|
|
|
|
return self._stream.title
|
|
|
|
|
|
|
|
@title.setter
|
|
|
|
def title(self, title: str):
|
|
|
|
self._stream.title = title
|
|
|
|
|
|
|
|
@property
|
|
|
|
def author(self) -> str:
|
|
|
|
return self._stream.author
|
|
|
|
|
|
|
|
@author.setter
|
|
|
|
def author(self, author: str):
|
|
|
|
self._stream.author = author
|
|
|
|
|
|
|
|
@property
|
|
|
|
def description(self) -> str:
|
|
|
|
return self._stream.description
|
|
|
|
|
|
|
|
@description.setter
|
|
|
|
def description(self, description: str):
|
|
|
|
self._stream.description = description
|
|
|
|
|
|
|
|
@property
|
|
|
|
def media_type(self) -> str:
|
|
|
|
return self._stream.media_type
|
|
|
|
|
|
|
|
@media_type.setter
|
|
|
|
def media_type(self, media_type: str):
|
|
|
|
self._stream.media_type = media_type
|
|
|
|
|
|
|
|
@property
|
|
|
|
def license(self) -> str:
|
|
|
|
return self._stream.license
|
|
|
|
|
|
|
|
@license.setter
|
|
|
|
def license(self, license: str):
|
|
|
|
self._stream.license = license
|
|
|
|
|
|
|
|
@property
|
|
|
|
def license_url(self) -> str:
|
|
|
|
return self._stream.license_url
|
|
|
|
|
|
|
|
@license_url.setter
|
|
|
|
def license_url(self, license_url: str):
|
|
|
|
self._stream.license_url = license_url
|
|
|
|
|
|
|
|
@property
|
|
|
|
def thumbnail_url(self) -> str:
|
|
|
|
return self._stream.thumbnail_url
|
|
|
|
|
|
|
|
@thumbnail_url.setter
|
|
|
|
def thumbnail_url(self, thumbnail_url: str):
|
|
|
|
self._stream.thumbnail_url = thumbnail_url
|
|
|
|
|
|
|
|
@property
|
|
|
|
def duration(self) -> int:
|
|
|
|
return self._stream.duration
|
|
|
|
|
|
|
|
@duration.setter
|
|
|
|
def duration(self, duration: int):
|
|
|
|
self._stream.duration = duration
|
|
|
|
|
|
|
|
@property
|
|
|
|
def release_time(self) -> int:
|
|
|
|
return self._stream.release_time
|
|
|
|
|
|
|
|
@release_time.setter
|
|
|
|
def release_time(self, release_time: int):
|
|
|
|
self._stream.release_time = release_time
|